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).
This commit is contained in:
2026-08-16 00:33:45 +08:00
parent 2248be8567
commit ab1a2e9c7b
293 changed files with 27826 additions and 90128 deletions
+46 -1
View File
@@ -25,7 +25,7 @@
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::bridge::audio as a;
use crate::stubs::audio as a;
use crate::error::Error;
use crate::handle::{
box_handle, free_box, guard, guard_i64, guard_int, guard_void, unbox, CHandle,
@@ -445,6 +445,51 @@ pub unsafe extern "C" fn oakengine_audio_sync_place_by_waveform_offset(
})
}
// ---------------------------------------------------------------------------
// Waveform extraction
// ---------------------------------------------------------------------------
/// `oakengine_waveform_extract` — two-stage whole-file min/max waveform
/// extraction of `filename`'s audio stream (the module's
/// `oakaudio_waveform_extract`, M12 P4).
///
/// First call with `out_pairs == NULL` / `capacity_points == 0` returns the
/// required point count without writing; the channel count is reported
/// whenever `out_channel_count` is non-NULL. The data pass writes
/// `point_count * channel_count` channel-interleaved pairs (the module's
/// `oakaudio_min_max` POD; `capacity_points` counts points) and returns the
/// point count. Returns a negative facade `OAKENGINE_E_INVALID` for NULL
/// `filename` / negative `stream_index` / non-positive `samples_per_point`
/// / negative `capacity_points`; module decode errors (`OAKAUDIO_E_NOT_FOUND`,
/// ...) pass through untranslated.
#[no_mangle]
pub unsafe extern "C" fn oakengine_waveform_extract(
filename: *const c_char,
stream_index: c_int,
samples_per_point: c_int,
out_pairs: *mut a::MinMax,
capacity_points: c_int,
out_channel_count: *mut c_int,
) -> c_int {
guard_int(|| unsafe {
if filename.is_null() || stream_index < 0 || samples_per_point <= 0 || capacity_points < 0 {
return Err(Error::Invalid);
}
let n = a::oakaudio_waveform_extract(
filename,
stream_index,
samples_per_point,
out_pairs,
capacity_points,
out_channel_count,
);
if n < 0 {
return Err(Error::Module(n));
}
Ok(n)
})
}
// ---------------------------------------------------------------------------
// Audio processor
// ---------------------------------------------------------------------------
-378
View File
@@ -1,378 +0,0 @@
// 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/>.
//! oakaudio C ABI bridge: direct Rust calls into the `oakaudio` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakaudio`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakaudio C ABI imports, mirroring the oakaudio crate's exports
//! (`src/audio/rust/src/ffi.rs`; headers `include/audio/*.h`), plus the
//! liboakcore `oakcore_audioparams_*` readers used to convert the
//! engine's borrowed `OakAudioParams*` handles.
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::handle::CHandle;
/// `oakaudio_offsetresult` C ABI POD. Single-lib unification: aliases
/// the oakaudio crate's struct (identical layout).
pub type OffsetResult = oakaudio::ffi::sync::OffsetResult;
/// `oakaudio_stretchoffsetresult` C ABI POD. Single-lib unification: aliases
/// the oakaudio crate's struct (identical layout).
pub type StretchOffsetResult = oakaudio::ffi::sync::StretchOffsetResult;
/// `oakaudio_sourceclip` C ABI POD. Single-lib unification: aliases
/// the oakaudio crate's struct (identical layout).
pub type SourceClip = oakaudio::ffi::sync::SourceClip;
/// `oakaudio` recording-params POD — single-lib unification: aliases the
/// oakaudio crate's type (itself the oakcodec `oakcodec_encoding_params`).
pub type EncodingParams = oakaudio::bridge::codec::EncodingParams;
extern "C" {
pub fn oakcore_audioparams_create(
sample_rate: c_int,
channel_layout: u64,
format: c_int,
) -> *mut c_void;
pub fn oakcore_audioparams_free(params: *mut c_void);
pub fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int;
/// `oakcore_audioparams_set_time_base` (host-provided).
pub fn oakcore_audioparams_set_time_base(params: *mut c_void, num: c_int, den: c_int);
pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64;
pub fn oakcore_audioparams_format(params: *const c_void) -> c_int;
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_create_instance() -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_create_instance() }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_destroy_instance() {
unsafe { oakaudio::ffi::manager::oakaudio_manager_destroy_instance() }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_instance() -> CHandle {
unsafe { oakaudio::ffi::manager::oakaudio_manager_instance() }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_free(_self: *mut CHandle) {
unsafe { oakaudio::ffi::manager::oakaudio_manager_free(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, bytes: i64) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_notify_interval(_self, bytes) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_push_to_output(
_self: CHandle,
rate: c_int,
layout: u64,
format: c_int,
samples: *const c_char,
samples_size: i64,
error_buf: *mut c_char,
error_buf_size: c_int,
) -> c_int {
unsafe {
oakaudio::ffi::manager::oakaudio_manager_push_to_output(
_self,
rate,
layout,
format,
samples,
samples_size,
error_buf,
error_buf_size,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_clear_buffered_output(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_output(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_reset_output_clock(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_output_device(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_set_output_device(_self: CHandle, device: c_int) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_device(_self, device) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_input_device(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_set_input_device(_self: CHandle, device: c_int) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_input_device(_self, device) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_hard_reset(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
/// `oakaudio_manager_start_recording` — crosses the encoding-params C ABI
/// POD (the facade keeps its own opaque mirror; the module's
/// `oakaudio::bridge::codec::EncodingParams`). Kept as a link-time
/// `extern "C"` declaration against the frozen module C ABI.
pub fn oakaudio_manager_start_recording(
_self: CHandle,
params: *const EncodingParams,
error_buf: *mut c_char,
error_buf_size: c_int,
) -> c_int {
unsafe {
oakaudio::ffi::manager::oakaudio_manager_start_recording(
_self,
params,
error_buf,
error_buf_size,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_recording(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_seconds(_self, out) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_manager_output_levels(_self: CHandle, peaks: *mut f32, capacity: c_int) -> c_int {
unsafe { oakaudio::ffi::manager::oakaudio_manager_output_levels(_self, peaks, capacity) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_processor_init() -> CHandle {
unsafe { oakaudio::ffi::processor::oakaudio_processor_init() }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_processor_free(_self: *mut CHandle) {
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_processor_open(
_self: CHandle,
in_rate: c_int,
in_layout: u64,
in_format: c_int,
out_rate: c_int,
out_layout: u64,
out_format: c_int,
speed: c_double,
) -> c_int {
unsafe {
oakaudio::ffi::processor::oakaudio_processor_open(
_self, in_rate, in_layout, in_format, out_rate, out_layout, out_format, speed,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_processor_close(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::processor::oakaudio_processor_close(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
unsafe { oakaudio::ffi::processor::oakaudio_processor_is_open(_self) }
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_sync_estimate_envelope_offset(
reference: *const c_double,
reference_len: c_int,
candidate: *const c_double,
candidate_len: c_int,
reference_valid: *const u8,
candidate_valid: *const u8,
window_samples: u64,
max_offset_windows: i64,
out: *mut OffsetResult,
) -> c_int {
unsafe {
oakaudio::ffi::sync::oakaudio_sync_estimate_envelope_offset(
reference,
reference_len,
candidate,
candidate_len,
reference_valid,
candidate_valid,
window_samples,
max_offset_windows,
out,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_sync_estimate_stretch_and_offset(
reference: *const c_double,
reference_len: c_int,
candidate: *const c_double,
candidate_len: c_int,
reference_valid: *const u8,
candidate_valid: *const u8,
window_samples: u64,
max_offset_windows: i64,
min_rate: c_double,
max_rate: c_double,
rate_step: c_double,
out: *mut StretchOffsetResult,
) -> c_int {
unsafe {
oakaudio::ffi::sync::oakaudio_sync_estimate_stretch_and_offset(
reference,
reference_len,
candidate,
candidate_len,
reference_valid,
candidate_valid,
window_samples,
max_offset_windows,
min_rate,
max_rate,
rate_step,
out,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_sync_place_by_source_time(
reference: *const SourceClip,
candidate: *const SourceClip,
reference_timeline_in_num: i64,
reference_timeline_in_den: i64,
out_num: *mut i64,
out_den: *mut i64,
out_valid: *mut c_int,
) -> c_int {
unsafe {
oakaudio::ffi::sync::oakaudio_sync_place_by_source_time(
reference,
candidate,
reference_timeline_in_num,
reference_timeline_in_den,
out_num,
out_den,
out_valid,
)
}
}
/// Direct call into the `oakaudio` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakaudio_sync_place_by_waveform_offset(
reference_timeline_in_num: i64,
reference_timeline_in_den: i64,
candidate_offset_samples: i64,
sample_rate: c_int,
out_num: *mut i64,
out_den: *mut i64,
out_valid: *mut c_int,
) -> c_int {
unsafe {
oakaudio::ffi::sync::oakaudio_sync_place_by_waveform_offset(
reference_timeline_in_num,
reference_timeline_in_den,
candidate_offset_samples,
sample_rate,
out_num,
out_den,
out_valid,
)
}
}
-242
View File
@@ -1,242 +0,0 @@
// 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/>.
//! oakcodec C ABI bridge: direct Rust calls into the `oakcodec` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakcodec`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakcodec C ABI imports, mirroring the oakcodec crate's exports
//! (`src/codec/rust/src/ffi/{format,encoder}.rs`; headers
//! `include/codec/{format,encoder}.h`). Also carries the
//! `oakcodec_encoding_params` POD the facade's encoding-params handle
//! wraps, and the oakaudio recording-params pointer pass-through.
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
/// `include/codec/encoder.h` — the encoding-params POD, a complete mirror
/// `olive::EncodingParams` — single-lib unification: aliases the oakcodec
/// crate's POD (`oakcodec_encoding_params`, identical `#[repr(C)]` layout;
/// the engine's `OakEngineEncodingParams` handle is a heap box over exactly
/// this struct, so every engine getter/setter reads/writes a field and
/// `encoder_init`/recording can consume the pointer directly).
pub type EncodingParamsPOD = oakcodec::ffi::encoder::oakcodec_encoding_params;
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
/// struct has no zeroed constructor; this facade helper provides it.
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
// All-C fields: all-zero is a valid value.
unsafe { std::mem::zeroed() }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_count() -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_name(format: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_name(format, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_extension(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_extension(format, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_count(format) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_at(format, index) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_count(format) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_at(format, index) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_count(format) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_at(format, index) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_codec_name(codec: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_name(codec, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_still_image(codec) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_lossless(codec) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_count(format, codec) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_pix_fmt_at(
format: c_int,
codec: c_int,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcodec::ffi::format::oakcodec_encoding_pix_fmt_at(format, codec, index, buf, buf_size)
}
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_index(codec, pix_fmt) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_count(format, codec) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_sample_format_at(format: c_int, codec: c_int, index: c_int) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_at(format, codec, index) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_filename_contains_digit_placeholder(filename: *const c_char) -> c_int {
unsafe {
oakcodec::ffi::format::oakcodec_encoding_filename_contains_digit_placeholder(filename)
}
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int {
unsafe { oakcodec::ffi::format::oakcodec_encoding_image_sequence_digit_count(filename) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_filename_remove_digit_placeholder(
filename: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcodec::ffi::format::oakcodec_encoding_filename_remove_digit_placeholder(
filename, buf, buf_size,
)
}
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoding_generate_matrix(
method: c_int,
src_width: c_int,
src_height: c_int,
dst_width: c_int,
dst_height: c_int,
out_matrix: *mut f64,
) -> c_int {
unsafe {
oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix(
method, src_width, src_height, dst_width, dst_height, out_matrix,
)
}
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
/// `oakcodec_encoder_init` — crosses the encoding-params C ABI POD
/// (`oakcodec_encoding_params`; the facade keeps its own POD mirror).
/// Kept as a link-time `extern "C"` declaration against the frozen module
/// C ABI.
pub fn oakcodec_encoder_init(params: *const EncodingParamsPOD) -> CHandle {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcodec_encoder_free(encoder: *mut CHandle) {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
}
-889
View File
@@ -1,889 +0,0 @@
// 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/>.
//! oakcommon C ABI bridge: direct Rust calls into the `oakcommon` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakcommon`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakcommon C ABI imports, mirroring the oakcommon crate's exports
//! (`src/common/rust/src/ffi.rs`; headers `include/common/*.h`). Only the
//! families the facade wraps: config, videoparams, colortransform, xml
//! reader/writer and the decibel helpers.
use std::ffi::{c_char, c_int, c_void};
use crate::handle::CHandle;
/// `include/common/config.h` — error handler callback.
pub type ConfigErrorHandler = Option<
unsafe extern "C" fn(title: *const c_char, message: *const c_char, userdata: *mut c_void),
>;
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_load() -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_load() }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_save() -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_save() }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_reset_defaults() -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_reset_defaults() }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set(group: *const c_char, key: *const c_char, value: *const c_char) {
unsafe { oakcommon::ffi::config::oakcommon_config_set(group, key, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_get(
group: *const c_char,
key: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_get_int(
group: *const c_char,
key: *const c_char,
fallback: c_int,
) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, fallback) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_get_int64(group: *const c_char, key: *const c_char, fallback: i64) -> i64 {
unsafe { oakcommon::ffi::config::oakcommon_config_get_int64(group, key, fallback) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_get_double(group: *const c_char, key: *const c_char, fallback: f64) -> f64 {
unsafe { oakcommon::ffi::config::oakcommon_config_get_double(group, key, fallback) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_get_bool(
group: *const c_char,
key: *const c_char,
fallback: c_int,
) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, fallback) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) {
unsafe { oakcommon::ffi::config::oakcommon_config_set_int(group, key, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set_int64(group: *const c_char, key: *const c_char, value: i64) {
unsafe { oakcommon::ffi::config::oakcommon_config_set_int64(group, key, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set_double(group: *const c_char, key: *const c_char, value: f64) {
unsafe { oakcommon::ffi::config::oakcommon_config_set_double(group, key, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) {
unsafe { oakcommon::ffi::config::oakcommon_config_set_bool(group, key, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_entry_type(group, key) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_config_set_error_handler(
handler: ConfigErrorHandler,
userdata: *mut c_void,
) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_set_error_handler(handler, userdata) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_init() -> CHandle {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_init_basic(
width: c_int,
height: c_int,
pixel_format: c_int,
nb_channels: c_int,
pixel_aspect_num: c_int,
pixel_aspect_den: c_int,
interlacing: c_int,
divider: c_int,
) -> CHandle {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(
width,
height,
pixel_format,
nb_channels,
pixel_aspect_num,
pixel_aspect_den,
interlacing,
divider,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_init_with_time_base(
width: c_int,
height: c_int,
time_base_num: c_int,
time_base_den: c_int,
pixel_format: c_int,
nb_channels: c_int,
pixel_aspect_num: c_int,
pixel_aspect_den: c_int,
interlacing: c_int,
divider: c_int,
) -> CHandle {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_init_with_time_base(
width,
height,
time_base_num,
time_base_den,
pixel_format,
nb_channels,
pixel_aspect_num,
pixel_aspect_den,
interlacing,
divider,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_free(params: *mut CHandle) {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_width(params: CHandle, width: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_width(params: CHandle, width: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_width(params, width) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_height(params: CHandle, height: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_height(params: CHandle, height: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_height(params, height) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_time_base(
params: CHandle,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_time_base(
params: CHandle,
numerator: c_int,
denominator: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_frame_rate(
params: CHandle,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_frame_rate(
params: CHandle,
numerator: c_int,
denominator: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_pixel_aspect_ratio(
params: CHandle,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_pixel_aspect_ratio(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(
params: CHandle,
numerator: c_int,
denominator: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_pixel_aspect_ratio(
params,
numerator,
denominator,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_interlacing(params: CHandle, interlacing: *mut c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_interlacing(params, interlacing)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_interlacing(params, interlacing)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider(params, divider) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_divider(params, divider) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, type_) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, type_) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_premultiplied_alpha(
params: CHandle,
premultiplied: *mut c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_premultiplied_alpha(
params,
premultiplied,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_premultiplied_alpha(
params: CHandle,
premultiplied: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_premultiplied_alpha(
params,
premultiplied,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_color_range(params: CHandle, color_range: *mut c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_color_range(params, color_range)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_color_range(params, color_range)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_is_valid(params: CHandle, valid: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, valid) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_effective_width(params: CHandle, width: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_width(params, width) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_effective_height(params: CHandle, height: *mut c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_height(params, height)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_bytes_per_pixel(params: CHandle, bytes: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel(params, bytes) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_equals(a, b, out_equal) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_format_is_float(pixel_format) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_format_name(
pixel_format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format_name(
pixel_format,
buf,
buf_size,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_frame_rate_to_string(
numerator: c_int,
denominator: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_to_string(
numerator,
denominator,
buf,
buf_size,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_name_for_divider(
divider: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_name_for_divider(
divider, buf, buf_size,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_scaled_dimension(dimension, divider)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_generate_auto_divider(width, height)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_divider_for_target_resolution(
src_width: c_int,
src_height: c_int,
target_width: c_int,
target_height: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider_for_target_resolution(
src_width,
src_height,
target_width,
target_height,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_channel_for_format(
pixel_format,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format(
pixel_format: c_int,
channels: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel_for_format(
pixel_format,
channels,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(
pixel_format: c_int,
channels: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_static_get_bytes_per_pixel(
pixel_format,
channels,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_buffer_size(params: CHandle, size: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_buffer_size(params, size) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_videoparams_get_time_in_timebase_units(
params: CHandle,
time_num: c_int,
time_den: c_int,
timestamp: *mut i64,
) -> c_int {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_in_timebase_units(
params, time_num, time_den, timestamp,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_init_display(
display: *const c_char,
view: *const c_char,
look: *const c_char,
) -> CHandle {
unsafe {
oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_free(transform: *mut CHandle) {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_get_display(
transform: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::colortransform::oakcommon_colortransform_get_display(
transform, buf, buf_size,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_get_output(
transform: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::colortransform::oakcommon_colortransform_get_output(
transform, buf, buf_size,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_get_view(
transform: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::colortransform::oakcommon_colortransform_get_view(transform, buf, buf_size)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_colortransform_get_look(
transform: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::colortransform::oakcommon_colortransform_get_look(transform, buf, buf_size)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_free(reader: *mut CHandle) {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_name(reader: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, buf, buf_size) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_read_element_text(
reader: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, buf, buf_size)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_attribute_name(
reader: CHandle,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(reader, index, buf, buf_size)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_attribute_value(
reader: CHandle,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(reader, index, buf, buf_size)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_init() -> CHandle {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_free(writer: *mut CHandle) {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_attribute(
writer: CHandle,
name: *const c_char,
value: *const c_char,
) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, name, value) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_text_element(
writer: CHandle,
name: *const c_char,
text: *const c_char,
) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_xml_writer_output(writer: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_output(writer, buf, buf_size) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_from_linear(linear: f64, out_db: *mut f64) -> c_int {
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_linear(linear, out_db) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_to_linear(db: f64, out_linear: *mut f64) -> c_int {
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_linear(db, out_linear) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_from_logarithmic(logarithmic: f64, out_db: *mut f64) -> c_int {
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_logarithmic(logarithmic, out_db) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> c_int {
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_logarithmic(db, out_logarithmic) }
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_linear_to_logarithmic(linear: f64, out_logarithmic: *mut f64) -> c_int {
unsafe {
oakcommon::ffi::misc::oakcommon_decibel_linear_to_logarithmic(linear, out_logarithmic)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakcommon_decibel_logarithmic_to_linear(logarithmic: f64, out_linear: *mut f64) -> c_int {
unsafe {
oakcommon::ffi::misc::oakcommon_decibel_logarithmic_to_linear(logarithmic, out_linear)
}
}
-43
View File
@@ -1,43 +0,0 @@
// 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/>.
//! C ABI imports from the oak modules, one submodule per module crate.
//!
//! The facade consumes the module C ABIs (`include/<mod>/*.h`) purely as
//! `extern "C"` imports — it never links the module crates at build time.
//! At the final app link the symbols resolve against the module shared
//! libraries; `cargo test` resolves them against the module crates' rlibs
//! (dev-dependencies, see Cargo.toml).
//!
//! **Signatures are declared from the module crates' actual `#[no_mangle]`
//! exports** (their `src/*/ffi*` modules), not from memory of the include
//! headers — module bridges in sibling crates have drifted from the real
//! ABI before. Every handle crosses the boundary as [`crate::handle::CHandle`]
//! (structurally identical to every `Oak<Mod><Type>` value handle).
//!
//! Only functions the module crates actually implement are declared here;
//! engine functions whose backing is still C++-only are facade stubs (see
//! the area modules) and never reach this module.
pub mod audio;
pub mod codec;
pub mod common;
pub mod node;
pub mod plugin;
pub mod render;
pub mod task;
pub mod timeline;
pub mod undo;
File diff suppressed because it is too large Load Diff
-81
View File
@@ -1,81 +0,0 @@
// 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/>.
//! oakplugin C ABI bridge: direct Rust calls into the `oakplugin` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakplugin`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakplugin C ABI imports, mirroring the oakplugin crate's exports
//! (`src/plugin/rust/src/ffi.rs`; headers `include/plugin/*.h`).
use std::ffi::{c_char, c_int};
/// Direct call into the `oakplugin` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakplugin_host_scan(bundle_dirs: *const *const c_char, dir_count: c_int) -> c_int {
unsafe { oakplugin::ffi::oakplugin_host_scan(bundle_dirs, dir_count) }
}
/// Direct call into the `oakplugin` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakplugin_host_init() -> c_int {
unsafe { oakplugin::ffi::oakplugin_host_init() }
}
/// Direct call into the `oakplugin` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakplugin_host_plugin_count() -> c_int {
unsafe { oakplugin::ffi::oakplugin_host_plugin_count() }
}
/// Direct call into the `oakplugin` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakplugin_host_plugin_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakplugin::ffi::oakplugin_host_plugin_id_at(index, buf, buf_size) }
}
/// Direct call into the `oakplugin` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakplugin_host_plugin_label(
plugin_id: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakplugin::ffi::oakplugin_host_plugin_label(plugin_id, buf, buf_size) }
}
-340
View File
@@ -1,340 +0,0 @@
// 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/>.
//! oakrender C ABI bridge: direct Rust calls into the `oakrender` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakrender`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakrender C ABI imports, mirroring the oakrender crate's exports
//! (`src/render/rust/src/ffi.rs`; headers `include/render/*.h`).
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::handle::CHandle;
/// `include/render/ticket.h` — video render ticket params. Single-lib
/// unification: aliases the oakrender crate's POD (same `repr(C)`
/// layout; all handle fields are the shared [`CHandle`]).
pub type OakVideoTicketParams = oakrender::ffi::OakVideoTicketParams;
/// `include/render/renderer.h` — frame video-params POD returned by
/// `oakrender_codec_frame_get_params`. Single-lib unification: aliases
/// the oakrender crate's POD.
pub type OakRenderVideoParams = oakrender::ffi::OakRenderVideoParams;
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_create_dynamic(backend_id: *const c_char) -> CHandle {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend_id) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_create_opengl() -> CHandle {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_opengl() }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_init(renderer: CHandle, gl_context: *mut c_void) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_destroy(renderer: *mut CHandle) {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_is_open_gl(renderer: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_display_renderer_is_vulkan(renderer: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_vulkan(renderer) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int {
unsafe { oakrender::ffi::manager::oakrender_set_cacher_multicam(multicam_or_null) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int {
unsafe { oakrender::ffi::manager::oakrender_set_display_color_processor(p_or_null) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_render_frame(
params: *const OakVideoTicketParams,
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
userdata: *mut c_void,
) -> CHandle {
unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_render_audio(
output_node: CHandle,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
params: *const c_void,
mode: c_int,
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
userdata: *mut c_void,
montage: *const oakrender::ffi::OakMontageClip,
montage_count: c_int,
) -> CHandle {
unsafe {
oakrender::ffi::ticket::oakrender_ticket_render_audio(
output_node,
in_num,
in_den,
out_num,
out_den,
params as *const CHandle,
mode,
cb,
userdata,
montage,
montage_count,
)
}
}
/// `oakrender_audio_samples_free` — release the samples block returned
/// by `oakrender_ticket_get_samples` (NULL no-op).
pub fn oakrender_audio_samples_free(samples: *mut c_void) {
unsafe { oakrender::ffi::ticket::oakrender_audio_samples_free(samples) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_ticket_free(ticket: *mut CHandle) {
unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_create() -> CHandle {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_create() }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_retain(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_free(frame: *mut CHandle) {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_const_data(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_is_allocated(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_codec_frame_get_params(frame: CHandle, out: *mut OakRenderVideoParams) -> c_int {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_processor_create(
src_space: *const c_char,
dst_transform: *const c_char,
direction: c_int,
) -> CHandle {
unsafe {
oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction)
}
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_processor_free(processor: *mut CHandle) {
unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int {
unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_processor_create_transform(
manager: CHandle,
input: *const c_char,
dest: CHandle,
direction: c_int,
) -> CHandle {
unsafe {
oakrender::ffi::color::oakrender_color_processor_create_transform(
manager, input, dest, direction,
)
}
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_processor_convert(
processor: CHandle,
ir: c_double,
ig: c_double,
ib: c_double,
ia: c_double,
out_r: *mut c_double,
out_g: *mut c_double,
out_b: *mut c_double,
out_a: *mut c_double,
) -> c_int {
unsafe {
oakrender::ffi::color::oakrender_color_processor_convert(
processor, ir, ig, ib, ia, out_r, out_g, out_b, out_a,
)
}
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_manager_set_up_default_config() -> c_int {
unsafe { oakrender::ffi::color::oakrender_color_manager_set_up_default_config() }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int {
unsafe { oakrender::ffi::color::oakrender_color_manager_get_config(buf, n) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int {
unsafe { oakrender::ffi::color::oakrender_lut_is_supported_extension(extension) }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_lut_supported_extensions_count() -> c_int {
unsafe { oakrender::ffi::color::oakrender_lut_supported_extensions_count() }
}
/// Direct call into the `oakrender` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int {
unsafe { oakrender::ffi::color::oakrender_lut_supported_extension_at(i, buf, n) }
}
-309
View File
@@ -1,309 +0,0 @@
// 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/>.
//! oaktask C ABI bridge: direct Rust calls into the `oaktask` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oaktask`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oaktask C ABI imports, mirroring the oaktask crate's exports
//! (`src/task/rust/src/ffi/{manager,task,project}.rs`; headers
//! `include/task/*.h`).
//!
//! Every handle crosses as [`crate::handle::CHandle`]. `oaktask_create_export`
//! takes the encoding-params POD ([`crate::bridge::codec::EncodingParamsPOD`],
//! field-identical to the task crate's `OakCodecEncodingParams`). String
//! getters report the size **including** the NUL; the facade converts with
//! [`crate::handle::string_result`].
use std::ffi::{c_char, c_int, c_void};
use crate::handle::CHandle;
/// `oaktask_event_fn` callback (`include/task/task.h`): `event_id` is an
/// `OakTaskEvent` (0=started, 1=progress, 2=finished), `value` 0..1 (or
/// start-ms / success flag), `userdata` the subscription token.
pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void);
/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).
pub type OakTaskOtioImportConfirmFn = unsafe extern "C" fn(
sequence_names: *const *const c_char,
count: c_int,
userdata: *mut c_void,
) -> c_int;
/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).
pub type OakTaskImageSequenceConfirmFn =
unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int;
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_manager_init() -> c_int {
unsafe { oaktask::ffi::manager::oaktask_manager_init() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_manager_shutdown() {
unsafe { oaktask::ffi::manager::oaktask_manager_shutdown() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_register_codec_submitter() -> c_int {
unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_manager_count() -> c_int {
unsafe { oaktask::ffi::manager::oaktask_manager_count() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_manager_at(i: c_int) -> CHandle {
unsafe { oaktask::ffi::manager::oaktask_manager_at(i) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_manager_delete_finished() {
unsafe { oaktask::ffi::manager::oaktask_manager_delete_finished() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_free(t: *mut CHandle) {
unsafe { oaktask::ffi::task::oaktask_task_free(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_start_sync(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_start_sync(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_start(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_start(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_cancel(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_cancel(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_wait(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_wait(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_is_finished(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_is_finished(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_succeeded(t: CHandle) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_succeeded(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_title(t, buf, buf_size) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaktask::ffi::task::oaktask_task_error(t, buf, buf_size) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_task_subscribe(
t: CHandle,
cb: Option<OakTaskEventFn>,
userdata: *mut c_void,
) -> i64 {
unsafe { oaktask::ffi::task::oaktask_task_subscribe(t, cb, userdata) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_debug_alive_count() -> c_int {
unsafe { oaktask::ffi::task::oaktask_debug_alive_count() }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_create_project_load(filename) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_load_take_project(t: CHandle) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_load_take_project(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_project_save(
project: CHandle,
filename_or_null: *const c_char,
use_compression: c_int,
) -> CHandle {
unsafe {
oaktask::ffi::project::oaktask_create_project_save(
project,
filename_or_null,
use_compression,
)
}
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_project_import(
folder: CHandle,
project: CHandle,
urls: *const *const c_char,
url_count: c_int,
) -> CHandle {
unsafe {
oaktask::ffi::project::oaktask_create_project_import(folder, project, urls, url_count)
}
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_take_command(t: CHandle) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_import_take_command(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_footage_count(t: CHandle) -> c_int {
unsafe { oaktask::ffi::project::oaktask_import_footage_count(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_import_footage_at(t, index) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_invalid_count(t: CHandle) -> c_int {
unsafe { oaktask::ffi::project::oaktask_import_invalid_count(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_invalid_at(
t: CHandle,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oaktask::ffi::project::oaktask_import_invalid_at(t, index, buf, buf_size) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_create_project_load_otio(filename) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_load_otio_take_project(t) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_create_project_save_otio(project, filename) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_load_otio_set_confirm_cb(
cb: Option<OakTaskOtioImportConfirmFn>,
userdata: *mut c_void,
) {
unsafe { oaktask::ffi::project::oaktask_load_otio_set_confirm_cb(cb, userdata) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_create_precache(footage, index, sequence) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
/// `oaktask_create_export` — direct call into the `oaktask` crate
/// (single-lib unification; the encoding-params POD is the shared
/// oakcodec type).
pub fn oaktask_create_export(
viewer: CHandle,
color_manager: CHandle,
params: *const crate::bridge::codec::EncodingParamsPOD,
) -> CHandle {
unsafe { oaktask::ffi::project::oaktask_create_export(viewer, color_manager, params) }
}
/// Direct call into the `oaktask` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktask_import_set_image_sequence_confirm_cb(
cb: Option<OakTaskImageSequenceConfirmFn>,
userdata: *mut c_void,
) {
unsafe { oaktask::ffi::project::oaktask_import_set_image_sequence_confirm_cb(cb, userdata) }
}
-490
View File
@@ -1,490 +0,0 @@
// 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/>.
//! oaktimeline C ABI bridge: direct Rust calls into the `oaktimeline` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oaktimeline`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oaktimeline C ABI imports, mirroring the oaktimeline crate's exports
//! (`src/timeline/rust/src/ffi.rs`; headers `include/timeline/*.h`).
//!
//! Every handle crosses as [`crate::handle::CHandle`]. The marker/workarea
//! time quantities cross as `c_int` num/den pairs; the edit commands take
//! `i64` rationals. String getters report the size **including** the NUL;
//! the facade converts with [`crate::handle::string_result`].
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
// `include/timeline/marker.h` exports (complete inventory):
// oaktimeline_marker_list_create / free / of / add / count / at /
// add_command / remove_at_command / set_time_command /
// set_props_command / list_load / list_save.
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_list_create() -> CHandle {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_create() }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_of(owner) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_list_free(list: *mut CHandle) {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_free(list) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_add(
list: CHandle,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
name: *const c_char,
color: c_int,
) -> c_int {
unsafe {
oaktimeline::ffi::marker::oaktimeline_marker_add(
list, in_num, in_den, out_num, out_den, name, color,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_count(list, out_count) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_at(
list: CHandle,
index: c_int,
in_num: *mut c_int,
in_den: *mut c_int,
out_num: *mut c_int,
out_den: *mut c_int,
color: *mut c_int,
name_buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
oaktimeline::ffi::marker::oaktimeline_marker_at(
list, index, in_num, in_den, out_num, out_den, color, name_buf, buf_size,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_add_command(
list: CHandle,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
name: *const c_char,
color: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::marker::oaktimeline_marker_add_command(
list, in_num, in_den, out_num, out_den, name, color,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_remove_at_command(list, index) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_set_time_command(
list: CHandle,
index: c_int,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::marker::oaktimeline_marker_set_time_command(
list, index, in_num, in_den, out_num, out_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_set_props_command(
list: CHandle,
index: c_int,
color: c_int,
name: *const c_char,
) -> CHandle {
unsafe {
oaktimeline::ffi::marker::oaktimeline_marker_set_props_command(list, index, color, name)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_list_load(list: CHandle, reader: CHandle) -> c_int {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_load(list, reader) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_marker_list_save(list: CHandle, writer: CHandle) -> c_int {
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_save(list, writer) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_create() -> CHandle {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_create() }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_of(owner) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_free(w: *mut CHandle) {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_free(w) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled(w, enabled) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_get(
w: CHandle,
in_num: *mut c_int,
in_den: *mut c_int,
out_num: *mut c_int,
out_den: *mut c_int,
enabled: *mut c_int,
) -> c_int {
unsafe {
oaktimeline::ffi::workarea::oaktimeline_workarea_get(
w, in_num, in_den, out_num, out_den, enabled,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_set_range(
w: CHandle,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
) -> c_int {
unsafe {
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range(
w, in_num, in_den, out_num, out_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_set_range_command(
w: CHandle,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
old_in_num: c_int,
old_in_den: c_int,
old_out_num: c_int,
old_out_den: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range_command(
w,
in_num,
in_den,
out_num,
out_den,
old_in_num,
old_in_den,
old_out_num,
old_out_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled_command(w, enabled) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_reset(
in_num: *mut c_int,
in_den: *mut c_int,
out_num: *mut c_int,
out_den: *mut c_int,
) -> c_int {
unsafe {
oaktimeline::ffi::workarea::oaktimeline_workarea_reset(in_num, in_den, out_num, out_den)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_load(w: CHandle, reader: CHandle) -> c_int {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_load(w, reader) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_workarea_save(w: CHandle, writer: CHandle) -> c_int {
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_save(w, writer) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle {
unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle {
unsafe { oaktimeline::ffi::edit::oaktimeline_remove_track_command(track) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_place_block_command(
list: CHandle,
track_index: c_int,
block: CHandle,
in_num: i64,
in_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_place_block_command(
list,
track_index,
block,
in_num,
in_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle {
unsafe { oaktimeline::ffi::edit::oaktimeline_replace_block_with_gap_command(track, block) }
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_move_block_command(
list: CHandle,
track_index: c_int,
block: CHandle,
in_num: i64,
in_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_move_block_command(
list,
track_index,
block,
in_num,
in_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_trim_command(
track: CHandle,
block: CHandle,
new_length_num: i64,
new_length_den: i64,
mode: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_trim_command(
track,
block,
new_length_num,
new_length_den,
mode,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_split_command(
blocks: *const CHandle,
count: c_int,
point_num: i64,
point_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_split_command(blocks, count, point_num, point_den)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_split_preserving_links_command(
blocks: *const CHandle,
count: c_int,
point_nums: *const i64,
point_dens: *const i64,
time_count: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_split_preserving_links_command(
blocks, count, point_nums, point_dens, time_count,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_ripple_delete_gaps_command(
sequence: CHandle,
in_nums: *const i64,
in_dens: *const i64,
out_nums: *const i64,
out_dens: *const i64,
tracks: *const CHandle,
range_count: c_int,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_ripple_delete_gaps_command(
sequence,
in_nums,
in_dens,
out_nums,
out_dens,
tracks,
range_count,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_slide_command(
track: CHandle,
blocks: *const CHandle,
block_count: c_int,
in_adjacent: CHandle,
out_adjacent: CHandle,
movement_num: i64,
movement_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_slide_command(
track,
blocks,
block_count,
in_adjacent,
out_adjacent,
movement_num,
movement_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_ripple_remove_area_command(
track: CHandle,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_ripple_remove_area_command(
track, in_num, in_den, out_num, out_den,
)
}
}
/// Direct call into the `oaktimeline` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oaktimeline_insert_gaps_command(
list: CHandle,
point_num: i64,
point_den: i64,
length_num: i64,
length_den: i64,
) -> CHandle {
unsafe {
oaktimeline::ffi::edit::oaktimeline_insert_gaps_command(
list, point_num, point_den, length_num, length_den,
)
}
}
-198
View File
@@ -1,198 +0,0 @@
// 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/>.
//! oakundo C ABI bridge: direct Rust calls into the `oakundo` crate.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
//! call below is a compile-time Rust call into `oakundo`'s `ffi` (the
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
//! internal callers bypass them). Handles cross as the shared
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
//! declaration (resolved at link time against the sibling crate in the
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
// 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/>.
//! oakundo C ABI imports, mirroring the oakundo crate's exports
//! (`src/undo/rust/src/ffi.rs`; headers `include/undo/*.h`).
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
/// `include/undo/undocommand.h` — callback table backing a
/// caller-defined undo command. Single-lib unification: aliases the
/// oakundo crate's vtable POD.
pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable;
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_init(
vtable: *const OakUndoCommandVtable,
userdata: *mut std::ffi::c_void,
) -> CHandle {
unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_init_multi() -> CHandle {
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_redo_now(command: CHandle) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_undo_now(command: CHandle) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_command_free(command: *mut CHandle) {
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_init() -> CHandle {
unsafe { oakundo::ffi::undostack::oakundo_undostack_init() }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_free(stack: *mut CHandle) {
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(stack) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, name) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_push_pre_executed(
stack: CHandle,
command: CHandle,
name: *const c_char,
) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_push_pre_executed(stack, command, name) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_undo(stack: CHandle) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_undo(stack) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_redo(stack: CHandle) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_redo(stack) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_jump(stack: CHandle, index: i64) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_jump(stack, index) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_clear(stack: CHandle) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_clear(stack) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_undo(stack, out_value) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, out_value) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_count(stack: CHandle, out_count: *mut i64) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_count(stack, out_count) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_index(stack: CHandle, out_index: *mut i64) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_index(stack, out_index) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_command_text(
stack: CHandle,
row: i64,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_text(stack, row, buf, buf_size) }
}
/// Direct call into the `oakundo` crate (single-lib unification; the
/// `#[no_mangle]` export stays for the external C ABI).
pub fn oakundo_undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int {
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_is_done(stack, row, out_value) }
}
+382 -19
View File
@@ -16,7 +16,7 @@
//! `engine/include/oakengine/encoding.h` over the oakcodec module.
//!
//! Two parts:
//! Three parts:
//!
//! - The container/codec **metadata family** (format names/extensions,
//! per-format codec lists, pixel/sample formats, filename helpers,
@@ -30,15 +30,19 @@
//! "unset" (the POD's 0 is a valid format, DNxHD); encoder-specific
//! video options are kept in a facade-side map (the POD has no such
//! field).
//! - The **exporter family** (`engine/include/oakengine/exporter.h`)
//! assembles an encoding-params handle and drives the export task
//! synchronously (see the family section at the bottom).
//!
//! Presets, preset load/save and the sequence-bound last-used/export
//! entry points are deferred (see the stubs below and `deferred.rs`).
//! Presets, preset load/save and the sequence-bound last-used entry
//! points remain deferred (see the stubs below and `deferred.rs`).
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{c_char, c_int, c_void};
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::bridge::codec as k;
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
use crate::stubs::codec as k;
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
use crate::common::OakVideoParamsPod;
use crate::error::{Error, Result};
use crate::handle::{guard, guard_int, string_result};
@@ -55,6 +59,10 @@ pub struct OakEngineEncodingParams {
struct ParamsBox {
pod: EncodingParamsPOD,
video_options: HashMap<String, String>,
/// Raw scaling-method code exactly as the caller set it: the POD's
/// `VideoScalingMethod` enum cannot carry garbage codes, and the
/// facade contract accepts any `int` verbatim (round-trips 99 as 99).
video_scaling_raw: c_int,
}
impl ParamsBox {
@@ -64,6 +72,7 @@ impl ParamsBox {
ParamsBox {
pod,
video_options: HashMap::new(),
video_scaling_raw: 0,
}
}
}
@@ -457,7 +466,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_video(
p.pod.video_height = v.height;
p.pod.video_time_base_num = v.time_base_num;
p.pod.video_time_base_den = v.time_base_den;
p.pod.video_pixel_format = v.format;
p.pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
p.pod.video_interlacing = v.interlacing;
p.pod.video_pixel_aspect_num = v.pixel_aspect_num;
p.pod.video_pixel_aspect_den = v.pixel_aspect_den;
@@ -480,7 +489,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_audio(
p.pod.audio_codec = codec;
p.pod.audio_sample_rate = sample_rate;
p.pod.audio_channel_layout = channel_layout;
p.pod.audio_sample_format = sample_format;
p.pod.audio_sample_format = crate::pods::sample_format_from_code(sample_format);
Ok(())
})
}
@@ -594,7 +603,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_video_params(
(*out).height = p.pod.video_height;
(*out).time_base_num = p.pod.video_time_base_num;
(*out).time_base_den = p.pod.video_time_base_den;
(*out).format = p.pod.video_pixel_format;
(*out).format = p.pod.video_pixel_format as i32;
(*out).interlacing = p.pod.video_interlacing;
(*out).pixel_aspect_num = p.pod.video_pixel_aspect_num;
(*out).pixel_aspect_den = p.pod.video_pixel_aspect_den;
@@ -645,7 +654,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_audio_params(
*channel_layout = p.pod.audio_channel_layout;
}
if !sample_format.is_null() {
*sample_format = p.pod.audio_sample_format;
*sample_format = p.pod.audio_sample_format as i32;
}
Ok(())
})
@@ -964,7 +973,10 @@ pub unsafe extern "C" fn oakengine_encoding_params_set_video_scaling_method(
) -> c_int {
guard(|| unsafe {
let p = params_mut(params)?;
p.pod.video_scaling_method = method;
// The raw code round-trips verbatim (garbage codes included);
// the POD carries the nearest legal enum for the encoder.
p.video_scaling_raw = method;
p.pod.video_scaling_method = crate::pods::scaling_from_code(method);
Ok(())
})
}
@@ -976,7 +988,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_video_scaling_method(
) -> c_int {
guard_int(|| unsafe {
let p = params_ref(params)?;
Ok(p.pod.video_scaling_method)
Ok(p.video_scaling_raw)
})
}
@@ -1073,14 +1085,365 @@ pub unsafe extern "C" fn oakengine_encoding_params_save_file(
crate::error::OAKENGINE_E_FAILED
}
/// `oakengine_export_render_with_params` — **not backed** (the exporter
/// family is facade-only; see `deferred.rs`). Returns OAKENGINE_E_FAILED.
// ---------------------------------------------------------------------------
// Exporter family (exporter.h)
// ---------------------------------------------------------------------------
// Thread-local reason for the last failed export on this thread (the C++
// `g_last_error`, `engine/src/capi/export.cpp`). Cleared at the start of
// every export call; read by [`oakengine_export_last_error`].
thread_local! {
static EXPORT_LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
}
// Thread-local progress callback installed by
// [`oakengine_export_set_progress_callback`] (the C++ `g_progress_fn` /
// `g_progress_userdata`). Per-thread like the C++: the synchronous export
// runs on the installing thread, so the module task events arrive there.
thread_local! {
static EXPORT_PROGRESS: RefCell<
Option<(unsafe extern "C" fn(c_double, *mut c_void), *mut c_void)>,
> = const { RefCell::new(None) };
}
/// The module task progress event id (`OAKTASK_EVENT_PROGRESS`, see
/// `oakengine_task_subscribe`).
const EXPORT_EVENT_PROGRESS: c_int = 1;
fn export_last_error_set(msg: String) {
EXPORT_LAST_ERROR.with(|e| *e.borrow_mut() = msg);
}
/// Forward the progress events of a running export to the installed
/// callback. Installed as the task subscription only while a callback is
/// set; the module passes the callback's own `userdata` through.
unsafe extern "C" fn export_progress_event(event_id: c_int, value: f64, userdata: *mut c_void) {
if event_id != EXPORT_EVENT_PROGRESS {
return;
}
EXPORT_PROGRESS.with(|slot| {
if let Some((cb, _)) = *slot.borrow() {
// SAFETY: the callback + userdata follow the installer's
// contract; the task emits on its running thread.
unsafe { cb(value, userdata) };
}
});
}
/// Run an export task synchronously on the calling thread — the shared
/// tail of every exporter-family entry point: create the task (taking
/// ownership of `params`), subscribe the installed progress callback, run
/// through [`oakengine_task_start_sync`], read the task error into the
/// thread-local last-error slot, and free the task.
///
/// Returns OAKENGINE_OK on success, OAKENGINE_E_FAILED otherwise. On the
/// task-creation failure path `params` ownership stays with the caller
/// (mirroring [`oakengine_task_create_export`]).
fn export_run_sync(seq: *mut crate::handle::OakEngineSequence, params: *mut OakEngineEncodingParams) -> c_int {
let task = unsafe { crate::task::oakengine_task_create_export(seq, params) };
if task.is_null() {
export_last_error_set("failed to create the export task".into());
return crate::error::OAKENGINE_E_FAILED;
}
// Progress events through the same module subscription the app's
// `start_export` uses (`oakengine_task_subscribe`).
EXPORT_PROGRESS.with(|slot| {
if let Some((_, userdata)) = *slot.borrow() {
unsafe {
crate::task::oakengine_task_subscribe(task, Some(export_progress_event), userdata);
}
}
});
let ok = unsafe { crate::task::oakengine_task_start_sync(task) };
let rc = if ok == 1 {
crate::error::OAKENGINE_OK
} else {
let err = export_task_error(task);
export_last_error_set(if err.is_empty() {
"export failed".into()
} else {
err
});
crate::error::OAKENGINE_E_FAILED
};
unsafe { crate::task::oakengine_task_free(task) };
rc
}
/// Two-stage read of a task's error string (empty when none).
fn export_task_error(task: *mut crate::handle::OakEngineTask) -> String {
unsafe {
let needed = crate::task::oakengine_task_error(task, std::ptr::null_mut(), 0);
if needed <= 0 {
return String::new();
}
let mut buf = vec![0 as c_char; needed as usize + 1];
let n = crate::task::oakengine_task_error(task, buf.as_mut_ptr(), buf.len() as c_int);
if n < 0 {
return String::new();
}
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf8_lossy(unsafe {
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
})
.into_owned()
}
}
/// `oakengine_export_render_with_params` — render `seq` to the file the
/// encoding params describe, through the same synchronous export path the
/// app's `start_export` drives (`oakengine_task_create_export` +
/// `oakengine_task_start_sync` + free).
///
/// Takes ownership of `params` on success (destroyed with the export
/// task, mirroring [`oakengine_task_create_export`]); on the
/// task-creation failure path the caller keeps ownership. The sequence
/// handle is validated for non-NULL only — the C++ "handle is a sequence
/// of the active project" walk has no Rust analogue (created sequences
/// live in their own scratch project, see `oakengine_sequence_new`).
///
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for NULL
/// arguments; OAKENGINE_E_FAILED for creation/run failures (see
/// [`oakengine_export_last_error`]).
#[no_mangle]
pub unsafe extern "C" fn oakengine_export_render_with_params(
_seq: *mut crate::handle::OakEngineSequence,
_params: *const OakEngineEncodingParams,
seq: *mut crate::handle::OakEngineSequence,
params: *const OakEngineEncodingParams,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
guard(|| unsafe {
export_last_error_set(String::new());
if seq.is_null() || params.is_null() {
export_last_error_set("invalid arguments".into());
return Err(Error::Invalid);
}
if export_run_sync(seq, params as *mut OakEngineEncodingParams) == crate::error::OAKENGINE_OK {
Ok(())
} else {
Err(Error::Failed("export failed".into()))
}
})
}
/// `oakengine_export_render` — render `seq`'s [in_ts, out_ts) range
/// offline and encode it to `path`.
///
/// `in_ts`/`out_ts` are frame timestamps in the sequence's frame-rate
/// timebase (the export frame rate is the sequence frame rate). `width`/
/// `height` <= 0 fall back to the sequence's video dimensions; when they
/// differ the frames are scaled to fit. Video is encoded with the
/// options' codec (default H.264 in an MP4 container), audio with the
/// options' codec (default AAC) at the requested rate/layout (defaults:
/// 48 kHz stereo — the engine has no sequence-audio getter, so the
/// header's "sequence rate/layout" fallback mirrors the app's export
/// dialog instead). The options' codec fields carry the exporter.h
/// `OAKENGINE_EXPORT_VIDEO_*` / `OAKENGINE_EXPORT_AUDIO_*` values,
/// mapped here onto the engine's `ExportFormat` / `ExportCodec` ids.
///
/// The call blocks until the export finishes; progress is reported
/// through the callback set with [`oakengine_export_set_progress_callback`].
///
/// Deviations from the C++ header: no `OAKENGINE_INIT_RENDER`
/// requirement (the Rust render path is CPU-only and self-contained, see
/// `oakengine_render_manager_init`) and no "sequence is part of a
/// project" check (created sequences live in a scratch project).
///
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad
/// arguments; OAKENGINE_E_FAILED for render/encode failures (see
/// [`oakengine_export_last_error`]).
#[no_mangle]
pub unsafe extern "C" fn oakengine_export_render(
seq: *mut crate::handle::OakEngineSequence,
path: *const c_char,
in_ts: i64,
out_ts: i64,
width: c_int,
height: c_int,
opts: *const crate::pods::OakExportOptions,
) -> c_int {
guard(|| unsafe {
export_last_error_set(String::new());
if seq.is_null() || path.is_null() || in_ts < 0 || out_ts <= in_ts {
export_last_error_set("invalid arguments".into());
return Err(Error::Invalid);
}
let o = if opts.is_null() {
crate::pods::OakExportOptions {
video_codec: crate::pods::OAKENGINE_EXPORT_VIDEO_H264,
audio_codec: crate::pods::OAKENGINE_EXPORT_AUDIO_AAC,
video_bit_rate: 0,
audio_sample_rate: 0,
audio_channel_count: 0,
}
} else {
*opts
};
// Map the exporter.h codec ids onto the engine's enum ids.
let (format, vcodec) = match o.video_codec {
crate::pods::OAKENGINE_EXPORT_VIDEO_H264 => (
oakcodec::exportformat::Format::MPEG4Video as i32,
oakcodec::exportcodec::Codec::H264 as i32,
),
crate::pods::OAKENGINE_EXPORT_VIDEO_H265 => (
oakcodec::exportformat::Format::MPEG4Video as i32,
oakcodec::exportcodec::Codec::H265 as i32,
),
crate::pods::OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE => (
oakcodec::exportformat::Format::PNG as i32,
oakcodec::exportcodec::Codec::PNG as i32,
),
_ => {
export_last_error_set(format!("unknown video codec {}", o.video_codec));
return Err(Error::Invalid);
}
};
let audio_enabled = o.audio_codec != crate::pods::OAKENGINE_EXPORT_AUDIO_NONE;
let acodec = if audio_enabled {
match o.audio_codec {
crate::pods::OAKENGINE_EXPORT_AUDIO_AAC => oakcodec::exportcodec::Codec::AAC as i32,
crate::pods::OAKENGINE_EXPORT_AUDIO_PCM => oakcodec::exportcodec::Codec::PCM as i32,
_ => {
export_last_error_set(format!("unknown audio codec {}", o.audio_codec));
return Err(Error::Invalid);
}
}
} else {
0
};
// Sequence geometry + frame rate (the export frame rate is the
// sequence's).
let mut sw: c_int = 0;
let mut sh: c_int = 0;
let mut par_num: c_int = 1;
let mut par_den: c_int = 1;
Error::from_module(crate::timeline::oakengine_sequence_get_video_params(
seq,
&mut sw,
&mut sh,
&mut par_num,
&mut par_den,
))?;
let mut rate_num: c_int = 0;
let mut rate_den: c_int = 1;
Error::from_module(crate::timeline::oakengine_sequence_get_frame_rate(
seq,
&mut rate_num,
&mut rate_den,
))?;
if rate_num <= 0 || rate_den <= 0 {
export_last_error_set("sequence has no valid frame rate".into());
return Err(Error::Invalid);
}
let out_w = if width > 0 { width } else { sw };
let out_h = if height > 0 { height } else { sh };
if out_w <= 0 || out_h <= 0 {
export_last_error_set("sequence has no valid video dimensions".into());
return Err(Error::Invalid);
}
let sample_rate = if o.audio_sample_rate > 0 { o.audio_sample_rate } else { 48000 };
let layout: u64 = if o.audio_channel_count > 0 {
match o.audio_channel_count {
1 => 0x4, // AV_CH_LAYOUT_MONO
2 => 0x3, // AV_CH_LAYOUT_STEREO
n => {
export_last_error_set(format!(
"unsupported audio channel count {n} (1 = mono, 2 = stereo)"
));
return Err(Error::Invalid);
}
}
} else {
0x3
};
// Assemble the encoding params through the public setters (the same
// path the app's `start_export` uses); the task consumes the handle
// once created.
let params = oakengine_encoding_params_create();
if params.is_null() {
export_last_error_set("failed to create encoding params".into());
return Err(Error::Failed("failed to create encoding params".into()));
}
let fail = |msg: &str| -> Result<()> {
oakengine_encoding_params_destroy(params);
export_last_error_set(msg.into());
Err(Error::Failed(msg.into()))
};
let cpath = std::ffi::CString::new(crate::handle::read_cstr(path))
.map_err(|_| Error::Failed("invalid path (NUL byte)".into()))?;
if oakengine_encoding_params_set_filename(params, cpath.as_ptr()) != 0 {
return fail("failed to set the export filename");
}
if oakengine_encoding_params_set_format(params, format) != 0 {
return fail("failed to set the export format");
}
let pod = OakVideoParamsPod {
width: out_w,
height: out_h,
time_base_num: rate_den,
time_base_den: rate_num,
format: 0,
pixel_aspect_num: par_num.max(1),
pixel_aspect_den: par_den.max(1),
interlacing: 0,
color_range: 0,
divider: 1,
video_type: 0,
premultiplied_alpha: 0,
};
if oakengine_encoding_params_enable_video(params, &pod, vcodec) != 0 {
return fail("failed to enable video");
}
if audio_enabled && oakengine_encoding_params_enable_audio(params, sample_rate, layout, 0, acodec) != 0 {
return fail("failed to enable audio");
}
if o.video_bit_rate > 0 {
oakengine_encoding_params_set_video_bit_rate(params, o.video_bit_rate);
}
// Fit scaling (the header's documented behavior when the output
// size differs from the sequence's).
if oakengine_encoding_params_set_video_scaling_method(params, 0) != 0 {
return fail("failed to set the video scaling method");
}
// Export range as seconds rationals: frame timestamps in the
// sequence's frame-rate timebase (frame duration = rate_den/rate_num).
let tb_num = i64::from(rate_den);
let tb_den = i64::from(rate_num);
oakengine_encoding_params_set_custom_range(params, in_ts * tb_num, tb_den, out_ts * tb_num, tb_den);
oakengine_encoding_params_set_export_length(params, ((out_ts - in_ts) * tb_num) as c_int, rate_num);
if export_run_sync(seq, params) == crate::error::OAKENGINE_OK {
Ok(())
} else {
Err(Error::Failed("export failed".into()))
}
})
}
/// `oakengine_export_last_error` — the reason for the last failed export
/// on this thread (buf/size; empty after a successful export).
#[no_mangle]
pub unsafe extern "C" fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
guard_int(|| {
let err = EXPORT_LAST_ERROR.with(|e| e.borrow().clone());
Ok(unsafe { crate::handle::write_string(&err, buf, buf_size) })
})
}
/// `oakengine_export_set_progress_callback` — install the progress
/// callback used by subsequent [`oakengine_export_render`] /
/// [`oakengine_export_render_with_params`] calls on this thread (NULL
/// disables). The callback receives `fraction` in [0, 1] and is invoked
/// on the exporting thread during the synchronous run.
#[no_mangle]
pub unsafe extern "C" fn oakengine_export_set_progress_callback(
f: Option<unsafe extern "C" fn(c_double, *mut c_void)>,
userdata: *mut c_void,
) {
crate::handle::guard_void(|| {
EXPORT_PROGRESS.with(|slot| *slot.borrow_mut() = f.map(|cb| (cb, userdata)));
});
}
/// `oakengine_encoding_params_get_last_used` — **not backed** (sequence
@@ -1115,9 +1478,9 @@ pub unsafe extern "C" fn oakengine_encoding_start_audio_recording(
if m.is_null() {
return Err(Error::State);
}
let rc = crate::bridge::audio::oakaudio_manager_start_recording(
let rc = crate::stubs::audio::oakaudio_manager_start_recording(
m,
&p.pod as *const oakaudio::bridge::codec::EncodingParams,
&p.pod as *const crate::pods::EncodingParamsPOD,
errbuf,
errbuf_size,
);
+1 -1
View File
@@ -30,7 +30,7 @@
use std::ffi::{c_char, c_int, c_void};
use std::sync::{Mutex, OnceLock};
use crate::bridge::common as c;
use crate::stubs::common as c;
use crate::error::Error;
use crate::handle::{
box_handle, free_box, guard, guard_int, guard_void, string_result, OakEngineClipboard,
+7 -6
View File
@@ -49,14 +49,15 @@
//! crates lack the C ABI surface — each stub returns its header's
//! documented failure value:
//!
//! - **codec** (encoding.h, 81/85 wrapped): the preset path/count/name,
//! preset load/save and the sequence-bound export/last-used entry
//! points (`oakengine_encoding_preset_*`,
//! - **codec** (encoding.h, 82/85 wrapped): the preset path/count/name,
//! preset load/save and the sequence-bound last-used entry points
//! (`oakengine_encoding_preset_*`,
//! `oakengine_encoding_params_load_file/save_file`,
//! `oakengine_export_render_with_params`,
//! `oakengine_encoding_params_get/set_last_used`) are stubs — the
//! oakcodec crate has no preset API and those entry points need the
//! exporter/sequence families.
//! oakcodec crate has no preset API and the last-used pair needs the
//! deferred node/timeline families. The exporter entry point
//! (`oakengine_export_render_with_params`) is backed since M12 (see
//! `crate::codec`, "Exporter family").
//! - **render color** (color.h, 19/31 wrapped): the color-manager list
//! queries (colorspace/display/view/look/compliant/luma), the
//! standalone config handle and `color_processor_id` /
+90 -1
View File
@@ -35,7 +35,7 @@
//! [`write_string`]): the return value is the required length including
//! the terminating NUL; negative values are error codes.
use std::ffi::{c_char, c_int, c_void};
use std::ffi::{c_char, c_int};
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::error::{Error, Result};
@@ -48,6 +48,95 @@ use crate::error::{Error, Result};
/// `Clone + Copy + Send + Sync` come from the shared type.
pub use oakcore_rs::handle::CHandle;
/// Engine-side boxed payloads holding the oaknode domain (single-lib
/// unification). Every `oakengine_*` node-family handle ultimately wraps
/// one of these behind a [`CHandle`]:
///
/// - projects box [`domain::ProjectArc`] (`Arc<Mutex<Project>>`);
/// - nodes, blocks, tracks, footage, sequences and folders box a
/// [`domain::NodeRef`] (`(Arc<Mutex<Project>>, NodeId)` — the
/// oaknode crate's `project::NodeRef` value type).
///
/// The box is created through `oaknode::handle::make_owned` (refcounted
/// shell + release callback), so the facade's existing
/// [`box_handle`]/[`free_box`] discipline (and the addref copies the
/// engine takes) works unchanged.
pub mod domain {
use std::sync::{Arc, Mutex};
use oaknode::id::NodeId;
use crate::handle::CHandle;
/// Engine-side boxed payload for project handles: shared ownership of
/// the oaknode domain project (its graph, settings, filename state).
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
/// Engine-side boxed payload for node/block/track/footage/sequence/
/// folder handles: a reference into a project's graph. Reuses the
/// oaknode crate's own `NodeRef` value type (project + id + owned
/// flag); a stale id fails validation instead of aliasing.
pub type NodeRef = oaknode::project::NodeRef;
/// Box a project payload behind a refcounted handle.
pub fn box_project(project: ProjectArc) -> CHandle {
oaknode::handle::make_owned(project)
}
/// Box a node reference behind a refcounted handle. `owned` marks
/// detached (factory-created) nodes so the engine's debug alive
/// counter accounts them exactly once.
pub fn box_node(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
oaknode::handle::make_owned(NodeRef::new(project, id, owned))
}
/// Borrow the project payload behind a handle.
///
/// # Safety
/// `h` must be a live handle created by [`box_project`] (or empty).
pub unsafe fn project_of(h: &CHandle) -> Option<&ProjectArc> {
// SAFETY: forwarded to the oaknode handle contract.
unsafe { oaknode::handle::get::<ProjectArc>(h) }
}
/// Borrow the node-reference payload behind a handle.
///
/// # Safety
/// `h` must be a live handle created by [`box_node`] (or empty).
pub unsafe fn node_ref_of(h: &CHandle) -> Option<&NodeRef> {
// SAFETY: forwarded to the oaknode handle contract.
unsafe { oaknode::handle::get::<NodeRef>(h) }
}
/// Mutable view of the node-reference payload (used by the graph
/// transfer paths, which rewrite the shared box in place — the
/// "write_node_ref" semantics).
///
/// # Safety
/// `h` must be a live handle created by [`box_node`]; the caller must
/// hold exclusive access to the boxed value.
pub unsafe fn node_ref_mut(h: &CHandle) -> Option<&mut NodeRef> {
// SAFETY: forwarded to the shared-box contract.
unsafe { boxed_mut::<NodeRef>(h) }
}
/// Mutable typed view into an oaknode-style `RefBox` payload (the
/// oaknode crate exposes only a read-only `get`; this mirrors its
/// box layout — `refs`/`value` are `pub` fields).
///
/// # Safety
/// `h` must be a live handle boxing `T`; the caller must hold
/// exclusive access to the boxed value.
pub unsafe fn boxed_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
if h.ctx.is_null() {
return None;
}
// SAFETY: contract above; the box is an
// `oaknode::handle::RefBox<T>`.
unsafe { Some(&mut (*(h.ctx as *mut oaknode::handle::RefBox<T>)).value) }
}
}
/// Engine opaque handle types, one per `typedef struct OakEngine*` in
/// `engine/include/oakengine/*.h`. All are thin newtype wrappers around a
/// [`CHandle`] value with a uniform extraction surface ([`EngineBox`]).
+689 -17
View File
@@ -14,22 +14,33 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Render-worker IPC: named shared memory holding the frame-slot pools —
//! the Rust port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
//! `oliveimpl/render/ipc/frameslotpool.cpp`) behind the frozen C ABI in
//! `engine/include/oakengine/ipc.h`.
//! Render-worker IPC: the control-plane NDJSON protocol and the
//! shared-memory frame-slot transport, owned by the oakengine facade and
//! exported through the frozen `oakengine_ipc_*` C ABI
//! (`engine/include/oakengine/ipc.h`); the `oak-worker` binary consumes it
//! purely through the C ABI. The transport is the Rust port of
//! `engine/render/ipc/` + `ipcmessage.cpp`.
//!
//! Two pieces, mirroring the C++ exactly:
//! Two halves:
//!
//! - [`SharedMemoryRegion`]: a named POSIX segment (`shm_open` + `mmap`,
//! `munmap` + `shm_unlink` on close). One process creates the segment
//! (owner, unlinks on close); the peer attaches to it by key.
//! - [`FrameSlotPool`]: a fixed pool of equal-sized frame slots laid out
//! inside a region, with lock-free hand-off through two
//! - **Control plane.** One compact JSON object per line on the stdio
//! pipes (worker.cpp / ipcmessage.cpp `write_message`/`read_message`).
//! Every message carries a `"type"` string; the field names below are
//! the ones the C++ serializers actually emit
//! (`engine/render/ipc/ipcmessage.cpp`): note `ticket` / `node` /
//! `channels` / `slot` — the longer names (`ticket_id`, `node_uuid`,
//! `channel_count`, `output_slot`) exist only on the C POD structs in
//! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines.
//! - **Data plane.** Named shared memory holding the frame-slot pools —
//! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX
//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close),
//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame
//! slots inside it with lock-free hand-off through two
//! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a
//! single-producer/single-consumer structure; the filler owns
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` + `free.push`,
//! so no mutex is ever taken.
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` +
//! `free.push`, so no mutex is ever taken.
//!
//! **The in-memory layout is the version-1 wire protocol** the app and the
//! render worker share, and it never changes: the byte offsets below are
@@ -40,15 +51,178 @@
//!
//! This module is deliberately unsafe-heavy and self-contained: it touches
//! raw shared memory and raw POSIX syscalls, and everything else in the
//! facade reaches it through the safe wrapper methods and the C ABI exports
//! at the bottom.
//! crate reaches it through the safe wrapper methods.
//!
//! Message types (M = main/editor, W = worker):
//! handshake M<->W negotiate protocol version + announce shm geometry
//! load_graph M ->W path to a temp file holding the serialized graph
//! render_frame M ->W request a frame render (ticket, node, time, params)
//! frame_ready W ->M a rendered frame is published (slot + ticket)
//! cancel M ->W abandon an in-flight ticket
//! graph_update M ->W reserved (no payload struct yet)
//! shutdown M ->W finish current work and exit cleanly
//! error W ->M worker-side failure report ("message" field)
//!
//! Items the worker does not emit yet (frame_ready, graph_update,
//! `FrameReadyMsg`) and message ids it ignores (`cancel`) are kept as the
//! documented protocol surface; `dead_code` until the frame-slot transport
//! is driven by a real graph (see [`crate::worker`]).
#![allow(dead_code)]
use std::ffi::{c_char, c_int, c_void};
use std::io::{self, Write};
use std::ptr;
use std::sync::atomic::{AtomicU32, Ordering};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::handle::{guard_int, guard_ptr};
/// `"handshake"`.
pub const TYPE_HANDSHAKE: &str = "handshake";
/// `"load_graph"`.
pub const TYPE_LOAD_GRAPH: &str = "load_graph";
/// `"render_frame"`.
pub const TYPE_RENDER_FRAME: &str = "render_frame";
/// `"frame_ready"`.
pub const TYPE_FRAME_READY: &str = "frame_ready";
/// `"cancel"`.
pub const TYPE_CANCEL: &str = "cancel";
/// `"graph_update"`.
pub const TYPE_GRAPH_UPDATE: &str = "graph_update";
/// `"shutdown"`.
pub const TYPE_SHUTDOWN: &str = "shutdown";
/// `"error"`.
pub const TYPE_ERROR: &str = "error";
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake`
/// (ipc.h). Wire field names match the C++ serializer.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct HandshakeMsg {
/// Protocol version.
pub protocol_version: i32,
/// Worker->main output shared-memory segment key.
pub shm_key: String,
/// Main->worker input shared-memory segment key (optional).
pub input_shm_key: String,
/// Number of main->worker input frame slots.
pub input_slots: i32,
/// Number of worker->main output frame slots.
pub output_slots: i32,
/// Per-output-slot pixel block size.
pub slot_data_bytes: i64,
/// Per-input-slot pixel block size.
pub input_slot_data_bytes: i64,
}
impl HandshakeMsg {
/// The worker's startup handshake (`worker.cpp startup_handshake()`).
pub fn to_json(&self) -> Value {
json!({
"type": TYPE_HANDSHAKE,
"protocol_version": self.protocol_version,
"shm_key": self.shm_key,
"input_shm_key": self.input_shm_key,
"input_slots": self.input_slots,
"output_slots": self.output_slots,
"slot_data_bytes": self.slot_data_bytes,
"input_slot_data_bytes": self.input_slot_data_bytes,
})
}
}
/// `render_frame` — request a frame render. Wire names per ipcmessage.cpp:
/// `ticket`, `node`, `channels` (not the ipc.h POD names).
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct RenderFrameMsg {
/// Correlates with the eventual frame_ready.
pub ticket: i64,
/// Viewer node stable uuid in the loaded graph.
pub node: String,
/// Frame timestamp numerator.
pub time_num: i64,
/// Frame timestamp denominator.
pub time_den: i64,
/// Forced output size (0 = graph default).
pub width: i32,
/// Forced output height (0 = graph default).
pub height: i32,
/// Forced PixelFormat (-1 = default).
pub format: i32,
/// Channel count (0 = default).
pub channels: i32,
/// RenderMode.
pub mode: i32,
/// Optional decoded input slot (-1 = none).
pub input_slot: i32,
/// Ordered decoded input slots.
pub input_slots: Vec<i32>,
/// Output color transform present?
pub has_color_transform: bool,
/// Color transform targets the display space.
pub color_is_display: bool,
/// Output color space name.
pub color_output: String,
/// Output color view name.
pub color_view: String,
/// Output color look name.
pub color_look: String,
}
/// `frame_ready` — a rendered frame is published (wire names `ticket`/
/// `slot`).
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct FrameReadyMsg {
/// Correlates with the render_frame request.
pub ticket: i64,
/// Index into the worker->main output FrameSlotPool.
pub slot: i32,
}
/// `cancel` — abandon an in-flight ticket by id.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct CancelMsg {
/// The in-flight ticket id to abandon.
pub ticket: i64,
}
/// `load_graph` — path to a temporary file holding the serialized graph.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct LoadGraphMsg {
/// Path to the temporary file holding the serialized graph.
pub path: String,
}
/// Build a worker-side error report, mirroring `error_message()` in
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
/// non-zero.
pub fn error_message(message: &str, ticket: Option<i64>) -> Value {
match ticket.filter(|t| *t != 0) {
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
None => json!({ "type": TYPE_ERROR, "message": message }),
}
}
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
/// `ipcmessage.cpp write_message()`.
pub fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
let line =
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
w.write_all(line.as_bytes())?;
w.write_all(b"\n")
}
// ---------------------------------------------------------------------------
// Shared-memory frame-slot transport
// ---------------------------------------------------------------------------
/// `OAK_IPC_SHM_KEY_CAP` — capacity of shm key strings (ipc.h), incl. NUL.
pub const OAK_IPC_SHM_KEY_CAP: usize = 128;
/// `OAK_IPC_COLORSPACE_CAP` — capacity of `oak_frame_slot_meta::colorspace`.
@@ -77,6 +251,8 @@ pub enum ShmMode {
}
impl ShmMode {
/// Map the C ABI mode integer (`OAK_IPC_SHM_MODE_CREATE` = 0,
/// `OAK_IPC_SHM_MODE_ATTACH` = 1) back to the enum.
fn from_c(v: c_int) -> ShmMode {
match v {
0 => ShmMode::Create,
@@ -748,7 +924,7 @@ impl SharedMemoryRegion {
/// Unmap and (if owner) unlink the segment. Also called by `Drop`.
pub fn close(&mut self) {
if !self.data.is_null() {
unsafe { libc::munmap(self.data as *mut c_void, self.size) };
unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) };
self.data = ptr::null_mut();
}
if self.fd >= 0 {
@@ -804,7 +980,503 @@ impl Drop for SharedMemoryRegion {
}
}
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// ---- Control-plane protocol ------------------------------------------
#[test]
fn handshake_wire_format_matches_cpp_field_names() {
let hs = HandshakeMsg {
protocol_version: 1,
shm_key: "olive-rw-1234-0-out".into(),
input_shm_key: "".into(),
input_slots: 0,
output_slots: 6,
slot_data_bytes: 4096,
input_slot_data_bytes: 0,
};
let value = hs.to_json();
// Key order is not part of the contract (JSON objects; the C++
// QJsonObject is hash-ordered too), but the names must match the
// C++ serializer exactly.
assert_eq!(value["type"], "handshake");
assert_eq!(value["protocol_version"], 1);
assert_eq!(value["shm_key"], "olive-rw-1234-0-out");
assert_eq!(value["input_shm_key"], "");
assert_eq!(value["input_slots"], 0);
assert_eq!(value["output_slots"], 6);
assert_eq!(value["slot_data_bytes"], 4096);
assert_eq!(value["input_slot_data_bytes"], 0);
// And the serialized line must parse back to the same object.
let round: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap();
assert_eq!(round, value);
}
#[test]
fn render_frame_parse_accepts_cpp_field_names() {
let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#;
let m: RenderFrameMsg = serde_json::from_str(json).unwrap();
assert_eq!(m.ticket, 42);
assert_eq!(m.node, "abcd");
assert_eq!(m.time_num, 1);
assert_eq!(m.time_den, 24);
assert_eq!(m.width, 1920);
assert_eq!(m.input_slot, -1);
}
#[test]
fn render_frame_defaults_on_missing_fields() {
// The C++ parser defaults missing fields (QJsonValue defaults);
// serde(default) mirrors that.
let m: RenderFrameMsg =
serde_json::from_str(r#"{"type":"render_frame","ticket":7}"#).unwrap();
assert_eq!(m.ticket, 7);
assert_eq!(m.time_den, 0);
assert!(m.node.is_empty());
assert!(!m.has_color_transform);
}
#[test]
fn error_message_carries_ticket_only_when_nonzero() {
assert_eq!(
error_message("boom", None),
json!({ "type": "error", "message": "boom" })
);
assert_eq!(
error_message("boom", Some(0)),
json!({ "type": "error", "message": "boom" })
);
assert_eq!(
error_message("boom", Some(9)),
json!({ "type": "error", "message": "boom", "ticket": 9 })
);
}
#[test]
fn write_message_emits_one_json_line() {
let mut buf = Vec::new();
write_message(&mut buf, &json!({ "type": "shutdown" })).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "{\"type\":\"shutdown\"}\n");
}
// ---- Shared-memory transport -----------------------------------------
/// A unique, temporary POSIX segment key for a test (pid + counter), so
/// parallel test runs never collide.
fn test_key(name: &str) -> String {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
+ &format!("-{name}")
}
/// Create one segment and map it a second time — the in-process
/// equivalent of two processes sharing a segment. Returns
/// `(owner_region, peer_region)`; both must be kept alive for the
/// whole test (the peer is an attach that does not unlink).
fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) {
let mut owner = SharedMemoryRegion::new();
assert!(
owner.open(key, size, ShmMode::Create),
"create failed: {}",
owner.error()
);
let mut peer = SharedMemoryRegion::new();
assert!(
peer.open(key, size, ShmMode::Attach),
"attach failed: {}",
peer.error()
);
(owner, peer)
}
// ---- SpscRingBuffer -------------------------------------------------
#[test]
fn ring_bytes_needed_matches_cpp_layout() {
// 12 header bytes + capacity * 4.
assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16);
assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20);
assert_eq!(SpscRingBuffer::bytes_needed(0), 12);
}
#[test]
fn ring_empty_full_and_single_entry() {
let key = test_key("ring-empty");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and at least `size` bytes.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
assert!(unsafe { cons.is_empty_approx() });
let mut v = 99;
assert!(!unsafe { cons.pop(&mut v) });
assert_eq!(v, 99);
assert!(unsafe { prod.push(7) });
assert!(!unsafe { cons.is_empty_approx() });
assert_eq!(unsafe { cons.size_approx() }, 1);
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, 7);
assert!(unsafe { cons.is_empty_approx() });
}
#[test]
fn ring_capacity_minus_one_live_entries() {
// A ring of capacity N holds at most N-1 entries (one slot is
// always left empty to tell full from empty).
let key = test_key("ring-cap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
// The 4th push must fail: head would collide with tail.
assert!(!unsafe { prod.push(99) });
let mut v = 0;
for expected in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
assert!(!unsafe { cons.pop(&mut v) });
}
#[test]
fn ring_wraparound_preserves_order() {
// Fill, drain, then wrap past the end of the slot array: cursors
// are modulo-capacity, order must be preserved across the wrap.
let key = test_key("ring-wrap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
let mut v = 0;
for _ in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
}
// Ring is empty again; push past the wrap point.
for i in 3..6 {
assert!(unsafe { prod.push(i) });
}
for expected in 3..6 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
}
// ---- FrameSlotPool --------------------------------------------------
#[test]
fn framepool_bytes_needed_matches_cpp_offsets() {
// Recompute by hand with the C++ layout: header 64, each ring
// align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data
// align_up(slot_bytes, 64) * n.
let check = |n: u32, slot: usize| {
let ring = align_up(12 + 4 * (n as usize + 1), 64);
let expected =
64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize;
assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected);
};
check(4, 4096);
check(6, 1_000_000);
check(1, 64);
check(3, 100);
}
#[test]
fn framepool_create_attach_two_processes_both_directions() {
// "Two processes": two mappings of the same segment. Owner creates
// the pool; the peer attaches. A filler on one side and a drainer
// on the other exchange slots in both directions.
let key = test_key("pool-bidi");
let slots = 4u32;
let slot_bytes = 64usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and sized by bytes_needed.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(filler.is_valid());
assert!(drainer.is_valid());
assert_eq!(drainer.slot_count(), slots);
assert_eq!(drainer.slot_data_bytes(), slot_bytes);
// Filler acquires every slot exactly once (seeded free ring), then
// the free ring is empty.
let mut got = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
got.push(s);
}
got.sort_unstable();
assert_eq!(got, vec![0, 1, 2, 3]);
let mut extra = 0;
assert!(!unsafe { filler.acquire(&mut extra) });
// Drainer sees nothing ready yet.
assert!(!unsafe { drainer.consume(&mut extra) });
// Filler writes pixels + meta into two slots and publishes them.
for (i, slot) in [0u32, 2u32].iter().enumerate() {
// SAFETY: `slot` was acquired above.
let data = unsafe { filler.slot_data(*slot) };
unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(*slot) };
meta.id = 100 + *slot as i64;
meta.width = 8;
meta.height = 8;
meta.data_size = slot_bytes as i32;
assert!(unsafe { filler.publish(*slot) });
}
// Drainer consumes them through its own mapping and sees the same
// payloads and metadata.
let mut consumed = Vec::new();
for _ in 0..2 {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: s was consumed.
let data = unsafe { drainer.slot_data_const(s) };
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, 100 + s as i64);
assert_eq!(meta.width, 8);
assert_eq!(meta.data_size, slot_bytes as i32);
// SAFETY: slot_bytes readable in the slot block.
let first = unsafe { *data };
assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8);
consumed.push(s);
}
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 2]);
assert!(!unsafe { drainer.consume(&mut extra) });
// Drainer releases the slots back; the filler can acquire them
// again — the full round trip through both rings.
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
assert_eq!(s, 0);
}
#[test]
fn framepool_wraparound_and_full_edges() {
// Small pool: cycle every slot many times, verifying the rings'
// modulo behavior end to end.
let key = test_key("pool-wrap");
let slots = 3u32;
let slot_bytes = 32usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
for cycle in 0..4u32 {
let mut published = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}");
// SAFETY: acquired slot.
unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(s) };
meta.id = i64::from(cycle * 100 + s);
assert!(unsafe { filler.publish(s) });
published.push(s);
}
// Pool is full on the filler side.
let mut x = 0;
assert!(!unsafe { filler.acquire(&mut x) });
// Drain everything on the drainer side.
let mut consumed = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: consumed slot.
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, i64::from(cycle * 100 + s));
// SAFETY: 1 byte readable.
assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8);
consumed.push(s);
}
assert!(!unsafe { drainer.consume(&mut x) });
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 1, 2]);
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
}
}
#[test]
fn framepool_attach_rejects_wrong_magic() {
let key = test_key("pool-badmagic");
let size = FrameSlotPool::bytes_needed(2, 16);
let (owner, _peer) = two_mappings(&key, size);
// Overwrite the header area with garbage — no pool magic.
// SAFETY: owner mapping is live.
unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) };
// SAFETY: buffer is live.
let pool = unsafe { FrameSlotPool::attach(owner.data()) };
assert!(!pool.is_valid());
assert_eq!(pool.slot_count(), 0);
assert_eq!(pool.slot_data_bytes(), 0);
}
#[test]
fn framepool_pool_over_reused_segment_is_consistent() {
// A pool that has been cycled fully and then attached fresh reports
// the same geometry as bytes_needed computed it.
let key = test_key("pool-geometry");
let slots = 5u32;
let slot_bytes = 1000usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let attached = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(attached.is_valid());
assert_eq!(attached.slot_count(), slots);
assert_eq!(attached.slot_data_bytes(), slot_bytes);
// Slot stride is 64-aligned (matches the C++ data layout).
// SAFETY: valid pool.
let s0 = unsafe { attached.slot_data(0) };
let s1 = unsafe { attached.slot_data(1) };
assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN));
}
// ---- SharedMemoryRegion ---------------------------------------------
#[test]
fn region_create_attach_write_visibility() {
let key = test_key("region-vis");
let size = 4096usize;
let (mut owner, mut peer) = two_mappings(&key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
assert_eq!(owner.size(), size);
assert_eq!(peer.size(), size);
assert_eq!(owner.key(), key);
assert_eq!(peer.key(), key);
// Owner writes; peer sees it through its own mapping.
// SAFETY: both mappings are live with `size` bytes.
unsafe {
let dst = owner.data() as *mut u32;
*dst = 0xDEADBEEF;
}
// SAFETY: peer mapping live.
let seen = unsafe { *(peer.data() as *const u32) };
assert_eq!(seen, 0xDEADBEEF);
// Peer writes back; owner sees it.
// SAFETY: peer mapping live.
unsafe {
let dst = peer.data() as *mut u32;
*dst = 0x12345678;
}
// SAFETY: owner mapping live.
assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678);
// Closing the ATTACH side does not unlink: while the owner lives,
// a third mapping can still open the name.
peer.close();
assert!(!peer.is_valid());
let mut third = SharedMemoryRegion::new();
assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error());
assert!(third.is_valid());
third.close();
// Closing the OWNER unlinks the segment; further attaches fail.
owner.close();
assert!(!owner.is_valid());
let mut fourth = SharedMemoryRegion::new();
assert!(!fourth.open(&key, size, ShmMode::Attach));
}
#[test]
fn region_create_replaces_stale_segment() {
// Mirrors the C++: Create unlinks any stale segment with the same
// name first (crash cleanup), so a second Create SUCCEEDS and owns
// a fresh, zeroed segment.
let key = test_key("region-exists");
let size = 128usize;
let (mut owner, _peer) = two_mappings(&key, size);
assert!(owner.is_valid());
// SAFETY: owner mapping live.
unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE };
let mut second = SharedMemoryRegion::new();
assert!(
second.open(&key, size, ShmMode::Create),
"{}",
second.error()
);
assert!(second.is_valid());
// The replacement segment is fresh (zeroed by create).
// SAFETY: second mapping live.
assert_eq!(unsafe { *(second.data() as *const u32) }, 0);
}
#[test]
fn region_attach_fails_when_segment_too_small() {
// macOS rounds shm segment sizes up to a 16 KiB minimum, so use
// sizes above that to exercise the size check.
let key = test_key("region-small");
let (owner, _peer) = two_mappings(&key, 4096);
assert!(owner.is_valid());
// Attaching with a larger size than the segment must fail (the
// fstat check, mirroring the C++).
let mut big = SharedMemoryRegion::new();
assert!(!big.open(&key, 65536, ShmMode::Attach));
assert!(!big.is_valid());
assert!(!big.error().is_empty());
}
#[test]
fn region_make_key_format() {
assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3");
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
}
#[test]
fn region_keys_are_isolation_safe() {
// Keys with slashes are flattened to a single-slash POSIX name.
let key = "a/b/c";
let size = 64usize;
let (mut owner, mut peer) = two_mappings(key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
// The actual POSIX name is "/a_b_c".
// SAFETY: mapping live.
unsafe { *(owner.data() as *mut u32) = 7 };
// SAFETY: peer mapping live.
assert_eq!(unsafe { *(peer.data() as *const u32) }, 7);
}
}
// C ABI exports (engine/include/oakengine/ipc.h)
// ---------------------------------------------------------------------------
@@ -1219,7 +1891,7 @@ pub unsafe extern "C" fn oakengine_ipc_framepool_release(
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
mod cabi_tests {
use super::*;
/// A unique, temporary POSIX segment key for a test (pid + counter), so
+14 -8
View File
@@ -66,7 +66,6 @@
pub use oaknode;
pub mod audio;
pub mod bridge;
pub mod codec;
pub mod common;
pub mod deferred;
@@ -77,28 +76,35 @@ pub mod ipc;
pub mod linkage;
pub mod node;
pub mod plugin;
pub mod pods;
pub mod render;
pub mod stubs;
pub mod task;
pub mod testmedia;
pub mod timeline;
pub mod undo;
pub mod worker;
/// The former `tests/*.rs` integration tests, now unit tests (the facade
/// is cdylib-only, so integration tests cannot link it as an rlib crate;
/// see `test_support/mod.rs`).
#[cfg(test)]
#[path = "test_support/mod.rs"]
mod tests;
#[cfg(test)]
mod test_link {
// The lib's own unit-test binary must link the module crates' rlibs to
// satisfy the facade's `extern "C"` imports that the unit tests compile
// in — e.g. the worker session's oakrender display renderer (src/worker.rs).
// satisfy the facade's imports that the unit tests compile in — e.g.
// the render family's oakrender display renderer (src/render.rs).
// The integration tests do the same through tests/common/mod.rs
// `force_link()`; this covers the `cargo test` unit-test binary.
#![allow(dead_code)]
fn force_link() -> usize {
let fns: [usize; 4] = [
oakrender::ffi::renderer::oakrender_display_renderer_create_opengl as *const ()
as usize,
oaknode::ffi::project::oaknode_project_init as *const () as usize,
oaktimeline::ffi::marker::oaktimeline_marker_list_create as *const () as usize,
oaktask::ffi::manager::oaktask_manager_init as *const () as usize,
oakrender::backend::DisplayRenderer::new as *const () as usize,
oaknode::project::Project::new as *const () as usize,
oaktimeline::marker::TimelineMarkerList::new as *const () as usize,
oaktask::manager::TaskManager::init as *const () as usize,
];
fns.iter().sum()
}
+16 -15
View File
@@ -39,23 +39,24 @@
/// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib
/// for that lookup to succeed.
fn force_link() -> usize {
let fns: [usize; 13] = [
let fns: [usize; 11] = [
// oakcore-rs (pure value types; referenced so its rlib is linked).
oakcore_rs::Rational::new(1, 2).numerator() as usize,
// One exported C ABI symbol per module crate.
oakundo::ffi::undostack::oakundo_undostack_init as usize,
oakcommon::ffi::config::oakcommon_config_get_int as usize,
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
oaktask::ffi::manager::oaktask_manager_init as usize,
oakplugin::ffi::oakplugin_host_plugin_count as usize,
oaknode::ffi::project::oaknode_project_init as usize,
// oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs).
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
oakundo::ffi::command::oakundo_command_init as usize,
// One public direct-Rust symbol per module crate. oakundo/oakcommon
// no longer export a C ABI; their handle-level Rust API functions
// serve as the link anchors.
oakundo::undostack::undostack_init as usize,
oakcommon::configstore::ConfigStore::instance as usize,
oaktimeline::marker::TimelineMarkerList::new as usize,
oakcodec::exportformat::Format::get_name as usize,
oakrender::manager::RenderManager::init as usize,
oaktask::manager::TaskManager::init as usize,
oaknode::project::Project::new as usize,
// oaknode's serializer resolves oakcommon XML/undo symbols at
// runtime; anchors for the dylib.
oakcommon::xmlutils::XmlWriter::new as usize,
oakcommon::xmlutils::XmlReader::new as usize,
oakundo::undocommand::command_init as usize,
];
fns.iter().sum()
}
+520 -166
View File
@@ -19,8 +19,8 @@
use std::ffi::{c_char, c_int, c_void};
use crate::bridge::common as c;
use crate::bridge::node as n;
use crate::stubs::common as c;
use crate::stubs::node as n;
use crate::common::OakVideoParamsPod;
use crate::error::{Error, Result};
use crate::handle::{
@@ -2518,13 +2518,16 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed(
self_: *const OakEngineNode,
input_id: *const c_char,
) -> c_int {
// Stub: the oaknode module has no keyframing-enabled query.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
Ok(0)
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(rc)
}
})
}
@@ -2534,13 +2537,16 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count(
self_: *const OakEngineNode,
input_id: *const c_char,
) -> c_int {
// Stub: the oaknode module has no keyframe enumeration C ABI.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
Ok(0)
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(rc)
}
})
}
@@ -2553,16 +2559,20 @@ pub unsafe extern "C" fn oakengine_node_keyframe_at(
time_ts: *mut i64,
value: *mut OakNodeValue,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
if self_.is_null() || input_id.is_null() || time_ts.is_null() || value.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = index;
let _ = time_ts;
let _ = value;
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let mut num: i64 = 0;
let mut den: i64 = 0;
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, value);
Error::from_module(rc)?;
// Rational seconds -> frame timestamp in the project time base.
let ts = num as i128 * tb.1 as i128 / den as i128 / tb.0 as i128;
*time_ts = ts as i64;
Ok(())
})
}
@@ -2578,14 +2588,38 @@ pub unsafe extern "C" fn oakengine_node_keyframe_get_easing(
y2: *mut f32,
type_: *mut c_int,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
if self_.is_null() || input_id.is_null() || type_.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (index, x1, y1, x2, y2, type_);
Err(Error::NotFound)
let h = unbox(self_)?;
// Locate the key's time through the count/at pair.
let mut num: i64 = 0;
let mut den: i64 = 0;
let mut dummy = OakNodeValue::none();
Error::from_module(n::oaknode_node_keyframe_at(
h, input_id, index, &mut num, &mut den, &mut dummy,
))?;
Error::from_module(n::oaknode_node_keyframe_type_at(
h, input_id, num, den, type_,
))?;
if !x1.is_null() {
let mut bx: f64 = 0.0;
let mut by: f64 = 0.0;
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 0, &mut bx, &mut by) == 0 {
*x1 = bx as f32;
*y1 = by as f32;
}
}
if !x2.is_null() {
let mut bx: f64 = 0.0;
let mut by: f64 = 0.0;
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 1, &mut bx, &mut by) == 0 {
*x2 = bx as f32;
*y2 = by as f32;
}
}
Ok(())
})
}
@@ -2634,14 +2668,14 @@ pub unsafe extern "C" fn oakengine_node_keyframe_remove(
input_id: *const c_char,
time_ts: i64,
) -> c_int {
// Stub: the oaknode module has no remove-keyframe C ABI.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = time_ts;
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
})
}
@@ -2685,11 +2719,31 @@ pub unsafe extern "C" fn oakengine_node_insert_keyframe_command(
pub unsafe extern "C" fn oakengine_node_remove_keyframe_command(
keyframe: *mut OakEngineKeyframe,
) -> *mut c_void {
// Stub: the module has no remove-keyframe command creator (the module
// keyframe handles are detached values, not track members).
guard_ptr(|| unsafe {
let _ = unbox(keyframe);
Ok(std::ptr::null_mut())
let h = unbox(keyframe)?;
let mut num: i64 = 0;
let mut den: i64 = 0;
Error::from_module(n::oaknode_keyframe_get_time(h, &mut num, &mut den))?;
let mut input_buf = [0 as c_char; 256];
let rc = n::oaknode_keyframe_get_input(h, input_buf.as_mut_ptr(), 256);
if rc < 0 {
return Ok(std::ptr::null_mut());
}
let mut parent = CHandle::null();
Error::from_module(n::oaknode_keyframe_get_parent(h, &mut parent))?;
// Build the undo command as a closure over the remove path (the
// module has no remove-key command creator; the closure carries
// the same semantics).
let cmd = crate::stubs::node::box_keyframe_remove_command(
parent,
input_buf.as_ptr(),
num,
den,
);
if cmd.ctx.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(command_box(cmd)?.cast())
})
}
@@ -2746,17 +2800,52 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_easing(
x2: f32,
y2: f32,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count` (no easing accessors).
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (time_ts, type_, x1, y1, x2, y2);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
Error::from_module(n::oaknode_node_keyframe_set_type(
h, input_id, t_num, t_den, type_,
))?;
Error::from_module(n::oaknode_node_keyframe_set_bezier(
h, input_id, t_num, t_den, 0, x1 as f64, y1 as f64,
))?;
Error::from_module(n::oaknode_node_keyframe_set_bezier(
h, input_id, t_num, t_den, 1, x2 as f64, y2 as f64,
))
})
}
/// Apply a value-at-time write WITHOUT pushing an undo row (the
/// `*_many` keyframe editors apply live writes; documented deviation
/// from the C++ multi commands).
///
/// # Safety
/// `h` must be a live module node handle; `input_id` a valid
/// NUL-terminated string; `v` a live POD.
unsafe fn live_set_value_at_time(
h: CHandle,
input_id: *const c_char,
t_num: i64,
t_den: i64,
v: *const OakNodeValue,
) -> Result<()> {
unsafe {
let mut cmd: CHandle = CHandle::null();
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
h, input_id, t_num, t_den, v, 0, &mut cmd,
))?;
let rc = oakundo::undocommand::command_redo_now(cmd);
let mut cmd_h = cmd;
oakundo::undocommand::command_free(&mut cmd_h);
Error::from_module(rc)
}
}
/// `oakengine_node_keyframes_set_type_many`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
@@ -2768,14 +2857,23 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
count: c_int,
type_: c_int,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
// Live per-key type writes (the C++ grouped them into one command;
// documented deviation — no undo row is created).
guard(|| unsafe {
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, times_ts, tracks, count, type_);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let _ = (element, tracks);
for i in 0..count as usize {
let ts = *times_ts.add(i);
let (t_num, t_den) = ts_to_time(ts, tb);
Error::from_module(n::oaknode_node_keyframe_set_type(
h, input_id, t_num, t_den, type_,
))?;
}
Ok(())
})
}
@@ -2790,14 +2888,29 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_time_many(
count: c_int,
new_time_ts: i64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
// Live per-key re-times: each key is removed from its old time and
// re-inserted at the new one with its value (no undo row; documented).
guard(|| unsafe {
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && old_times_ts.is_null())
{
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, old_times_ts, tracks, count, new_time_ts);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (new_num, new_den) = ts_to_time(new_time_ts, tb);
let _ = (element, tracks);
for i in 0..count as usize {
let old_ts = *old_times_ts.add(i);
let (old_num, old_den) = ts_to_time(old_ts, tb);
let mut v = OakNodeValue::none();
let rc = n::oaknode_node_get_input_at_time(h, input_id, old_num, old_den, &mut v);
if rc != 0 {
return Err(Error::Module(rc));
}
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, old_num, old_den))?;
live_set_value_at_time(h, input_id, new_num, new_den, &v)?;
}
Ok(())
})
}
@@ -2813,14 +2926,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_value_many(
values: *const OakNodeValue,
old_values: *const OakNodeValue,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
// Live per-key value writes (no undo row; the C++ grouped them —
// documented deviation).
guard(|| unsafe {
if self_.is_null()
|| input_id.is_null()
|| count < 0
|| (count > 0 && (times_ts.is_null() || values.is_null()))
{
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, times_ts, tracks, count, values, old_values);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let _ = (element, tracks, old_values);
for i in 0..count as usize {
let ts = *times_ts.add(i);
let (t_num, t_den) = ts_to_time(ts, tb);
live_set_value_at_time(h, input_id, t_num, t_den, values.add(i))?;
}
Ok(())
})
}
@@ -2838,14 +2962,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_bezier_many(
out_x: f64,
out_y: f64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
// Live per-key bezier writes (no undo row; documented deviation).
guard(|| unsafe {
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, times_ts, tracks, count, in_x, in_y, out_x, out_y);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let _ = (element, tracks);
for i in 0..count as usize {
let ts = *times_ts.add(i);
let (t_num, t_den) = ts_to_time(ts, tb);
Error::from_module(n::oaknode_node_keyframe_set_bezier(
h, input_id, t_num, t_den, 0, in_x, in_y,
))?;
Error::from_module(n::oaknode_node_keyframe_set_bezier(
h, input_id, t_num, t_den, 1, out_x, out_y,
))?;
}
Ok(())
})
}
@@ -2863,14 +2998,17 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_bezier_point(
old_x: f64,
old_y: f64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
if self_.is_null() || input_id.is_null() || (point_index != 0 && point_index != 1) {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, time_ts, track, point_index, x, y, old_x, old_y);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
let _ = (element, track, old_x, old_y);
Error::from_module(n::oaknode_node_keyframe_set_bezier(
h, input_id, t_num, t_den, point_index, x, y,
))
})
}
@@ -2881,14 +3019,11 @@ pub unsafe extern "C" fn oakengine_node_keyframes_clear(
self_: *mut OakEngineNode,
input_id: *const c_char,
) -> c_int {
// Stub: the module has no clear-keyframes command; since it also has
// no keyframes, the documented no-op result is returned.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
Ok(())
Error::from_module(n::oaknode_node_clear_keyframes(unbox(self_)?, input_id))
})
}
@@ -3040,14 +3175,19 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed_ex(
input_id: *const c_char,
element: c_int,
) -> c_int {
// Stub: see `oakengine_node_input_is_keyframed`.
// Whole-value tracks: the element selector is recorded but the track
// query covers the (input, -1) track (documented deviation).
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
let _ = element;
Ok(0)
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(rc)
}
})
}
@@ -3101,14 +3241,29 @@ pub unsafe extern "C" fn oakengine_node_input_get_default_value(
track: c_int,
out: *mut OakNodeValue,
) -> c_int {
// Stub: the oaknode module has no default-value export.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() || out.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = track;
Err(Error::NotFound)
// The declared type's default maps back into the POD (same
// conversion as the getter family).
let mut ty: c_int = 0;
Error::from_module(n::oaknode_node_input_get_type(
unbox(self_)?,
input_id,
&mut ty,
))?;
// Best-effort: the default of the enabled input is Boolean(true);
// other inputs report the None POD (documented deviation).
let default = OakNodeValue {
kind: ty,
num: if ty == value_type::BOOL { 1 } else { 0 },
den: 0,
f: [0.0; 4],
};
*out = default;
Ok(())
})
}
@@ -5271,14 +5426,19 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count_on_track(
element: c_int,
track: c_int,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
// Whole-value tracks: the element/track selectors are recorded but
// the count covers the (input, -1) track (documented deviation).
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
let _ = (element, track);
Ok(0)
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(rc)
}
})
}
@@ -5293,14 +5453,35 @@ pub unsafe extern "C" fn oakengine_node_keyframes_toggle_at_time(
on: c_int,
undo_name: *const c_char,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, time_ts, track, on, undo_name);
Err(Error::NotFound)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
let has = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
if has < 0 {
return Err(Error::Module(has));
}
let _ = (element, track);
if on != 0 && has == 0 {
// Insert a key at the time carrying the current value.
let mut v = OakNodeValue::none();
let rc = n::oaknode_node_get_input_at_time(h, input_id, t_num, t_den, &mut v);
if rc != 0 {
return Err(Error::Module(rc));
}
let mut cmd: CHandle = CHandle::null();
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
h, input_id, t_num, t_den, &v, 0, &mut cmd,
))?;
push_command(cmd, if undo_name.is_null() { "Toggle Keyframe" } else { "" })
} else if on == 0 && has == 1 {
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
} else {
Ok(())
}
})
}
@@ -5313,17 +5494,70 @@ pub unsafe extern "C" fn oakengine_node_has_keyframe_at_time(
time_ts: i64,
track: c_int,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
let _ = (element, time_ts, track);
Ok(0)
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
let _ = (element, track);
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(rc)
}
})
}
/// First (earliest) or last (latest) keyframe time of an input, written
/// as a rational-seconds pair (0/1 when the input has no keys).
///
/// # Safety
/// `self_` must be a live engine node handle; `input_id` a valid
/// NUL-terminated string.
unsafe fn keyframe_extreme_time(
self_: *const OakEngineNode,
input_id: *const c_char,
earliest: bool,
num: *mut i64,
den: *mut i64,
) -> Result<c_int> {
unsafe {
if self_.is_null() || input_id.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let count = n::oaknode_node_keyframe_count(h, input_id);
if count <= 0 {
if !num.is_null() {
*num = 0;
}
if !den.is_null() {
*den = 1;
}
return Ok(0);
}
let index = if earliest { 0 } else { count - 1 };
let mut k_num: i64 = 0;
let mut k_den: i64 = 0;
let mut dummy = OakNodeValue::none();
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut k_num, &mut k_den, &mut dummy);
if rc != 0 {
return Err(Error::Module(rc));
}
if !num.is_null() {
*num = k_num;
}
if !den.is_null() {
*den = k_den;
}
Ok(1)
}
}
/// `oakengine_node_keyframe_earliest_time`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
@@ -5333,21 +5567,9 @@ pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
num: *mut i64,
den: *mut i64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
set_node_error("invalid arguments");
return Ok(0);
}
let _ = unbox(self_)?;
let _ = element;
if !num.is_null() {
*num = 0;
}
if !den.is_null() {
*den = 1;
}
Ok(0)
keyframe_extreme_time(self_, input_id, true, num, den)
})
}
@@ -5360,22 +5582,85 @@ pub unsafe extern "C" fn oakengine_node_keyframe_latest_time(
num: *mut i64,
den: *mut i64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
let _ = element;
keyframe_extreme_time(self_, input_id, false, num, den)
})
}
/// Closest keyframe time strictly before (or at/after) a frame timestamp,
/// written as rational seconds (0/1 when none — the documented neutral
/// result).
///
/// # Safety
/// `self_` must be a live engine node handle; `input_id` a valid
/// NUL-terminated string.
unsafe fn closest_keyframe_time(
self_: *const OakEngineNode,
input_id: *const c_char,
element: c_int,
time_ts: i64,
track: c_int,
before: bool,
num: *mut i64,
den: *mut i64,
) -> Result<c_int> {
unsafe {
if self_.is_null() || input_id.is_null() {
set_node_error("invalid arguments");
return Ok(0);
}
let _ = unbox(self_)?;
let _ = element;
if !num.is_null() {
*num = 0;
let h = unbox(self_)?;
let tb = time_base_for(h);
let (t_num, t_den) = ts_to_time(time_ts, tb);
let target = t_num as f64 / t_den as f64;
let _ = (element, track);
let count = n::oaknode_node_keyframe_count(h, input_id);
let mut best: Option<(i64, i64)> = None;
for i in 0..count {
let mut k_num: i64 = 0;
let mut k_den: i64 = 0;
let mut dummy = OakNodeValue::none();
if n::oaknode_node_keyframe_at(h, input_id, i, &mut k_num, &mut k_den, &mut dummy) != 0 {
continue;
}
let k = k_num as f64 / k_den as f64;
let matches = if before { k < target } else { k >= target };
if !matches {
continue;
}
match best {
Some((bn, bd)) => {
let b = bn as f64 / bd as f64;
let closer = if before { k > b } else { k < b };
if closer {
best = Some((k_num, k_den));
}
}
None => best = Some((k_num, k_den)),
}
}
if !den.is_null() {
*den = 1;
match best {
Some((n_, d_)) => {
if !num.is_null() {
*num = n_;
}
if !den.is_null() {
*den = d_;
}
Ok(1)
}
None => {
if !num.is_null() {
*num = 0;
}
if !den.is_null() {
*den = 1;
}
Ok(0)
}
}
Ok(0)
})
}
}
/// `oakengine_node_keyframe_closest_time_before`.
@@ -5389,21 +5674,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_before(
num: *mut i64,
den: *mut i64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
set_node_error("invalid arguments");
return Ok(0);
}
let _ = unbox(self_)?;
let _ = (element, time_ts, track);
if !num.is_null() {
*num = 0;
}
if !den.is_null() {
*den = 1;
}
Ok(0)
closest_keyframe_time(self_, input_id, element, time_ts, track, true, num, den)
})
}
@@ -5418,21 +5690,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_after(
num: *mut i64,
den: *mut i64,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() {
set_node_error("invalid arguments");
return Ok(0);
}
let _ = unbox(self_)?;
let _ = (element, time_ts, track);
if !num.is_null() {
*num = 0;
}
if !den.is_null() {
*den = 1;
}
Ok(0)
closest_keyframe_time(self_, input_id, element, time_ts, track, false, num, den)
})
}
@@ -5445,10 +5704,23 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_on_track(
track: c_int,
index: c_int,
) -> *mut OakEngineKeyframe {
// Stub: see `oakengine_node_keyframe_count`.
guard_ptr(|| {
let _ = (self_, input_id, element, track, index);
Ok(std::ptr::null_mut())
guard_ptr(|| unsafe {
if self_.is_null() || input_id.is_null() || index < 0 {
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let mut num: i64 = 0;
let mut den: i64 = 0;
let mut dummy = OakNodeValue::none();
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, &mut dummy);
if rc != 0 {
return Ok(std::ptr::null_mut());
}
let kf = n::oaknode_keyframe_create(num, den, &dummy, 0, track, element, input_id, h);
if kf.ctx.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineKeyframe>(kf))
})
}
@@ -5462,10 +5734,24 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_at_time(
time_num: i64,
time_den: i64,
) -> *mut OakEngineKeyframe {
// Stub: see `oakengine_node_keyframe_count`.
guard_ptr(|| {
let _ = (self_, input_id, element, track, time_num, time_den);
Ok(std::ptr::null_mut())
guard_ptr(|| unsafe {
if self_.is_null() || input_id.is_null() || time_den == 0 {
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den);
if rc != 1 {
return Ok(std::ptr::null_mut());
}
let mut dummy = OakNodeValue::none();
if n::oaknode_node_get_input_at_time(h, input_id, time_num, time_den, &mut dummy) != 0 {
return Ok(std::ptr::null_mut());
}
let kf = n::oaknode_keyframe_create(time_num, time_den, &dummy, 0, track, element, input_id, h);
if kf.ctx.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineKeyframe>(kf))
})
}
@@ -5480,17 +5766,26 @@ pub unsafe extern "C" fn oakengine_node_keyframes_at_time(
out_handles: *mut *mut OakEngineKeyframe,
max_handles: c_int,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count`.
guard_int(|| {
let _ = (
self_,
input_id,
element,
time_num,
time_den,
out_handles,
max_handles,
);
guard_int(|| unsafe {
if self_.is_null() || input_id.is_null() || max_handles < 0 {
return Ok(0);
}
if max_handles == 0 {
return Ok(0);
}
let h = unbox(self_)?;
if n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den) != 1 {
return Ok(0);
}
if !out_handles.is_null() {
let kf =
n::oaknode_keyframe_create(time_num, time_den, &OakNodeValue::none(), 0, 0, element, input_id, h);
if !kf.ctx.is_null() {
// SAFETY: the caller guarantees `max_handles` slots.
*out_handles = box_handle::<OakEngineKeyframe>(kf);
return Ok(1);
}
}
Ok(0)
})
}
@@ -5506,15 +5801,20 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing(
enable_all_tracks: c_int,
undo_name: *const c_char,
) -> c_int {
// Stub: the module has no keyframing-enable command; its keyframe
// tracks are never populated through the C ABI.
// In the Rust model an input "is keyframed" when its track holds
// keys; disabling therefore clears the keys, enabling is a no-op
// (documented deviation from the C++ enable flag).
guard(|| unsafe {
if self_.is_null() || input_id.is_null() {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let _ = (element, keyframing, track, enable_all_tracks, undo_name);
Err(Error::Invalid)
let h = unbox(self_)?;
let _ = (element, track, enable_all_tracks, undo_name);
if keyframing != 0 {
Ok(())
} else {
Error::from_module(n::oaknode_node_clear_keyframes(h, input_id))
}
})
}
@@ -5526,9 +5826,19 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing_command(
element: c_int,
keyframing: c_int,
) -> *mut c_void {
// Stub: see `oakengine_node_set_input_keyframing`.
guard_ptr(|| {
let _ = (self_, input_id, element, keyframing);
guard_ptr(|| unsafe {
let rc = oakengine_node_set_input_keyframing(
self_,
input_id,
element,
keyframing,
0,
0,
std::ptr::null(),
);
Error::from_module(rc)?;
// The live write has no command counterpart (see the export
// notes); return NULL like the other non-undoable creators.
Ok(std::ptr::null_mut())
})
}
@@ -5541,14 +5851,33 @@ pub unsafe extern "C" fn oakengine_node_keyframes_paste(
count: c_int,
undo_name: *const c_char,
) -> c_int {
// Stub: see `oakengine_node_set_input_keyframing` (no insert path).
// Live key inserts at each source key's time (no undo row; the C++
// built one command — documented deviation).
guard(|| unsafe {
if self_.is_null() || keyframes.is_null() || count <= 0 {
return Err(Error::Invalid);
}
let _ = unbox(self_)?;
let h = unbox(self_)?;
let _ = undo_name;
Err(Error::Invalid)
for i in 0..count as usize {
let kf = *keyframes.add(i);
if kf.is_null() {
continue;
}
let kh = unbox(kf)?;
let mut num: i64 = 0;
let mut den: i64 = 0;
let mut v = OakNodeValue::none();
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
Error::from_module(n::oaknode_keyframe_get_value(kh, &mut v))?;
let mut input_buf = [0 as c_char; 256];
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
if rc < 0 {
return Err(Error::Module(rc));
}
live_set_value_at_time(h, input_buf.as_ptr(), num, den, &v)?;
}
Ok(())
})
}
@@ -5816,13 +6145,38 @@ pub unsafe extern "C" fn oakengine_keyframes_remove_many(
count: c_int,
undo_name: *const c_char,
) -> c_int {
// Stub: see `oakengine_node_keyframe_count` (no remove-key path).
guard(|| {
// Live per-key removals through each keyframe's track reference (no
// undo row; documented deviation).
guard(|| unsafe {
if keyframes.is_null() || count <= 0 {
return Err(Error::Invalid);
}
let _ = undo_name;
Err(Error::Invalid)
for i in 0..count as usize {
let kf = *keyframes.add(i);
if kf.is_null() {
continue;
}
let kh = unbox(kf)?;
let mut num: i64 = 0;
let mut den: i64 = 0;
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
let mut input_buf = [0 as c_char; 256];
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
if rc < 0 {
return Err(Error::Module(rc));
}
let mut parent = CHandle::null();
Error::from_module(n::oaknode_keyframe_get_parent(kh, &mut parent))?;
Error::from_module(n::oaknode_node_remove_keyframe(
parent,
input_buf.as_ptr(),
num,
den,
))?;
release_handle(parent);
}
Ok(())
})
}
+1 -1
View File
@@ -26,7 +26,7 @@
use std::ffi::{c_char, c_int, c_void};
use std::sync::{Mutex, OnceLock};
use crate::bridge::plugin as p;
use crate::stubs::plugin as p;
use crate::error::Error;
use crate::handle::guard;
+275
View File
@@ -0,0 +1,275 @@
// 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/>.
//! POD mirrors for the deleted `src/bridge/` (single-lib unification).
//!
//! The facade still exchanges plain-`repr(C)` PODs with the module crates
//! (and, upward, with the host app through the frozen `oakengine_*` C
//! ABI). The deleted bridge aliased these types to the module crates'
//! `ffi` declarations; those ffi modules are gone, so the engine keeps
//! its own mirrors here. Where a module crate still owns the canonical
//! POD (oakcodec's [`EncodingParamsPOD`]) the facade aliases it directly.
use std::ffi::c_int;
/// `oakcodec_encoding_params` (`include/codec/encoder.h`) — single-lib
/// unification: aliases the oakcodec crate's POD
/// ([`oakcodec::encodingparams::EncodingParams`], identical `#[repr(C)]`
/// layout), so the facade's encoding-params handle reads/writes fields of
/// exactly the struct the oakcodec/oakaudio creators consume.
pub type EncodingParamsPOD = oakcodec::encodingparams::EncodingParams;
/// `oak_export_options` (`engine/include/oakengine/exporter.h`) — POD
/// export parameters for [`crate::codec::oakengine_export_render`]. 0 (or
/// negative) fields select the documented per-field default; the codec
/// fields carry the exporter.h `OAKENGINE_EXPORT_VIDEO_*` /
/// `OAKENGINE_EXPORT_AUDIO_*` values (NOT the engine's `ExportCodec`
/// ids — see the mapping notes on `oakengine_export_render`).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakExportOptions {
/// `OAKENGINE_EXPORT_VIDEO_*` value; default H264.
pub video_codec: c_int,
/// `OAKENGINE_EXPORT_AUDIO_*` value; default AAC;
/// [`OAKENGINE_EXPORT_AUDIO_NONE`] disables the audio track.
pub audio_codec: c_int,
/// Video bit rate in bit/s; <= 0 lets the encoder choose (FFmpeg
/// defaults).
pub video_bit_rate: i64,
/// Audio sample rate in Hz; <= 0 uses the engine's default (48 kHz).
pub audio_sample_rate: c_int,
/// Audio channel count (1 = mono, 2 = stereo); <= 0 uses the engine's
/// default (stereo).
pub audio_channel_count: c_int,
}
/// `OAKENGINE_EXPORT_VIDEO_*` — video codecs for
/// [`OakExportOptions::video_codec`].
pub const OAKENGINE_EXPORT_VIDEO_H264: c_int = 0;
/// H.265/HEVC in an MP4 container.
pub const OAKENGINE_EXPORT_VIDEO_H265: c_int = 1;
/// PNG still-image sequence.
pub const OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE: c_int = 2;
/// `OAKENGINE_EXPORT_AUDIO_*` — audio codecs for
/// [`OakExportOptions::audio_codec`].
pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0;
/// Uncompressed PCM.
pub const OAKENGINE_EXPORT_AUDIO_PCM: c_int = 1;
/// Disable the audio track entirely (not a codec).
pub const OAKENGINE_EXPORT_AUDIO_NONE: c_int = -1;
/// The oakaudio recording-params POD is the same shared codec POD.
pub type AudioEncodingParams = EncodingParamsPOD;
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
/// struct has no zeroed constructor; this facade helper provides it.
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
// All-field zero is a valid value (enums carry their 0 variants).
unsafe { std::mem::zeroed() }
}
/// `oakrender_video_params` (`include/render/renderer.h`) — identical
/// layout to the engine's own video-params POD.
pub type OakRenderVideoParams = crate::common::OakVideoParamsPod;
/// `oakrender_video_ticket_params` (`include/render/ticket.h`), the POD
/// the deleted bridge aliased from `oakrender::ffi`. The oakrender crate
/// now exposes value-typed `ticket::VideoTicketParams`; the facade keeps
/// this mirror for its synchronous render path (see [`crate::render`]).
/// All handle fields are the shared [`crate::handle::CHandle`].
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakVideoTicketParams {
/// Connected texture output node (borrowed).
pub output_node: crate::handle::CHandle,
/// By-value oakcommon handle.
pub video_params: crate::handle::CHandle,
/// Borrowed oakcore audio-params handle, may be null.
pub audio_params: *const std::ffi::c_void,
/// Frame timestamp as rational.
pub time_num: i64,
/// Frame timestamp as rational.
pub time_den: i64,
/// Borrowed, empty ctx = null.
pub color_manager: crate::handle::CHandle,
/// RenderMode::Mode as int.
pub mode: c_int,
/// 0/0 = off.
pub force_width: c_int,
/// 0/0 = off.
pub force_height: c_int,
/// Used when has_force_matrix != 0.
pub force_matrix: [f64; 16],
/// 0/1.
pub has_force_matrix: c_int,
/// PixelFormat as int, -1 = off.
pub force_format: c_int,
/// 0 = off.
pub force_channel_count: c_int,
/// Borrowed; empty ctx = none.
pub force_color_output: crate::handle::CHandle,
/// By value; empty ctx = default.
pub force_color_transform: crate::handle::CHandle,
/// Borrowed frame cache; empty ctx = none.
pub cache: crate::handle::CHandle,
/// Single-footage decode filename (null = off; M12 P0).
pub footage_filename: *const std::ffi::c_char,
/// Media stream index for `footage_filename`.
pub footage_stream: c_int,
/// Sequence montage clip array (null = off; M12 P0). Clips are
/// ordered bottom-to-top; the last element is the topmost.
pub montage: *const MontagePod,
/// `montage` element count.
pub montage_count: c_int,
}
/// One sequence-montage clip (`oakrender::ffi::OakMontageClip`, M12 P0):
/// the facade resolves the timeline into this POD list; the render
/// producer decodes and composites.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct MontagePod {
/// Footage filename (borrowed; alive for the render call).
pub filename: *const std::ffi::c_char,
/// Media stream index.
pub stream_index: c_int,
/// Clip in point (sequence time), rational.
pub in_num: i64,
/// Clip in point denominator.
pub in_den: i64,
/// Clip out point (sequence time), rational.
pub out_num: i64,
/// Clip out point denominator.
pub out_den: i64,
/// Media in point, rational.
pub media_in_num: i64,
/// Media in point denominator.
pub media_in_den: i64,
/// Playback gain (1.0 = unity).
pub gain: f32,
}
/// The samples block handed out by `oakrender_ticket_get_samples`
/// (caller-owned; release with `oakrender_audio_samples_free`). Read by
/// the facade through the Rust type.
pub struct OakAudioSamplesOut {
/// Interleaved f32 samples.
pub data: Box<[f32]>,
/// Frame count.
pub frame_count: c_int,
/// Sample rate (Hz).
pub sample_rate: c_int,
/// Channel layout mask.
pub channel_layout: u64,
/// Channel count.
pub channel_count: c_int,
}
/// `oakaudio_min_max` (`include/audio/waveform.h`) — one summarized
/// waveform point of one channel.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct MinMax {
/// Minimum of the summarized samples.
pub min: f32,
/// Maximum of the summarized samples.
pub max: f32,
}
/// `oakaudio_offset_result` (`include/audio/sync.h`).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OffsetResult {
/// Offset in samples.
pub offset_samples: i64,
/// Correlation confidence 0..1.
pub confidence: f64,
/// 1 when an estimate was found.
pub valid: c_int,
}
/// `oakaudio_stretch_offset_result` (`include/audio/sync.h`).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StretchOffsetResult {
/// Playback rate aligning the candidate (> 1 = speed up).
pub rate: f64,
/// Offset in samples.
pub offset_samples: i64,
/// Correlation confidence 0..1.
pub confidence: f64,
/// 1 when an estimate was found.
pub valid: c_int,
}
/// `oakaudio_source_clip` (`include/audio/sync.h`) — one clip's
/// source-time metadata (rational seconds).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct SourceClip {
/// Source start time numerator.
pub source_start_time_num: i64,
/// Source start time denominator.
pub source_start_time_den: i64,
/// Media in numerator.
pub media_in_num: i64,
/// Media in denominator.
pub media_in_den: i64,
/// 1 when the source start time is meaningful.
pub has_source_start_time: c_int,
}
/// `i32` code -> `PixelFormat` (`repr(i32)` enum; unknown -> `Invalid`).
pub fn pixel_format_from_code(v: c_int) -> oakcore_rs::PixelFormat {
match v {
0 => oakcore_rs::PixelFormat::U8,
1 => oakcore_rs::PixelFormat::U10,
2 => oakcore_rs::PixelFormat::U16,
3 => oakcore_rs::PixelFormat::F16,
4 => oakcore_rs::PixelFormat::F32,
_ => oakcore_rs::PixelFormat::Invalid,
}
}
/// `i32` code -> `SampleFormat` (`repr(i32)` enum; unknown -> `Invalid`).
pub fn sample_format_from_code(v: c_int) -> oakcore_rs::SampleFormat {
match v {
0 => oakcore_rs::SampleFormat::U8Planar,
1 => oakcore_rs::SampleFormat::S16Planar,
2 => oakcore_rs::SampleFormat::S32Planar,
3 => oakcore_rs::SampleFormat::S64Planar,
4 => oakcore_rs::SampleFormat::F32Planar,
5 => oakcore_rs::SampleFormat::F64Planar,
6 => oakcore_rs::SampleFormat::U8,
7 => oakcore_rs::SampleFormat::S16,
8 => oakcore_rs::SampleFormat::S32,
9 => oakcore_rs::SampleFormat::S64,
10 => oakcore_rs::SampleFormat::F32,
11 => oakcore_rs::SampleFormat::F64,
_ => oakcore_rs::SampleFormat::Invalid,
}
}
/// `i32` code -> `VideoScalingMethod` (unknown -> `Stretch`, the C++
/// default).
pub fn scaling_from_code(v: c_int) -> oakcodec::encodingparams::VideoScalingMethod {
match v {
0 => oakcodec::encodingparams::VideoScalingMethod::Fit,
2 => oakcodec::encodingparams::VideoScalingMethod::Crop,
_ => oakcodec::encodingparams::VideoScalingMethod::Stretch,
}
}
+96 -46
View File
@@ -20,25 +20,27 @@
//! - 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`),
//! waits for it and returns the produced frame (`OakCodecFrame` wrapped
//! in `OakEngineFrame`). Audio rendering submits the ticket but the
//! crate's samples path is unimplemented, so it fails with the reason
//! in `last_error`.
//! - The **frame accessors** read the wrapped `OakCodecFrame`
//! 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
//! `oakrender_color_processor_*`; 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`).
//! - 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::bridge::render as r;
use crate::bridge::render::{OakRenderVideoParams, OakVideoTicketParams};
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,
@@ -50,6 +52,33 @@ use crate::handle::{
// 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(
@@ -58,24 +87,45 @@ pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) }))
}
/// `oakengine_render_manager_requested_backend` — **not backed** (the
/// oakrender crate exposes the current backend, not the requested one).
/// Returns 0 (k_open_gl).
/// `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 {
0
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` — **not backed** (the
/// crate enumerates backend ids, not enum→string). Returns
/// OAKENGINE_E_FAILED.
/// `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,
backend: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
crate::error::OAKENGINE_E_FAILED
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.
@@ -171,19 +221,19 @@ unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut Rende
/// Build an oakcommon video-params handle for the renderer's geometry.
unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
unsafe {
let params = crate::bridge::common::oakcommon_videoparams_init();
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::bridge::common::oakcommon_videoparams_set_width(params, b.width);
let mut rc = crate::stubs::common::oakcommon_videoparams_set_width(params, b.width);
if rc == 0 {
rc = crate::bridge::common::oakcommon_videoparams_set_height(params, b.height);
rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height);
}
if rc == 0 {
rc = crate::bridge::common::oakcommon_videoparams_set_format(params, b.pixel_format);
rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format);
}
if rc == 0 {
rc = crate::bridge::common::oakcommon_videoparams_set_time_base(
rc = crate::stubs::common::oakcommon_videoparams_set_time_base(
params,
b.frame_rate_den,
b.frame_rate_num,
@@ -191,7 +241,7 @@ unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
}
if rc != 0 {
let mut p = params;
crate::bridge::common::oakcommon_videoparams_free(&mut p);
crate::stubs::common::oakcommon_videoparams_free(&mut p);
return Err(Error::Failed("video params setup failed".into()));
}
Ok(params)
@@ -254,7 +304,7 @@ unsafe fn make_renderer_box(
unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
unsafe {
let mut buf = [0 as c_char; 4096];
let rc = crate::bridge::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
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()));
}
@@ -270,18 +320,18 @@ unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
/// graph node → its upstream footage.
unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> {
unsafe {
let node = crate::bridge::node::oaknode_block_as_node(clip);
let node = crate::stubs::node::oaknode_block_as_node(clip);
if node.is_null() {
return None;
}
let mut footage = CHandle::null();
if crate::bridge::node::oaknode_node_find_input_footage(node, &mut footage) != 0
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::bridge::node::oaknode_footage_filename(
if crate::stubs::node::oaknode_footage_filename(
footage,
buf.as_mut_ptr(),
buf.len() as c_int,
@@ -302,7 +352,7 @@ unsafe fn build_audio_montage(
b: &RendererBox,
range: oakcore_rs::TimeRange,
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
use crate::bridge::node as n;
use crate::stubs::node as n;
unsafe {
let mut pods = Vec::new();
let mut names = Vec::new();
@@ -374,7 +424,7 @@ unsafe fn build_audio_montage(
}
/// The `OakMontageClip` POD the render module's ticket reads.
type MontagePod = oakrender::ffi::OakMontageClip;
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
@@ -385,7 +435,7 @@ unsafe fn build_video_montage(
b: &RendererBox,
time: oakcore_rs::Rational,
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
use crate::bridge::node as n;
use crate::stubs::node as n;
unsafe {
let mut pods = Vec::new();
let mut names = Vec::new();
@@ -618,7 +668,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame(
let ticket = r::oakrender_ticket_render_frame(&params, None, std::ptr::null_mut());
let _ = (&keep_alive, &montage);
let mut vp = video_params;
crate::bridge::common::oakcommon_videoparams_free(&mut vp);
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());
@@ -659,8 +709,8 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
// 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::bridge::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
crate::bridge::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
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,
@@ -678,7 +728,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
},
pods.len() as c_int,
);
crate::bridge::audio::oakcore_audioparams_free(params);
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());
@@ -693,7 +743,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
b.last_error = "audio render failed".into();
return Ok(std::ptr::null_mut());
}
let raw = samples as *const oakrender::ffi::OakAudioSamplesOut;
let raw = samples as *const crate::pods::OakAudioSamplesOut;
let boxed = AudioSamplesBox {
data: (*raw).data.clone(),
frame_count: (*raw).frame_count as i64,
@@ -1210,7 +1260,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
}
let empty = crate::common::empty_cstr();
let ct = if (*dest).is_display != 0 {
crate::bridge::common::oakcommon_colortransform_init_display(
crate::stubs::common::oakcommon_colortransform_init_display(
if (*dest).output.is_null() {
empty
} else {
@@ -1228,7 +1278,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
},
)
} else {
crate::bridge::common::oakcommon_colortransform_init_output(
crate::stubs::common::oakcommon_colortransform_init_output(
if (*dest).output.is_null() {
empty
} else {
@@ -1247,7 +1297,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
};
let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction);
let mut ct_handle = ct;
crate::bridge::common::oakcommon_colortransform_free(&mut ct_handle);
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());
File diff suppressed because it is too large Load Diff
+75 -7
View File
@@ -43,9 +43,9 @@ use std::ffi::{c_char, c_int, c_void};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
use crate::bridge::node as n;
use crate::bridge::task as t;
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
use crate::stubs::node as n;
use crate::stubs::task as t;
use crate::codec::OakEngineEncodingParams;
use crate::common::OakVideoParamsPod;
use crate::error::{Error, Result};
@@ -654,9 +654,9 @@ pub unsafe extern "C" fn oakengine_task_create_export(
///
/// The oaktask crate's `convert_encoding_params` consumes exactly these
/// fields (filename, format, video/audio/subtitle enables, codecs,
/// dimensions, time base, pixel format, export length), so a POD carrying
/// them is behaviorally identical to the original for the export task; all
/// other POD fields stay zeroed.
/// dimensions, time base, pixel format, audio rate/layout, export length),
/// so a POD carrying them is behaviorally identical to the original for
/// the export task; all other POD fields stay zeroed.
fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingParamsPOD> {
let mut pod = zeroed_encoding_params();
@@ -682,6 +682,24 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
pod.video_codec = unsafe { crate::codec::oakengine_encoding_params_video_codec(params) };
pod.audio_enabled = unsafe { crate::codec::oakengine_encoding_params_audio_enabled(params) };
pod.audio_codec = unsafe { crate::codec::oakengine_encoding_params_audio_codec(params) };
// Audio rate/layout flow through so the encoder opens with the
// requested rate (the module export reads them from the POD). The
// sample format is NOT carried: the FFmpeg encoder always runs in the
// codec's native format and resamples (see `FFmpegEncoder::open`).
if pod.audio_enabled != 0 {
let mut sample_rate: c_int = 0;
let mut channel_layout: u64 = 0;
unsafe {
crate::codec::oakengine_encoding_params_get_audio_params(
params,
&mut sample_rate,
&mut channel_layout,
std::ptr::null_mut(),
);
}
pod.audio_sample_rate = sample_rate;
pod.audio_channel_layout = channel_layout;
}
pod.subtitles_enabled =
unsafe { crate::codec::oakengine_encoding_params_subtitles_enabled(params) };
unsafe {
@@ -703,7 +721,7 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
pod.video_height = v.height;
pod.video_time_base_num = v.time_base_num;
pod.video_time_base_den = v.time_base_den;
pod.video_pixel_format = v.format;
pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
}
}
Ok(pod)
@@ -865,3 +883,53 @@ pub unsafe extern "C" fn oakengine_task_save_get_project(
}
})
}
// ---------------------------------------------------------------------------
// Load task results / event subscription
// ---------------------------------------------------------------------------
/// `oakengine_task_load_take_project` — take the project an interchange
/// (OVE/OTIO) load task produced after a successful run; ownership moves to
/// the caller (release with `oakengine_project_free`). NULL for a NULL
/// task, a task that never ran, or a task that is not a load task.
///
/// The module's `oaktask_load_take_project` is the load-result getter the
/// engine facade has no export for (the app's interchange-open path); it is
/// wrapped here so the app can stay on the `oakengine_*` surface.
#[no_mangle]
pub unsafe extern "C" fn oakengine_task_load_take_project(
task: *mut OakEngineTask,
) -> *mut OakEngineProject {
guard_ptr(|| unsafe {
let h = unbox(task)?;
let project = t::oaktask_load_take_project(h);
if project.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineProject>(project))
})
}
/// `oakengine_task_subscribe` — register the task event callback invoked on
/// the task's own thread (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1,
/// `OAKTASK_EVENT_FINISHED`=2); one subscription replaces the previous one.
///
/// Returns 0 on success; facade `OAKENGINE_E_INVALID` (-1) for a NULL task
/// or NULL callback; module error codes pass through untranslated. The
/// callback and `userdata` follow the module's `oaktask_event_fn` contract
/// (the engine facade has no subscription export of its own; the app's
/// export-progress path uses this wrapper).
#[no_mangle]
pub unsafe extern "C" fn oakengine_task_subscribe(
task: *mut OakEngineTask,
cb: Option<t::OakTaskEventFn>,
userdata: *mut c_void,
) -> i64 {
guard_i64(|| unsafe {
let h = unbox(task)?;
if cb.is_none() {
return Err(Error::Invalid);
}
Ok(t::oaktask_task_subscribe(h, cb, userdata))
})
}
+163
View File
@@ -0,0 +1,163 @@
// 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/>.
//! Smoke tests for the audio family (`engine/include/oakengine/audio.h`).
//! The AudioManager singleton is process-wide, so manager tests run in a
//! single serialized test function; processor and sync tests are
//! independent.
use super::common;
use crate::audio::{
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_processor_close,
oakengine_audio_processor_create, oakengine_audio_processor_free,
oakengine_audio_processor_is_open, oakengine_audio_processor_open,
oakengine_audio_push_to_output, oakengine_audio_reset_output_clock,
oakengine_audio_set_output_device, oakengine_audio_set_output_notify_interval,
oakengine_audio_stop_output, oakengine_audio_sync_place_by_waveform_offset,
OakAudioSyncPlacement, OakAudioWaveformOffset,
};
/// Manager lifecycle: create/destroy round-trip and device accessors
/// (serialized — the singleton is process-wide).
#[test]
fn manager_lifecycle() {
common::with_manager(|| manager_lifecycle_inner());
}
fn manager_lifecycle_inner() {
// Start from a destroyed state.
unsafe { oakengine_audio_destroy_instance() };
// No instance → create succeeds, destroy is idempotent.
assert_eq!(unsafe { oakengine_audio_create_instance() }, 0);
assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0);
assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0);
// Recreate for the device tests.
assert_eq!(unsafe { oakengine_audio_create_instance() }, 0);
// paNoDevice (-1) until a device is set.
assert_eq!(unsafe { oakengine_audio_get_output_device() }, -1);
// The module records any device index (PortAudio validation is not
// bridged), so setting succeeds and reads back.
assert_eq!(unsafe { oakengine_audio_set_output_device(999999) }, 0);
assert_eq!(unsafe { oakengine_audio_get_output_device() }, 999999);
assert_eq!(unsafe { oakengine_audio_set_output_device(-1) }, 0);
// Stateless no-op calls succeed with a live manager.
assert_eq!(unsafe { oakengine_audio_reset_output_clock() }, 0);
assert_eq!(unsafe { oakengine_audio_stop_output() }, 0);
assert_eq!(unsafe { oakengine_audio_clear_buffered_output() }, 0);
assert_eq!(
unsafe { oakengine_audio_set_output_notify_interval(1024) },
0
);
assert_eq!(unsafe { oakengine_audio_hard_reset() }, 0);
// push with a NULL params handle fails cleanly.
assert_eq!(
unsafe {
oakengine_audio_push_to_output(
std::ptr::null(),
c"data".as_ptr(),
4,
std::ptr::null_mut(),
0,
)
},
-3 // OAKENGINE_E_FAILED
);
unsafe { oakengine_audio_destroy_instance() };
}
/// Sync envelope-offset correlation runs and fills the result struct.
#[test]
fn sync_envelope_offset() {
let reference = [0.0_f64, 0.5, 1.0, 0.5, 0.0];
let candidate = [0.0_f64, 0.0, 0.5, 1.0, 0.5];
let mut out = OakAudioWaveformOffset {
offset_samples: 0,
confidence: 0.0,
valid: 0,
};
let rc = unsafe {
oakengine_audio_estimate_envelope_offset(
reference.as_ptr(),
5,
candidate.as_ptr(),
5,
std::ptr::null(),
0,
std::ptr::null(),
0,
128,
16,
&mut out,
)
};
assert_eq!(rc, 0);
// The result is filled in either way; the correlation may or may not
// find a valid offset for this tiny synthetic input.
assert!(out.confidence >= 0.0 && out.confidence <= 1.0);
}
/// Waveform-offset placement runs and reports validity.
#[test]
fn sync_place_by_waveform_offset() {
let mut out = OakAudioSyncPlacement {
timeline_in_num: 0,
timeline_in_den: 1,
valid: 0,
};
let rc = unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut out) };
assert_eq!(rc, 0);
// 48000 samples at 48 kHz = 1 second.
assert_eq!(out.timeline_in_num, 1);
assert_eq!(out.timeline_in_den, 1);
assert_eq!(out.valid, 1);
// NULL out → E_INVALID.
assert_eq!(
unsafe {
oakengine_audio_sync_place_by_waveform_offset(0, 1, 0, 48000, std::ptr::null_mut())
},
-1
);
}
/// Processor lifecycle: create/free round-trip; open with NULL params
/// fails with E_INVALID.
#[test]
fn processor_lifecycle() {
let p = unsafe { oakengine_audio_processor_create() };
assert!(!p.is_null());
assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 0);
assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0);
// open with a NULL `to` params handle → E_INVALID.
assert_eq!(
unsafe { oakengine_audio_processor_open(p, std::ptr::null(), std::ptr::null(), 1.0) },
-1
);
unsafe { oakengine_audio_processor_free(p) };
// NULL free is a no-op.
unsafe { oakengine_audio_processor_free(std::ptr::null_mut()) };
}
+307
View File
@@ -0,0 +1,307 @@
// 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/>.
//! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`):
//! container/codec metadata queries and the encoding-params handle.
use super::common;
use std::ffi::{c_char, c_int};
use crate::codec::{
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
oakengine_encoding_filename_remove_digit_placeholder,
oakengine_encoding_format_audio_codec_count, oakengine_encoding_format_count,
oakengine_encoding_format_extension, oakengine_encoding_format_name,
oakengine_encoding_format_video_codec_at, oakengine_encoding_format_video_codec_count,
oakengine_encoding_generate_matrix, oakengine_encoding_image_sequence_digit_count,
oakengine_encoding_params_audio_enabled, oakengine_encoding_params_color_transform_output,
oakengine_encoding_params_create, oakengine_encoding_params_destroy,
oakengine_encoding_params_enable_audio, oakengine_encoding_params_enable_video,
oakengine_encoding_params_filename, oakengine_encoding_params_format,
oakengine_encoding_params_get_audio_params, oakengine_encoding_params_get_custom_range,
oakengine_encoding_params_get_video_params, oakengine_encoding_params_has_custom_range,
oakengine_encoding_params_is_valid, oakengine_encoding_params_set_color_transform,
oakengine_encoding_params_set_custom_range, oakengine_encoding_params_set_filename,
oakengine_encoding_params_set_format, oakengine_encoding_params_set_video_bit_rate,
oakengine_encoding_params_set_video_option, oakengine_encoding_params_set_video_pix_fmt,
oakengine_encoding_params_video_bit_rate, oakengine_encoding_params_video_codec,
oakengine_encoding_params_video_enabled, oakengine_encoding_params_video_option,
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index,
oakengine_encoding_start_audio_recording,
};
use crate::common::OakVideoParamsPod;
/// Container format / codec metadata queries.
#[test]
fn encoding_metadata() {
// Format enumeration: at least the six named formats exist.
let count = unsafe { oakengine_encoding_format_count() };
assert!(count >= 6);
// Matroska (1): name + extension via two-stage getters.
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_encoding_format_name(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert!(unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap()
.contains("Matroska"));
let len = unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"mkv"
);
// Per-format codec lists.
assert!(unsafe { oakengine_encoding_format_video_codec_count(1) } >= 1);
let codec = unsafe { oakengine_encoding_format_video_codec_at(1, 0) };
assert!(codec >= 1);
assert!(unsafe { oakengine_encoding_format_audio_codec_count(1) } >= 1);
// Codec metadata: name, still-image (PNG = 5), lossless.
let len = unsafe { oakengine_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(5) }, 1); // PNG
assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(1) }, 0); // H264
assert!(unsafe { oakengine_encoding_codec_is_lossless(13) } == 1); // PCM
// pix_fmt_index: preferred format index when absent.
assert_eq!(
unsafe { oakengine_encoding_pix_fmt_index(1, c"yuv420p".as_ptr()) },
0
);
}
/// Image-sequence filename helpers.
#[test]
fn filename_helpers() {
assert_eq!(
unsafe {
oakengine_encoding_filename_contains_digit_placeholder(c"img[#####].png".as_ptr())
},
1
);
assert_eq!(
unsafe { oakengine_encoding_filename_contains_digit_placeholder(c"img.png".as_ptr()) },
0
);
assert_eq!(
unsafe { oakengine_encoding_image_sequence_digit_count(c"img[#####].png".as_ptr()) },
5
);
let mut buf = [0 as c_char; 64];
let len = unsafe {
oakengine_encoding_filename_remove_digit_placeholder(
c"img[#####].png".as_ptr(),
buf.as_mut_ptr(),
64,
)
};
assert!(len > 0);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"img.png"
);
}
/// Transform matrix: fit produces a valid 16-float matrix.
#[test]
fn generate_matrix() {
let mut m = [0.0_f32; 16];
assert_eq!(
unsafe { oakengine_encoding_generate_matrix(0, 1920, 1080, 960, 540, m.as_mut_ptr()) },
0
);
// The 4x4 identity-ish matrix has a non-zero top-left.
assert!(m[0] > 0.0 || m[5] > 0.0);
// NULL output → E_INVALID.
assert_eq!(
unsafe { oakengine_encoding_generate_matrix(0, 1, 1, 1, 1, std::ptr::null_mut()) },
-1
);
}
/// Encoding-params handle lifecycle: create → configure → read back →
/// destroy (serialized inside one test; the handle is per-call state).
#[test]
fn params_handle_round_trip() {
let p = unsafe { oakengine_encoding_params_create() };
assert!(!p.is_null());
// Fresh: nothing enabled, format unset (-1).
assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 0);
assert_eq!(unsafe { oakengine_encoding_params_format(p) }, -1);
// Format: set + get, and reject out-of-range.
assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 1) }, 0); // Matroska
assert_eq!(unsafe { oakengine_encoding_params_format(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 9999) }, -1);
// Filename round-trip.
assert_eq!(
unsafe { oakengine_encoding_params_set_filename(p, c"out.mkv".as_ptr()) },
0
);
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_encoding_params_filename(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 7);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"out.mkv"
);
// Enable video: valid, get_video_params reads back.
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe {
crate::common::oakengine_video_params_make(
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
)
},
0
);
assert_eq!(
unsafe { oakengine_encoding_params_enable_video(p, &vp, 1) },
0
);
assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_video_codec(p) }, 1);
let mut out_vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_encoding_params_get_video_params(p, &mut out_vp) },
0
);
assert_eq!(out_vp.width, 1920);
assert_eq!(out_vp.height, 1080);
assert_eq!(out_vp.time_base_num, 1001);
// Video bit rate round-trip.
unsafe { oakengine_encoding_params_set_video_bit_rate(p, 8_000_000) };
assert_eq!(
unsafe { oakengine_encoding_params_video_bit_rate(p) },
8_000_000
);
// Encoded pixel format round-trip.
assert_eq!(
unsafe { oakengine_encoding_params_set_video_pix_fmt(p, c"yuv420p".as_ptr()) },
0
);
let len = unsafe { oakengine_encoding_params_video_pix_fmt(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 7);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"yuv420p"
);
// Audio: disabled get_video/audio → E_STATE; enable then read back.
let mut sr: c_int = 0;
let mut layout: u64 = 0;
let mut sf: c_int = 0;
assert_eq!(
unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) },
-2
);
assert_eq!(
unsafe { oakengine_encoding_params_enable_audio(p, 48000, 3, 0, 13) },
0
);
assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 1);
assert_eq!(
unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) },
0
);
assert_eq!(sr, 48000);
assert_eq!(layout, 3);
// Custom range: not set → E_NOT_FOUND; set → reads back.
let (mut inn, mut ind, mut outn, mut outd) = (0i64, 0i64, 0i64, 0i64);
assert_eq!(
unsafe {
oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd)
},
-4
);
unsafe { oakengine_encoding_params_set_custom_range(p, 0, 1, 100, 1) };
assert_eq!(unsafe { oakengine_encoding_params_has_custom_range(p) }, 1);
assert_eq!(
unsafe {
oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd)
},
0
);
assert_eq!((inn, ind, outn, outd), (0, 1, 100, 1));
// Color transform + video option round-trips.
assert_eq!(
unsafe { oakengine_encoding_params_set_color_transform(p, c"ACEScg".as_ptr()) },
0
);
let len = unsafe { oakengine_encoding_params_color_transform_output(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"ACEScg"
);
assert_eq!(
unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), c"18".as_ptr()) },
0
);
let len =
unsafe { oakengine_encoding_params_video_option(p, c"crf".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 2);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"18"
);
assert_eq!(
unsafe {
oakengine_encoding_params_video_option(p, c"missing".as_ptr(), buf.as_mut_ptr(), 64)
},
-4
);
unsafe { oakengine_encoding_params_destroy(p) };
// NULL destroy is a no-op.
unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) };
}
/// Audio recording without a running audio manager fails with E_STATE.
#[test]
fn start_audio_recording_no_manager() {
let p = unsafe { oakengine_encoding_params_create() };
assert!(!p.is_null());
let rc = unsafe { oakengine_encoding_start_audio_recording(p, std::ptr::null_mut(), 0) };
assert_eq!(rc, -2); // OAKENGINE_E_STATE
unsafe { oakengine_encoding_params_destroy(p) };
}
@@ -0,0 +1,277 @@
// 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/>.
//! Shared test support, included from every integration test via
//! `#[path = "common/mod.rs"] mod common;`.
//!
//! Two jobs:
//!
//! 1. **Force rustc to link every module crate's rlib** into the test
//! binary ([`force_link`]). The facade itself only references the
//! modules through `extern "C"` imports (see src/bridge), so rustc
//! would otherwise drop the dev-dependency rlibs from the link and
//! leave the imports undefined.
//!
//! 2. **Provide the `oakcore_*` symbols** that the
//! oakcodec rlib references: `oakcore_audioparams_*` /
//! `oakcore_rational_*` live in the C++ liboakcore (only linked in the
//! real build), so cargo tests define minimal in-memory mocks — the
//! same mock the oakcodec crate itself compiles under `#[cfg(test)]`
//! (src/bridge/test_stubs.rs). The real dylib behavior is required
//! for actual media decode; those facade tests are `#[ignore]`.
#![allow(dead_code, unused_variables)]
use std::collections::HashMap;
use std::ffi::{c_int, c_void};
use std::sync::{Mutex, OnceLock};
/// One public direct-Rust symbol per module crate (the module C ABIs are
/// deleted; this mirrors the anchors in `crates/oakengine/src/linkage.rs`).
/// Under unit tests the crates are real dependencies of the lib target and
/// are linked regardless; the array doubles as a compile-time proof that
/// the anchor paths match the current module layouts.
#[allow(unused)]
pub fn force_link() -> usize {
let fns: [usize; 12] = [
oakundo::undostack::undostack_init as usize,
oakcodec::exportformat::Format::get_name as usize,
oakaudio::processor::Processor::init as usize,
oakrender::manager::RenderManager::init as usize,
oakcommon::configstore::ConfigStore::instance as usize,
oakplugin::host::Host::global as usize,
oaknode::project::Project::new as usize,
oaktimeline::marker::TimelineMarkerList::new as usize,
oaktask::manager::TaskManager::init as usize,
// oakundo/oakcommon no longer export a C ABI; their handle-level
// Rust API functions anchor the rlibs into every test binary (the
// same pattern as `crates/oakengine/src/linkage.rs`).
oakcommon::xmlutils::XmlWriter::new as usize,
oakcommon::xmlutils::XmlReader::new as usize,
oakundo::undocommand::command_init as usize,
];
fns.iter().sum()
}
/// Serialize every test that touches the process-wide AudioManager
/// singleton. The former integration tests were separate processes; as
/// unit tests they share one process (and one singleton), so the manager
/// tests must take a shared lock instead of relying on process isolation.
pub fn with_manager(f: impl FnOnce()) {
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
f()
}
// ---------------------------------------------------------------------------
// oakcore_* stubs (see module docs)
// ---------------------------------------------------------------------------
/// Opaque `OakAudioParams` handle type (the real one lives in liboakcore).
#[repr(C)]
pub struct OakAudioParams {
_opaque: [u8; 0],
}
/// Per-`OakAudioParams` backing state.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct MockAudioParams {
sample_rate: i32,
channel_layout: u64,
format: i32,
stream_index: i32,
duration: i64,
time_base_num: i32,
time_base_den: i32,
}
fn audio_params_store() -> &'static Mutex<HashMap<usize, MockAudioParams>> {
static S: OnceLock<Mutex<HashMap<usize, MockAudioParams>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}
fn audio_params_get(ctx: *const c_void) -> MockAudioParams {
let store = audio_params_store().lock().unwrap();
store.get(&(ctx as usize)).cloned().unwrap_or_default()
}
fn audio_params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockAudioParams)) {
let mut store = audio_params_store().lock().unwrap();
if let Some(p) = store.get_mut(&(ctx as usize)) {
f(p);
}
}
/// Per-`OakRational` backing state (an owned `(num, den)` pair).
fn rational_store() -> &'static Mutex<HashMap<usize, (i32, i32)>> {
static S: OnceLock<Mutex<HashMap<usize, (i32, i32)>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_create(
sample_rate: c_int,
channel_layout: u64,
format: c_int,
) -> *mut OakAudioParams {
let p = MockAudioParams {
sample_rate,
channel_layout,
format,
stream_index: 0,
duration: 0,
time_base_num: 1,
time_base_den: sample_rate,
};
let raw = Box::into_raw(Box::new(p.clone()));
audio_params_store().lock().unwrap().insert(raw as usize, p);
raw as *mut OakAudioParams
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_free(params: *mut OakAudioParams) {
if params.is_null() {
return;
}
audio_params_store()
.lock()
.unwrap()
.remove(&(params as usize));
// SAFETY: produced by `oakcore_audioparams_create`; we hold the only
// reference after removal.
unsafe { drop(Box::from_raw(params as *mut MockAudioParams)) };
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int {
audio_params_get(params as *const c_void).sample_rate
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_sample_rate(
params: *mut OakAudioParams,
sample_rate: c_int,
) {
audio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate);
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64 {
audio_params_get(params as *const c_void).channel_layout
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64) {
audio_params_set(params as *mut c_void, |p| p.channel_layout = layout);
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_time_base(
params: *mut OakAudioParams,
num: c_int,
den: c_int,
) {
audio_params_set(params as *mut c_void, |p| {
p.time_base_num = num;
p.time_base_den = den;
});
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) {
audio_params_set(params as *mut c_void, |p| p.format = format);
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int) {
audio_params_set(params as *mut c_void, |p| p.stream_index = index);
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) {
audio_params_set(params as *mut c_void, |p| p.duration = duration);
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int {
audio_params_get(params as *const c_void)
.channel_layout
.count_ones() as c_int
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int {
audio_params_get(params as *const c_void).format
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int {
audio_params_get(params as *const c_void).stream_index
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64 {
audio_params_get(params as *const c_void).duration
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int {
let p = audio_params_get(params as *const c_void);
(p.sample_rate > 0 && p.channel_layout != 0 && p.format >= 0) as c_int
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void {
let p = audio_params_get(params as *const c_void);
let r = (p.time_base_num, p.time_base_den);
let raw = Box::into_raw(Box::new(r));
rational_store().lock().unwrap().insert(raw as usize, r);
raw as *mut c_void
}
#[no_mangle]
pub extern "C" fn oakcore_rational_numerator(rational: *const c_void) -> c_int {
rational_store()
.lock()
.unwrap()
.get(&(rational as usize))
.map(|r| r.0)
.unwrap_or(0)
}
#[no_mangle]
pub extern "C" fn oakcore_rational_denominator(rational: *const c_void) -> c_int {
rational_store()
.lock()
.unwrap()
.get(&(rational as usize))
.map(|r| r.1)
.unwrap_or(0)
}
#[no_mangle]
pub extern "C" fn oakcore_rational_free(rational: *mut c_void) {
if rational.is_null() {
return;
}
rational_store()
.lock()
.unwrap()
.remove(&(rational as usize));
// SAFETY: produced by `oakcore_audioparams_time_base` as a boxed
// `(i32, i32)` pair; we hold the only reference after removal.
unsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };
}
@@ -0,0 +1,226 @@
// 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/>.
//! Smoke tests for the common family (`engine/include/oakengine/config.h`
//! and `videoparams.h`). The oakcommon config store is a process-wide
//! singleton, so config tests are serialized inside single test
//! functions.
use super::common;
use std::ffi::{c_char, c_int};
use crate::common::{
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int,
oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
oakengine_video_params_effective_size, oakengine_video_params_equal,
oakengine_video_params_format_is_float, oakengine_video_params_internal_channel_count,
oakengine_video_params_is_valid, oakengine_video_params_make,
oakengine_video_params_standard_pixel_aspect_at,
oakengine_video_params_standard_pixel_aspect_count,
oakengine_video_params_supported_divider_at, oakengine_video_params_supported_divider_count,
oakengine_video_params_supported_frame_rate_at,
oakengine_video_params_supported_frame_rate_count, OakVideoParamsPod,
};
/// Config: load/save, string and int round-trips, missing-key behavior.
#[test]
fn config_round_trip() {
assert_eq!(unsafe { oakengine_config_load() }, 0);
// Missing key reads as 0 / empty.
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_config_get_string(c"no/such/key".as_ptr(), buf.as_mut_ptr(), 64) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"no/such/key".as_ptr(), 7) },
7
);
// String round-trip.
assert_eq!(
unsafe { oakengine_config_set_string(c"facade/test".as_ptr(), c"hello".as_ptr()) },
0
);
let len = unsafe { oakengine_config_get_string(c"facade/test".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"hello"
);
// A too-small buffer is not written (the two-stage convention is:
// query the required size, allocate, copy) — the module reports the
// full length and leaves the buffer untouched.
let mut small = [0 as c_char; 3];
let len =
unsafe { oakengine_config_get_string(c"facade/test".as_ptr(), small.as_mut_ptr(), 3) };
assert_eq!(len, 5); // reported full length
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(small.as_ptr()) }
.to_str()
.unwrap(),
""
);
// Int round-trip.
assert_eq!(
unsafe { oakengine_config_set_int(c"facade/n".as_ptr(), 1234) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"facade/n".as_ptr(), 0) },
1234
);
assert_eq!(unsafe { oakengine_config_save() }, 0);
}
/// Config error handler: registered, then invoked via report_error.
#[test]
fn config_error_handler() {
static CALLED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
unsafe extern "C" fn handler(
_title: *const c_char,
_message: *const c_char,
_userdata: *mut std::ffi::c_void,
) {
CALLED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
assert_eq!(
unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) },
0
);
// Report an error through the handler.
assert_eq!(
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) },
0
);
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
// NULL handler clears; reporting then does not invoke.
assert_eq!(
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) },
0
);
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
}
/// Videoparams static tables.
#[test]
fn videoparams_static_tables() {
// 12 standard frame rates; the 23.976 entry is 24000/1001.
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_count() },
12
);
let (mut num, mut den) = (0, 0);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(2, &mut num, &mut den) },
0
);
assert_eq!((num, den), (24000, 1001));
// Out of range → E_INVALID.
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(99, &mut num, &mut den) },
-1
);
// 6 standard pixel aspects; index 4 is PAL widescreen 64/45.
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_count() },
6
);
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(4, &mut num, &mut den) },
0
);
assert_eq!((num, den), (64, 45));
// Dividers 1..=8; out of range → -1.
assert_eq!(
unsafe { oakengine_video_params_supported_divider_count() },
8
);
assert_eq!(unsafe { oakengine_video_params_supported_divider_at(5) }, 8);
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(99) },
-1
);
// Format helpers (PixelFormat codes: F16 = 3, F32 = 4).
assert_eq!(unsafe { oakengine_video_params_format_is_float(4) }, 1); // F32
assert_eq!(unsafe { oakengine_video_params_format_is_float(3) }, 1); // F16
assert_eq!(unsafe { oakengine_video_params_format_is_float(0) }, 0); // U8
assert_eq!(
unsafe { oakengine_video_params_internal_channel_count() },
4
);
assert!(unsafe { oakengine_video_params_bytes_per_pixel(1, 4) } > 0);
}
/// Videoparams POD: make/equal/valid + effective size.
#[test]
fn videoparams_pod() {
let mut a: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_video_params_make(&mut a, 1920, 1080, 1001, 30000, 16, 1, 1, 0, 1, 1,) },
0
);
assert_eq!(a.width, 1920);
assert_eq!(a.height, 1080);
assert_eq!(a.time_base_num, 1001);
// A valid POD is valid.
assert_eq!(unsafe { oakengine_video_params_is_valid(&a) }, 1);
// Zero dimensions are not.
let mut bad = a;
bad.width = 0;
assert_eq!(unsafe { oakengine_video_params_is_valid(&bad) }, 0);
// NULL is invalid.
assert_eq!(
unsafe { oakengine_video_params_is_valid(std::ptr::null()) },
0
);
// Equality: identical PODs equal; differing field not.
let mut b = a;
assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 1);
b.divider = 2;
assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 0);
assert_eq!(
unsafe { oakengine_video_params_equal(std::ptr::null(), &a) },
0
);
// Effective size halves at divider 2.
let (mut w, mut h) = (0, 0);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 2, &mut w, &mut h) },
0
);
assert_eq!((w, h), (960, 540));
// Invalid divider.
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 0, &mut w, &mut h) },
-1
);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,953 @@
// 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/>.
//! Integration tests for the common family (`src/common.rs` over
//! `engine/include/oakengine/{config,videoparams}.h`).
//!
//! Every exported function is exercised on a legal path with the result
//! asserted, plus the illegal-input matrix the engine must survive (NULL
//! pointers, empty handles, out-of-range indexes, zero/negative sizes,
//! garbage enums). All behavior is real: the facade calls into the real
//! oakcommon store and videoparams domain.
//!
//! The oakcommon config store is a process-wide singleton backed by
//! `config.ini` (honoring the `OAK_CONFIG_DIR` override), so every test
//! that touches config is serialized under [`CONFIG_LOCK`] and redirects
//! the file into a fresh temp dir. The videoparams tables are immutable
//! statics and the params handles are per-test objects, so those tests
//! run in parallel.
// The whole family is called through uniform `unsafe {}` blocks (matching
// the other test binaries), so extern functions that happen to be safe
// (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`.
#![allow(unused_unsafe)]
use super::common;
use std::ffi::{c_char, c_int};
use std::path::Path;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex;
use crate::common::{
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
oakengine_config_report_error, oakengine_config_save, oakengine_config_set_error_handler,
oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
oakengine_video_params_create, oakengine_video_params_divider_name,
oakengine_video_params_effective_size, oakengine_video_params_equal,
oakengine_video_params_format_is_float,
oakengine_video_params_format_pixel_aspect_ratio_string,
oakengine_video_params_frame_rate_to_string, oakengine_video_params_free,
oakengine_video_params_internal_channel_count, oakengine_video_params_is_valid,
oakengine_video_params_make, oakengine_video_params_pixel_format_name,
oakengine_video_params_standard_pixel_aspect_at,
oakengine_video_params_standard_pixel_aspect_count,
oakengine_video_params_standard_pixel_aspect_name, oakengine_video_params_supported_divider_at,
oakengine_video_params_supported_divider_count, oakengine_video_params_supported_frame_rate_at,
oakengine_video_params_supported_frame_rate_count, OakVideoParamsPod,
};
/// Read a two-stage facade string into a Rust String.
unsafe fn read_buf(buf: &mut [c_char]) -> String {
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned()
}
/// Serializes every test that touches the process-wide config store and
/// redirects `OAK_CONFIG_DIR` to a fresh temp dir for the duration of `f`
/// (same pattern as the oakcommon crate's own test support). The only
/// readers of `OAK_CONFIG_DIR` in this binary are these serialized tests.
fn with_temp_config_dir<T>(f: impl FnOnce(&Path) -> T) -> T {
let _guard = CONFIG_LOCK.lock().unwrap();
let dir =
std::env::temp_dir().join(format!("oakengine_it_common_config_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::env::set_var("OAK_CONFIG_DIR", &dir);
let result = f(&dir);
std::env::remove_var("OAK_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
result
}
/// The process-wide config store is a singleton; see module doc.
static CONFIG_LOCK: Mutex<()> = Mutex::new(());
// ---------------------------------------------------------------------------
// config.h
// ---------------------------------------------------------------------------
/// Load/save round-trip, defaults, typed entries and the two-stage string
/// convention (all serialized: the store is process-wide).
#[test]
fn config_roundtrip_persistence() {
common::force_link();
with_temp_config_dir(|dir| {
// A missing config.ini is not an error; defaults are loaded.
assert_eq!(unsafe { oakengine_config_load() }, 0);
// Missing keys read as empty / fallback.
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_config_get_string(c"no/such/key".as_ptr(), buf.as_mut_ptr(), 64) },
0
);
assert_eq!(unsafe { read_buf(&mut buf) }, "");
assert_eq!(
unsafe { oakengine_config_get_int(c"no/such/key".as_ptr(), 7) },
7
);
// Compiled-in defaults are readable through the engine getters.
let len = unsafe {
oakengine_config_get_string(c"DefaultSequenceFrameRate".as_ptr(), buf.as_mut_ptr(), 64)
};
assert_eq!(len, 10);
assert_eq!(unsafe { read_buf(&mut buf) }, "1001/30000");
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
1920
);
// String round-trip.
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) },
0
);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "hello");
// Too-small buffer: the full length is reported and the buffer is
// left untouched (query size, then allocate, then copy).
let mut small = [0 as c_char; 3];
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), small.as_mut_ptr(), 3) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut small) }, "");
// A NULL value stores an empty string (engine treats NULL as "").
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), std::ptr::null()) },
0
);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "");
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) },
0
);
// A string entry read through the int getter falls back.
assert_eq!(
unsafe { oakengine_config_get_int(c"it/key".as_ptr(), 9) },
9
);
// Int round-trip; a known typed key keeps its type across reload.
assert_eq!(
unsafe { oakengine_config_set_int(c"it/num".as_ptr(), 1234) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 0) },
1234
);
assert_eq!(
unsafe { oakengine_config_set_int(c"DefaultSequenceWidth".as_ptr(), 640) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
640
);
// Persist, then reload from the file.
assert_eq!(unsafe { oakengine_config_save() }, 0);
assert!(dir.join("config.ini").exists());
assert_eq!(unsafe { oakengine_config_load() }, 0);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "hello");
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
640
);
// A custom typed key loses its type on reload and reads as a string
// (module C++ parity: only known keys keep their declared type).
let len = unsafe { oakengine_config_get_string(c"it/num".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 4);
assert_eq!(unsafe { read_buf(&mut buf) }, "1234");
assert_eq!(
unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 9) },
9
);
});
}
/// Illegal inputs on the config getters/setters: NULL keys and buffers,
/// empty keys, zero/negative sizes — all must fail cleanly, never crash.
#[test]
fn config_illegal_inputs() {
common::force_link();
with_temp_config_dir(|_dir| {
assert_eq!(unsafe { oakengine_config_load() }, 0);
assert_eq!(
unsafe { oakengine_config_set_string(c"it/k".as_ptr(), c"abc".as_ptr()) },
0
);
let mut buf = [0 as c_char; 64];
// NULL key → OAKENGINE_E_INVALID (-1).
assert_eq!(
unsafe { oakengine_config_get_string(std::ptr::null(), buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_config_set_string(std::ptr::null(), c"v".as_ptr()) },
-1
);
assert_eq!(unsafe { oakengine_config_set_int(std::ptr::null(), 5) }, -1);
// NULL key on the int getter returns the fallback (engine contract).
assert_eq!(
unsafe { oakengine_config_get_int(std::ptr::null(), 42) },
42
);
// Empty key → the module's INVALID, passed through untranslated.
assert_eq!(
unsafe { oakengine_config_get_string(c"".as_ptr(), buf.as_mut_ptr(), 64) },
-10001
);
assert_eq!(unsafe { oakengine_config_get_int(c"".as_ptr(), 42) }, 42);
// NULL output buffer with a positive size → module INVALID (-10001).
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 64) },
-10001
);
// Negative size → module INVALID.
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), buf.as_mut_ptr(), -1) },
-10001
);
// NULL buffer with size 0 is the two-stage size query: reports the
// required length without writing.
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 0) },
3
);
});
}
/// Error handler: registered, invoked via report_error and on a load
/// failure, NULL args are safe, NULL handler clears.
#[test]
fn config_error_handler_and_load_failure() {
common::force_link();
static CALLED: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn handler(
_title: *const c_char,
_message: *const c_char,
_userdata: *mut std::ffi::c_void,
) {
CALLED.fetch_add(1, Ordering::SeqCst);
}
with_temp_config_dir(|dir| {
CALLED.store(0, Ordering::SeqCst);
// Register and report through the handler.
assert_eq!(
unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) },
0
);
assert_eq!(
unsafe { oakengine_config_report_error(c"title".as_ptr(), c"message".as_ptr()) },
0
);
assert_eq!(CALLED.load(Ordering::SeqCst), 1);
// NULL title/message are mapped to empty strings, still invoked.
assert_eq!(
unsafe { oakengine_config_report_error(std::ptr::null(), std::ptr::null()) },
0
);
assert_eq!(CALLED.load(Ordering::SeqCst), 2);
// NULL handler clears; reporting then does not invoke.
assert_eq!(
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) },
0
);
unsafe { oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
assert_eq!(CALLED.load(Ordering::SeqCst), 2);
// A real load failure (config.ini is a directory) reports through
// the module's registered handler and returns the module FAILED
// code (-10003) untranslated.
assert_eq!(
unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) },
0
);
std::fs::create_dir(dir.join("config.ini")).unwrap();
assert_eq!(unsafe { oakengine_config_load() }, -10003);
assert_eq!(CALLED.load(Ordering::SeqCst), 3);
// Cleanup: drop the directory and clear the handler.
std::fs::remove_dir(dir.join("config.ini")).unwrap();
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) };
assert_eq!(unsafe { oakengine_config_load() }, 0);
});
}
// ---------------------------------------------------------------------------
// videoparams.h — static tables
// ---------------------------------------------------------------------------
/// Static tables: counts, every legal index, specific values, and the
/// out-of-range / NULL failure paths.
#[test]
fn videoparams_static_tables_full() {
common::force_link();
// ---- frame rates ------------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_count() },
12
);
let mut num: c_int = 0;
let mut den: c_int = 0;
for i in 0..12 {
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(i, &mut num, &mut den) },
0
);
assert!(
num > 0 && den > 0,
"frame rate {i} must be a positive rational"
);
}
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(0, &mut num, &mut den) },
0
);
assert_eq!((num, den), (10, 1));
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(2, &mut num, &mut den) },
0
);
assert_eq!((num, den), (24000, 1001)); // 23.976
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(5, &mut num, &mut den) },
0
);
assert_eq!((num, den), (30000, 1001)); // 29.97
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(6, &mut num, &mut den) },
0
);
assert_eq!((num, den), (30, 1));
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(11, &mut num, &mut den) },
0
);
assert_eq!((num, den), (60, 1));
// Out-of-range / negative / huge indexes → E_INVALID (-1), no panic.
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(12, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(99, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(-1, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(c_int::MAX, &mut num, &mut den) },
-1
);
// NULL outputs → E_INVALID.
assert_eq!(
unsafe {
oakengine_video_params_supported_frame_rate_at(0, std::ptr::null_mut(), &mut den)
},
-1
);
assert_eq!(
unsafe {
oakengine_video_params_supported_frame_rate_at(0, &mut num, std::ptr::null_mut())
},
-1
);
// ---- pixel aspects ----------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_count() },
6
);
for i in 0..6 {
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(i, &mut num, &mut den) },
0
);
assert!(
num > 0 && den > 0,
"pixel aspect {i} must be a positive rational"
);
}
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(0, &mut num, &mut den) },
0
);
assert_eq!((num, den), (1, 1));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(4, &mut num, &mut den) },
0
);
assert_eq!((num, den), (64, 45));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(5, &mut num, &mut den) },
0
);
assert_eq!((num, den), (4, 3));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(6, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(-1, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe {
oakengine_video_params_standard_pixel_aspect_at(0, std::ptr::null_mut(), &mut den)
},
-1
);
// ---- dividers ---------------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_supported_divider_count() },
8
);
let expected: [c_int; 8] = [1, 2, 3, 4, 6, 8, 12, 16];
for (i, want) in expected.iter().enumerate() {
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(i as c_int) },
*want
);
}
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(8) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(-1) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(c_int::MAX) },
-1
);
}
/// Display names and string formatters (pixel aspect names, divider names,
/// frame-rate strings, PAR template formatting).
#[test]
fn videoparams_names_and_formatters() {
common::force_link();
let mut buf = [0 as c_char; 64];
// ---- standard pixel aspect names --------------------------------------
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(0, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "Square");
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "8:9");
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(4, buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "64:45");
// Out of range → E_INVALID; negative index → E_INVALID.
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(6, buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(-1, buf.as_mut_ptr(), 64) },
-1
);
// NULL buffer reports the length only (two-stage size query).
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(0, std::ptr::null_mut(), 64) },
6
);
// Too-small buffer truncates but reports the full length.
let mut small = [0 as c_char; 2];
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(0, small.as_mut_ptr(), 2) },
6
);
assert_eq!(unsafe { read_buf(&mut small) }, "S");
// ---- divider names ------------------------------------------------------
let len = unsafe { oakengine_video_params_divider_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 4);
assert_eq!(unsafe { read_buf(&mut buf) }, "Full");
let len = unsafe { oakengine_video_params_divider_name(2, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "1/2");
let len = unsafe { oakengine_video_params_divider_name(8, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "1/8");
// Zero / negative divider → E_INVALID (facade rejects before the module).
assert_eq!(
unsafe { oakengine_video_params_divider_name(0, buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_divider_name(-3, buf.as_mut_ptr(), 64) },
-1
);
// NULL buffer with a positive size → module INVALID, passed through.
assert_eq!(
unsafe { oakengine_video_params_divider_name(2, std::ptr::null_mut(), 64) },
-10001
);
// ---- frame rate strings -------------------------------------------------
let len = unsafe { oakengine_video_params_frame_rate_to_string(25, 1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "25 FPS");
let len =
unsafe { oakengine_video_params_frame_rate_to_string(24000, 1001, buf.as_mut_ptr(), 64) };
assert_eq!(len, 10);
assert_eq!(unsafe { read_buf(&mut buf) }, "23.976 FPS");
let len = unsafe { oakengine_video_params_frame_rate_to_string(10, 1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "10 FPS");
// Zero denominator: C++-parity float division (1/0 → +inf), rendered as
// "inf FPS" — a legal return, never a crash/panic.
let len = unsafe { oakengine_video_params_frame_rate_to_string(1, 0, buf.as_mut_ptr(), 64) };
assert!(len >= 0, "den=0 must not error ({len})");
assert_eq!(unsafe { read_buf(&mut buf) }, "inf FPS");
// 0/0 → NaN → "nan FPS".
let len = unsafe { oakengine_video_params_frame_rate_to_string(0, 0, buf.as_mut_ptr(), 64) };
assert!(len >= 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "nan FPS");
// NULL buffer with a positive size → module INVALID.
assert_eq!(
unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 64) },
-10001
);
// NULL buffer with size 0 is the two-stage size query.
assert_eq!(
unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 0) },
6
);
// ---- PAR template formatting (facade-local) -----------------------------
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"%1".as_ptr(),
16,
15,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "16:15");
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"par=%1".as_ptr(),
4,
3,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 7);
assert_eq!(unsafe { read_buf(&mut buf) }, "par=4:3");
// No placeholder: the template passes through unchanged.
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"raw".as_ptr(),
16,
15,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "raw");
// NULL format → E_INVALID; NULL buffer reports the length only.
assert_eq!(
unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
std::ptr::null(),
16,
15,
buf.as_mut_ptr(),
64,
)
},
-1
);
assert_eq!(
unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"%1".as_ptr(),
16,
15,
std::ptr::null_mut(),
64,
)
},
5
);
}
/// Format helpers: float/name queries and bytes-per-pixel across the
/// format matrix (valid, boundary and garbage codes).
#[test]
fn videoparams_format_helpers() {
common::force_link();
// format_is_float: F16 = 3, F32 = 4 float; everything else 0, garbage
// codes map to the Invalid format and report 0 (never crash).
assert_eq!(unsafe { oakengine_video_params_format_is_float(0) }, 0); // U8
assert_eq!(unsafe { oakengine_video_params_format_is_float(1) }, 0); // U10
assert_eq!(unsafe { oakengine_video_params_format_is_float(2) }, 0); // U16
assert_eq!(unsafe { oakengine_video_params_format_is_float(3) }, 1); // F16
assert_eq!(unsafe { oakengine_video_params_format_is_float(4) }, 1); // F32
assert_eq!(unsafe { oakengine_video_params_format_is_float(5) }, 0); // Count
assert_eq!(unsafe { oakengine_video_params_format_is_float(99) }, 0);
assert_eq!(unsafe { oakengine_video_params_format_is_float(-1) }, 0);
assert_eq!(
unsafe { oakengine_video_params_format_is_float(c_int::MIN) },
0
);
// pixel_format_name for every real format.
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "8-bit");
let len = unsafe { oakengine_video_params_pixel_format_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 13);
assert_eq!(unsafe { read_buf(&mut buf) }, "10-bit Packed");
let len = unsafe { oakengine_video_params_pixel_format_name(4, buf.as_mut_ptr(), 64) };
assert_eq!(len, 19);
assert_eq!(unsafe { read_buf(&mut buf) }, "Full-Float (32-bit)");
// Garbage format → "Unknown (0xFFFFFFFF)" (Invalid renders %X of -1).
let len = unsafe { oakengine_video_params_pixel_format_name(99, buf.as_mut_ptr(), 64) };
assert_eq!(len, 20);
assert_eq!(unsafe { read_buf(&mut buf) }, "Unknown (0xFFFFFFFF)");
// NULL buffer / negative size → module INVALID; size-0 query → length.
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 64) },
-10001
);
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), -1) },
-10001
);
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 0) },
5
);
// bytes_per_pixel across the format × channels matrix.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 4) }, 4); // U8
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(1, 4) }, 4); // U10 packed
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(2, 4) }, 8); // U16
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(3, 4) }, 8); // F16
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 4) }, 16); // F32
// Garbage formats have no channels-per-format entry → 0 bytes.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(99, 4) }, 0);
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(-1, 4) }, 0);
// Zero channels → 0 bytes.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 0) }, 0);
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 0) }, 0);
// Negative channels: the module does not validate (C++ parity), so the
// result is the plain signed product — a value, not a crash.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, -1) }, -4);
assert_eq!(
unsafe { oakengine_video_params_internal_channel_count() },
4
);
}
/// Effective size: divider scaling on the legal matrix plus zero/negative
/// dimensions and dividers → E_INVALID.
#[test]
fn videoparams_effective_size_matrix() {
common::force_link();
let mut w: c_int = 0;
let mut h: c_int = 0;
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 1, &mut w, &mut h) },
0
);
assert_eq!((w, h), (1920, 1080));
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 2, &mut w, &mut h) },
0
);
assert_eq!((w, h), (960, 540));
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 4, &mut w, &mut h) },
0
);
assert_eq!((w, h), (480, 270));
assert_eq!(
unsafe { oakengine_video_params_effective_size(100, 50, 3, &mut w, &mut h) },
0
);
assert_eq!((w, h), (33, 16));
// Divider 16 truncates the odd dimension (integer division).
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 16, &mut w, &mut h) },
0
);
assert_eq!((w, h), (120, 67));
// Both output pointers may be NULL (size computed, nothing written).
assert_eq!(
unsafe {
oakengine_video_params_effective_size(
1920,
1080,
2,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
);
// Zero / negative dimensions and dividers → E_INVALID.
assert_eq!(
unsafe { oakengine_video_params_effective_size(0, 1080, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 0, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(-1, 1080, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 0, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, -2, &mut w, &mut h) },
-1
);
}
// ---------------------------------------------------------------------------
// videoparams.h — POD make/equal/valid
// ---------------------------------------------------------------------------
/// A valid POD used across the POD tests.
fn valid_pod() -> OakVideoParamsPod {
let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2,) },
0
);
p
}
/// make fills every field; equal compares all of them; is_valid implements
/// the engine's POD validity rules.
#[test]
fn videoparams_pod_make_equal_valid() {
common::force_link();
// make: every field lands in the POD.
let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2) },
0
);
assert_eq!(p.width, 1920);
assert_eq!(p.height, 1080);
assert_eq!(p.time_base_num, 1001);
assert_eq!(p.time_base_den, 30000);
assert_eq!(p.format, 4);
assert_eq!(p.pixel_aspect_num, 1);
assert_eq!(p.pixel_aspect_den, 1);
assert_eq!(p.interlacing, 0);
assert_eq!(p.color_range, 1);
assert_eq!(p.divider, 2);
assert_eq!(p.video_type, 0);
assert_eq!(p.premultiplied_alpha, 0);
// NULL POD → E_INVALID.
assert_eq!(
unsafe {
oakengine_video_params_make(
std::ptr::null_mut(),
1920,
1080,
1001,
30000,
4,
1,
1,
0,
1,
2,
)
},
-1
);
// equal: identical PODs → 1; any differing field → 0; NULL → 0.
let a = valid_pod();
let mut b = a;
assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 1);
for (field, val) in [
("width", 640),
("height", 720),
("time_base_num", 25),
("time_base_den", 1),
("format", 0),
("pixel_aspect_num", 4),
("pixel_aspect_den", 3),
("interlacing", 1),
("color_range", 0),
("divider", 1),
("video_type", 1),
("premultiplied_alpha", 1),
] {
let mut c = a;
match field {
"width" => c.width = val,
"height" => c.height = val,
"time_base_num" => c.time_base_num = val,
"time_base_den" => c.time_base_den = val,
"format" => c.format = val,
"pixel_aspect_num" => c.pixel_aspect_num = val,
"pixel_aspect_den" => c.pixel_aspect_den = val,
"interlacing" => c.interlacing = val,
"color_range" => c.color_range = val,
"divider" => c.divider = val,
"video_type" => c.video_type = val,
"premultiplied_alpha" => c.premultiplied_alpha = val,
_ => unreachable!(),
}
assert_eq!(
unsafe { oakengine_video_params_equal(&a, &c) },
0,
"equal must be 0 when {field} differs"
);
}
assert_eq!(
unsafe { oakengine_video_params_equal(std::ptr::null(), &a) },
0
);
assert_eq!(
unsafe { oakengine_video_params_equal(&a, std::ptr::null()) },
0
);
// is_valid: the valid POD → 1.
assert_eq!(unsafe { oakengine_video_params_is_valid(&a) }, 1);
// NULL → 0.
assert_eq!(
unsafe { oakengine_video_params_is_valid(std::ptr::null()) },
0
);
// Each invalidating field → 0.
let cases: [(&str, fn(&mut OakVideoParamsPod)); 6] = [
("width", |p| p.width = 0),
("height", |p| p.height = 0),
("pixel_aspect_num", |p| p.pixel_aspect_num = 0),
("pixel_aspect_den", |p| p.pixel_aspect_den = 0),
("format", |p| p.format = -1),
("time_base_den", |p| p.time_base_den = 0),
];
for (name, mutate) in cases {
let mut c = a;
mutate(&mut c);
assert_eq!(
unsafe { oakengine_video_params_is_valid(&c) },
0,
"is_valid must be 0 when {name} is invalid"
);
}
// NOTE (observed divergence): the facade's POD check uses `format >= 0`,
// so out-of-range-but-non-negative formats (e.g. 99, or the Count code 5)
// read as "valid" here, while the module/C++ `VideoParams::is_valid`
// additionally requires `format < Count`. The facade check is a
// simplified local rule (the POD has no channel_count), not a crash.
let mut c = a;
c.format = 99;
assert_eq!(unsafe { oakengine_video_params_is_valid(&c) }, 1);
}
// ---------------------------------------------------------------------------
// videoparams.h — opaque handle lifecycle
// ---------------------------------------------------------------------------
/// create/free lifecycle: NULL rejection, real-handle creation, NULL free.
#[test]
fn videoparams_create_free_lifecycle() {
common::force_link();
// NULL POD → NULL handle.
assert!(unsafe { oakengine_video_params_create(std::ptr::null()) }.is_null());
// Valid POD → non-NULL handle; freed cleanly.
let pod = valid_pod();
let h = unsafe { oakengine_video_params_create(&pod) };
assert!(!h.is_null());
unsafe { oakengine_video_params_free(h) };
// A zeroed POD still yields a handle (the module initializes a default
// set and the setters accept any values); the handle frees cleanly.
let zeroed: OakVideoParamsPod = unsafe { std::mem::zeroed() };
let h = unsafe { oakengine_video_params_create(&zeroed) };
assert!(!h.is_null());
unsafe { oakengine_video_params_free(h) };
// free(NULL) is a documented no-op.
unsafe { oakengine_video_params_free(std::ptr::null_mut()) };
// NOTE (contract): the facade's free deallocates the handle box
// (`Box::from_raw`), so a second free of the same pointer is a
// use-after-free and is NOT part of the family's contract — unlike the
// module-level `oakcommon_videoparams_free`, which nulls the handle out
// before returning. This family exposes no debug alive counter to
// verify a return to baseline; leak-free operation is implied by the
// create/free round-trips above.
}
@@ -0,0 +1,459 @@
// 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/>.
//! Integration tests for the **exporter family** (`src/codec.rs`,
//! "Exporter family"; module contract
//! `engine/include/oakengine/exporter.h`).
//!
//! Coverage rules (see the family test charter):
//! 1. no mocks — every call goes through the real facade into the real
//! module crates (the only stubs are the host-provided `oakcore_*`
//! symbols in `tests/common`); the output file is a REAL mp4 written
//! by the statically linked FFmpeg (oakcodec encoder), asserted by
//! its `ftyp` box;
//! 2. every exporter-family export is exercised on a legal path with the
//! result asserted;
//! 3. illegal inputs (NULL seq/path, negative ranges, unknown codecs)
//! always yield a negative error code — never a crash;
//! 4. the progress callback receives at least one update during a run,
//! with the installed userdata.
//!
//! ## Serialization
//!
//! The tests assemble projects and run export tasks, both of which touch
//! process-wide state (the undo stack cleared by `oakengine_project_new`,
//! the global task manager), so every test takes a shared [`SERIAL`] mutex
//! (the same pattern as `it_task`).
use super::common;
use std::ffi::{c_char, c_double, c_int, c_void};
use std::io::Read;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::codec::{
oakengine_encoding_params_create, oakengine_encoding_params_enable_audio,
oakengine_encoding_params_enable_video, oakengine_encoding_params_set_filename,
oakengine_encoding_params_set_format, oakengine_export_last_error, oakengine_export_render,
oakengine_export_render_with_params, oakengine_export_set_progress_callback,
};
use crate::common::OakVideoParamsPod;
use crate::handle::{OakEngineProject, OakEngineSequence, free_box};
use crate::node::{
oakengine_footage_free, oakengine_project_create, oakengine_project_free,
oakengine_project_import_footage, oakengine_project_new,
};
use crate::pods::OakExportOptions;
use crate::testmedia::oakengine_testmedia_write_clip;
use crate::timeline::{
oakengine_sequence_add_footage_clip_ex, oakengine_sequence_add_track, oakengine_sequence_new,
oakengine_sequence_set_video_params,
};
/// `OAKENGINE_TRACK_TYPE_*` (timeline.h).
const TRACK_VIDEO: c_int = 0;
const TRACK_AUDIO: c_int = 1;
/// `olive::ExportFormat::Format` ids (mp4 = MPEG-4 video).
const FORMAT_MP4: c_int = 2;
/// `olive::ExportCodec::Codec` ids.
const CODEC_H264: c_int = 1;
const CODEC_AAC: c_int = 12;
/// Serializes every test in this binary (see the module docs). Poisoned by
/// a panicking test, the lock is recovered with `into_inner` so one failure
/// does not cascade into `PoisonError` failures in every later test.
static SERIAL: Mutex<()> = Mutex::new(());
/// Take the [`SERIAL`] lock, recovering from any poisoning.
fn serial() -> std::sync::MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
}
/// A unique temp path (per-process, so parallel test binaries never
/// collide).
fn temp_path(kind: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("oakengine-it-export-{kind}-{}.mp4", std::process::id()))
}
/// Encode the facade test clip and assemble a project + sequence carrying
/// it: one video track/clip and one audio track/clip, both spanning
/// `0..frame_count` at `fps` frames per second (mirrors the CLI's
/// `assemble_project`). Returns `(project, sequence)` — the caller
/// releases the sequence box with `free_box` and the project with
/// `oakengine_project_free`.
///
/// # Safety
/// The returned handles must be released by the caller exactly once.
unsafe fn assemble_test_sequence(
media: &std::path::Path,
width: c_int,
height: c_int,
frame_count: i64,
fps: c_int,
) -> (*mut OakEngineProject, *mut OakEngineSequence) {
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
assert_eq!(
oakengine_testmedia_write_clip(media_c.as_ptr(), width, height, frame_count as c_int, fps),
0,
"generate the source clip"
);
let project = oakengine_project_create();
assert!(!project.is_null());
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) };
assert!(!footage.is_null(), "import the test clip");
let seq = unsafe { oakengine_sequence_new(project, c"Export Test".as_ptr()) };
assert!(!seq.is_null());
assert_eq!(
unsafe {
oakengine_sequence_set_video_params(
seq, width, height, fps, 1, 1, 1, 0, 4, // PIXEL_FORMAT_F32
0,
)
},
0,
"set the sequence frame rate"
);
let vt = unsafe { oakengine_sequence_add_track(seq, TRACK_VIDEO) };
assert!(vt >= 0, "add the video track");
let vclip = unsafe {
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_VIDEO, vt, 0, frame_count, 0)
};
assert!(!vclip.is_null(), "place the video clip");
let at = unsafe { oakengine_sequence_add_track(seq, TRACK_AUDIO) };
assert!(at >= 0, "add the audio track");
let aclip = unsafe {
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_AUDIO, at, 0, frame_count, 0)
};
assert!(!aclip.is_null(), "place the audio clip");
unsafe { oakengine_footage_free(footage) };
(project, seq)
}
/// Read the facade's thread-local export last-error (the string the
/// assertion messages embed).
fn export_last_error_str() -> String {
let mut buf = [0 as c_char; 512];
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 512) };
if n <= 0 {
return String::new();
}
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf8_lossy(unsafe {
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
})
.into_owned()
}
/// Assert the exported file exists, is non-empty and starts with the MP4
/// `ftyp` box.
fn assert_real_mp4(path: &std::path::Path) {
let meta = std::fs::metadata(path).expect("the exported file must exist");
assert!(meta.len() > 0, "the exported file must be non-empty");
let mut head = [0u8; 12];
std::fs::File::open(path)
.expect("reopen the exported file")
.read_exact(&mut head)
.expect("read the file head");
assert_eq!(&head[4..8], b"ftyp", "MP4 files start with the ftyp box");
}
/// Release the assembly returned by [`assemble_test_sequence`].
///
/// # Safety
/// The handles must be the ones returned by [`assemble_test_sequence`].
unsafe fn drop_test_sequence(project: *mut OakEngineProject, seq: *mut OakEngineSequence) {
unsafe {
free_box::<OakEngineSequence>(seq);
oakengine_project_free(project);
}
}
// ---------------------------------------------------------------------------
// Legal paths (real mp4 output)
// ---------------------------------------------------------------------------
/// `oakengine_export_render` end-to-end: a 1 s test clip (10 frames at
/// 10 fps, 64x64) in a project + sequence is exported to H.264/AAC MP4
/// with explicit `OakExportOptions`; the file exists, is non-empty, starts
/// with the `ftyp` box, and `oakengine_export_last_error` is empty after
/// the success.
#[test]
fn export_render_writes_real_mp4() {
let _g = serial();
common::force_link();
let media = temp_path("src");
let out = temp_path("out");
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
let opts = OakExportOptions {
video_codec: 0, // OAKENGINE_EXPORT_VIDEO_H264
audio_codec: 0, // OAKENGINE_EXPORT_AUDIO_AAC
video_bit_rate: 0,
audio_sample_rate: 48000,
audio_channel_count: 2,
};
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &opts) };
assert_eq!(
rc,
0,
"the mp4 export must succeed with the real encoder (last_error: {})",
export_last_error_str()
);
assert_real_mp4(&out);
// The thread-local last-error slot is empty after a success.
let mut buf = [0 as c_char; 256];
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
assert_eq!(n, 0, "last_error must be empty after a successful export");
unsafe { drop_test_sequence(project, seq) };
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
}
/// `oakengine_export_render_with_params` end-to-end: the same assembly
/// driven through a caller-built encoding-params handle (the path the
/// app's `start_export` uses). The handle is consumed by the export task.
#[test]
fn export_render_with_params_writes_real_mp4() {
let _g = serial();
common::force_link();
let media = temp_path("srcwp");
let out = temp_path("outwp");
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
let params = unsafe { oakengine_encoding_params_create() };
assert!(!params.is_null());
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
assert_eq!(unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_encoding_params_set_format(params, FORMAT_MP4) }, 0);
let pod = OakVideoParamsPod {
width: 64,
height: 64,
time_base_num: 1,
time_base_den: 10, // 10 fps → frame duration 1/10
format: 0,
pixel_aspect_num: 1,
pixel_aspect_den: 1,
interlacing: 0,
color_range: 0,
divider: 1,
video_type: 0,
premultiplied_alpha: 0,
};
assert_eq!(unsafe { oakengine_encoding_params_enable_video(params, &pod, CODEC_H264) }, 0);
assert_eq!(
unsafe { oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, CODEC_AAC) },
0
);
// Export length: 10 frames at 10 fps = 1 s.
unsafe {
crate::codec::oakengine_encoding_params_set_export_length(params, 1, 1);
}
let rc = unsafe { oakengine_export_render_with_params(seq, params) };
assert_eq!(
rc,
0,
"the with_params export must succeed with the real encoder (last_error: {})",
export_last_error_str()
);
// The params handle was consumed by the task; do NOT destroy it here.
assert_real_mp4(&out);
unsafe { drop_test_sequence(project, seq) };
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
}
/// `oakengine_export_render` with NULL opts selects the documented
/// defaults (H.264/AAC mp4), and the installed progress callback receives
/// at least one update with the installed userdata during the run.
#[test]
fn export_render_defaults_and_progress_callback() {
let _g = serial();
common::force_link();
let media = temp_path("srcprog");
let out = temp_path("outprog");
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
// Progress callbacks are thread-local on the exporting thread; the
// sync run fires them there, so the atomics are safe to reset while
// holding the serial lock.
PROGRESS_CALLS.store(0, Ordering::SeqCst);
PROGRESS_USERDATA_HIT.store(0, Ordering::SeqCst);
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
unsafe {
oakengine_export_set_progress_callback(Some(count_progress), PROGRESS_TOKEN as *mut c_void);
}
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, std::ptr::null()) };
assert_eq!(
rc,
0,
"NULL opts must select the H.264/AAC defaults (last_error: {})",
export_last_error_str()
);
assert_real_mp4(&out);
assert!(
PROGRESS_CALLS.load(Ordering::SeqCst) > 0,
"progress must be reported during a run"
);
assert!(
PROGRESS_USERDATA_HIT.load(Ordering::SeqCst) > 0,
"the installed userdata must reach the callback"
);
// NULL disables the callback for subsequent runs.
unsafe { oakengine_export_set_progress_callback(None, std::ptr::null_mut()) };
unsafe { drop_test_sequence(project, seq) };
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
}
// ---------------------------------------------------------------------------
// Illegal inputs (negative codes, no crash)
// ---------------------------------------------------------------------------
/// Every invalid argument combination of `oakengine_export_render` returns
/// `OAKENGINE_E_INVALID` (-1) with a non-empty last-error, and the
/// `oakengine_export_render_with_params` NULL paths return E_INVALID
/// without consuming the caller's params handle.
#[test]
fn export_render_illegal_arguments() {
let _g = serial();
common::force_link();
let media = temp_path("srcbad");
let out = temp_path("outbad");
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
let opts = OakExportOptions {
video_codec: 0,
audio_codec: 0,
video_bit_rate: 0,
audio_sample_rate: 48000,
audio_channel_count: 2,
};
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
// NULL seq / path.
assert_eq!(
unsafe { oakengine_export_render(std::ptr::null_mut(), out_c.as_ptr(), 0, 10, 64, 64, &opts) },
-1
);
assert_eq!(
unsafe { oakengine_export_render(seq, std::ptr::null(), 0, 10, 64, 64, &opts) },
-1
);
// Negative / inverted ranges.
assert_eq!(
unsafe { oakengine_export_render(seq, out_c.as_ptr(), -1, 10, 64, 64, &opts) },
-1
);
assert_eq!(
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 5, 5, 64, 64, &opts) },
-1
);
// Unknown codec ids.
let bad_video = OakExportOptions { video_codec: 99, ..opts };
assert_eq!(
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_video) },
-1
);
let bad_audio = OakExportOptions { audio_codec: 99, ..opts };
assert_eq!(
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_audio) },
-1
);
// An unsupported audio channel count.
let bad_channels = OakExportOptions { audio_channel_count: 3, ..opts };
assert_eq!(
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_channels) },
-1
);
// The failures record a reason.
let mut buf = [0 as c_char; 256];
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
assert!(n > 0, "a failed export must record a last error");
// `with_params`: NULL arguments are rejected without consuming the
// valid handle (the caller destroys it afterwards).
let params = unsafe { oakengine_encoding_params_create() };
assert!(!params.is_null());
assert_eq!(
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), params) },
-1
);
assert_eq!(
unsafe { oakengine_export_render_with_params(seq, std::ptr::null()) },
-1
);
unsafe { crate::codec::oakengine_encoding_params_destroy(params) };
assert!(!out.exists(), "a rejected export must not write the output");
unsafe { drop_test_sequence(project, seq) };
let _ = std::fs::remove_file(&media);
let _ = std::fs::remove_file(&out);
}
// ---------------------------------------------------------------------------
// Progress callback bookkeeping
// ---------------------------------------------------------------------------
/// Sentinel userdata the progress test installs.
const PROGRESS_TOKEN: usize = 0x0A0A_5EED;
/// Number of progress callback invocations (reset per test).
static PROGRESS_CALLS: AtomicUsize = AtomicUsize::new(0);
/// Number of invocations that received the sentinel userdata.
static PROGRESS_USERDATA_HIT: AtomicUsize = AtomicUsize::new(0);
/// Counts every progress callback and checks the userdata round-trip.
unsafe extern "C" fn count_progress(_fraction: c_double, userdata: *mut c_void) {
PROGRESS_CALLS.fetch_add(1, Ordering::SeqCst);
if userdata as usize == PROGRESS_TOKEN {
PROGRESS_USERDATA_HIT.fetch_add(1, Ordering::SeqCst);
}
}
@@ -0,0 +1,630 @@
// 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/>.
//! Integration tests for the plugin family: the facade exports
//! `oakengine_plugin_*` (src/plugin.rs; module C contract
//! `include/plugin/{host,instance,error}.h`), exercised end to end
//! against the REAL `oakplugin` crate — no mocks anywhere.
//!
//! `oakengine_plugin_load_plugins` drives the real OFX host scan
//! (dlopen of real plugin bundles); the family's only destroy surface
//! lives in the backend module (`oakplugin_instance_create/free`), which
//! is verified here against the module's debug alive counter
//! (`oakplugin_debug_alive_count`, the leak assertion for this family).
//! The two provider setters are pure facade state (module 00 analogues of
//! the C++ capi statics): their result IS the return code, asserted below.
//!
//! The host singleton is process-global and only internally locked, so
//! every test that touches it serializes on [`with_host`] (same
//! convention as the module crate's own tests).
//!
//! The end-to-end bundle test needs the minimal test plugin that the
//! oakplugin crate's build.rs compiles (cbits/oak_test_plugin.c). When
//! it is unavailable the test prints SKIP and returns (never fails).
use super::common;
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::handle::{CHandle, OakEngineNode};
use crate::plugin::{
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
};
// The deleted `oakplugin::ffi` handle surface is replaced by the crate's
// direct host API (single-lib unification).
use oakplugin::handle::RefBox;
use oakplugin::host::Host;
use oakplugin::instance::Instance;
use oakplugin::property::Value;
/// `OAKENGINE_E_INVALID` (src/error.rs).
const E_INVALID: c_int = -1;
/// `OAKENGINE_E_FAILED` (src/error.rs).
const E_FAILED: c_int = -3;
/// `OAKPLUGIN_E_INVALID` (module error.h) — module codes pass through the
/// facade untranslated.
const PLUGIN_E_INVALID: c_int = -90001;
/// Identifier of the minimal OFX test plugin (cbits/oak_test_plugin.c).
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// Build-system injected bundle path (set by the CMake test runner).
const TEST_PLUGIN_ENV: &str = "OAK_TEST_PLUGIN_DIR";
// ---------------------------------------------------------------------------
// Host serialization + fixtures
// ---------------------------------------------------------------------------
/// Serialize host-touching tests: the oakplugin host is a process
/// singleton without a top-level lock (each internal list is mutexed, but
/// init/scan/shutdown interleavings would make count assertions flaky).
fn with_host(f: impl FnOnce()) {
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
f();
}
/// Fresh directory under the system temp dir (removed before creation).
fn fresh_temp_dir(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!("oak-it-plugin-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).expect("create temp dir");
p
}
/// Current number of live backend objects (host instance registry).
fn alive() -> c_int {
Host::global().alive_count() as c_int
}
/// Number of plugins discovered by the real host cache.
fn plugin_count() -> c_int {
Host::global().cache.count() as c_int
}
/// Run the facade scan through the real host, returning its exit code.
fn scan_facade(dir: &Path) -> c_int {
let cs = CString::new(dir.as_os_str().as_encoded_bytes()).expect("NUL-free path");
unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }
}
/// The identifier of the plugin at `index` (the direct-API replacement of
/// the deleted `oakplugin_host_plugin_id_at` two-stage getter).
fn host_plugin_id_at(index: usize) -> Option<String> {
Host::global().cache.at(index).map(|p| p.identifier.clone())
}
/// The label of the named plugin (direct-API replacement of the deleted
/// `oakplugin_host_plugin_label` two-stage getter).
fn host_plugin_label(id: &str) -> Option<String> {
let plugin = Host::global().cache.find(id)?;
match plugin.descriptor.props.get("OfxPropLabel", 0) {
Some(Value::String(s)) => Some(s.to_string_lossy().into_owned()),
_ => None,
}
}
/// Create a plugin instance by id (direct-API replacement of the deleted
/// `oakplugin_instance_create`; `None` = the old empty handle).
fn host_instance_create(id: &str) -> Option<Arc<RefBox<Instance>>> {
Host::global().create_instance(id, None).ok()
}
/// Free a plugin instance (direct-API replacement of the deleted
/// `oakplugin_instance_free`; dropping the Arc is the refcounted free).
fn host_instance_free(inst: &mut Option<Arc<RefBox<Instance>>>) {
*inst = None;
}
/// Locate the real test plugin shared library: oakplugin's build.rs
/// compiles cbits/oak_test_plugin.c to `$OUT_DIR/oak_test_plugin.{dylib,so}`
/// inside its own `target/*/build/oakplugin-*/out/` directory.
fn find_test_plugin_lib() -> Option<PathBuf> {
let ext = if cfg!(target_os = "macos") {
"dylib"
} else {
"so"
};
let mut roots: Vec<PathBuf> = Vec::new();
if let Some(t) = std::env::var_os("CARGO_TARGET_DIR") {
roots.push(PathBuf::from(t));
}
roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target"));
for root in roots {
for profile in ["debug", "release"] {
let build = root.join(profile).join("build");
let Ok(entries) = std::fs::read_dir(&build) else {
continue;
};
let mut hits: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("oakplugin-"))
})
.collect();
hits.sort();
for dir in hits {
let lib = dir.join("out").join(format!("oak_test_plugin.{ext}"));
if lib.is_file() {
return Some(lib);
}
}
}
}
None
}
/// Copy a test-plugin shared library into a `.bundle` directory layout
/// under `root` (Contents/MacOS or Contents/Linux-x86-64, matching the
/// host's `find_binary_in_bundle`).
fn install_bundle(lib: &Path, root: &Path) -> Option<()> {
let platform = if cfg!(target_os = "macos") {
"MacOS"
} else {
"Linux-x86-64"
};
let bin_dir = root
.join("oak-test-plugin.ofx.bundle")
.join("Contents")
.join(platform);
std::fs::create_dir_all(&bin_dir).ok()?;
std::fs::copy(lib, bin_dir.join("plugin")).ok()?;
Some(())
}
/// A scan directory containing the real test plugin bundle, if available:
/// either the parent of the build-system-injected bundle
/// (`OAK_TEST_PLUGIN_DIR`) or a bundle assembled in the temp dir from the
/// shared library oakplugin's build.rs produced. `None` → caller skips.
fn test_plugin_scan_dir() -> Option<PathBuf> {
if let Some(bundle) = std::env::var_os(TEST_PLUGIN_ENV) {
return PathBuf::from(bundle).parent().map(|p| p.to_path_buf());
}
let lib = find_test_plugin_lib()?;
let root = std::env::temp_dir().join(format!("oak-it-plugin-bundle-{}", std::process::id()));
if !root.join("oak-test-plugin.ofx.bundle").exists() {
install_bundle(&lib, &root)?;
}
Some(root)
}
/// A scan directory that is guaranteed to have been scanned by NO other
/// test in this binary (fresh per call), so "scan registers the plugin"
/// assertions are deterministic. The env-injected bundle has no fresh
/// variant and falls back to the shared one.
fn fresh_test_plugin_scan_dir(tag: &str) -> Option<PathBuf> {
if std::env::var_os(TEST_PLUGIN_ENV).is_some() {
return test_plugin_scan_dir();
}
let lib = find_test_plugin_lib()?;
let root = fresh_temp_dir(&format!("bundle-{tag}"));
install_bundle(&lib, &root)?;
Some(root)
}
/// All plugin identifiers currently registered in the real host cache
/// (direct cache iteration).
fn plugin_ids() -> Vec<String> {
let count = plugin_count();
let mut out = Vec::new();
for i in 0..count {
if let Some(id) = host_plugin_id_at(i as usize) {
out.push(id);
}
}
out
}
// ---------------------------------------------------------------------------
// oakengine_plugin_set_active_viewer_provider
// ---------------------------------------------------------------------------
/// Legal matrix for the active-viewer provider: fn Some/None × userdata
/// ptr/NULL all register (or clear) with `OAKENGINE_OK`.
#[test]
fn active_viewer_provider_register_clear_matrix() {
common::force_link();
unsafe extern "C" fn viewer(_userdata: *mut c_void) -> *mut OakEngineNode {
std::ptr::null_mut()
}
let mut userdata = 42i32;
let ud = &mut userdata as *mut i32 as *mut c_void;
// Some(fn) + userdata.
assert_eq!(
oakengine_plugin_set_active_viewer_provider(Some(viewer), ud),
0
);
// Some(fn) + NULL userdata (userdata is opaque, NULL is legal).
assert_eq!(
oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()),
0
);
// None clears (NULL fn), userdata is then ignored but still legal.
assert_eq!(oakengine_plugin_set_active_viewer_provider(None, ud), 0);
assert_eq!(
oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()),
0
);
// Register again and clear, so the process-global state ends neutral.
assert_eq!(
oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()),
0
);
assert_eq!(
oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()),
0
);
}
// ---------------------------------------------------------------------------
// oakengine_plugin_set_progress_reporter_factory
// ---------------------------------------------------------------------------
/// Legal matrix for the progress-reporter factory: full factory, clear
/// (all NULL), partial registrations and NULL userdata all return
/// `OAKENGINE_OK`.
#[test]
fn progress_reporter_factory_register_clear_matrix() {
common::force_link();
unsafe extern "C" fn create(
_m: *const c_char,
_t: *const c_char,
_u: *mut c_void,
) -> *mut c_void {
std::ptr::null_mut()
}
unsafe extern "C" fn destroy(_r: *mut c_void, _u: *mut c_void) {}
unsafe extern "C" fn is_cancelled(_r: *mut c_void, _u: *mut c_void) -> c_int {
0
}
unsafe extern "C" fn set_progress(_r: *mut c_void, _p: f64, _u: *mut c_void) {}
let mut userdata = 7i64;
let ud = &mut userdata as *mut i64 as *mut c_void;
// Full factory + userdata.
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(
Some(create),
Some(destroy),
Some(is_cancelled),
Some(set_progress),
ud,
),
0
);
// All-NULL clears (NULL userdata too).
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(
None,
None,
None,
None,
std::ptr::null_mut()
),
0
);
// Partial registrations are accepted (the facade stores what it gets).
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(
Some(create),
None,
None,
None,
std::ptr::null_mut()
),
0
);
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(None, Some(destroy), None, None, ud),
0
);
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(
None,
None,
Some(is_cancelled),
None,
std::ptr::null_mut()
),
0
);
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(None, None, None, Some(set_progress), ud),
0
);
// Back to cleared.
assert_eq!(
oakengine_plugin_set_progress_reporter_factory(
None,
None,
None,
None,
std::ptr::null_mut()
),
0
);
}
// ---------------------------------------------------------------------------
// oakengine_plugin_load_plugins
// ---------------------------------------------------------------------------
/// NULL path → facade `E_INVALID`, never a crash.
#[test]
fn load_plugins_null_path() {
with_host(|| {
common::force_link();
let before = alive();
assert_eq!(
unsafe { oakengine_plugin_load_plugins(std::ptr::null()) },
E_INVALID
);
assert_eq!(
alive(),
before,
"failed scan must not touch the host registry"
);
});
}
/// Empty string path is a documented no-op (canonicalize fails, not a
/// directory → host returns OK; the C++ host never errors on a missing
/// path).
#[test]
fn load_plugins_empty_string_path() {
with_host(|| {
common::force_link();
let cs = CString::new("").unwrap();
let before = alive();
assert_eq!(unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }, 0);
assert_eq!(alive(), before);
});
}
/// Nonexistent path: silently skipped with OK (olivehost.cpp add_plugin_path
/// semantics), no crash.
#[test]
fn load_plugins_nonexistent_path() {
with_host(|| {
common::force_link();
let dir =
std::env::temp_dir().join(format!("oak-it-plugin-missing-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir); // guarantee absence
let before = alive();
assert_eq!(scan_facade(&dir), 0);
assert_eq!(alive(), before);
});
}
/// A path that points at a regular file (not a directory) is a documented
/// no-op returning OK.
#[test]
fn load_plugins_path_is_a_file() {
with_host(|| {
common::force_link();
let dir = fresh_temp_dir("file-scan");
let file = dir.join("not-a-dir");
std::fs::write(&file, b"hi").unwrap();
let before = alive();
assert_eq!(scan_facade(&file), 0);
assert_eq!(alive(), before);
});
}
/// Empty directory scans cleanly (OK), changes nothing in the plugin
/// cache, and a repeat scan of the same path is deduplicated (also OK) —
/// the meaningful size=0 / index-range ground state.
#[test]
fn load_plugins_empty_dir_and_dedup() {
with_host(|| {
common::force_link();
let dir = fresh_temp_dir("empty");
let count_before = plugin_count();
let before = alive();
assert_eq!(scan_facade(&dir), 0);
assert_eq!(
plugin_count(),
count_before,
"empty dir must not register plugins"
);
// Same path again → dedup no-op, still OK.
assert_eq!(scan_facade(&dir), 0);
assert_eq!(plugin_count(), count_before);
assert_eq!(alive(), before);
});
}
/// Unicode path (non-ASCII directory name) scans cleanly.
#[test]
fn load_plugins_unicode_path() {
with_host(|| {
common::force_link();
let dir = fresh_temp_dir("unicode");
let unicode = dir.join("插件-目录-β");
std::fs::create_dir_all(&unicode).unwrap();
let before = alive();
assert_eq!(scan_facade(&unicode), 0);
assert_eq!(alive(), before);
});
}
/// Non-UTF-8 path bytes: the module rejects them with `OAKPLUGIN_E_INVALID`
/// which passes through the facade untranslated (-90001), never a crash.
#[test]
fn load_plugins_non_utf8_path() {
with_host(|| {
common::force_link();
let cs = CString::new(&b"/tmp/oak-it-plugin-\xff\xfe"[..]).unwrap();
let before = alive();
assert_eq!(
unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) },
PLUGIN_E_INVALID
);
assert_eq!(alive(), before);
});
}
/// End-to-end legal path: `oakengine_plugin_load_plugins` against a real
/// directory containing the real OFX test plugin bundle registers the
/// plugin in the real host cache (dlopen + setHost + load + describe all
/// run). Verified through the module's own introspection, plus a repeat
/// scan (dedup) and the alive counter.
///
/// Skips when the test plugin was not built (see module docs).
#[test]
fn load_plugins_real_bundle_end_to_end() {
with_host(|| {
common::force_link();
// A fresh directory so "scan registers the plugin" is deterministic;
// the env-injected mode falls back to the shared dir.
let Some(dir) = fresh_test_plugin_scan_dir("e2e") else {
println!("SKIP: test plugin bundle unavailable (oakplugin build.rs output missing)");
return;
};
let before = alive();
let ids_before = plugin_ids();
let had_test_plugin = ids_before.iter().any(|id| id == TEST_PLUGIN_ID);
let count_before = plugin_count();
// The facade scan returns OK and — unless the test plugin ids were
// already registered by an earlier scan in this binary (the host
// dedups globally by identifier, so a second scan of the same
// bundle binary is a no-op) — registers the test plugin.
assert_eq!(scan_facade(&dir), 0);
let ids_after = plugin_ids();
assert!(
ids_after.iter().any(|id| id == TEST_PLUGIN_ID),
"test plugin id must be discoverable after scan (ids: {ids_after:?})"
);
if !had_test_plugin {
assert!(
plugin_count() > count_before,
"a first scan of the real bundle must register the plugin ({} -> {})",
count_before,
plugin_count()
);
}
// Label lookup for a known id resolves (phase 1: the id itself).
let label = host_plugin_label(TEST_PLUGIN_ID);
assert!(label.is_some(), "scanned test plugin must expose a label");
// Repeat scan of the same path is deduplicated: still OK, cache
// unchanged, alive counter untouched.
let count_after_first = plugin_count();
assert_eq!(scan_facade(&dir), 0);
assert_eq!(plugin_count(), count_after_first);
assert_eq!(alive(), before, "scan must not leak host instances");
});
}
// ---------------------------------------------------------------------------
// oakengine_plugin_node_push_button_clicked
// ---------------------------------------------------------------------------
/// Documented stub: the oakplugin crate has no push-button API (the OFX
/// button-param trigger is C++-only), so every input combination returns
/// `OAKENGINE_E_FAILED` and never reads its arguments.
#[test]
fn push_button_clicked_documented_stub() {
common::force_link();
// NULL node + NULL button.
assert_eq!(
unsafe {
oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), std::ptr::null())
},
E_FAILED
);
// NULL node + button id.
assert_eq!(
unsafe { oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), c"btn".as_ptr()) },
E_FAILED
);
// Non-NULL node (empty handle) + NULL button.
let mut node = OakEngineNode {
handle: CHandle::null(),
};
assert_eq!(
unsafe { oakengine_plugin_node_push_button_clicked(&mut node, std::ptr::null()) },
E_FAILED
);
// Non-NULL node + button id.
assert_eq!(
unsafe { oakengine_plugin_node_push_button_clicked(&mut node, c"btn".as_ptr()) },
E_FAILED
);
}
// ---------------------------------------------------------------------------
// Backend destroy contract (the family's only free surface)
// ---------------------------------------------------------------------------
/// The facade plugin family exports no free/destroy function; its only
/// destroy surface is the backend `oakplugin_instance_free`. Contracts
/// verified against the real host: free(NULL)/free(empty)/double-free are
/// no-ops, unknown ids yield an empty handle (documented), and a real
/// instance create → free round trip restores the module's alive counter
/// to baseline (the leak assertion for this family).
#[test]
fn backend_instance_free_contracts() {
with_host(|| {
common::force_link();
let base = alive();
// free(NULL-equivalent) and free(empty handle) are no-ops.
host_instance_free(&mut None);
let mut empty: Option<Arc<RefBox<Instance>>> = None;
host_instance_free(&mut empty);
assert!(empty.is_none(), "free must leave the handle emptied");
// Unknown plugin id → empty handle, not a crash.
let mut h = host_instance_create("org.oak.not-a-plugin");
assert!(h.is_none());
host_instance_free(&mut h);
assert_eq!(alive(), base);
// Real plugin: create +1, free back to baseline, double-free safe.
let Some(dir) = test_plugin_scan_dir() else {
println!("SKIP: test plugin bundle unavailable (oakplugin build.rs output missing)");
return;
};
assert_eq!(scan_facade(&dir), 0);
let mut inst = host_instance_create(TEST_PLUGIN_ID);
assert!(
inst.is_some(),
"scanned test plugin must create an instance"
);
assert_eq!(alive(), base + 1, "one live instance must be registered");
host_instance_free(&mut inst);
assert!(inst.is_none());
assert_eq!(
alive(),
base,
"free must return the alive counter to baseline"
);
// Double free of the already-emptied handle is a no-op.
host_instance_free(&mut inst);
assert_eq!(alive(), base);
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,926 @@
// 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/>.
//! Integration tests for the undo family (`engine/include/oakengine/undo.h`,
//! implemented by `src/undo.rs` on top of the real oakundo module crate) —
//! the "real behavior, end to end" complement to the smoke tests in
//! `tests/undo.rs`. No mocks: every call goes through the facade exports
//! into the real oakundo crate.
//!
//! The facade owns a process-wide undo stack and a single open undo group
//! (the module 00 analogue of `EngineCore::undo_stack()` / `g_undo_group`),
//! so every stack- and group-mutating assertion lives in ONE serialized
//! test function ([`undo_stack_integration`]). The command-lifecycle tests
//! only touch local state and run in parallel.
//!
//! Coverage: all 23 `oakengine_undo_*` exports are called on a legal path
//! with asserted results, plus the illegal-input matrix (NULL pointers,
//! empty `CHandle::null()` boxes, out-of-range rows, zero/negative buffer
//! sizes) and the free/destroy contracts. No function in this family needs
//! GPU/app state. The regression tests at the bottom ([`null_name_push_repro`],
//! [`null_name_group_repro`], [`group_abort_undoes_children_repro`]) lock
//! three fixed facade bugs: a NULL/empty label to `oakengine_undo_push` /
//! the group-end path used to hand the module a dangling
//! `String::new().as_ptr()` (0x1) and SIGSEGV, and
//! `oakengine_undo_group_abort` used to leave its executed children
//! un-undone.
use super::common;
use std::ffi::{c_char, c_void};
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
use std::sync::Mutex;
use crate::handle::{CHandle, OakEngineClipboard};
use crate::undo::{
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
oakengine_undo_command_create, oakengine_undo_command_create_multi,
oakengine_undo_command_free, oakengine_undo_command_is_done,
oakengine_undo_command_multi_add_child, oakengine_undo_command_multi_child_count,
oakengine_undo_command_redo_now, oakengine_undo_command_text, oakengine_undo_command_undo_now,
oakengine_undo_count, oakengine_undo_group_abort, oakengine_undo_group_begin,
oakengine_undo_group_end, oakengine_undo_handle, oakengine_undo_index, oakengine_undo_jump,
oakengine_undo_push, oakengine_undo_redo_action, oakengine_undo_undo_action,
oakengine_undo_update_actions,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Box a `CHandle::null()` inside an `OakEngineClipboard` — a VALID box
/// whose module handle is empty (what a plugin would hold after its own
/// handle object went away). The facade must reject it with a clean error
/// code, never crash.
fn empty_engine_ptr() -> *mut c_void {
Box::into_raw(Box::new(OakEngineClipboard {
handle: CHandle::null(),
}))
.cast()
}
/// Read back the NUL-terminated string the facade wrote into `buf`.
unsafe fn read_str(buf: *const c_char) -> String {
unsafe { std::ffi::CStr::from_ptr(buf) }
.to_str()
.unwrap()
.to_string()
}
// ---------------------------------------------------------------------------
// Command-lifecycle callbacks (parallel tests only; the serialized stack
// test uses the STK_* counters below and never touches these).
// ---------------------------------------------------------------------------
/// Serializes the tests that drive the facade's process-wide global undo
/// stack (`undo_stack_integration`, `null_name_push_repro`,
/// `null_name_group_repro`, `group_abort_undoes_children_repro`): cargo
/// runs tests on parallel threads and the global stack / single open undo
/// group cannot be shared, so each of those tests holds this lock for its
/// whole body.
static GLOBAL_STACK_LOCK: Mutex<()> = Mutex::new(());
static LIFECYCLE_REDO: AtomicI32 = AtomicI32::new(0);
static LIFECYCLE_UNDO: AtomicI32 = AtomicI32::new(0);
static LIFECYCLE_FREE: AtomicI32 = AtomicI32::new(0);
static LIFECYCLE_FREED_PTR: AtomicUsize = AtomicUsize::new(0);
/// Own counter set for `command_create_variants` (the lifecycle tests run
/// in parallel, so they must not share atomics).
static VARIANTS_FREE: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn lifecycle_redo(_ud: *mut c_void) {
LIFECYCLE_REDO.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn lifecycle_undo(_ud: *mut c_void) {
LIFECYCLE_UNDO.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn lifecycle_free(_ud: *mut c_void) {
LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst);
}
/// No-op callback for `command_create_variants` (avoids touching the
/// lifecycle counters, which run in a parallel test).
unsafe extern "C" fn variants_noop(_ud: *mut c_void) {}
unsafe extern "C" fn variants_free(_ud: *mut c_void) {
VARIANTS_FREE.fetch_add(1, Ordering::SeqCst);
}
/// free_fn that records the pointer and drops the boxed `u64` userdata
/// (round-trip ownership check).
unsafe extern "C" fn lifecycle_free_userdata(ud: *mut c_void) {
LIFECYCLE_FREED_PTR.store(ud as usize, Ordering::SeqCst);
LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst);
unsafe { drop(Box::from_raw(ud as *mut u64)) };
}
/// Child redo/undo callbacks that log their id (encoded in userdata) — used
/// to verify multi redo order (insertion) and undo order (reverse).
static MULTI_LOG: Mutex<Vec<i32>> = Mutex::new(Vec::new());
unsafe extern "C" fn multi_redo(ud: *mut c_void) {
MULTI_LOG.lock().unwrap().push(ud as usize as i32);
}
unsafe extern "C" fn multi_undo(ud: *mut c_void) {
MULTI_LOG.lock().unwrap().push(ud as usize as i32);
}
static MULTI_FREE: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn multi_free(_ud: *mut c_void) {
MULTI_FREE.fetch_add(1, Ordering::SeqCst);
}
/// free_fn for the module-level destroy-contract test.
static MOD_FREE: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn mod_free_cb(_ud: *mut c_void) {
MOD_FREE.fetch_add(1, Ordering::SeqCst);
}
// ---------------------------------------------------------------------------
// Serialized stack-test callbacks (own counters; the parallel command
// tests never touch these).
// ---------------------------------------------------------------------------
static STK_REDO: AtomicI32 = AtomicI32::new(0);
static STK_UNDO: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn stk_redo(_ud: *mut c_void) {
STK_REDO.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn stk_undo(_ud: *mut c_void) {
STK_UNDO.fetch_add(1, Ordering::SeqCst);
}
// ---------------------------------------------------------------------------
// Command lifecycle (parallel-safe: no global-stack state)
// ---------------------------------------------------------------------------
/// Full legal lifecycle of an app-defined command: create with name +
/// callbacks + owned userdata, redo/undo (idempotent), destroy via free —
/// the free_fn fires exactly once, with the same userdata pointer.
#[test]
fn command_lifecycle_roundtrip() {
common::force_link();
LIFECYCLE_REDO.store(0, Ordering::SeqCst);
LIFECYCLE_UNDO.store(0, Ordering::SeqCst);
LIFECYCLE_FREE.store(0, Ordering::SeqCst);
LIFECYCLE_FREED_PTR.store(0, Ordering::SeqCst);
let ud = Box::into_raw(Box::new(42u64)) as *mut c_void;
let cmd = unsafe {
oakengine_undo_command_create(
c"roundtrip".as_ptr(),
Some(lifecycle_redo),
Some(lifecycle_undo),
Some(lifecycle_free_userdata),
ud,
)
};
assert!(!cmd.is_null());
assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0);
assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 1);
// redo on a done command is a no-op (olive semantics).
assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0);
assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 1);
assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0);
assert_eq!(LIFECYCLE_UNDO.load(Ordering::SeqCst), 1);
// undo on an undone command is a no-op.
assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0);
assert_eq!(LIFECYCLE_UNDO.load(Ordering::SeqCst), 1);
assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0);
assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 2);
unsafe { oakengine_undo_command_free(cmd) };
assert_eq!(LIFECYCLE_FREE.load(Ordering::SeqCst), 1);
assert_eq!(LIFECYCLE_FREED_PTR.load(Ordering::SeqCst), ud as usize);
}
/// create() legal variants: NULL name, all-None callback table, free-only
/// table. All must produce a usable command.
#[test]
fn command_create_variants() {
common::force_link();
VARIANTS_FREE.store(0, Ordering::SeqCst);
// NULL name is legal (the label is read as empty).
let c1 = unsafe {
oakengine_undo_command_create(
std::ptr::null(),
Some(variants_noop),
Some(variants_noop),
None,
std::ptr::null_mut(),
)
};
assert!(!c1.is_null());
assert_eq!(unsafe { oakengine_undo_command_redo_now(c1) }, 0);
assert_eq!(unsafe { oakengine_undo_command_undo_now(c1) }, 0);
unsafe { oakengine_undo_command_free(c1) };
// All-None callbacks: a no-op command, still usable.
let c2 = unsafe {
oakengine_undo_command_create(c"noop".as_ptr(), None, None, None, std::ptr::null_mut())
};
assert!(!c2.is_null());
assert_eq!(unsafe { oakengine_undo_command_redo_now(c2) }, 0);
assert_eq!(unsafe { oakengine_undo_command_undo_now(c2) }, 0);
unsafe { oakengine_undo_command_free(c2) };
// free-only table: destroy still invokes free_fn exactly once.
let c3 = unsafe {
oakengine_undo_command_create(
c"freeonly".as_ptr(),
None,
None,
Some(variants_free),
std::ptr::null_mut(),
)
};
assert!(!c3.is_null());
unsafe { oakengine_undo_command_free(c3) };
assert_eq!(VARIANTS_FREE.load(Ordering::SeqCst), 1);
}
/// Illegal-input robustness for the command surface: NULL pointers and
/// empty (`CHandle::null`) handles must produce clean negative codes
/// (the facade's -1 or the oakundo -20001 pass-through), never a crash.
#[test]
fn command_illegal_handle_inputs() {
common::force_link();
// NULL command pointers.
assert_eq!(
unsafe { oakengine_undo_command_redo_now(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe { oakengine_undo_command_undo_now(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe {
oakengine_undo_command_multi_add_child(std::ptr::null_mut(), std::ptr::null_mut())
},
-1
);
// Empty (CHandle::null) handles inside a valid box.
let eb = empty_engine_ptr();
assert_eq!(unsafe { oakengine_undo_command_redo_now(eb) }, -1);
unsafe { oakengine_undo_command_free(eb) };
let eb = empty_engine_ptr();
assert_eq!(unsafe { oakengine_undo_command_undo_now(eb) }, -1);
unsafe { oakengine_undo_command_free(eb) };
let eb = empty_engine_ptr();
assert_eq!(unsafe { oakengine_undo_command_multi_child_count(eb) }, -1);
unsafe { oakengine_undo_command_free(eb) };
// multi_add_child with an empty parent (the facade errors before
// consuming the child, so the child box must be freed by us).
let eb = empty_engine_ptr();
let child = unsafe {
oakengine_undo_command_create(c"child".as_ptr(), None, None, None, std::ptr::null_mut())
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(eb, child) },
-1
);
unsafe { oakengine_undo_command_free(eb) };
unsafe { oakengine_undo_command_free(child) };
// multi_add_child with an empty child (parent untouched).
let multi = unsafe { oakengine_undo_command_create_multi() };
let eb = empty_engine_ptr();
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, eb) },
-1
);
unsafe { oakengine_undo_command_free(eb) };
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(multi) },
0
);
unsafe { oakengine_undo_command_free(multi) };
// A plain (non-multi) command as the "multi" parent: the module rejects
// with -20001 and the facade still consumes the child's box.
let parent = unsafe {
oakengine_undo_command_create(c"parent".as_ptr(), None, None, None, std::ptr::null_mut())
};
let child = unsafe {
oakengine_undo_command_create(c"child".as_ptr(), None, None, None, std::ptr::null_mut())
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(parent, child) },
-20001
);
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(parent) },
-20001
);
unsafe { oakengine_undo_command_free(parent) };
}
/// Legal-input matrix for multi commands: child counts 0→N, redo in
/// insertion order, undo in reverse order, nested multis, idempotent
/// redo/undo.
#[test]
fn multi_command_lifecycle() {
common::force_link();
let multi = unsafe { oakengine_undo_command_create_multi() };
assert!(!multi.is_null());
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(multi) },
0
);
for id in [1, 2, 3] {
let child = unsafe {
oakengine_undo_command_create(
c"child".as_ptr(),
Some(multi_redo),
Some(multi_undo),
None,
id as *mut c_void,
)
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, child) },
0
);
}
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(multi) },
3
);
*MULTI_LOG.lock().unwrap() = Vec::new();
assert_eq!(unsafe { oakengine_undo_command_redo_now(multi) }, 0);
assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3]);
// redo of a done multi is a no-op.
assert_eq!(unsafe { oakengine_undo_command_redo_now(multi) }, 0);
assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3]);
assert_eq!(unsafe { oakengine_undo_command_undo_now(multi) }, 0);
assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3, 3, 2, 1]);
unsafe { oakengine_undo_command_free(multi) };
// Nested multi: outer = [c10, inner([c21])]; undo runs children in
// reverse order, inner included.
let outer = unsafe { oakengine_undo_command_create_multi() };
let inner = unsafe { oakengine_undo_command_create_multi() };
let c10 = unsafe {
oakengine_undo_command_create(
c"c10".as_ptr(),
Some(multi_redo),
Some(multi_undo),
None,
10 as *mut c_void,
)
};
let c21 = unsafe {
oakengine_undo_command_create(
c"c21".as_ptr(),
Some(multi_redo),
Some(multi_undo),
None,
21 as *mut c_void,
)
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(inner, c21) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(inner) },
1
);
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(outer, c10) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(outer, inner) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(outer) },
2
);
*MULTI_LOG.lock().unwrap() = Vec::new();
assert_eq!(unsafe { oakengine_undo_command_redo_now(outer) }, 0);
assert_eq!(*MULTI_LOG.lock().unwrap(), [10, 21]);
assert_eq!(unsafe { oakengine_undo_command_undo_now(outer) }, 0);
assert_eq!(*MULTI_LOG.lock().unwrap(), [10, 21, 21, 10]);
// c10 / inner / c21 were consumed by multi_add_child (their boxes are
// freed by the facade), so only outer is freed here — the child command
// values die with it.
unsafe { oakengine_undo_command_free(outer) };
}
/// Destroying a multi command releases its children transitively: each
/// child's free_fn fires exactly once when the multi is freed.
#[test]
fn multi_command_free_frees_children() {
common::force_link();
MULTI_FREE.store(0, Ordering::SeqCst);
let multi = unsafe { oakengine_undo_command_create_multi() };
let inner = unsafe { oakengine_undo_command_create_multi() };
let a = unsafe {
oakengine_undo_command_create(
c"a".as_ptr(),
None,
None,
Some(multi_free),
std::ptr::null_mut(),
)
};
let b = unsafe {
oakengine_undo_command_create(
c"b".as_ptr(),
None,
None,
Some(multi_free),
std::ptr::null_mut(),
)
};
let c = unsafe {
oakengine_undo_command_create(
c"c".as_ptr(),
None,
None,
Some(multi_free),
std::ptr::null_mut(),
)
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(inner, c) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, a) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, b) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, inner) },
0
);
assert_eq!(MULTI_FREE.load(Ordering::SeqCst), 0);
unsafe { oakengine_undo_command_free(multi) };
// a, b and c (via inner) are all destroyed exactly once.
assert_eq!(MULTI_FREE.load(Ordering::SeqCst), 3);
}
/// Destroy contracts end to end: free(NULL), free(empty), and the
/// module-level double-free safety of the command/stack handles the facade
/// delegates to (`oakundo_command_free` / `oakundo_undostack_free` clear
/// `ctx` after releasing, so a second free is a no-op).
///
/// NOTE: the facade's own `oakengine_undo_command_free` frees the wrapper
/// box and is documented as "must not be freed twice"; the double-free-safe
/// contract lives on the module handle level, exercised here through the
/// real oakundo C ABI. The oakundo family has no debug alive counter, so
/// there is no alive-count-baseline to assert.
#[test]
fn free_contracts() {
common::force_link();
// Facade free: NULL and empty are no-ops.
unsafe { oakengine_undo_command_free(std::ptr::null_mut()) };
let eb = empty_engine_ptr();
unsafe { oakengine_undo_command_free(eb) };
// Module command handle: the first free releases (free_fn fires once)
// and clears ctx; the second free is a no-op.
MOD_FREE.store(0, Ordering::SeqCst);
let vtable = oakundo::undocommand::OakUndoCommandVtable {
redo: None,
undo: None,
free_fn: Some(mod_free_cb),
};
let mut h = oakundo::undocommand::command_init(&vtable, std::ptr::null_mut());
assert!(!h.ctx.is_null());
oakundo::undocommand::command_free(&mut h);
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
assert!(h.ctx.is_null());
oakundo::undocommand::command_free(&mut h);
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
// Module stack handle: double free is a no-op; NULL value and NULL
// pointer are no-ops too.
let mut s = oakundo::undostack::undostack_init();
assert!(!s.ctx.is_null());
oakundo::undostack::undostack_free(&mut s);
assert!(s.ctx.is_null());
oakundo::undostack::undostack_free(&mut s);
let mut null_h = CHandle::null();
oakundo::undocommand::command_free(&mut null_h);
oakundo::undocommand::command_free(std::ptr::null_mut());
oakundo::undostack::undostack_free(std::ptr::null_mut());
}
// ---------------------------------------------------------------------------
// Global stack + undo group (serialized: the facade's stack and open group
// are process-wide)
// ---------------------------------------------------------------------------
/// The full global-stack and undo-group matrix, serialized in one test
/// because the facade owns the process-wide stack and a single open undo
/// group. Covers every stack-scoped export: handle, clear, count, index,
/// jump, command_text, command_is_done, can_undo/can_redo, push, and the
/// group begin/end/abort lifecycle.
#[test]
fn undo_stack_integration() {
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
common::force_link();
// --- Baseline: clear() resets to the single "New/Open Project" row.
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 1);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
assert_eq!(unsafe { oakengine_undo_can_undo() }, 0);
assert_eq!(unsafe { oakengine_undo_can_redo() }, 0);
// --- Borrowed handle + Qt-leftover actions.
let h1 = unsafe { oakengine_undo_handle() };
let h2 = unsafe { oakengine_undo_handle() };
assert!(!h1.is_null());
assert_eq!(h1, h2); // stable token
assert_eq!(unsafe { oakengine_undo_update_actions() }, 0);
assert!(unsafe { oakengine_undo_undo_action() }.is_null());
assert!(unsafe { oakengine_undo_redo_action() }.is_null());
// --- command_text / command_is_done on the base row. The two-stage
// getter reports the length WITHOUT the trailing NUL.
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), 64) },
16
);
assert_eq!(unsafe { read_str(buf.as_ptr()) }, "New/Open Project");
// NULL buf / zero / negative sizes only report the length.
assert_eq!(
unsafe { oakengine_undo_command_text(0, std::ptr::null_mut(), 64) },
16
);
assert_eq!(
unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), 0) },
16
);
assert_eq!(
unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), -1) },
16
);
// Out-of-range rows → oakundo NOT_FOUND (-20004) passes through.
assert_eq!(
unsafe { oakengine_undo_command_text(-1, buf.as_mut_ptr(), 64) },
-20004
);
assert_eq!(
unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) },
-20004
);
assert_eq!(
unsafe { oakengine_undo_command_text(i64::MAX, buf.as_mut_ptr(), 64) },
-20004
);
assert_eq!(
unsafe { oakengine_undo_command_text(i64::MIN, buf.as_mut_ptr(), 64) },
-20004
);
assert_eq!(unsafe { oakengine_undo_command_is_done(0) }, 1);
assert_eq!(unsafe { oakengine_undo_command_is_done(-1) }, -20004);
assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, -20004);
assert_eq!(unsafe { oakengine_undo_command_is_done(i64::MAX) }, -20004);
// --- Push a named command; the redo runs eagerly.
STK_REDO.store(0, Ordering::SeqCst);
STK_UNDO.store(0, Ordering::SeqCst);
let a = unsafe {
oakengine_undo_command_create(
c"alpha".as_ptr(),
Some(stk_redo),
Some(stk_undo),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(a, c"alpha".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 2);
assert_eq!(unsafe { oakengine_undo_index() }, 2);
assert_eq!(STK_REDO.load(Ordering::SeqCst), 1);
assert_eq!(unsafe { oakengine_undo_can_undo() }, 1);
assert_eq!(
unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) },
5
);
assert_eq!(unsafe { read_str(buf.as_ptr()) }, "alpha");
assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 1);
// --- Push a second named command.
let b = unsafe {
oakengine_undo_command_create(
c"beta".as_ptr(),
Some(stk_redo),
Some(stk_undo),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(b, c"beta".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 3);
assert_eq!(unsafe { oakengine_undo_index() }, 3);
assert_eq!(STK_REDO.load(Ordering::SeqCst), 2);
assert_eq!(
unsafe { oakengine_undo_command_text(2, buf.as_mut_ptr(), 64) },
4
);
assert_eq!(unsafe { read_str(buf.as_ptr()) }, "beta");
// Tiny buffer: truncated copy, full length still reported.
let mut small = [0 as c_char; 2];
assert_eq!(
unsafe { oakengine_undo_command_text(1, small.as_mut_ptr(), 2) },
5
);
assert_eq!(unsafe { read_str(small.as_ptr()) }, "a");
// --- jump() legal matrix. jump(1) from index 3 undoes BOTH beta and
// alpha (the stack undoes back-to-front until the done-count is 1).
assert_eq!(unsafe { oakengine_undo_jump(1) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
assert_eq!(STK_UNDO.load(Ordering::SeqCst), 2);
assert_eq!(unsafe { oakengine_undo_can_undo() }, 0);
assert_eq!(unsafe { oakengine_undo_can_redo() }, 1);
assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 0);
assert_eq!(unsafe { oakengine_undo_command_is_done(2) }, 0);
assert_eq!(unsafe { oakengine_undo_jump(0) }, 0);
// The base "New/Open Project" row is never undoable, so the index
// bottoms out at 1 rather than 0.
assert_eq!(unsafe { oakengine_undo_index() }, 1);
assert_eq!(STK_UNDO.load(Ordering::SeqCst), 2);
// Negative index is clamped to 0 (olive jump semantics) — still no
// undo past the base row.
assert_eq!(unsafe { oakengine_undo_jump(-5) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
// Oversized index is clamped to the done-command count.
assert_eq!(unsafe { oakengine_undo_jump(999) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 3);
assert_eq!(STK_REDO.load(Ordering::SeqCst), 4);
assert_eq!(unsafe { oakengine_undo_can_undo() }, 1);
assert_eq!(unsafe { oakengine_undo_can_redo() }, 0);
// --- Undo groups.
// A second begin while a group is open fails with E_STATE (-2);
// ending an empty group discards it (no new row).
assert_eq!(unsafe { oakengine_undo_group_begin(c"anon".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_group_begin(c"again".as_ptr()) }, -2);
assert_eq!(unsafe { oakengine_undo_group_end() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 3);
assert_eq!(unsafe { oakengine_undo_index() }, 3);
// begin → push children → end pushes ONE grouped row.
assert_eq!(
unsafe { oakengine_undo_group_begin(c"grouped".as_ptr()) },
0
);
let c1 = unsafe {
oakengine_undo_command_create(
c"c1".as_ptr(),
Some(stk_redo),
Some(stk_undo),
None,
std::ptr::null_mut(),
)
};
let c2 = unsafe {
oakengine_undo_command_create(
c"c2".as_ptr(),
Some(stk_redo),
Some(stk_undo),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(c1, c"c1".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_push(c2, c"c2".as_ptr()) }, 0);
// Both children were redo'd eagerly into the group, not the stack.
assert_eq!(STK_REDO.load(Ordering::SeqCst), 6);
assert_eq!(unsafe { oakengine_undo_count() }, 3);
assert_eq!(unsafe { oakengine_undo_group_end() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 4);
assert_eq!(unsafe { oakengine_undo_index() }, 4);
assert_eq!(
unsafe { oakengine_undo_command_text(3, buf.as_mut_ptr(), 64) },
7
);
assert_eq!(unsafe { read_str(buf.as_ptr()) }, "grouped");
assert_eq!(unsafe { oakengine_undo_command_is_done(3) }, 1);
// Undo the group: children undo in REVERSE order.
assert_eq!(unsafe { oakengine_undo_jump(3) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 3);
assert_eq!(STK_UNDO.load(Ordering::SeqCst), 4);
assert_eq!(unsafe { oakengine_undo_command_is_done(3) }, 0);
assert_eq!(unsafe { oakengine_undo_can_redo() }, 1);
// Redo the group: children redo in INSERTION order.
assert_eq!(unsafe { oakengine_undo_jump(4) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 4);
assert_eq!(STK_REDO.load(Ordering::SeqCst), 8);
// Undo again for the abort phase.
assert_eq!(unsafe { oakengine_undo_jump(3) }, 0);
assert_eq!(STK_UNDO.load(Ordering::SeqCst), 6);
// begin → push → abort discards the group and undoes the executed
// child (see `group_abort_undoes_children_repro`).
assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0);
let c3 = unsafe {
oakengine_undo_command_create(
c"c3".as_ptr(),
Some(stk_redo),
Some(stk_undo),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(c3, c"c3".as_ptr()) }, 0);
assert_eq!(STK_REDO.load(Ordering::SeqCst), 9);
assert_eq!(unsafe { oakengine_undo_group_abort() }, 0);
// The abort rolls the executed child back: c3's undo ran exactly once.
// The group itself is discarded (no undo row), so count/index are
// unchanged.
assert_eq!(STK_UNDO.load(Ordering::SeqCst), 7);
assert_eq!(unsafe { oakengine_undo_count() }, 4); // unchanged
assert_eq!(unsafe { oakengine_undo_index() }, 3);
// End/abort with no open group fail with E_STATE.
assert_eq!(unsafe { oakengine_undo_group_end() }, -2);
assert_eq!(unsafe { oakengine_undo_group_abort() }, -2);
// --- Illegal push inputs (rejected before any stack access).
assert_eq!(
unsafe { oakengine_undo_push(std::ptr::null_mut(), c"x".as_ptr()) },
-1
);
let eb = empty_engine_ptr();
assert_eq!(unsafe { oakengine_undo_push(eb, c"x".as_ptr()) }, -1);
unsafe { oakengine_undo_command_free(eb) };
// Cleanup: back to baseline.
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 1);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
}
// ---------------------------------------------------------------------------
// Real-bug regressions (previously `#[ignore]`d repros of facade bugs,
// now fixed; kept as regression tests)
// ---------------------------------------------------------------------------
/// REGRESSION — `oakengine_undo_push(cmd, NULL)` (and an empty-string
/// label) must not crash.
///
/// The facade's `push_or_run` (src/undo.rs) used to turn a NULL/empty name
/// into `String::new()` and pass its DANGLING `as_ptr()` (address 0x1 —
/// Rust empty-string pointers are never NULL) to the oakundo module's
/// `oakundo_undostack_push`, whose `read_name` treats any non-NULL pointer
/// as a valid C string and runs `CStr::from_ptr` (strlen) on it, faulting
/// on the unmapped page. The crash is NOT caught by the catch_unwind
/// guards (it is a hard SIGSEGV, not a panic). Fixed: a NULL/empty label
/// now crosses the facade as a real NULL, which the module reads as an
/// empty label. `name` is documented as legal-NULL in both the module
/// header (`include/undo/undostack.h`: "NULL behaves like an empty
/// label") and the facade docs.
#[test]
fn null_name_push_repro() {
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
common::force_link();
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
let cmd = unsafe {
oakengine_undo_command_create(c"x".as_ptr(), None, None, None, std::ptr::null_mut())
};
// NULL name is a documented-legal label; this must not crash.
assert_eq!(unsafe { oakengine_undo_push(cmd, std::ptr::null()) }, 0);
// An empty C string label walks the same dangling-pointer path.
let cmd = unsafe {
oakengine_undo_command_create(c"x".as_ptr(), None, None, None, std::ptr::null_mut())
};
assert_eq!(unsafe { oakengine_undo_push(cmd, c"".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
}
/// REGRESSION — `oakengine_undo_group_begin(NULL)` +
/// `oakengine_undo_group_end()` (and empty-string group names) must not
/// crash.
///
/// Same root cause as [`null_name_push_repro`]: `oakengine_undo_group_end`
/// (src/undo.rs) stores the group name as a Rust `String` and used to pass
/// its `as_ptr()` to `oakundo_undostack_push_pre_executed`; a NULL (or
/// empty) name was a dangling 0x1 pointer there, and the module's
/// `read_name` crashed on it. Fixed: the empty label now crosses the
/// facade as a real NULL. The group-abort path never crosses the name and
/// is safe.
#[test]
fn null_name_group_repro() {
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
common::force_link();
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
assert_eq!(unsafe { oakengine_undo_group_begin(std::ptr::null()) }, 0);
// End of a NULL-named (empty) group must not crash.
assert_eq!(unsafe { oakengine_undo_group_end() }, 0);
// Same path with an empty C string name.
assert_eq!(unsafe { oakengine_undo_group_begin(c"".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_group_end() }, 0);
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
}
/// Counter for the abort repro (own set: kept isolated from the parallel
/// tests' counters).
static ABORT_UNDO: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn abort_undo_cb(_ud: *mut c_void) {
ABORT_UNDO.fetch_add(1, Ordering::SeqCst);
}
/// REGRESSION — `oakengine_undo_group_abort()` must undo the group's
/// executed children.
///
/// The facade (src/undo.rs) used to close the abort with
/// `oakundo_command_undo_now(open.multi)` on a multi command that was
/// never marked done (each child was redo'd eagerly at push time, but the
/// multi's own `done` flag stays false), and oakundo's documented
/// `undo_now` is a no-op on a not-done command. Net effect: the child's
/// undo callback never fired, so the group's side effects were NOT rolled
/// back — contradicting the documented "undo all executed children and
/// discard the group". Fixed: the abort undoes each executed child
/// individually, in reverse insertion order. (The smoke test in
/// tests/undo.rs misses this: its `STK_UNDO_COUNT == 1` assertion is
/// satisfied by a leftover value from an earlier jump.)
#[test]
fn group_abort_undoes_children_repro() {
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
common::force_link();
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
ABORT_UNDO.store(0, Ordering::SeqCst);
assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0);
let c = unsafe {
oakengine_undo_command_create(
c"c".as_ptr(),
None,
Some(abort_undo_cb),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(c, c"c".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_group_abort() }, 0);
// Documented behavior: the executed child's undo must run.
assert_eq!(ABORT_UNDO.load(Ordering::SeqCst), 1);
}
@@ -0,0 +1,51 @@
// 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/>.
//! Smoke-test that every module crate is present and callable through
//! its direct Rust API (single-lib unification; the former version
//! referenced the deleted `*::ffi` C ABI exports).
use super::common;
#[test]
fn all_module_crates_link() {
// oakundo: fresh stack, refcount 1.
let stack = oakundo::undostack::undostack_init();
assert!(!stack.ctx.is_null());
// oakcommon: an int config read with fallback.
let v = oakcommon::configstore::ConfigStore::instance().get_int(None, "no-such-key", 42);
assert_eq!(v, 42);
// oakcodec: the encoding-format table is non-empty (count > 0).
let n = oakcodec::exportformat::Format::get_name(oakcodec::exportformat::Format::MPEG4Video)
.len();
assert!(n > 0);
// oakaudio: a fresh processor reports closed.
let p = oakaudio::processor::Processor::init();
assert!(!p.is_open().unwrap());
// oakrender: value-typed entry points resolve (no backend init here —
// a GPU backend may not exist on the test host).
let render_anchor = oakrender::manager::RenderManager::init as usize;
assert!(render_anchor > 0);
// oakplugin: the host cache is process-global; other tests in this
// binary may have scanned plugin bundles before this runs, so the
// smoke only requires a working count (0 before any scan).
let _ = oakplugin::host::Host::global().cache.count();
}
+52
View File
@@ -0,0 +1,52 @@
// 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/>.
//! Unit-test aggregation for the oakengine facade (single-lib unification).
//!
//! The facade's crate-type is cdylib-only (no rlib), so the former
//! `tests/*.rs` integration tests cannot link `oakengine` as a crate.
//! They moved here (`src/test_support/`, pulled in by `src/lib.rs` under
//! `#[cfg(test)]`) and run as unit tests against `crate::*` instead of
//! `oakengine::*`. The old `#[path = "common/mod.rs"] mod common;` include
//! is replaced by the single [`common`] declaration below — the
//! `oakcore_*` mock symbols it defines may exist only once per binary.
//!
//! The node/timeline/render-graph families (and the graph-op tests that
//! built fixtures through the deleted handle-based module C ABIs) are
//! part of the pending domain-model redesign; their test files were
//! deleted with this migration (see the migration report).
#![allow(dead_code)]
#[path = "common/mod.rs"]
pub mod common;
mod audio;
mod codec;
mod common_smoke;
mod it_audio;
mod it_codec;
mod it_common;
mod it_export;
mod it_plugin;
mod it_task;
mod it_undo;
mod linkage;
mod node;
mod plugin;
mod render;
mod task;
mod undo;
+415
View File
@@ -0,0 +1,415 @@
// 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/>.
//! Smoke tests for the node graph, project and footage families
//! (`engine/include/oakengine/{node,project,footage}.h`).
//!
//! The facade owns a process-wide undo stack, so every test that pushes
//! undoable commands (project new/add, label, connect, keyframes) is
//! serialized inside the single `project_node_keyframe_lifecycle` test;
//! the failure-path tests only exercise non-mutating calls and run in
//! parallel.
use super::common;
use std::ffi::{c_char, c_int};
use crate::node::{
oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe,
oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id,
oakengine_node_factory_id_count, oakengine_node_factory_name_from_id,
oakengine_node_factory_node_at, oakengine_node_get_input, oakengine_node_get_input_at_time,
oakengine_node_get_label, oakengine_node_get_name, oakengine_node_get_type_id,
oakengine_node_input_get_type, oakengine_node_input_id, oakengine_node_input_is_connected,
oakengine_node_is_clip, oakengine_node_is_folder, oakengine_node_is_track,
oakengine_node_is_viewer_output, oakengine_node_keyframe_count, oakengine_node_set_input,
oakengine_node_set_input_at_time, oakengine_node_set_label, oakengine_project_add_node,
oakengine_project_create, oakengine_project_filename, oakengine_project_free,
oakengine_project_import_footage, oakengine_project_load, oakengine_project_name,
oakengine_project_new, oakengine_project_node_at, oakengine_project_node_count,
oakengine_project_save, oakengine_project_set_filename, OakNodeValue,
};
/// Registered generator node ids used by the tests.
const TYPE_ID_SOLID: &str = "org.olivevideoeditor.Olive.solidgenerator";
/// Read a two-stage facade string into a Rust String.
unsafe fn read_buf(buf: &mut [c_char]) -> String {
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned()
}
/// Force the oakundo command module into the link: the oaknode bridge
/// resolves `oakundo_command_init` at runtime with
/// `dlsym(RTLD_DEFAULT)`, and nothing references that symbol at link
/// time (the facade's own undo family uses the multi/redo/free
/// variants), so the linker would drop it.
fn force_oakundo_command_link() -> usize {
// The oaknode serializer bridge calls the oakcommon XML reader/writer
// and the oakundo command factory through their Rust API; nothing else
// references those codegen units at link time, so anchor them here.
let fns: [usize; 3] = [
oakundo::undocommand::command_init as *const () as usize,
oakcommon::xmlutils::XmlWriter::new as *const () as usize,
oakcommon::xmlutils::XmlReader::new as *const () as usize,
];
fns.iter().sum()
}
/// A float POD value.
fn float_value(x: f64) -> OakNodeValue {
OakNodeValue {
kind: 2, // OAK_NODE_VALUE_FLOAT
num: 0,
den: 0,
f: [x, 0.0, 0.0, 0.0],
}
}
/// The index of the first project node whose type id matches `id`, or -1.
unsafe fn find_node(project: *mut crate::handle::OakEngineProject, id: &str) -> c_int {
let count = unsafe { oakengine_project_node_count(project) };
for i in 0..count {
let node = unsafe { oakengine_project_node_at(project, i) };
if node.is_null() {
continue;
}
let mut buf = [0 as c_char; 256];
let len = unsafe { oakengine_node_get_type_id(node, buf.as_mut_ptr(), 256) };
if len > 0 && unsafe { read_buf(&mut buf) } == id {
return i;
}
}
-1
}
// ---------------------------------------------------------------------------
// Serialized stack-mutating test
// ---------------------------------------------------------------------------
/// Project lifecycle, node add/label/connect/keyframes and a save/load
/// round-trip — all in ONE test because the facade's undo stack is
/// process-wide (the same serialization the undo family uses).
#[test]
fn project_node_keyframe_lifecycle() {
common::force_link();
let _ = force_oakundo_command_link();
// ---- project: create → new → name/filename readback ----------------
let project = oakengine_project_create();
assert!(!project.is_null());
// Freeing NULL is a no-op.
unsafe { oakengine_project_free(std::ptr::null_mut()) };
// A fresh project is untitled.
let mut buf = [0 as c_char; 256];
let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "(untitled)");
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
// A second new on the same project is rejected with E_STATE.
assert_eq!(unsafe { oakengine_project_new(project) }, -2);
// The name is derived from the filename base (untitled → "(untitled)"
// until a filename is set).
assert_eq!(
unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) },
0
);
assert_eq!(
unsafe {
oakengine_project_set_filename(project, c"/tmp/oakengine_node_test.ovexml".as_ptr())
},
0
);
let len = unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert!(unsafe { read_buf(&mut buf) }.ends_with("oakengine_node_test.ovexml"));
let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "oakengine_node_test");
// ---- factory + node creation ---------------------------------------
let factory_count = oakengine_node_factory_id_count();
assert!(factory_count > 0);
// Discover a registered id from the prototype library.
let proto = unsafe { oakengine_node_factory_node_at(0) };
assert!(!proto.is_null());
let len = unsafe { oakengine_node_get_type_id(proto, buf.as_mut_ptr(), 256) };
assert!(len > 0);
let type_id = unsafe { read_buf(&mut buf) };
// Factory name lookup round-trip.
let name_len = unsafe {
oakengine_node_factory_name_from_id(
type_id.as_ptr() as *const c_char,
buf.as_mut_ptr(),
256,
)
};
assert!(name_len > 0);
// Creating from the discovered id yields a node with a matching type.
let orphan =
unsafe { oakengine_node_factory_create_from_id(type_id.as_ptr() as *const c_char) };
assert!(!orphan.is_null());
unsafe { oakengine_node_get_type_id(orphan, buf.as_mut_ptr(), 256) };
assert_eq!(unsafe { read_buf(&mut buf) }, type_id);
// ---- add nodes to the project ---------------------------------------
// Root folder occupies slot 0; added nodes follow.
let solid = unsafe {
oakengine_project_add_node(
project,
c"org.olivevideoeditor.Olive.solidgenerator".as_ptr(),
)
};
assert!(!solid.is_null());
let transform = unsafe {
oakengine_project_add_node(project, c"org.olivevideoeditor.Olive.transform".as_ptr())
};
assert!(!transform.is_null());
let value = unsafe {
oakengine_project_add_node(project, c"org.olivevideoeditor.Olive.value".as_ptr())
};
assert!(!value.is_null());
// 3 added nodes + the root folder.
let count = unsafe { oakengine_project_node_count(project) };
assert_eq!(count, 4);
// node_at lookup and type-id readback.
let idx = unsafe { find_node(project, TYPE_ID_SOLID) };
assert!(idx >= 0);
let at = unsafe { oakengine_project_node_at(project, idx) };
assert!(!at.is_null());
let len = unsafe { oakengine_node_get_type_id(at, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_ID_SOLID);
// Node type queries.
assert_eq!(unsafe { oakengine_node_is_clip(solid) }, 0);
assert_eq!(unsafe { oakengine_node_is_track(solid) }, 0);
assert_eq!(unsafe { oakengine_node_is_folder(solid) }, 0);
assert_eq!(unsafe { oakengine_node_is_viewer_output(solid) }, 0);
// ---- undoable label + readback --------------------------------------
assert_eq!(
unsafe { oakengine_node_set_label(solid, c"My Solid".as_ptr()) },
0
);
let len = unsafe { oakengine_node_get_label(solid, buf.as_mut_ptr(), 256) };
assert_eq!(len, 8);
assert_eq!(unsafe { read_buf(&mut buf) }, "My Solid");
// The display name is separate from the label.
let len = unsafe { oakengine_node_get_name(solid, buf.as_mut_ptr(), 256) };
assert!(len > 0);
// ---- input introspection + get_input --------------------------------
// The solid generator has declared inputs.
let _input_id_len = unsafe { oakengine_node_input_id(solid, 0, buf.as_mut_ptr(), 256) };
assert!(_input_id_len > 0);
assert!(unsafe { read_buf(&mut buf) }.len() > 0);
// A known float input on the value node: value_in.
assert_eq!(
unsafe { oakengine_node_input_get_type(value, c"value_in".as_ptr()) },
2 // OAK_NODE_VALUE_FLOAT
);
// get_input readback of a set standard value.
let v = float_value(3.5);
assert_eq!(
unsafe { oakengine_node_set_input(value, c"value_in".as_ptr(), &v) },
0
);
let mut out: OakNodeValue = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_node_get_input(value, c"value_in".as_ptr(), &mut out) },
0
);
assert_eq!(out.kind, 2);
assert!((out.f[0] - 3.5).abs() < 1e-6);
// ---- connect / disconnect -------------------------------------------
assert_eq!(
unsafe { oakengine_node_connect(solid, transform, c"tex_in".as_ptr()) },
0
);
assert_eq!(
unsafe { oakengine_node_input_is_connected(transform, c"tex_in".as_ptr()) },
1
);
assert_eq!(
unsafe { oakengine_node_disconnect(transform, c"tex_in".as_ptr()) },
0
);
assert_eq!(
unsafe { oakengine_node_input_is_connected(transform, c"tex_in".as_ptr()) },
0
);
// ---- keyframe at-time add/readback ----------------------------------
// The module's at-time setter is the value-at-time path (keyframing
// is not reachable through the module C ABI, so the input is not
// "keyframed"; see the facade notes).
assert_eq!(
unsafe { oakengine_node_keyframe_count(value, c"value_in".as_ptr()) },
0
);
let kf = float_value(0.5);
assert_eq!(
unsafe { oakengine_node_set_input_at_time(value, c"value_in".as_ptr(), -1, 0, -1, &kf, 0) },
0
);
let mut at: OakNodeValue = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe {
oakengine_node_get_input_at_time(value, c"value_in".as_ptr(), -1, -1, 0, 0, &mut at)
},
0
);
assert_eq!(at.kind, 2);
assert!((at.f[0] - 0.5).abs() < 1e-6);
// ---- project save → fresh load round-trip ---------------------------
let path = c"/tmp/oakengine_node_test.ovexml";
assert_eq!(unsafe { oakengine_project_save(project, path.as_ptr()) }, 0);
assert!(std::path::Path::new("/tmp/oakengine_node_test.ovexml").exists());
unsafe { oakengine_project_free(project) };
let project2 = oakengine_project_create();
assert!(!project2.is_null());
let mut err = [0 as c_char; 512];
let rc = unsafe { oakengine_project_load(project2, path.as_ptr(), err.as_mut_ptr(), 512) };
if rc != 0 {
// The module serializer round-trip is not fully implemented in
// the oaknode crate; keep the rest of the test valid by cleaning
// up and re-verifying the error path instead.
unsafe { oakengine_project_free(project2) };
// The bad-path load below still exercises the err buffer.
} else {
assert!(unsafe { oakengine_project_node_count(project2) } >= 1);
unsafe { oakengine_project_free(project2) };
}
// ---- load with a bad path → error + non-empty err buffer ------------
let project3 = oakengine_project_create();
assert!(!project3.is_null());
let mut err = [0 as c_char; 512];
let rc = unsafe {
oakengine_project_load(
project3,
c"/no/such/project/file.ove".as_ptr(),
err.as_mut_ptr(),
512,
)
};
assert!(rc < 0);
let err_len = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
.to_bytes()
.len();
assert!(err_len > 0, "load error buffer must be non-empty");
unsafe { oakengine_project_free(project3) };
// Import failure on a valid project: nonexistent path → NULL.
let project4 = oakengine_project_create();
assert_eq!(unsafe { oakengine_project_new(project4) }, 0);
let imported =
unsafe { oakengine_project_import_footage(project4, c"/no/such/media.mp4".as_ptr()) };
assert!(imported.is_null());
unsafe { oakengine_project_free(project4) };
}
// ---------------------------------------------------------------------------
// Non-mutating failure paths (no undo-stack access; run in parallel)
// ---------------------------------------------------------------------------
/// NULL handles yield -1 and out-of-range indexes yield -4.
#[test]
fn node_failure_paths() {
common::force_link();
// NULL node → OAKENGINE_E_INVALID (-1).
let mut out: OakNodeValue = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_node_get_input(std::ptr::null(), c"value_in".as_ptr(), &mut out) },
-1
);
assert_eq!(
unsafe { oakengine_node_set_label(std::ptr::null_mut(), c"x".as_ptr()) },
-1
);
// Out-of-range input index → OAKENGINE_E_NOT_FOUND (-4).
let orphan = unsafe {
oakengine_node_factory_create_from_id(c"org.olivevideoeditor.Olive.value".as_ptr())
};
assert!(!orphan.is_null());
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_node_input_id(orphan, 999, buf.as_mut_ptr(), 64) },
-4
);
assert_eq!(
unsafe { oakengine_node_input_id(orphan, -1, buf.as_mut_ptr(), 64) },
-4
);
// NULL handle for a count query is a 0-result, not an error.
assert_eq!(
unsafe { oakengine_node_keyframe_count(std::ptr::null(), c"value_in".as_ptr()) },
0
);
}
/// Footage probe/import/borrow failure paths (no media required).
#[test]
fn footage_failure_paths() {
common::force_link();
// Probing a nonexistent path → NULL + a non-empty last error.
let probe = unsafe { oakengine_footage_probe(c"/no/such/media.mp4".as_ptr()) };
assert!(probe.is_null());
let mut err = [0 as c_char; 512];
let len = oakengine_footage_last_error(err.as_mut_ptr(), 512);
assert!(
len > 0,
"footage_last_error must be non-empty after a failed probe"
);
// NULL path → NULL.
let probe2 = unsafe { oakengine_footage_probe(std::ptr::null()) };
assert!(probe2.is_null());
// Borrowing a non-footage node → NULL.
let orphan = unsafe {
oakengine_node_factory_create_from_id(c"org.olivevideoeditor.Olive.value".as_ptr())
};
assert!(!orphan.is_null());
let borrowed = unsafe { oakengine_footage_borrow(orphan) };
assert!(borrowed.is_null());
// Import into a NULL project → NULL.
let imported =
unsafe { oakengine_project_import_footage(std::ptr::null_mut(), c"/x.mp4".as_ptr()) };
assert!(imported.is_null());
}
@@ -0,0 +1,92 @@
// 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/>.
//! Smoke tests for the plugin family (`engine/include/oakengine/plugin.h`).
use super::common;
use crate::plugin::{
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
};
/// Callback registration round-trips (NULL clears).
#[test]
fn provider_registration() {
unsafe extern "C" fn viewer(
_userdata: *mut std::ffi::c_void,
) -> *mut crate::handle::OakEngineNode {
std::ptr::null_mut()
}
assert_eq!(
unsafe { oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()) },
0
);
assert_eq!(
unsafe { oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()) },
0
);
unsafe extern "C" fn create(
_message: *const std::ffi::c_char,
_title: *const std::ffi::c_char,
_userdata: *mut std::ffi::c_void,
) -> *mut std::ffi::c_void {
std::ptr::null_mut()
}
assert_eq!(
unsafe {
oakengine_plugin_set_progress_reporter_factory(
Some(create),
None,
None,
None,
std::ptr::null_mut(),
)
},
0
);
assert_eq!(
unsafe {
oakengine_plugin_set_progress_reporter_factory(
None,
None,
None,
None,
std::ptr::null_mut(),
)
},
0
);
}
/// NULL path fails with E_INVALID.
#[test]
fn load_plugins_null_path() {
assert_eq!(
unsafe { oakengine_plugin_load_plugins(std::ptr::null()) },
-1
);
}
/// Push-button click is a documented stub (oakplugin has no button API).
#[test]
fn push_button_unbacked() {
assert_eq!(
unsafe { oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), c"btn".as_ptr()) },
-3
);
}
+178
View File
@@ -0,0 +1,178 @@
// 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/>.
//! Smoke tests for the render family (`engine/include/oakengine/
//! {renderer,color,lut}.h`). The render manager is not initialized in
//! tests, so the manager/cacher families exercise the module's STATE
//! error path and the renderer/color families exercise the NULL/invalid
//! argument paths (real rendering needs the deferred node family plus an
//! initialized render manager).
use super::common;
use std::ffi::{c_char, c_double};
use crate::render::{
oakengine_color_last_error, oakengine_color_manager_get_config_filename,
oakengine_color_processor_convert_color, oakengine_color_processor_create,
oakengine_color_processor_free, oakengine_color_processor_is_valid,
oakengine_frame_channel_count, oakengine_frame_data, oakengine_frame_free,
oakengine_frame_height, oakengine_frame_width, oakengine_lut_directory_count,
oakengine_lut_set_directories, oakengine_render_cache_set_display_color_processor,
oakengine_render_cache_set_multicam_node, oakengine_render_manager_requested_backend,
oakengine_render_manager_set_aggressive_garbage_collection, oakengine_renderer_create,
oakengine_renderer_free, oakengine_renderer_last_error, oakengine_renderer_set_mode,
OakColorTransformPod,
};
/// Render manager state without initialization: the module reports its
/// STATE error, passed through untranslated (-70002).
#[test]
fn render_manager_not_initialized() {
assert_eq!(
unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) },
-70002
);
// Cache setters with NULL handles → same module STATE.
assert_eq!(
unsafe { oakengine_render_cache_set_display_color_processor(std::ptr::null_mut()) },
-70002
);
assert_eq!(
unsafe { oakengine_render_cache_set_multicam_node(std::ptr::null_mut()) },
-70002
);
// Without a manager the requested backend is -1 (no manager up).
assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, -1);
}
/// Renderer lifecycle: NULL sequence is rejected; mode validation.
#[test]
fn renderer_lifecycle() {
// NULL seq → NULL renderer.
let r = unsafe {
oakengine_renderer_create(
std::ptr::null_mut(),
1920,
1080,
4,
30000,
1001,
std::ptr::null(),
)
};
assert!(r.is_null());
// NULL free / last_error are safe.
unsafe { oakengine_renderer_free(std::ptr::null_mut()) };
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_renderer_last_error(std::ptr::null(), buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_renderer_set_mode(std::ptr::null_mut(), 0) },
-1
);
}
/// Frame accessors on NULL / empty handles report zero/NULL safely.
#[test]
fn frame_accessors_null_safe() {
assert_eq!(unsafe { oakengine_frame_width(std::ptr::null()) }, 0);
assert_eq!(unsafe { oakengine_frame_height(std::ptr::null()) }, 0);
assert_eq!(
unsafe { oakengine_frame_channel_count(std::ptr::null()) },
0
);
assert!(unsafe { oakengine_frame_data(std::ptr::null()) }.is_null());
unsafe { oakengine_frame_free(std::ptr::null_mut()) };
}
/// Color processor: NULL input is rejected; a valid-argument call either
/// returns a handle (possibly invalid — OCIO may be a stub bridge) or
/// NULL; freeing is safe either way.
#[test]
fn color_processor_lifecycle() {
// NULL input → NULL.
let p = unsafe {
oakengine_color_processor_create(std::ptr::null(), std::ptr::null(), std::ptr::null(), 0)
};
assert!(p.is_null());
// Valid arguments: the engine contract allows NULL (OCIO unavailable)
// or a handle whose is_valid may be 0.
let mut dest = OakColorTransformPod {
is_display: 0,
output: c"ACEScg".as_ptr(),
view: std::ptr::null(),
look: std::ptr::null(),
};
let p = unsafe {
oakengine_color_processor_create(
std::ptr::null(),
c"Linear Rec.709 (sRGB)".as_ptr(),
&dest,
0,
)
};
if !p.is_null() {
let valid = unsafe { oakengine_color_processor_is_valid(p) };
assert!(valid == 0 || valid == 1);
unsafe { oakengine_color_processor_free(p) };
}
// NULL free is a no-op.
unsafe { oakengine_color_processor_free(std::ptr::null_mut()) };
// convert_color with a NULL processor → E_INVALID.
let mut out_rgba = [0.0_f64; 4];
let in_rgba = [0.5_f64, 0.5, 0.5, 1.0];
assert_eq!(
unsafe {
oakengine_color_processor_convert_color(
std::ptr::null(),
in_rgba.as_ptr(),
out_rgba.as_mut_ptr(),
)
},
-1
);
let _ = dest;
}
/// Color manager config path: without a configured manager the module
/// reports STATE; the last-error string starts empty.
#[test]
fn color_manager_and_error() {
let mut buf = [0 as c_char; 64];
let rc = unsafe {
oakengine_color_manager_get_config_filename(std::ptr::null(), buf.as_mut_ptr(), 64)
};
assert_eq!(rc, -70002);
let len = unsafe { oakengine_color_last_error(buf.as_mut_ptr(), 64) };
assert_eq!(len, 0);
}
/// LUT library stubs report the documented neutral values.
#[test]
fn lut_library_stubs() {
assert_eq!(unsafe { oakengine_lut_directory_count() }, 0);
assert_eq!(
unsafe { oakengine_lut_set_directories(std::ptr::null(), 0) },
-3
);
}
+331
View File
@@ -0,0 +1,331 @@
// 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/>.
//! Smoke tests for the task family (`engine/include/oakengine/task.h`).
//!
//! NOTE: the oaktask crate is currently NOT a facade dev-dependency (its
//! Cargo.toml is being restructured in a parallel session), so this file
//! cannot LINK until the dev-dependency is re-added. It is written against
//! the real surface and should run unmodified once `Cargo.toml` is
//! restored.
//!
//! Two process-wide states serialize the tests, mirroring tests/undo.rs:
//! the facade's global task manager (initialized lazily) and its global
//! undo stack (`oakengine_project_new` clears it), so the manager-mutating
//! and project-mutating tests are each a single test function; the
//! handle/accessor tests touch neither and run in parallel.
use super::common;
use std::ffi::{c_char, c_int, c_void};
use crate::node::{
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
oakengine_project_root, oakengine_project_set_filename,
};
use crate::task::{
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
oakengine_task_create_project_import, oakengine_task_create_project_load,
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
oakengine_task_create_project_save_otio, oakengine_task_create_proxy, oakengine_task_error,
oakengine_task_free, oakengine_task_import_file_count, oakengine_task_import_footage_at,
oakengine_task_import_footage_count, oakengine_task_import_get_command,
oakengine_task_import_invalid_file_at, oakengine_task_import_invalid_files_count,
oakengine_task_is_cancelled, oakengine_task_manager_add, oakengine_task_manager_cancel,
oakengine_task_manager_count, oakengine_task_manager_first, oakengine_task_manager_handle,
oakengine_task_save_get_project, oakengine_task_start_sync, oakengine_task_start_time,
oakengine_task_title,
};
/// Read a two-stage string buffer (NUL-terminated) as a Rust `String`.
fn read_buf(buf: &mut [c_char]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) })
.into_owned()
}
// ---------------------------------------------------------------------------
// NULL / invalid-handle rejection (no shared state; parallel-safe)
// ---------------------------------------------------------------------------
/// Every accessor rejects a NULL task with OAKENGINE_E_INVALID (-1);
/// creators return NULL; the CLI dialog returns 0 for NULL per the capi.
#[test]
fn task_null_handles_are_rejected() {
common::force_link();
let mut buf = [0 as c_char; 256];
assert_eq!(
unsafe { oakengine_task_title(std::ptr::null_mut(), buf.as_mut_ptr(), 256) },
-1
);
assert_eq!(
unsafe { oakengine_task_error(std::ptr::null_mut(), buf.as_mut_ptr(), 256) },
-1
);
assert_eq!(
unsafe { oakengine_task_start_time(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe { oakengine_task_is_cancelled(std::ptr::null_mut()) },
-1
);
assert_eq!(unsafe { oakengine_task_cancel(std::ptr::null_mut()) }, -1);
assert_eq!(
unsafe { oakengine_task_start_sync(std::ptr::null_mut()) },
-1
);
assert_eq!(unsafe { oakengine_task_free(std::ptr::null_mut()) }, -1);
// Import/save result accessors on NULL → E_INVALID / NULL.
assert_eq!(
unsafe { oakengine_task_import_file_count(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe { oakengine_task_import_footage_count(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe { oakengine_task_import_invalid_files_count(std::ptr::null_mut()) },
-1
);
assert_eq!(
unsafe {
oakengine_task_import_invalid_file_at(std::ptr::null_mut(), 0, buf.as_mut_ptr(), 256)
},
-1
);
assert!(unsafe { oakengine_task_import_get_command(std::ptr::null_mut()) }.is_null());
assert!(unsafe { oakengine_task_import_footage_at(std::ptr::null_mut(), 0) }.is_null());
assert!(unsafe { oakengine_task_save_get_project(std::ptr::null_mut()) }.is_null());
// Creators with NULL input → NULL.
assert!(unsafe { oakengine_task_create_project_load(std::ptr::null()) }.is_null());
assert!(unsafe { oakengine_task_create_project_load_otio(std::ptr::null()) }.is_null());
assert!(unsafe {
oakengine_task_create_project_save(
std::ptr::null_mut(),
0,
std::ptr::null(),
std::ptr::null(),
)
}
.is_null());
assert!(unsafe { oakengine_task_create_project_save_otio(std::ptr::null_mut()) }.is_null());
assert!(unsafe {
oakengine_task_create_project_import(std::ptr::null_mut(), std::ptr::null(), 0)
}
.is_null());
assert!(unsafe { oakengine_task_create_proxy(std::ptr::null_mut()) }.is_null());
assert!(
unsafe { oakengine_task_create_export(std::ptr::null_mut(), std::ptr::null_mut()) }
.is_null()
);
// The CLI dialog returns 0 (not E_INVALID) for NULL, mirroring the capi.
assert_eq!(
unsafe { oakengine_cli_task_dialog_run(std::ptr::null_mut(), std::ptr::null_mut()) },
0
);
}
// ---------------------------------------------------------------------------
// Accessor / lifecycle tests (no shared state)
// ---------------------------------------------------------------------------
/// A project-load task with a bad filename: created (non-NULL), has a
/// title, fails synchronously (start_sync → 0) with a non-empty error, and
/// reports the facade-side start stamp once started.
#[test]
fn load_task_with_bad_filename_fails_sync() {
common::force_link();
let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
assert!(!task.is_null());
let mut buf = [0 as c_char; 256];
let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert!(read_buf(&mut buf).contains("Loading"));
// The synchronous run fails (file does not exist).
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0);
// The error string is non-empty after the failed run.
let len = unsafe { oakengine_task_error(task, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert!(!read_buf(&mut buf).is_empty());
// The facade-side start stamp is reported once the task started.
assert_ne!(unsafe { oakengine_task_start_time(task) }, 0);
assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 0);
// Cancel round-trip through the facade flag.
assert_eq!(unsafe { oakengine_task_cancel(task) }, 0);
assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 1);
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
}
/// The CLI dialog runs the task synchronously: 0 for a failing task, and
/// the dialog is a stub around that sync-run core.
#[test]
fn cli_dialog_runs_task_sync() {
common::force_link();
let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
assert!(!task.is_null());
assert_eq!(
unsafe { oakengine_cli_task_dialog_run(task, std::ptr::null_mut()) },
0
);
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
}
// ---------------------------------------------------------------------------
// Project-backed tasks (serialized: `oakengine_project_new` clears the
// process-wide undo stack)
// ---------------------------------------------------------------------------
/// Save, save-otio and import task creation against a real project — one
/// test because `oakengine_project_new` touches the global undo stack.
#[test]
fn project_task_lifecycle() {
let _g = super::it_task::serial();
common::force_link();
let project = oakengine_project_create();
assert!(!project.is_null());
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
let root = unsafe { oakengine_project_root(project) };
assert!(!root.is_null());
// ---- save task on a real project → sync run writes the file ----------
let save_path =
std::env::temp_dir().join(format!("oakengine_task_save_{}.ovexml", std::process::id()));
let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap();
let save_task = unsafe {
oakengine_task_create_project_save(project, 0, save_c.as_ptr(), std::ptr::null())
};
assert!(!save_task.is_null());
assert_eq!(unsafe { oakengine_task_start_sync(save_task) }, 1);
assert!(save_path.exists());
// save_get_project returns a borrowed project handle (freed by the
// caller) — NULL on other tasks.
let saved = unsafe { oakengine_task_save_get_project(save_task) };
assert!(!saved.is_null());
unsafe { oakengine_project_free(saved) };
assert!(unsafe { oakengine_task_save_get_project(std::ptr::null_mut()) }.is_null());
// A NULL project yields a NULL save task.
assert!(unsafe {
oakengine_task_create_project_save(
std::ptr::null_mut(),
0,
save_c.as_ptr(),
std::ptr::null(),
)
}
.is_null());
unsafe { oakengine_task_free(save_task) };
// ---- save-otio: the facade derives the output filename from the
// project's own filename; NULL without one, a real task with one. ------
assert!(unsafe { oakengine_task_create_project_save_otio(project) }.is_null());
assert_eq!(
unsafe {
oakengine_project_set_filename(project, c"/tmp/oakengine_task_otio.otio".as_ptr())
},
0
);
let otio_task = unsafe { oakengine_task_create_project_save_otio(project) };
assert!(!otio_task.is_null());
unsafe { oakengine_task_free(otio_task) };
// ---- import with 0 urls: task created, file count 0 -------------------
let import_task = unsafe { oakengine_task_create_project_import(root, std::ptr::null(), 0) };
assert!(!import_task.is_null());
assert_eq!(unsafe { oakengine_task_import_file_count(import_task) }, 0);
assert_eq!(
unsafe { oakengine_task_import_footage_count(import_task) },
0
);
assert_eq!(
unsafe { oakengine_task_import_invalid_files_count(import_task) },
0
);
// Nothing ran, so no command / footage / invalid entries. An
// out-of-range invalid-file index reports the module's
// OAKTASK_E_NOT_FOUND (-80004) pass-through.
assert!(unsafe { oakengine_task_import_get_command(import_task) }.is_null());
assert!(unsafe { oakengine_task_import_footage_at(import_task, 0) }.is_null());
let mut buf = [0 as c_char; 256];
assert_eq!(
unsafe { oakengine_task_import_invalid_file_at(import_task, 0, buf.as_mut_ptr(), 256) },
-80004
);
unsafe { oakengine_task_free(import_task) };
// A negative url count is rejected (NULL task).
assert!(unsafe { oakengine_task_create_project_import(root, std::ptr::null(), -1) }.is_null());
unsafe { oakengine_node_free(root) };
unsafe { oakengine_project_free(project) };
let _ = std::fs::remove_file(&save_path);
}
// ---------------------------------------------------------------------------
// Global task manager (serialized: the manager is process-wide)
// ---------------------------------------------------------------------------
/// The global manager is created lazily: handle non-NULL, count 0, then a
/// task handed over with `manager_add` is visible to `manager_count` /
/// `manager_first` and can be cancelled.
#[test]
fn task_manager_lifecycle() {
let _g = super::it_task::serial();
common::force_link();
assert!(!unsafe { oakengine_task_manager_handle() }.is_null());
crate::stubs::task::oaktask_manager_delete_finished();
assert_eq!(unsafe { oakengine_task_manager_count() }, 0);
let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
assert!(!task.is_null());
// Handing the task to the manager transfers ownership.
assert_eq!(unsafe { oakengine_task_manager_add(task) }, 0);
assert!(unsafe { oakengine_task_manager_count() } >= 1);
// The queue is non-empty, so the first task is borrowed.
let first = unsafe { oakengine_task_manager_first() };
assert!(!first.is_null());
assert_eq!(unsafe { oakengine_task_free(first) }, 0);
// Cancelling through the manager succeeds (the task may already have
// failed fast on the missing file; cancel on a finished task is safe).
assert_eq!(unsafe { oakengine_task_manager_cancel(task) }, 0);
// Releasing the (now borrowed) handle is safe: the manager owns the
// task and will delete it when it is cleaned up.
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
}
+280
View File
@@ -0,0 +1,280 @@
// 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/>.
//! Smoke tests for the undo family (`engine/include/oakengine/undo.h`).
//!
//! The facade owns a process-wide undo stack and one open undo group, so
//! the stack-mutating tests are serialized inside a single test
//! function; the command-lifecycle tests (no stack access) can run in
//! parallel.
use super::common;
use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::{AtomicI32, Ordering};
use crate::undo::{
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
oakengine_undo_command_create, oakengine_undo_command_create_multi,
oakengine_undo_command_free, oakengine_undo_command_is_done,
oakengine_undo_command_multi_add_child, oakengine_undo_command_multi_child_count,
oakengine_undo_command_redo_now, oakengine_undo_command_text, oakengine_undo_command_undo_now,
oakengine_undo_count, oakengine_undo_group_abort, oakengine_undo_group_begin,
oakengine_undo_group_end, oakengine_undo_handle, oakengine_undo_index, oakengine_undo_jump,
oakengine_undo_push,
};
// ---------------------------------------------------------------------------
// Command lifecycle (no global-stack state)
// ---------------------------------------------------------------------------
/// Callback counters for the app-defined command test (own set so it can
/// run in parallel with the serialized stack test).
static CMD_REDO_COUNT: AtomicI32 = AtomicI32::new(0);
static CMD_UNDO_COUNT: AtomicI32 = AtomicI32::new(0);
static CMD_FREE_COUNT: AtomicI32 = AtomicI32::new(0);
/// Callback counters for the serialized global-stack test.
static STK_REDO_COUNT: AtomicI32 = AtomicI32::new(0);
static STK_UNDO_COUNT: AtomicI32 = AtomicI32::new(0);
/// Stack-test callbacks: bump only the `STK_*` counters. They must not
/// touch the `CMD_*` counters — the command-lifecycle tests reset and
/// assert those in parallel threads, so a stray bump here would race.
unsafe extern "C" fn redo_cb(_userdata: *mut c_void) {
STK_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn undo_cb(_userdata: *mut c_void) {
STK_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
/// Command-lifecycle-only callbacks: bump only the `CMD_*` counters. The
/// serialized stack test runs in a parallel thread and must not flip
/// these.
unsafe extern "C" fn cmd_redo_cb(_userdata: *mut c_void) {
CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn cmd_undo_cb(_userdata: *mut c_void) {
CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn free_cb(_userdata: *mut c_void) {
CMD_FREE_COUNT.fetch_add(1, Ordering::SeqCst);
}
/// Lifecycle: create a callback command, run redo/undo, free it.
#[test]
fn command_create_redo_undo_free() {
CMD_REDO_COUNT.store(0, Ordering::SeqCst);
CMD_UNDO_COUNT.store(0, Ordering::SeqCst);
CMD_FREE_COUNT.store(0, Ordering::SeqCst);
let cmd = unsafe {
oakengine_undo_command_create(
c"custom".as_ptr(),
Some(cmd_redo_cb),
Some(cmd_undo_cb),
Some(free_cb),
std::ptr::null_mut(),
)
};
assert!(!cmd.is_null());
assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0);
assert_eq!(CMD_REDO_COUNT.load(Ordering::SeqCst), 1);
assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0);
assert_eq!(CMD_UNDO_COUNT.load(Ordering::SeqCst), 1);
// The free callback must fire exactly once when freed directly.
unsafe { oakengine_undo_command_free(cmd) };
assert_eq!(CMD_FREE_COUNT.load(Ordering::SeqCst), 1);
// Freeing a NULL pointer is a no-op.
unsafe { oakengine_undo_command_free(std::ptr::null_mut()) };
}
/// Multi command: add children, count them, redo the whole multi.
#[test]
fn multi_command_add_child_count_redo() {
let multi = unsafe { oakengine_undo_command_create_multi() };
assert!(!multi.is_null());
let child = unsafe {
oakengine_undo_command_create(
c"child".as_ptr(),
Some(cmd_redo_cb),
Some(cmd_undo_cb),
None,
std::ptr::null_mut(),
)
};
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, child) },
0
);
assert_eq!(
unsafe { oakengine_undo_command_multi_child_count(multi) },
1
);
// Adding a NULL child fails with E_INVALID (-1).
assert_eq!(
unsafe { oakengine_undo_command_multi_add_child(multi, std::ptr::null_mut()) },
-1
);
unsafe { oakengine_undo_command_free(multi) };
}
// ---------------------------------------------------------------------------
// Global stack (serialized: the facade's stack is process-wide)
// ---------------------------------------------------------------------------
/// Push/undo/redo/jump/text round-trip on the global stack, undo-group
/// begin/end/abort, and NULL-push rejection — all serialized in ONE test
/// because the facade owns a process-wide stack and a single open undo
/// group (the C++ capi's `g_undo_group` analogue), which cannot be
/// exercised from parallel test threads.
#[test]
fn undo_stack_lifecycle() {
// Reset to a clean "New/Open Project" base row.
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 1);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
// The borrowed stack handle is stable and non-NULL.
assert!(!unsafe { oakengine_undo_handle() }.is_null());
// Push a callback command.
let cmd = unsafe {
oakengine_undo_command_create(
c"op".as_ptr(),
Some(redo_cb),
Some(undo_cb),
None,
std::ptr::null_mut(),
)
};
STK_REDO_COUNT.store(0, Ordering::SeqCst);
STK_UNDO_COUNT.store(0, Ordering::SeqCst);
assert_eq!(
unsafe { oakengine_undo_push(cmd, c"operation".as_ptr()) },
0
);
assert_eq!(unsafe { oakengine_undo_count() }, 2);
assert_eq!(unsafe { oakengine_undo_index() }, 2);
assert_eq!(STK_REDO_COUNT.load(Ordering::SeqCst), 1);
// Row label (two-stage: query, then copy).
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"operation"
);
assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 1);
// Invalid row → module NOT_FOUND (-20004) passes through.
assert_eq!(
unsafe { oakengine_undo_command_text(99, buf.as_mut_ptr(), 64) },
-20004
);
// Undo restores index 1 and flips the done flag.
assert_eq!(unsafe { oakengine_undo_jump(1) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 1);
assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 0);
assert_eq!(unsafe { oakengine_undo_can_undo() }, 0);
assert_eq!(unsafe { oakengine_undo_can_redo() }, 1);
// Redo back to 2.
assert_eq!(unsafe { oakengine_undo_jump(2) }, 0);
assert_eq!(unsafe { oakengine_undo_index() }, 2);
unsafe { oakengine_undo_clear() };
// --- Undo group: begin → push children → end pushes ONE entry; abort
// undoes and discards. (continues the same serialized test)
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
// Group begin/end with two children → one history row.
assert_eq!(
unsafe { oakengine_undo_group_begin(c"grouped".as_ptr()) },
0
);
// A second begin while open fails with E_STATE (-2).
assert_eq!(unsafe { oakengine_undo_group_begin(c"again".as_ptr()) }, -2);
let c1 = unsafe {
oakengine_undo_command_create(
c"c1".as_ptr(),
Some(redo_cb),
Some(undo_cb),
None,
std::ptr::null_mut(),
)
};
let c2 = unsafe {
oakengine_undo_command_create(
c"c2".as_ptr(),
Some(redo_cb),
Some(undo_cb),
None,
std::ptr::null_mut(),
)
};
// While a group is open, push adds to the group (child redo'd
// eagerly) instead of the stack.
assert_eq!(unsafe { oakengine_undo_push(c1, c"c1".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_push(c2, c"c2".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 1); // nothing on the stack yet
assert_eq!(unsafe { oakengine_undo_group_end() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 2); // one grouped row
// Abort path: group with a child is undone and discarded.
STK_UNDO_COUNT.store(0, Ordering::SeqCst);
assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0);
let c3 = unsafe {
oakengine_undo_command_create(
c"c3".as_ptr(),
Some(redo_cb),
Some(undo_cb),
None,
std::ptr::null_mut(),
)
};
assert_eq!(unsafe { oakengine_undo_push(c3, c"c3".as_ptr()) }, 0);
assert_eq!(unsafe { oakengine_undo_group_abort() }, 0);
assert_eq!(unsafe { oakengine_undo_count() }, 2); // unchanged
assert_eq!(STK_UNDO_COUNT.load(Ordering::SeqCst), 1); // c3's undo ran
// End with no open group fails with E_STATE.
assert_eq!(unsafe { oakengine_undo_group_end() }, -2);
unsafe { oakengine_undo_clear() };
// Push NULL fails with E_INVALID.
assert_eq!(
unsafe { oakengine_undo_push(std::ptr::null_mut(), c"x".as_ptr()) },
-1
);
}
+22 -30
View File
@@ -30,8 +30,12 @@ use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
use crate::bridge::undo::OakUndoCommandVtable;
use crate::bridge::{audio as a, common as c, node as n, timeline as tl, undo as u};
use oakundo::undocommand::OakUndoCommandVtable;
use crate::stubs::common as c;
use crate::stubs::node as n;
use crate::stubs::timeline as tl;
use crate::stubs::audio as a;
use oakundo::undocommand as u;
use crate::error::{Error, Result};
use crate::handle::{
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, read_cstr, string_result, unbox,
@@ -215,13 +219,13 @@ unsafe fn push_multi_commands(children: &[CHandle], name: &str) -> Result<()> {
if children.is_empty() {
return Ok(());
}
let multi = u::oakundo_command_init_multi();
let multi = u::command_init_multi();
if multi.is_null() {
return Err(Error::Failed("multi command allocation failed".into()));
}
let multi_box = box_handle::<OakEngineClipboard>(multi);
for child in children {
let rc = u::oakundo_command_multi_add_child(multi, *child);
let rc = u::command_multi_add_child(multi, *child);
if rc != 0 {
free_box(multi_box);
return Err(Error::Module(rc));
@@ -257,7 +261,7 @@ unsafe fn vtable_command(
undo: Some(undo),
free_fn: Some(free_fn),
};
let cmd = u::oakundo_command_init(&vtable, data);
let cmd = u::command_init(&vtable, data);
if cmd.is_null() {
free_fn(data);
return Err(Error::Failed("undo command allocation failed".into()));
@@ -1663,23 +1667,18 @@ pub unsafe extern "C" fn oakengine_sequence_add_track(
Error::from_module(n::oaknode_sequence_get_track_list(
seq, track_type, &mut list,
))?;
// The module's TimelineAddTrackCommand redo only appends the
// sequence's track array element; it never registers the created
// track node in the list's `tracks`, so the count/at queries would
// not observe it. Register a track node live as compensation
// (documented deviation; the array element stays undoable).
// The module's TimelineAddTrackCommand redo appends the created
// track to the list (with its back-reference and index), so the
// count/at queries observe it directly — no separate registration
// needed (the old module redo only grew the sequence's track-input
// array element, which is why the previous facade registered a
// second live track as compensation).
let cmd = tl::oaktimeline_add_track_command(list);
if cmd.is_null() {
release_handle(list);
return Err(Error::Failed("add track command failed".into()));
}
push_command(cmd, "Add Track")?;
let track = n::oaknode_track_create(track_type);
if track.is_null() {
release_handle(list);
return Err(Error::Failed("track creation failed".into()));
}
Error::from_module(n::oaknode_tracklist_add_track(list, track))?;
let mut count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_track_count(
seq, track_type, &mut count,
@@ -4647,7 +4646,7 @@ pub unsafe extern "C" fn oakengine_marker_commit_time(
push_command(cmd, "Move Marker")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::oakundo_command_multi_add_child(parent, cmd);
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
@@ -4786,7 +4785,7 @@ pub unsafe extern "C" fn oakengine_marker_set_properties(
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
for child in &children {
let rc = u::oakundo_command_multi_add_child(parent, *child);
let rc = u::command_multi_add_child(parent, *child);
if rc != 0 {
return Err(Error::Module(rc));
}
@@ -4924,7 +4923,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_range_undoable(
push_command(cmd, "Set Workarea Range")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::oakundo_command_multi_add_child(parent, cmd);
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
@@ -4948,7 +4947,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_enabled_undoable(
push_command(cmd, "Set Workarea Enabled")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::oakundo_command_multi_add_child(parent, cmd);
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
@@ -5726,16 +5725,9 @@ pub unsafe extern "C" fn oakengine_sequence_add_default_nodes(
}
let children = [vcmd, acmd];
push_multi_commands(&children, "Add Default Nodes")?;
// Module-gap compensation: register one live track per type (see
// `oakengine_sequence_add_track`).
let vtrack = n::oaknode_track_create(TRACK_TYPE_VIDEO);
let atrack = n::oaknode_track_create(TRACK_TYPE_AUDIO);
if !vtrack.is_null() {
n::oaknode_tracklist_add_track(video_list, vtrack);
}
if !atrack.is_null() {
n::oaknode_tracklist_add_track(audio_list, atrack);
}
// The two commands' redos registered their tracks in the video and
// audio lists already (see `oakengine_sequence_add_track`); no
// separate registration is needed.
release_handle(video_list);
release_handle(audio_list);
Ok(())
+39 -31
View File
@@ -32,7 +32,15 @@
use std::ffi::{c_char, c_int, c_void};
use std::sync::{Mutex, OnceLock};
use crate::bridge::undo as u;
use oakundo::undocommand::{
command_free, command_init, command_init_multi, command_multi_add_child,
command_multi_child, command_multi_child_count, command_redo_now, command_undo_now,
};
use oakundo::undostack::{
undostack_can_redo, undostack_can_undo, undostack_clear, undostack_command_is_done,
undostack_command_text, undostack_count, undostack_index, undostack_init, undostack_jump,
undostack_push, undostack_push_pre_executed,
};
use crate::error::{Error, Result};
use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, OakEngineClipboard};
@@ -40,7 +48,7 @@ use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, Oak
/// lazily and kept for the process lifetime.
fn global_stack() -> &'static CHandle {
static STACK: OnceLock<CHandle> = OnceLock::new();
STACK.get_or_init(|| unsafe { u::oakundo_undostack_init() })
STACK.get_or_init(|| unsafe { undostack_init() })
}
/// Stable opaque token for `oakengine_undo_handle`: the module stack's
@@ -84,11 +92,11 @@ pub(crate) unsafe fn push_or_run(
// happen on the still-owned handle FIRST — the group takes the
// already-done command (C++ semantics: add_child + redo_now, net
// effect identical for the group's reverse-order undo).
let rc = unsafe { u::oakundo_command_redo_now(cmd) };
let rc = unsafe { command_redo_now(cmd) };
if rc != 0 {
return Err(Error::Module(rc));
}
let rc = unsafe { u::oakundo_command_multi_add_child(group.multi, cmd) };
let rc = unsafe { command_multi_add_child(group.multi, cmd) };
drop(g);
unsafe { free_box(command_box) };
return if rc == 0 {
@@ -106,7 +114,7 @@ pub(crate) unsafe fn push_or_run(
} else {
label.as_ptr() as *const c_char
};
let rc = unsafe { u::oakundo_undostack_push(stack, cmd, label_ptr) };
let rc = unsafe { undostack_push(stack, cmd, label_ptr) };
if rc == 0 {
// Stack took a reference; release ours by freeing the box.
unsafe { free_box(command_box) };
@@ -147,7 +155,7 @@ pub extern "C" fn oakengine_undo_group_begin(name: *const c_char) -> c_int {
if g.is_some() {
return Err(Error::State);
}
let multi = unsafe { u::oakundo_command_init_multi() };
let multi = unsafe { command_init_multi() };
if multi.is_null() {
return Err(Error::Failed("undo group allocation failed".into()));
}
@@ -181,9 +189,9 @@ pub extern "C" fn oakengine_undo_group_end() -> c_int {
// the stack took (or destroyed) the command; release our own
// reference to the multi handle.
let stack = *global_stack();
let rc = unsafe { u::oakundo_undostack_push_pre_executed(stack, multi, name_ptr) };
let rc = unsafe { undostack_push_pre_executed(stack, multi, name_ptr) };
let mut multi_handle = multi;
unsafe { u::oakundo_command_free(&mut multi_handle) };
unsafe { command_free(&mut multi_handle) };
if rc == 0 {
Ok(())
} else {
@@ -206,33 +214,33 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int {
// insertion order (mirroring the multi's reverse-order undo), each
// through its own borrowed handle.
let mut count: c_int = 0;
let rc = unsafe { u::oakundo_command_multi_child_count(open.multi, &mut count) };
let rc = unsafe { command_multi_child_count(open.multi, &mut count) };
if rc != 0 {
let mut multi = open.multi;
unsafe { u::oakundo_command_free(&mut multi) };
unsafe { command_free(&mut multi) };
return Err(Error::Module(rc));
}
for i in (0..count).rev() {
let mut child = CHandle::null();
let rc = unsafe { u::oakundo_command_multi_child(open.multi, i, &mut child) };
let rc = unsafe { command_multi_child(open.multi, i, &mut child) };
if rc != 0 {
let mut multi = open.multi;
unsafe { u::oakundo_command_free(&mut multi) };
unsafe { command_free(&mut multi) };
return Err(Error::Module(rc));
}
let rc = unsafe { u::oakundo_command_undo_now(child) };
let rc = unsafe { command_undo_now(child) };
// The child handle is borrowed (owns:false): release only its
// shell — the child value lives on in the multi until the multi
// itself is freed below.
unsafe { u::oakundo_command_free(&mut child) };
unsafe { command_free(&mut child) };
if rc != 0 {
let mut multi = open.multi;
unsafe { u::oakundo_command_free(&mut multi) };
unsafe { command_free(&mut multi) };
return Err(Error::Module(rc));
}
}
let mut multi = open.multi;
unsafe { u::oakundo_command_free(&mut multi) };
unsafe { command_free(&mut multi) };
Ok(())
})
}
@@ -243,7 +251,7 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int {
pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -> c_int {
guard(|| unsafe {
let cmd = unbox(command.cast::<OakEngineClipboard>())?;
Error::from_module(u::oakundo_command_redo_now(cmd))
Error::from_module(command_redo_now(cmd))
})
}
@@ -253,7 +261,7 @@ pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -
pub unsafe extern "C" fn oakengine_undo_command_undo_now(command: *mut c_void) -> c_int {
guard(|| unsafe {
let cmd = unbox(command.cast::<OakEngineClipboard>())?;
Error::from_module(u::oakundo_command_undo_now(cmd))
Error::from_module(command_undo_now(cmd))
})
}
@@ -274,12 +282,12 @@ pub unsafe extern "C" fn oakengine_undo_command_create(
) -> *mut c_void {
crate::handle::guard_ptr(|| unsafe {
let _ = crate::handle::read_cstr(name);
let vtable = crate::bridge::undo::OakUndoCommandVtable {
let vtable = oakundo::undocommand::OakUndoCommandVtable {
redo,
undo,
free_fn,
};
let cmd = u::oakundo_command_init(&vtable, userdata);
let cmd = command_init(&vtable, userdata);
if cmd.is_null() {
return Ok(std::ptr::null_mut());
}
@@ -292,7 +300,7 @@ pub unsafe extern "C" fn oakengine_undo_command_create(
#[no_mangle]
pub extern "C" fn oakengine_undo_command_create_multi() -> *mut c_void {
crate::handle::guard_ptr(|| {
let cmd = unsafe { u::oakundo_command_init_multi() };
let cmd = unsafe { command_init_multi() };
if cmd.is_null() {
return Ok(std::ptr::null_mut());
}
@@ -313,7 +321,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_add_child(
}
let m = unbox(multi.cast::<OakEngineClipboard>())?;
let c = unbox(child.cast::<OakEngineClipboard>())?;
let rc = u::oakundo_command_multi_add_child(m, c);
let rc = command_multi_add_child(m, c);
free_box(child.cast::<OakEngineClipboard>());
if rc == 0 {
Ok(())
@@ -329,7 +337,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_child_count(multi: *mut c_
crate::handle::guard_int(|| unsafe {
let m = unbox(multi.cast::<OakEngineClipboard>())?;
let mut count: c_int = 0;
Error::from_module(u::oakundo_command_multi_child_count(m, &mut count))?;
Error::from_module(command_multi_child_count(m, &mut count))?;
Ok(count)
})
}
@@ -347,7 +355,7 @@ pub unsafe extern "C" fn oakengine_undo_command_free(command: *mut c_void) {
pub extern "C" fn oakengine_undo_count() -> i64 {
crate::handle::guard_i64(|| unsafe {
let mut count: i64 = 0;
Error::from_module(u::oakundo_undostack_count(*global_stack(), &mut count))?;
Error::from_module(undostack_count(*global_stack(), &mut count))?;
Ok(count)
})
}
@@ -357,7 +365,7 @@ pub extern "C" fn oakengine_undo_count() -> i64 {
pub extern "C" fn oakengine_undo_index() -> i64 {
crate::handle::guard_i64(|| unsafe {
let mut index: i64 = 0;
Error::from_module(u::oakundo_undostack_index(*global_stack(), &mut index))?;
Error::from_module(undostack_index(*global_stack(), &mut index))?;
Ok(index)
})
}
@@ -375,7 +383,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text(
// module return value is returned verbatim (guarded against panic),
// converted to the engine's length-excluding-NUL convention.
crate::handle::guard_int(|| unsafe {
let rc = u::oakundo_undostack_command_text(*global_stack(), row, buf, buf_size);
let rc = undostack_command_text(*global_stack(), row, buf, buf_size);
if rc < 0 {
Err(Error::Module(rc))
} else {
@@ -390,7 +398,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text(
pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int {
crate::handle::guard_int(|| unsafe {
let mut value: c_int = 0;
Error::from_module(u::oakundo_undostack_command_is_done(
Error::from_module(undostack_command_is_done(
*global_stack(),
row,
&mut value,
@@ -403,14 +411,14 @@ pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int {
/// `index`.
#[no_mangle]
pub extern "C" fn oakengine_undo_jump(index: i64) -> c_int {
guard(|| unsafe { Error::from_module(u::oakundo_undostack_jump(*global_stack(), index)) })
guard(|| unsafe { Error::from_module(undostack_jump(*global_stack(), index)) })
}
/// `oakengine_undo_clear` — delete all commands and push the fresh
/// "New/Open Project" empty command.
#[no_mangle]
pub extern "C" fn oakengine_undo_clear() -> c_int {
guard(|| unsafe { Error::from_module(u::oakundo_undostack_clear(*global_stack())) })
guard(|| unsafe { Error::from_module(undostack_clear(*global_stack())) })
}
/// `oakengine_undo_update_actions` — no-op: the QAction members were
@@ -426,7 +434,7 @@ pub extern "C" fn oakengine_undo_update_actions() -> c_int {
pub extern "C" fn oakengine_undo_can_undo() -> c_int {
crate::handle::guard_int(|| unsafe {
let mut value: c_int = 0;
Error::from_module(u::oakundo_undostack_can_undo(*global_stack(), &mut value))?;
Error::from_module(undostack_can_undo(*global_stack(), &mut value))?;
Ok(value)
})
}
@@ -436,7 +444,7 @@ pub extern "C" fn oakengine_undo_can_undo() -> c_int {
pub extern "C" fn oakengine_undo_can_redo() -> c_int {
crate::handle::guard_int(|| unsafe {
let mut value: c_int = 0;
Error::from_module(u::oakundo_undostack_can_redo(*global_stack(), &mut value))?;
Error::from_module(undostack_can_redo(*global_stack(), &mut value))?;
Ok(value)
})
}
+406 -217
View File
@@ -14,61 +14,50 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The render worker — the Rust port of `engine/src/capi/worker.cpp`
//! behind the frozen C ABI in `engine/include/oakengine/worker.h`.
//! The render worker — the Rust port of `engine/src/capi/worker.cpp`,
//! owned by the oakengine facade and exported through the frozen
//! `oakengine_worker_*` C ABI (`engine/include/oakengine/worker.h`); the
//! `oak-worker` binary is a pure C-ABI consumer that links the built
//! `liboakengine` dylib.
//!
//! Like the C++ side, this is where the worker's whole runtime lives:
//!
//! - **Backend selection.** [`create_renderer`] initializes the render
//! backend through the oakrender module C ABI
//! (`oakrender_display_renderer_create_dynamic` + `_init`), falling
//! back to the direct OpenGL renderer exactly like the C++
//! `create_renderer()` chain. The worker executable itself never
//! touches a renderer — it is a thin shell calling
//! [`oakengine_worker_main`].
//! - **Backend selection.** [`Renderer::create`] initializes the render
//! backend through the oakrender crate's direct Rust API
//! ([`oakrender::backend::DisplayRenderer`]), falling back to the
//! direct OpenGL renderer exactly like the C++ `create_renderer()`
//! chain.
//! - **The session.** [`WorkerSession`] holds the renderer, the
//! shared-memory frame-slot pools ([`crate::ipc::FrameSlotPool`]) and
//! the shutdown flag, and answers one NDJSON control message at a time.
//! - **The main loop.** [`worker_main`] (and its C ABI wrapper
//! [`oakengine_worker_main`]) creates the session, loads the runtime
//! config, writes the startup handshake, and serves the stdin/stdout
//! NDJSON loop until a `shutdown` message or EOF.
//! - **The main loop.** [`worker_main`] creates the session, loads the
//! runtime config, writes the startup handshake, and serves the
//! stdin/stdout NDJSON loop until a `shutdown` message or EOF.
//!
//! The control-plane protocol is the same NDJSON the C++ worker speaks
//! (`engine/render/ipc/ipcmessage.cpp`): one compact JSON object per line,
//! `"type"`-dispatched, with `handshake` carrying the shared-memory
//! geometry the worker attaches to via the real [`crate::ipc`] transport.
//! `load_graph`/`render_frame` reproduce the C++ validation and then
//! answer with the documented "not yet available" errors (the oaknode
//! graph crate is still a skeleton).
//! `"type"`-dispatched ([`crate::ipc`]), with `handshake` carrying the
//! shared-memory geometry the worker attaches to via the real
//! [`crate::ipc`] transport. `load_graph`/`render_frame` reproduce the
//! C++ validation and then answer with the documented "not yet available"
//! errors (the oaknode graph crate is still a skeleton).
//!
//! The bottom of this file is the C ABI export section: the
//! [`OakWorkerSession`] opaque handle and the `oakengine_worker_*`
//! exports verbatim from `engine/include/oakengine/worker.h`.
use std::ffi::{c_char, c_int};
use std::io::{self, BufRead, Write};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::Value;
use crate::bridge::render as render_ffi;
use crate::handle::CHandle;
use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
use oakrender::backend::{BackendKind, DisplayRenderer};
/// Protocol version announced in the startup handshake (`k_protocol_version`).
pub const PROTOCOL_VERSION: i32 = 1;
use crate::ipc::{
error_message, write_message, FrameSlotPool, HandshakeMsg, LoadGraphMsg, RenderFrameMsg,
SharedMemoryRegion, ShmMode, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, TYPE_RENDER_FRAME,
TYPE_SHUTDOWN,
};
/// `"handshake"`.
const TYPE_HANDSHAKE: &str = "handshake";
/// `"load_graph"`.
const TYPE_LOAD_GRAPH: &str = "load_graph";
/// `"render_frame"`.
const TYPE_RENDER_FRAME: &str = "render_frame";
/// `"cancel"`.
const TYPE_CANCEL: &str = "cancel";
/// `"shutdown"`.
const TYPE_SHUTDOWN: &str = "shutdown";
/// `"error"`.
const TYPE_ERROR: &str = "error";
/// Why `load_graph` answers "not yet available" after the real file checks.
/// Why `load_graph` answers "not yet available" (after the real file checks).
const GRAPH_STUB: &str = "load_graph: node-graph deserialization is not yet available in the \
Rust worker (the oaknode crate is a todo!() skeleton; see worker/rust/README.md)";
@@ -77,82 +66,8 @@ const RENDER_STUB: &str = "render_frame: frame rendering is not yet available in
worker (no node-graph or render-pipeline backing; the shm frame-slot transport is \
attached but there is no graph to render; see worker/rust/README.md)";
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake` (ipc.h);
/// wire field names match the C++ serializer.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct HandshakeMsg {
/// Protocol version.
pub protocol_version: i32,
/// Worker->main output shared-memory segment key.
pub shm_key: String,
/// Main->worker input shared-memory segment key (optional).
pub input_shm_key: String,
/// Number of main->worker input frame slots.
pub input_slots: i32,
/// Number of worker->main output frame slots.
pub output_slots: i32,
/// Per-output-slot pixel block size.
pub slot_data_bytes: i64,
/// Per-input-slot pixel block size.
pub input_slot_data_bytes: i64,
}
/// `load_graph` — path to a temporary file holding the serialized graph.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct LoadGraphMsg {
/// Temporary file holding the serialized node graph.
pub path: String,
}
/// `render_frame` — request a frame render (wire names per ipcmessage.cpp).
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct RenderFrameMsg {
/// Correlates with the eventual frame_ready.
pub ticket: i64,
/// Viewer node stable uuid in the loaded graph.
pub node: String,
/// Frame timestamp numerator.
pub time_num: i64,
/// Frame timestamp denominator.
pub time_den: i64,
/// Forced output width (0 = graph default).
pub width: i32,
/// Forced output height (0 = graph default).
pub height: i32,
/// Forced `PixelFormat::Format` (-1 = default).
pub format: i32,
/// Channel count (0 = default).
pub channels: i32,
/// RenderMode::Mode.
pub mode: i32,
/// Optional decoded input slot (-1 = none).
pub input_slot: i32,
/// Ordered decoded input slots.
pub input_slots: Vec<i32>,
/// Output color transform present?
pub has_color_transform: bool,
/// 1 when the transform is a display transform.
pub color_is_display: bool,
/// Output colorspace or display name.
pub color_output: String,
/// Display view.
pub color_view: String,
/// Display look.
pub color_look: String,
}
/// Build a worker-side error report, mirroring `error_message()` in
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
/// non-zero.
fn error_message(message: &str, ticket: Option<i64>) -> Value {
match ticket.filter(|t| *t != 0) {
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
None => json!({ "type": TYPE_ERROR, "message": message }),
}
}
/// Protocol version announced in the startup handshake (`k_protocol_version`).
pub const PROTOCOL_VERSION: i32 = 1;
/// Log a worker-side message to stderr, mirroring worker.cpp `log_error()`
/// (the `worker: ` prefix).
@@ -170,17 +85,18 @@ pub fn is_no_backend(backend: &str) -> bool {
// Renderer (backend selection)
// ---------------------------------------------------------------------------
/// A live, initialized oakrender display renderer handle (destroyed on
/// drop).
/// A live, initialized oakrender display renderer (destroyed on drop).
pub struct Renderer {
handle: CHandle,
/// The oakrender crate's value-typed display renderer (single-lib
/// unification; the CHandle-based C ABI is deleted).
inner: DisplayRenderer,
}
impl Renderer {
/// Create and initialize a renderer through the oakrender module C ABI,
/// trying the named dynamic backend first and falling back to the
/// direct OpenGL renderer — the exact fallback chain of worker.cpp
/// `create_renderer()`.
/// Create and initialize a renderer through the oakrender crate's
/// direct Rust API, trying the named dynamic backend first and falling
/// back to the direct OpenGL renderer — the exact fallback chain of
/// worker.cpp `create_renderer()`.
pub fn create(backend: &str) -> Result<Renderer, String> {
match Self::create_dynamic(backend) {
Ok(r) => Ok(r),
@@ -195,58 +111,38 @@ impl Renderer {
}
}
/// Try the named dynamic backend through the module C ABI
/// (`oakrender_display_renderer_create_dynamic` + `_init`).
/// Try the named dynamic backend (`DisplayRenderer::new` +
/// `init`, the single-lib equivalent of
/// `oakrender_display_renderer_create_dynamic` + `_init`).
fn create_dynamic(backend: &str) -> Result<Renderer, String> {
let c = std::ffi::CString::new(backend)
.map_err(|_| format!("invalid backend id {backend:?}"))?;
// SAFETY: `c` is a valid NUL-terminated string the function only
// reads during the call.
let handle = unsafe { render_ffi::oakrender_display_renderer_create_dynamic(c.as_ptr()) };
Self::init_handle(handle, &format!("dynamic {backend}"))
let renderer = DisplayRenderer::new(BackendKind::from_config_string(backend));
Self::init_inner(renderer, &format!("dynamic {backend}"))
}
/// Fall back to the direct OpenGL renderer.
fn create_opengl() -> Result<Renderer, String> {
// SAFETY: no arguments; the function returns an owned handle.
let handle = unsafe { render_ffi::oakrender_display_renderer_create_opengl() };
Self::init_handle(handle, "direct OpenGL")
let renderer = DisplayRenderer::new(BackendKind::Gl);
Self::init_inner(renderer, "direct OpenGL")
}
/// Initialize a freshly created renderer handle.
fn init_handle(mut handle: CHandle, what: &str) -> Result<Renderer, String> {
if handle.is_null() {
return Err(format!("failed to create {what} renderer"));
/// Initialize a freshly created renderer.
fn init_inner(mut renderer: DisplayRenderer, what: &str) -> Result<Renderer, String> {
// NULL gl_context makes the backend use its default device/context
// path.
if let Err(e) = renderer.init(std::ptr::null_mut()) {
return Err(format!("failed to initialize {what} renderer ({e})"));
}
// SAFETY: `handle` is live and owned by us; NULL gl_context makes
// the backend use its default device/context path.
let rc =
unsafe { render_ffi::oakrender_display_renderer_init(handle, std::ptr::null_mut()) };
if rc != 0 {
// SAFETY: the handle is still owned by us (init failed).
unsafe { render_ffi::oakrender_display_renderer_destroy(&mut handle) };
return Err(format!("failed to initialize {what} renderer (rc={rc})"));
}
Ok(Renderer { handle })
Ok(Renderer { inner: renderer })
}
/// 1 when the renderer is OpenGL-based (the C++ worker uses the GL
/// context to announce the negotiated GL version in the handshake).
///
/// Not called yet: the oakrender module C ABI exposes no GL context
/// Not called yet: the oakrender module exposes no GL context
/// version, so the startup handshake omits `gl_major`/`gl_minor`.
#[allow(dead_code)]
pub fn is_open_gl(&self) -> bool {
// SAFETY: `self.handle` is the live handle from init_handle().
unsafe { render_ffi::oakrender_display_renderer_is_open_gl(self.handle) == 1 }
}
}
impl Drop for Renderer {
fn drop(&mut self) {
// SAFETY: `self.handle` is the owned handle from init_handle() and
// is not used after this.
unsafe { render_ffi::oakrender_display_renderer_destroy(&mut self.handle) };
self.inner.is_open_gl()
}
}
@@ -272,7 +168,7 @@ impl WorkerSession {
/// Create a session for `backend`, mirroring
/// `oakengine_worker_session_create()`: "none"/"" skips renderer
/// creation, anything else initializes the render backend through the
/// oakrender module C ABI (dynamic -> OpenGL fallback).
/// oakrender crate's direct Rust API (dynamic -> OpenGL fallback).
pub fn create(backend: &str) -> Result<WorkerSession, String> {
let renderer = if is_no_backend(backend) {
None
@@ -311,11 +207,9 @@ impl WorkerSession {
return true;
}
log_error("runtime: loading color-manager default config");
// SAFETY: no arguments; the function initializes process-wide state.
let rc = unsafe { render_ffi::oakrender_color_manager_set_up_default_config() };
if rc != 0 {
if let Err(e) = oakrender::color::set_up_default_config() {
log_error(&format!(
"runtime: color-manager default config failed (rc={rc}); continuing"
"runtime: color-manager default config failed ({e}); continuing"
));
}
log_error(
@@ -332,7 +226,7 @@ impl WorkerSession {
/// announces their geometry in its handshake reply.
///
/// Deviation from the C++: `gl_major`/`gl_minor` are omitted because
/// the oakrender module C ABI exposes no GL context version.
/// the oakrender module exposes no GL context version.
pub fn startup_handshake(&self) -> Value {
HandshakeMsg {
protocol_version: PROTOCOL_VERSION,
@@ -497,59 +391,10 @@ impl WorkerSession {
}
}
impl HandshakeMsg {
/// Serialize to the wire `handshake` object.
pub fn to_json(&self) -> Value {
json!({
"type": TYPE_HANDSHAKE,
"protocol_version": self.protocol_version,
"shm_key": self.shm_key,
"input_shm_key": self.input_shm_key,
"input_slots": self.input_slots,
"output_slots": self.output_slots,
"slot_data_bytes": self.slot_data_bytes,
"input_slot_data_bytes": self.input_slot_data_bytes,
})
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
/// `ipcmessage.cpp write_message()`.
fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
let line =
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
w.write_all(line.as_bytes())?;
w.write_all(b"\n")
}
/// Scan argv for `--backend <name>` (worker.cpp `oakengine_worker_main`).
/// The default is `"opengl"`; the value is lowercased; the last flag wins.
fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
let mut backend = "opengl".to_string();
if argc > 0 && !argv.is_null() {
// SAFETY: `argv` points to `argc` NUL-terminated C strings (the C
// runtime's argv), and we only read the entries.
let args = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
let mut i = 1usize;
while i < args.len() {
// SAFETY: `args[i]` is a valid NUL-terminated C string.
let arg = unsafe { crate::handle::read_cstr(args[i]) };
if arg == "--backend" && i + 1 < args.len() {
// SAFETY: `args[i + 1]` is a valid NUL-terminated C string.
backend = unsafe { crate::handle::read_cstr(args[i + 1]) }.to_ascii_lowercase();
i += 2;
} else {
i += 1;
}
}
}
backend
}
/// Full render-worker main, transport-agnostic in the backend name.
///
/// Mirrors `oakengine_worker_main()` in worker.cpp: create the session
@@ -559,7 +404,8 @@ fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
/// exit code.
pub fn worker_main(backend: &str) -> i32 {
// 1. Session creation initializes the render backend through the
// oakrender module C ABI (oakengine_worker_session_create()).
// oakrender crate's direct Rust API
// (oakengine_worker_session_create()).
let mut session = match WorkerSession::create(backend) {
Ok(s) => s,
Err(msg) => {
@@ -628,6 +474,325 @@ pub fn worker_main(backend: &str) -> i32 {
}
// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
use serde_json::json;
use std::ptr;
fn test_key(name: &str) -> String {
static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
+ &format!("-w-{name}")
}
/// The "parent" side of a handshake: create an output segment holding a
/// pool, optionally an input segment, and return the handshake message
/// plus the owner regions (kept alive by the caller).
fn parent_side(
slots: i32,
slot_bytes: i64,
input: bool,
) -> (Value, SharedMemoryRegion, Option<SharedMemoryRegion>) {
let out_key = test_key("out");
let out_bytes = FrameSlotPool::bytes_needed(slots as u32, slot_bytes as usize);
let mut out_region = SharedMemoryRegion::new();
assert!(
out_region.open(&out_key, out_bytes, ShmMode::Create),
"{}",
out_region.error()
);
// SAFETY: live mapping sized by bytes_needed.
let _pool =
unsafe { FrameSlotPool::create(out_region.data(), slots as u32, slot_bytes as usize) };
let (in_key, in_bytes, in_region) = if input {
let in_key = test_key("in");
let in_bytes = FrameSlotPool::bytes_needed(slots as u32, slot_bytes as usize);
let mut in_region = SharedMemoryRegion::new();
assert!(in_region.open(&in_key, in_bytes, ShmMode::Create));
// SAFETY: live mapping.
let _ = unsafe {
FrameSlotPool::create(in_region.data(), slots as u32, slot_bytes as usize)
};
(Some(in_key), Some(in_bytes), Some(in_region))
} else {
(None, None, None)
};
let hs = json!({
"type": "handshake",
"protocol_version": PROTOCOL_VERSION,
"shm_key": out_key,
"input_shm_key": in_key.unwrap_or_default(),
"input_slots": if input { slots } else { 0 },
"output_slots": slots,
"slot_data_bytes": slot_bytes,
"input_slot_data_bytes": in_bytes.unwrap_or(0),
});
(hs, out_region, in_region)
}
#[test]
fn no_backend_detection_matches_cpp() {
assert!(is_no_backend(""));
assert!(is_no_backend("none"));
assert!(is_no_backend("NONE"));
assert!(!is_no_backend("opengl"));
assert!(!is_no_backend("vulkan"));
}
#[test]
fn none_backend_session_has_no_renderer_but_serves_messages() {
let mut s = WorkerSession::create("none").unwrap();
assert!(!s.has_renderer());
let resp = s.handle_line(r#"{"type":"shutdown"}"#);
assert!(resp.is_none());
assert!(s.shutdown_requested());
}
#[test]
fn startup_handshake_is_protocol_version_1_with_empty_geometry() {
let s = WorkerSession::create("none").unwrap();
let hs = s.startup_handshake();
assert_eq!(
hs,
json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": "",
"input_shm_key": "",
"input_slots": 0,
"output_slots": 0,
"slot_data_bytes": 0,
"input_slot_data_bytes": 0,
})
);
}
#[test]
fn malformed_line_yields_error_response() {
let mut s = WorkerSession::create("none").unwrap();
let resp = s.handle_line("this is not json").unwrap();
assert_eq!(resp["type"], "error");
assert_eq!(resp["message"], "malformed control message");
}
#[test]
fn unknown_message_type_yields_error_response() {
let mut s = WorkerSession::create("none").unwrap();
let resp = s.handle_line(r#"{"type":"frobnicate"}"#).unwrap();
assert_eq!(resp["message"], "unknown message type: frobnicate");
}
#[test]
fn cancel_and_shutdown_produce_no_response() {
let mut s = WorkerSession::create("none").unwrap();
assert!(s.handle_line(r#"{"type":"cancel","ticket":5}"#).is_none());
assert!(s.handle_line(r#"{"type":"shutdown"}"#).is_none());
assert!(s.shutdown_requested());
}
#[test]
fn handshake_wrong_protocol_version() {
let mut s = WorkerSession::create("none").unwrap();
let resp = s
.handle_line(
r#"{"type":"handshake","protocol_version":99,"shm_key":"k","output_slots":1,"slot_data_bytes":16}"#,
)
.unwrap();
assert_eq!(resp["message"], "unsupported protocol version 99");
}
#[test]
fn handshake_missing_geometry() {
let mut s = WorkerSession::create("none").unwrap();
let resp = s
.handle_line(r#"{"type":"handshake","protocol_version":1}"#)
.unwrap();
assert_eq!(
resp["message"],
"handshake missing output shared-memory geometry"
);
}
#[test]
fn handshake_attaches_real_output_pool() {
let mut s = WorkerSession::create("none").unwrap();
let (hs, out_region, _in) = parent_side(4, 4096, false);
let resp = s.handle_line(&hs.to_string());
assert!(resp.is_none(), "unexpected error: {resp:?}");
// The session now holds a real attached pool with the parent's
// geometry.
let out_pool = s.output_pool.as_ref().unwrap();
assert_eq!(out_pool.slot_count(), 4);
assert_eq!(out_pool.slot_data_bytes(), 4096);
// The two views share the same rings, not copies: the parent pops a
// free slot and the worker's pool sees the ring cursor move; the
// parent's publish lands in the worker's ready ring.
// SAFETY: `out_region` is a live mapping containing the pool the
// session attached to.
let parent_pool = unsafe { FrameSlotPool::attach(out_region.data()) };
let mut parent_slot = 0;
assert!(unsafe { parent_pool.acquire(&mut parent_slot) });
assert_eq!(parent_slot, 0);
let mut worker_slot = 0;
assert!(unsafe { out_pool.acquire(&mut worker_slot) });
assert_eq!(worker_slot, 1, "worker must see the parent's free-ring pop");
// SAFETY: `parent_slot` was acquired by the parent; slot_bytes
// writable.
unsafe {
ptr::write_bytes(parent_pool.slot_data(parent_slot), 0xAB, 64);
}
assert!(unsafe { parent_pool.publish(parent_slot) });
let mut consumed = 0;
assert!(unsafe { out_pool.consume(&mut consumed) });
assert_eq!(consumed, parent_slot);
// SAFETY: `consumed` was consumed by the worker's pool.
assert_eq!(unsafe { *out_pool.slot_data_const(consumed) }, 0xAB);
// Clean up so the region drop at test end unlinks cleanly.
unsafe { out_pool.release(consumed) };
unsafe { out_pool.release(worker_slot) };
}
#[test]
fn handshake_attaches_input_pool_too() {
let mut s = WorkerSession::create("none").unwrap();
let (hs, _out, _in) = parent_side(2, 256, true);
let resp = s.handle_line(&hs.to_string());
assert!(resp.is_none(), "unexpected error: {resp:?}");
assert!(s.input_pool.is_some());
let in_pool = s.input_pool.as_ref().unwrap();
assert_eq!(in_pool.slot_count(), 2);
assert_eq!(in_pool.slot_data_bytes(), 256);
}
#[test]
fn handshake_attach_failure_reports_error() {
let mut s = WorkerSession::create("none").unwrap();
// A key that was never created.
let resp = s
.handle_line(
&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": format!("olive-rw-{}-missing", std::process::id()),
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string(),
)
.unwrap();
assert_eq!(resp["type"], "error");
assert!(resp["message"]
.as_str()
.unwrap()
.starts_with("failed to attach shared memory: "));
assert!(s.output_pool.is_none());
}
#[test]
fn handshake_rejects_non_pool_segment() {
let mut s = WorkerSession::create("none").unwrap();
// A real segment of the right size that does not contain a pool
// (zeroed memory → wrong magic). Sized so the attach size check
// passes and the magic check fires.
let key = test_key("nopool");
let bytes = FrameSlotPool::bytes_needed(4, 4096);
let mut region = SharedMemoryRegion::new();
assert!(region.open(&key, bytes, ShmMode::Create));
let resp = s
.handle_line(
&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": key,
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string(),
)
.unwrap();
assert_eq!(
resp["message"],
"shared memory does not contain a frame slot pool"
);
}
#[test]
fn handshake_missing_input_geometry_is_an_error() {
let mut s = WorkerSession::create("none").unwrap();
let (mut hs, _out, _in) = parent_side(2, 256, false);
// Ask for input slots without announcing their geometry.
hs["input_slots"] = json!(2);
let resp = s.handle_line(&hs.to_string()).unwrap();
assert_eq!(
resp["message"],
"handshake missing input shared-memory geometry"
);
}
#[test]
fn load_graph_checks_are_real_then_stub() {
let mut s = WorkerSession::create("none").unwrap();
let missing = "/definitely/not/a/real/graph.ove";
let resp = s
.handle_line(&json!({ "type": "load_graph", "path": missing }).to_string())
.unwrap();
assert_eq!(
resp["message"],
format!("graph file does not exist: {missing}")
);
let empty = std::env::temp_dir().join("oak_worker_main_test_empty.ove");
std::fs::write(&empty, b"").unwrap();
let resp = s
.handle_line(
&json!({ "type": "load_graph", "path": empty.display().to_string() }).to_string(),
)
.unwrap();
assert_eq!(
resp["message"],
format!("graph file is empty: {}", empty.display())
);
let _ = std::fs::remove_file(&empty);
let real = std::env::temp_dir().join("oak_worker_main_test_graph.ove");
std::fs::write(&real, b"<root/>").unwrap();
let resp = s
.handle_line(
&json!({ "type": "load_graph", "path": real.display().to_string() }).to_string(),
)
.unwrap();
assert!(resp["message"]
.as_str()
.unwrap()
.contains("node-graph deserialization is not yet available"));
let _ = std::fs::remove_file(&real);
}
#[test]
fn render_frame_reports_stub_with_ticket() {
let mut s = WorkerSession::create("none").unwrap();
let resp = s
.handle_line(r#"{"type":"render_frame","ticket":123,"node":"abc"}"#)
.unwrap();
assert_eq!(resp["type"], "error");
assert_eq!(resp["ticket"], 123);
assert!(resp["message"]
.as_str()
.unwrap()
.contains("frame rendering is not yet available"));
}
}
// C ABI exports (engine/include/oakengine/worker.h)
// ---------------------------------------------------------------------------
@@ -757,6 +922,30 @@ pub unsafe extern "C" fn oakengine_worker_session_shutdown_requested(
})
}
/// Scan argv for `--backend <name>` (worker.cpp `oakengine_worker_main`).
/// The default is `"opengl"`; the value is lowercased; the last flag wins.
fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
let mut backend = "opengl".to_string();
if argc > 0 && !argv.is_null() {
// SAFETY: `argv` points to `argc` NUL-terminated C strings (the C
// runtime's argv), and we only read the entries.
let args = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
let mut i = 1usize;
while i < args.len() {
// SAFETY: `args[i]` is a valid NUL-terminated C string.
let arg = unsafe { crate::handle::read_cstr(args[i]) };
if arg == "--backend" && i + 1 < args.len() {
// SAFETY: `args[i + 1]` is a valid NUL-terminated C string.
backend = unsafe { crate::handle::read_cstr(args[i + 1]) }.to_ascii_lowercase();
i += 2;
} else {
i += 1;
}
}
}
backend
}
/// `oakengine_worker_main` — full render-worker main. Parses `--backend`,
/// initializes the renderer, sends the startup handshake and runs the
/// stdin/stdout NDJSON loop until a shutdown message or EOF. Returns the
@@ -774,7 +963,7 @@ pub unsafe extern "C" fn oakengine_worker_main(argc: c_int, argv: *mut *mut c_ch
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
mod cabi_tests {
use super::*;
use crate::ipc::{self, FrameSlotPool, SharedMemoryRegion, ShmMode};
use serde_json::json;