Files
oak-editor/crates/oakengine/src/render.rs
T
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +08:00

1430 lines
42 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! `engine/include/oakengine/{renderer,color,lut}.h` over the oakrender
//! module.
//!
//! - The **renderer** is a facade-owned box binding an output node
//! (usually a sequence, or any single node via
//! `oakengine_renderer_create_for_node`) to an output geometry; each
//! render call submits an oakrender ticket (`OakVideoTicketParams`)
//! through the manager's real ticket arena, waits for it and returns
//! the produced frame (`oakrender::texture::Frame` wrapped in
//! `OakEngineFrame`). Audio rendering goes through the arena's audio
//! path (`oakrender::eval::render_audio_samples`) and returns the
//! interleaved samples in `OakEngineAudioBuffer`.
//! - The **frame accessors** read the wrapped frame
//! (`channel_count` has no crate accessor and reports 0).
//! - The **color processor** family maps onto the real
//! `oakrender::color::ColorProcessor`; the engine's
//! `oak_color_transform` POD is converted into an oakcommon
//! colortransform handle for `create_transform`. The color-manager
//! list queries, standalone config handle and LUT directory/file
//! library have no crate backing and are documented stubs (see
//! `deferred.rs`).
use std::cell::RefCell;
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::stubs::render as r;
use crate::pods::{OakRenderVideoParams, OakVideoTicketParams};
use crate::error::{Error, Result};
use crate::handle::{
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, string_result, unbox, CHandle,
EngineBox, OakEngineAudioBuffer, OakEngineColorProcessor, OakEngineFrame, OakEngineNode,
OakEngineRenderer, OakEngineSequence,
};
// ---------------------------------------------------------------------------
// Render manager / cacher
// ---------------------------------------------------------------------------
/// `oakengine_render_manager_init` — bring up the module's process-global
/// render manager (0 on success; without it `render_frame` fails with NULL
/// + last_error). The module's `OAKRENDER_E_STATE` (-70002) passes through
/// when the manager is already initialized.
#[no_mangle]
pub extern "C" fn oakengine_render_manager_init() -> c_int {
guard(|| Error::from_module(unsafe { r::oakrender_manager_init() }))
}
/// `oakengine_render_manager_available` — 1 when the render manager is up,
/// 0 otherwise.
#[no_mangle]
pub extern "C" fn oakengine_render_manager_available() -> c_int {
guard_int(|| Ok(unsafe { r::oakrender_manager_available() }))
}
/// `oakengine_render_manager_shutdown` — tear down the module's render
/// manager (no-op when none is up; always 0, like
/// `oakengine_audio_destroy_instance`).
#[no_mangle]
pub extern "C" fn oakengine_render_manager_shutdown() -> c_int {
guard_void(|| unsafe {
r::oakrender_manager_shutdown();
});
crate::error::OAKENGINE_OK
}
/// `oakengine_render_manager_set_aggressive_garbage_collection`.
#[no_mangle]
pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
aggressive: c_int,
) -> c_int {
guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) }))
}
/// `oakengine_render_manager_requested_backend` — the backend the manager
/// was asked for (0 = k_open_gl, 1 = k_metal, 2 = k_vulkan, 3 = k_cpu;
/// -1 when the manager is down).
#[no_mangle]
pub extern "C" fn oakengine_render_manager_requested_backend() -> c_int {
guard_int(|| {
match oakrender::manager::RenderManager::global() {
Some(m) => Ok(match m.requested_backend {
oakrender::backend::BackendKind::Auto => 0,
oakrender::backend::BackendKind::Metal => 1,
oakrender::backend::BackendKind::Vulkan => 2,
oakrender::backend::BackendKind::Gl => 0,
oakrender::backend::BackendKind::Cpu => 3,
}),
None => Ok(-1),
}
})
}
/// `oakengine_render_manager_backend_to_string` — the config name of the
/// backend ordinal (E_INVALID out of range).
#[no_mangle]
pub unsafe extern "C" fn oakengine_render_manager_backend_to_string(
backend: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| {
let kind = match backend {
0 => oakrender::backend::BackendKind::Gl,
1 => oakrender::backend::BackendKind::Metal,
2 => oakrender::backend::BackendKind::Vulkan,
3 => oakrender::backend::BackendKind::Cpu,
_ => return Err(Error::Invalid),
};
Ok(unsafe {
crate::handle::write_string(kind.to_config_string(), buf, buf_size)
})
})
}
/// `oakengine_render_cache_set_display_color_processor` — NULL clears.
#[no_mangle]
pub unsafe extern "C" fn oakengine_render_cache_set_display_color_processor(
processor: *mut c_void,
) -> c_int {
guard(|| unsafe {
let proc = if processor.is_null() {
CHandle::null()
} else {
unbox(processor.cast::<OakEngineColorProcessor>())?
};
Error::from_module(r::oakrender_set_display_color_processor(proc))
})
}
/// `oakengine_render_cache_set_multicam_node` — NULL clears.
#[no_mangle]
pub unsafe extern "C" fn oakengine_render_cache_set_multicam_node(
node: *mut OakEngineNode,
) -> c_int {
guard(|| unsafe {
let n = if node.is_null() {
CHandle::null()
} else {
unbox(node)?
};
Error::from_module(r::oakrender_set_cacher_multicam(n))
})
}
// ---------------------------------------------------------------------------
// Renderer
// ---------------------------------------------------------------------------
/// Facade-side renderer box: the bound output node + output geometry.
struct RendererBox {
/// Unboxed output node handle (borrowed): a sequence, or any node the
/// module can evaluate (footage, generator, ...).
output_node: CHandle,
/// 1 when `output_node` is a sequence (its montage is built at render
/// time); 0 when it is a single node.
is_sequence: bool,
/// Footage spec resolved at create time (M12 P0): the filename +
/// video stream when `output_node` is a footage node. The
/// single-footage render path feeds these to the render ticket.
footage: Option<FootageSpec>,
/// Output width.
width: c_int,
/// Output height.
height: c_int,
/// Output pixel format (`PixelFormat::Format`).
pixel_format: c_int,
/// Frame-rate numerator.
frame_rate_num: c_int,
/// Frame-rate denominator.
frame_rate_den: c_int,
/// Render mode (0 offline / 1 online).
mode: c_int,
/// Last failure reason.
last_error: String,
}
/// Resolved single-footage spec (filename + media stream).
struct FootageSpec {
/// Footage file path.
filename: String,
/// Media stream index (0 = first video stream).
stream_index: c_int,
}
/// Borrow the renderer box (NULL → Invalid).
unsafe fn renderer(ptr: *const OakEngineRenderer) -> Result<&'static RendererBox> {
unsafe {
if ptr.is_null() {
return Err(Error::Invalid);
}
Ok(&*(ptr as *const RendererBox))
}
}
/// Borrow the renderer box mutably.
unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut RendererBox> {
unsafe {
if ptr.is_null() {
return Err(Error::Invalid);
}
Ok(&mut *(ptr as *mut RendererBox))
}
}
/// Build an oakcommon video-params handle for the renderer's geometry.
unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
unsafe {
let params = crate::stubs::common::oakcommon_videoparams_init();
if params.is_null() {
return Err(Error::Failed("video params allocation failed".into()));
}
let mut rc = crate::stubs::common::oakcommon_videoparams_set_width(params, b.width);
if rc == 0 {
rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height);
}
if rc == 0 {
rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format);
}
if rc == 0 {
rc = crate::stubs::common::oakcommon_videoparams_set_time_base(
params,
b.frame_rate_den,
b.frame_rate_num,
);
}
if rc != 0 {
let mut p = params;
crate::stubs::common::oakcommon_videoparams_free(&mut p);
return Err(Error::Failed("video params setup failed".into()));
}
Ok(params)
}
}
/// Build a renderer box for `output_node` (a sequence or any node the
/// module can evaluate) with the given geometry. Returns NULL for
/// non-positive geometry/rate or a pixel format outside the oakcore enum.
unsafe fn make_renderer_box(
output_node: CHandle,
is_sequence: bool,
width: c_int,
height: c_int,
pixel_format: c_int,
frame_rate_num: c_int,
frame_rate_den: c_int,
output_colorspace: *const c_char,
) -> Result<*mut OakEngineRenderer> {
unsafe {
if width <= 0 || height <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0 {
return Ok(std::ptr::null_mut());
}
// Validate the pixel format against the oakcore enum. The
// oakcommon format_name lookup succeeds for ANY code (unknowns
// format as "Unknown (0x…)"), so only the real formats (U8..F32)
// are accepted; Invalid (-1), the Count sentinel (5) and garbage
// codes are rejected.
if pixel_format < oakcore_rs::PixelFormat::U8 as c_int
|| pixel_format > oakcore_rs::PixelFormat::F32 as c_int
{
return Ok(std::ptr::null_mut());
}
let _ = crate::handle::read_cstr(output_colorspace); // resolved by the module at render time
// M12 P0: resolve the footage spec when the bound node is a
// footage node (single-node renderers only).
let footage = if !is_sequence {
resolve_footage(output_node).ok()
} else {
None
};
let boxed = Box::new(RendererBox {
output_node,
is_sequence,
footage,
width,
height,
pixel_format,
frame_rate_num,
frame_rate_den,
mode: 0,
last_error: String::new(),
});
Ok(Box::into_raw(boxed) as *mut OakEngineRenderer)
}
}
/// Resolve `node` as a footage node: `(filename, stream_index)` when it
/// carries footage, an error otherwise.
unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
unsafe {
let mut buf = [0 as c_char; 4096];
let rc = crate::stubs::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
if rc < 0 {
return Err(Error::Failed("node is not footage".into()));
}
let filename = crate::handle::read_cstr(buf.as_ptr());
Ok(FootageSpec {
filename,
stream_index: 0,
})
}
}
/// Resolve a clip's media: `(filename, stream_index)` via the clip's
/// graph node → its upstream footage.
unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> {
unsafe {
let node = crate::stubs::node::oaknode_block_as_node(clip);
if node.is_null() {
return None;
}
let mut footage = CHandle::null();
if crate::stubs::node::oaknode_node_find_input_footage(node, &mut footage) != 0
|| footage.is_null()
{
return None;
}
let mut buf = [0 as c_char; 4096];
if crate::stubs::node::oaknode_footage_filename(
footage,
buf.as_mut_ptr(),
buf.len() as c_int,
) < 0
{
return None;
}
let filename = crate::handle::read_cstr(buf.as_ptr());
Some((filename, 0))
}
}
/// Build the audio montage for `b` over `range` (M12 P1): every audio
/// clip overlapping the range, media times resolved from the clip
/// ranges, audio stream index 1. Returns the POD array plus the filename
/// CStrings that must outlive the render call.
unsafe fn build_audio_montage(
b: &RendererBox,
range: oakcore_rs::TimeRange,
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
use crate::stubs::node as n;
unsafe {
let mut pods = Vec::new();
let mut names = Vec::new();
let seq = b.output_node;
let mut audio: c_int = 0;
let _ = n::oaknode_sequence_get_track_count(seq, 1, &mut audio);
let is_clip = |block: CHandle| {
let mut kind: c_int = 0;
n::oaknode_block_get_kind(block, &mut kind) == 0 && kind == 1
};
for track_index in 0..audio {
let mut track = CHandle::null();
if n::oaknode_sequence_get_track_at(seq, 1, track_index, &mut track) != 0
|| track.is_null()
{
continue;
}
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(track, &mut block_count) != 0 {
continue;
}
for block_index in 0..block_count {
let mut block = CHandle::null();
if n::oaknode_track_get_block_at(track, block_index, &mut block) != 0
|| block.is_null()
|| !is_clip(block)
{
continue;
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let mut mi_num: c_int = 0;
let mut mi_den: c_int = 0;
if n::oaknode_block_get_in(block, &mut in_num, &mut in_den) != 0
|| n::oaknode_block_get_out(block, &mut out_num, &mut out_den) != 0
|| n::oaknode_clip_get_media_in(block, &mut mi_num, &mut mi_den) != 0
{
continue;
}
let in_time = oakcore_rs::Rational::new(in_num as i64, in_den as i64);
let out_time = oakcore_rs::Rational::new(out_num as i64, out_den as i64);
if out_time <= range.in_() || in_time >= range.out() {
continue;
}
let Some((filename, _)) = clip_media(block) else {
continue;
};
let media_in = oakcore_rs::Rational::new(mi_num as i64, mi_den as i64);
let name = std::ffi::CString::new(filename).unwrap_or_default();
let ptr = name.as_ptr();
names.push(name);
pods.push(MontagePod {
filename: ptr,
stream_index: 1, // audio stream
in_num: in_time.numerator(),
in_den: in_time.denominator(),
out_num: out_time.numerator(),
out_den: out_time.denominator(),
media_in_num: media_in.numerator(),
media_in_den: media_in.denominator(),
gain: 1.0,
});
}
}
(pods, names)
}
}
/// The `OakMontageClip` POD the render module's ticket reads.
type MontagePod = crate::pods::MontagePod;
/// Build the video montage for `b` at sequence time `time` (rational):
/// every clip covering `time` on video tracks, ordered bottom-to-top
/// (track index 0 is topmost → pushed last), media times resolved from
/// the clip ranges. Returns the POD array plus the filename CStrings
/// that must outlive the render call.
unsafe fn build_video_montage(
b: &RendererBox,
time: oakcore_rs::Rational,
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
use crate::stubs::node as n;
unsafe {
let mut pods = Vec::new();
let mut names = Vec::new();
let seq = b.output_node;
let mut video: c_int = 0;
let _ = n::oaknode_sequence_get_track_count(seq, 0, &mut video);
// Clip-blocks only (gaps skipped), track index 0 = topmost.
let is_clip = |block: CHandle| {
let mut kind: c_int = 0;
n::oaknode_block_get_kind(block, &mut kind) == 0 && kind == 1
};
for track_index in (0..video).rev() {
let mut track = CHandle::null();
if n::oaknode_sequence_get_track_at(seq, 0, track_index, &mut track) != 0
|| track.is_null()
{
continue;
}
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(track, &mut block_count) != 0 {
continue;
}
for block_index in 0..block_count {
let mut block = CHandle::null();
if n::oaknode_track_get_block_at(track, block_index, &mut block) != 0
|| block.is_null()
|| !is_clip(block)
{
continue;
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let mut mi_num: c_int = 0;
let mut mi_den: c_int = 0;
if n::oaknode_block_get_in(block, &mut in_num, &mut in_den) != 0
|| n::oaknode_block_get_out(block, &mut out_num, &mut out_den) != 0
|| n::oaknode_clip_get_media_in(block, &mut mi_num, &mut mi_den) != 0
{
continue;
}
let in_time = oakcore_rs::Rational::new(in_num as i64, in_den as i64);
let out_time = oakcore_rs::Rational::new(out_num as i64, out_den as i64);
if time < in_time || time >= out_time {
continue;
}
let Some((filename, stream_index)) = clip_media(block) else {
continue;
};
let media_in = oakcore_rs::Rational::new(mi_num as i64, mi_den as i64);
let name = std::ffi::CString::new(filename).unwrap_or_default();
let ptr = name.as_ptr();
names.push(name);
pods.push(MontagePod {
filename: ptr,
stream_index,
in_num: in_time.numerator(),
in_den: in_time.denominator(),
out_num: out_time.numerator(),
out_den: out_time.denominator(),
media_in_num: media_in.numerator(),
media_in_den: media_in.denominator(),
gain: 1.0,
});
}
}
(pods, names)
}
}
/// `oakengine_renderer_create` — NULL for invalid arguments.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_create(
seq: *mut OakEngineSequence,
width: c_int,
height: c_int,
pixel_format: c_int,
frame_rate_num: c_int,
frame_rate_den: c_int,
output_colorspace: *const c_char,
) -> *mut OakEngineRenderer {
guard_ptr(|| unsafe {
if seq.is_null() {
return Ok(std::ptr::null_mut());
}
let seq_handle = unbox(seq)?;
make_renderer_box(
seq_handle,
true,
width,
height,
pixel_format,
frame_rate_num,
frame_rate_den,
output_colorspace,
)
})
}
/// `oakengine_renderer_create_for_node` — like `oakengine_renderer_create`,
/// but binds any node instead of a sequence: the surface for rendering a
/// single footage/generator node (the source monitor). The renderer is
/// freed with `oakengine_renderer_free` and renders with
/// `oakengine_renderer_render_frame`, exactly like the sequence renderer.
/// NULL for invalid arguments.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_create_for_node(
node: *mut OakEngineNode,
width: c_int,
height: c_int,
pixel_format: c_int,
frame_rate_num: c_int,
frame_rate_den: c_int,
output_colorspace: *const c_char,
) -> *mut OakEngineRenderer {
guard_ptr(|| unsafe {
if node.is_null() {
return Ok(std::ptr::null_mut());
}
let node_handle = unbox(node)?;
make_renderer_box(
node_handle,
false,
width,
height,
pixel_format,
frame_rate_num,
frame_rate_den,
output_colorspace,
)
})
}
/// `oakengine_renderer_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_free(self_: *mut OakEngineRenderer) {
guard_void(|| unsafe {
if self_.is_null() {
return;
}
drop(Box::from_raw(self_ as *mut RendererBox));
})
}
/// `oakengine_renderer_set_mode` — 0/1 only.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_set_mode(
self_: *mut OakEngineRenderer,
mode: c_int,
) -> c_int {
guard(|| unsafe {
let b = renderer_mut(self_)?;
if mode != 0 && mode != 1 {
return Err(Error::Invalid);
}
b.mode = mode;
Ok(())
})
}
/// `oakengine_renderer_last_error` (buf/size).
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_last_error(
self_: *const OakEngineRenderer,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
let b = renderer(self_)?;
Ok(crate::handle::write_string(&b.last_error, buf, buf_size))
})
}
/// `oakengine_renderer_render_frame` — synchronous frame render.
///
/// M12 P0: a sequence renderer resolves the timeline into a montage
/// (video clips covering the timestamp, composited track 0 on top); a
/// footage renderer decodes its bound footage. Both run inside the
/// render ticket's producer.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_render_frame(
self_: *mut OakEngineRenderer,
timestamp: i64,
) -> *mut OakEngineFrame {
guard_ptr(|| unsafe {
let b = renderer_mut(self_)?;
let video_params = make_video_params(b)?;
let time =
oakcore_rs::Rational::new(timestamp * i64::from(b.frame_rate_den), i64::from(b.frame_rate_num));
let mut params = OakVideoTicketParams {
output_node: b.output_node,
video_params,
audio_params: std::ptr::null(),
time_num: time.numerator(),
time_den: time.denominator(),
color_manager: CHandle::null(),
mode: b.mode,
force_width: b.width,
force_height: b.height,
force_matrix: [0.0; 16],
has_force_matrix: 0,
force_format: -1,
force_channel_count: 0,
force_color_output: CHandle::null(),
force_color_transform: CHandle::null(),
cache: CHandle::null(),
footage_filename: std::ptr::null(),
footage_stream: 0,
montage: std::ptr::null(),
montage_count: 0,
};
// M12 P0: sequence renderers resolve the montage; footage
// renderers pass the resolved spec. Both stay alive until the
// synchronous ticket call copies them.
let mut keep_alive: Vec<std::ffi::CString> = Vec::new();
let mut montage: Vec<MontagePod> = Vec::new();
if b.is_sequence {
(montage, keep_alive) = build_video_montage(b, time);
if !montage.is_empty() {
params.montage = montage.as_ptr();
params.montage_count = montage.len() as c_int;
}
} else if let Some(footage) = &b.footage {
let name = std::ffi::CString::new(footage.filename.clone()).unwrap_or_default();
params.footage_filename = name.as_ptr();
params.footage_stream = footage.stream_index;
keep_alive.push(name);
}
let ticket = r::oakrender_ticket_render_frame(&params, None, std::ptr::null_mut());
let _ = (&keep_alive, &montage);
let mut vp = video_params;
crate::stubs::common::oakcommon_videoparams_free(&mut vp);
if ticket.is_null() {
b.last_error = "render ticket submission failed".into();
return Ok(std::ptr::null_mut());
}
let wait_rc = r::oakrender_ticket_wait(ticket);
let mut frame = CHandle::null();
let get_rc = r::oakrender_ticket_get_frame(ticket, &mut frame);
let mut t = ticket;
r::oakrender_ticket_free(&mut t);
if wait_rc != 0 || get_rc != 0 || frame.is_null() {
b.last_error = "render failed or timed out".into();
return Ok(std::ptr::null_mut());
}
b.last_error.clear();
Ok(box_handle::<OakEngineFrame>(frame))
})
}
/// `oakengine_renderer_render_audio` — synchronous audio render. The
/// oakrender crate's samples path is unimplemented, so this submits the
/// ticket and reports the failure reason.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_render_audio(
self_: *mut OakEngineRenderer,
start_timestamp: i64,
length_timestamp: i64,
) -> *mut OakEngineAudioBuffer {
guard_ptr(|| unsafe {
let b = renderer_mut(self_)?;
let start_num = start_timestamp * i64::from(b.frame_rate_den);
let end_num = (start_timestamp + length_timestamp) * i64::from(b.frame_rate_den);
let den = i64::from(b.frame_rate_num);
let range = oakcore_rs::TimeRange::new(
oakcore_rs::Rational::new(start_num, den),
oakcore_rs::Rational::new(end_num, den),
);
// M12 P1: resolve the audio montage (audio-track clips covering
// the range) and the output format.
let (pods, names) = build_audio_montage(b, range);
let mut keep: Vec<std::ffi::CString> = names;
let params = crate::stubs::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
crate::stubs::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
let ticket = r::oakrender_ticket_render_audio(
b.output_node,
start_num,
den,
end_num,
den,
params as *const c_void,
b.mode,
None,
std::ptr::null_mut(),
if pods.is_empty() {
std::ptr::null()
} else {
pods.as_ptr()
},
pods.len() as c_int,
);
crate::stubs::audio::oakcore_audioparams_free(params);
if ticket.is_null() {
b.last_error = "audio render ticket submission failed".into();
return Ok(std::ptr::null_mut());
}
let wait_rc = r::oakrender_ticket_wait(ticket);
let mut samples: *mut c_void = std::ptr::null_mut();
let get_rc = r::oakrender_ticket_get_samples(ticket, &mut samples);
let mut t = ticket;
r::oakrender_ticket_free(&mut t);
let _ = &keep;
if wait_rc != 0 || get_rc != 0 || samples.is_null() {
b.last_error = "audio render failed".into();
return Ok(std::ptr::null_mut());
}
let raw = samples as *const crate::pods::OakAudioSamplesOut;
let boxed = AudioSamplesBox {
data: (*raw).data.clone(),
frame_count: (*raw).frame_count as i64,
sample_rate: (*raw).sample_rate,
channel_layout: (*raw).channel_layout,
channel_count: (*raw).channel_count,
};
r::oakrender_audio_samples_free(samples);
b.last_error.clear();
Ok(Box::into_raw(Box::new(boxed)) as *mut OakEngineAudioBuffer)
})
}
/// `oakengine_renderer_cancel` — cancel the in-flight render call.
#[no_mangle]
pub unsafe extern "C" fn oakengine_renderer_cancel(self_: *mut OakEngineRenderer) {
guard_void(|| unsafe {
if let Ok(b) = renderer_mut(self_) {
let _ = b; // the crate tracks in-flight tickets internally
}
})
}
// ---------------------------------------------------------------------------
// OakEngineFrame accessors (wraps the module's OakCodecFrame)
// ---------------------------------------------------------------------------
/// Borrow the frame's module handle; `None` for NULL/empty (the engine
/// contract: NULL is a no-op yielding zero results).
unsafe fn frame_handle(ptr: *const OakEngineFrame) -> Option<CHandle> {
unsafe {
if ptr.is_null() {
return None;
}
let h = (*ptr).handle();
if h.is_null() {
None
} else {
Some(h)
}
}
}
/// `oakengine_frame_width`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int {
guard_int(|| unsafe {
Ok(match frame_handle(self_) {
Some(f) => r::oakrender_codec_frame_width(f),
None => 0,
})
})
}
/// `oakengine_frame_height`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int {
guard_int(|| unsafe {
Ok(match frame_handle(self_) {
Some(f) => r::oakrender_codec_frame_height(f),
None => 0,
})
})
}
/// `oakengine_frame_format` — the frame's params POD format
/// (`PixelFormat::Format`).
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int {
guard_int(|| unsafe {
let Some(f) = frame_handle(self_) else {
return Ok(0);
};
let mut params = OakRenderVideoParams {
width: 0,
height: 0,
time_base_num: 0,
time_base_den: 0,
format: 0,
pixel_aspect_num: 0,
pixel_aspect_den: 0,
interlacing: 0,
color_range: 0,
divider: 0,
video_type: 0,
premultiplied_alpha: 0,
};
Error::from_module(r::oakrender_codec_frame_get_params(f, &mut params))?;
Ok(params.format)
})
}
/// `oakengine_frame_channel_count` — **not backed** (the oakrender crate
/// exposes no frame channel count). Returns 0.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int {
let _ = self_;
0
}
/// `oakengine_frame_linesize_bytes`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int {
guard_int(|| unsafe {
Ok(match frame_handle(self_) {
Some(f) => r::oakrender_codec_frame_linesize_bytes(f),
None => 0,
})
})
}
/// `oakengine_frame_data` — borrowed pixel data.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void {
guard_ptr(|| unsafe {
Ok(match frame_handle(self_) {
Some(f) => r::oakrender_codec_frame_const_data(f) as *mut c_void,
None => std::ptr::null_mut(),
})
})
}
/// `oakengine_frame_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_frame_free(self_: *mut OakEngineFrame) {
guard_void(|| unsafe {
if self_.is_null() {
return;
}
let handle = (*self_).handle();
let mut h = handle;
r::oakrender_codec_frame_free(&mut h);
drop(Box::from_raw(self_));
})
}
// ---------------------------------------------------------------------------
// OakEngineAudioBuffer accessors (M12 P1: rendered audio samples)
// ---------------------------------------------------------------------------
/// The buffer box: the rendered interleaved f32 samples plus format.
struct AudioSamplesBox {
/// Interleaved samples.
data: Box<[f32]>,
/// Frame count.
frame_count: i64,
/// Sample rate (Hz).
sample_rate: c_int,
/// Channel layout mask.
channel_layout: u64,
/// Channel count.
channel_count: c_int,
}
/// `oakengine_audio_sample_rate`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_sample_rate(self_: *const OakEngineAudioBuffer) -> c_int {
unsafe {
if self_.is_null() {
return 0;
}
let b = self_ as *const AudioSamplesBox;
(*b).sample_rate
}
}
/// `oakengine_audio_channel_count`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_channel_count(
self_: *const OakEngineAudioBuffer,
) -> c_int {
unsafe {
if self_.is_null() {
return 0;
}
let b = self_ as *const AudioSamplesBox;
(*b).channel_count
}
}
/// `oakengine_audio_sample_count` — interleaved frame count.
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_sample_count(self_: *const OakEngineAudioBuffer) -> i64 {
unsafe {
if self_.is_null() {
return 0;
}
let b = self_ as *const AudioSamplesBox;
(*b).frame_count
}
}
/// `oakengine_audio_data` — pointer to the interleaved samples (channel
/// index ignored: interleaved layout).
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_data(
self_: *const OakEngineAudioBuffer,
_channel: c_int,
) -> *const f32 {
unsafe {
if self_.is_null() {
return std::ptr::null();
}
let b = &*(self_ as *const AudioSamplesBox);
b.data.as_ptr()
}
}
/// `oakengine_audio_free` — release the buffer box.
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer) {
unsafe {
if self_.is_null() {
return;
}
drop(Box::from_raw(self_ as *mut AudioSamplesBox));
}
}
// ---------------------------------------------------------------------------
// Color management
// ---------------------------------------------------------------------------
// Thread-local reason of the last failed color call.
thread_local! {
static LAST_COLOR_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
}
/// `oakengine_color_last_error` (buf/size).
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
crate::handle::guard_int(|| {
Ok(LAST_COLOR_ERROR.with(|e| {
// SAFETY: `buf` is the caller's buf/size buffer.
unsafe { crate::handle::write_string(&e.borrow(), buf, buf_size) }
}))
})
}
/// `oakengine_color_manager_from_project` — **not backed** (needs the
/// deferred oaknode project family). Returns NULL.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_from_project(
_project: *mut crate::handle::OakEngineProject,
) -> *mut crate::handle::OakEngineColorManager {
std::ptr::null_mut()
}
/// `oakengine_color_manager_get_config_filename` (buf/size).
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_get_config_filename(
_mgr: *const crate::handle::OakEngineColorManager,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
let rc = r::oakrender_color_manager_get_config(buf, buf_size);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(string_result(rc))
}
})
}
/// `oakengine_color_manager_set_config_filename` — **not backed** (the
/// crate only reads the config). Returns OAKENGINE_E_FAILED.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_set_config_filename(
_mgr: *mut crate::handle::OakEngineColorManager,
_filename: *const c_char,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
macro_rules! color_manager_stub {
($($name:ident),* $(,)?) => {
$(
/// Documented stub: the oakrender crate does not implement the
/// color-manager list queries (see `deferred.rs`).
#[no_mangle]
pub unsafe extern "C" fn $name() -> c_int {
crate::error::OAKENGINE_E_FAILED
}
)*
};
}
color_manager_stub! {
oakengine_color_manager_colorspace_count,
oakengine_color_manager_display_count,
oakengine_color_manager_look_count,
}
macro_rules! color_manager_stub_arg {
($($name:ident),* $(,)?) => {
$(
/// Documented stub: the oakrender crate does not implement the
/// color-manager list queries (see `deferred.rs`).
#[no_mangle]
pub unsafe extern "C" fn $name(
_mgr: *const crate::handle::OakEngineColorManager,
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
)*
};
}
color_manager_stub_arg! {
oakengine_color_manager_colorspace_at,
oakengine_color_manager_display_at,
}
/// `oakengine_color_manager_view_count` — **not backed**. -1.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_view_count(
_mgr: *const crate::handle::OakEngineColorManager,
_display: *const c_char,
) -> c_int {
-1
}
/// `oakengine_color_manager_view_at` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_view_at(
_mgr: *const crate::handle::OakEngineColorManager,
_display: *const c_char,
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_look_at` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_look_at(
_mgr: *const crate::handle::OakEngineColorManager,
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_default_display` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_default_display(
_mgr: *const crate::handle::OakEngineColorManager,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_default_view` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_default_view(
_mgr: *const crate::handle::OakEngineColorManager,
_display: *const c_char,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_default_input_color_space` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_default_input_color_space(
_mgr: *const crate::handle::OakEngineColorManager,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_set_default_input_color_space` — **not
/// backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_set_default_input_color_space(
_mgr: *mut crate::handle::OakEngineColorManager,
_colorspace: *const c_char,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_reference_color_space` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_reference_color_space(
_mgr: *const crate::handle::OakEngineColorManager,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_default_luma_coefs` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_default_luma_coefs(
_mgr: *const crate::handle::OakEngineColorManager,
_rgb: *mut c_double,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_compliant_color_space` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_compliant_color_space(
_mgr: *const crate::handle::OakEngineColorManager,
_name: *const c_char,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_manager_compliant_transform` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_manager_compliant_transform(
_mgr: *const crate::handle::OakEngineColorManager,
_in: *const OakColorTransformPod,
_force_display: c_int,
_out_is_display: *mut c_int,
_out_output: *mut c_char,
_output_size: c_int,
_out_view: *mut c_char,
_view_size: c_int,
_out_look: *mut c_char,
_look_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
// ---------------------------------------------------------------------------
// Color config handle (not backed)
// ---------------------------------------------------------------------------
/// `oakengine_color_config_load_default` — **not backed**. NULL.
#[no_mangle]
pub extern "C" fn oakengine_color_config_load_default() -> *mut crate::handle::OakEngineColorConfig
{
std::ptr::null_mut()
}
/// `oakengine_color_config_load_file` — **not backed**. NULL.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_config_load_file(
_filename: *const c_char,
) -> *mut crate::handle::OakEngineColorConfig {
std::ptr::null_mut()
}
/// `oakengine_color_config_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_config_free(
_config: *mut crate::handle::OakEngineColorConfig,
) {
}
/// `oakengine_color_config_colorspace_count` — **not backed**. 0.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_config_colorspace_count(
_config: *const crate::handle::OakEngineColorConfig,
) -> c_int {
0
}
/// `oakengine_color_config_colorspace_at` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_config_colorspace_at(
_config: *const crate::handle::OakEngineColorConfig,
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
// ---------------------------------------------------------------------------
// Color processor
// ---------------------------------------------------------------------------
/// `engine/include/oakengine/color.h` — `oak_color_transform` POD mirror.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakColorTransformPod {
/// 0: `output` is a colorspace; 1: display/view/look.
pub is_display: c_int,
/// Colorspace name, or display device when is_display.
pub output: *const c_char,
/// Display view (is_display only).
pub view: *const c_char,
/// Display look (is_display only).
pub look: *const c_char,
}
/// `oakengine_color_processor_create` — convert the `oak_color_transform`
/// POD into an oakcommon colortransform handle and hand it to
/// `oakrender_color_processor_create_transform`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_processor_create(
mgr: *const crate::handle::OakEngineColorManager,
input: *const c_char,
dest: *const OakColorTransformPod,
direction: c_int,
) -> *mut OakEngineColorProcessor {
guard_ptr(|| unsafe {
if input.is_null() || dest.is_null() {
return Ok(std::ptr::null_mut());
}
let empty = crate::common::empty_cstr();
let ct = if (*dest).is_display != 0 {
crate::stubs::common::oakcommon_colortransform_init_display(
if (*dest).output.is_null() {
empty
} else {
(*dest).output
},
if (*dest).view.is_null() {
empty
} else {
(*dest).view
},
if (*dest).look.is_null() {
empty
} else {
(*dest).look
},
)
} else {
crate::stubs::common::oakcommon_colortransform_init_output(
if (*dest).output.is_null() {
empty
} else {
(*dest).output
},
)
};
if ct.is_null() {
LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "invalid color transform".into());
return Ok(std::ptr::null_mut());
}
let mgr_handle = if mgr.is_null() {
CHandle::null()
} else {
unbox(mgr.cast::<crate::handle::OakEngineColorManager>())?
};
let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction);
let mut ct_handle = ct;
crate::stubs::common::oakcommon_colortransform_free(&mut ct_handle);
if proc.is_null() {
LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "could not create color processor".into());
return Ok(std::ptr::null_mut());
}
LAST_COLOR_ERROR.with(|e| e.borrow_mut().clear());
Ok(box_handle::<OakEngineColorProcessor>(proc))
})
}
/// `oakengine_color_processor_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_processor_free(proc: *mut OakEngineColorProcessor) {
guard_void(|| unsafe {
free_box(proc);
})
}
/// `oakengine_color_processor_is_valid` (1/0).
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_processor_is_valid(
proc: *const OakEngineColorProcessor,
) -> c_int {
guard_int(|| unsafe {
if proc.is_null() {
return Ok(0);
}
let p = unbox(proc)?;
Ok(r::oakrender_color_processor_is_valid(p))
})
}
/// `oakengine_color_processor_convert_color` — single RGBA color.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_processor_convert_color(
proc: *const OakEngineColorProcessor,
in_rgba: *const c_double,
out_rgba: *mut c_double,
) -> c_int {
guard(|| unsafe {
if in_rgba.is_null() || out_rgba.is_null() {
return Err(Error::Invalid);
}
let p = unbox(proc)?;
let rc = r::oakrender_color_processor_convert(
p,
*in_rgba,
*in_rgba.add(1),
*in_rgba.add(2),
*in_rgba.add(3),
out_rgba,
out_rgba.add(1),
out_rgba.add(2),
out_rgba.add(3),
);
Error::from_module(rc)
})
}
/// `oakengine_color_processor_id` — **not backed** (the crate exposes no
/// processor cache id). Returns OAKENGINE_E_FAILED.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_processor_id(
_proc: *const OakEngineColorProcessor,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_color_transform_job_set_processor` — **not backed** (the
/// job is a C++ type). Returns OAKENGINE_E_INVALID for a NULL job.
#[no_mangle]
pub unsafe extern "C" fn oakengine_color_transform_job_set_processor(
job: *mut c_void,
_proc: *const OakEngineColorProcessor,
) -> c_int {
if job.is_null() {
crate::error::OAKENGINE_E_INVALID
} else {
crate::error::OAKENGINE_E_FAILED
}
}
// ---------------------------------------------------------------------------
// LUT library (not backed)
// ---------------------------------------------------------------------------
/// `oakengine_lut_directory_count` — **not backed** (the LUT directory/
/// file library is facade-level over FileFunctions; the crate only
/// enumerates supported extensions). Returns 0.
#[no_mangle]
pub extern "C" fn oakengine_lut_directory_count() -> c_int {
0
}
/// `oakengine_lut_directory_at` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_lut_directory_at(
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_lut_file_count` — **not backed**. Returns 0.
#[no_mangle]
pub extern "C" fn oakengine_lut_file_count() -> c_int {
0
}
/// `oakengine_lut_file_at` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_lut_file_at(
_index: c_int,
_buf: *mut c_char,
_buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_lut_set_directories` — **not backed**.
#[no_mangle]
pub unsafe extern "C" fn oakengine_lut_set_directories(
_dirs: *const *const c_char,
_count: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
}