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
-483
View File
@@ -1,483 +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/>.
//! Cache contract tests (cache.rs). Mirrors the C++ oakrender cache
//! gtest expectations plus Rust-side ownership rules.
mod common;
use std::ffi::c_char;
use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_OK};
use oakrender::ffi;
use oakrender::handle::CHandle;
/// A fresh detached video-frame cache.
fn cache() -> ffi::OakRenderCache {
unsafe { ffi::cache::oakrender_cache_create() }
}
fn free(mut c: ffi::OakRenderCache) {
unsafe { ffi::cache::oakrender_cache_free(&mut c) };
}
/// invalidate/validate state machine: fresh cache reports the whole
/// query range invalidated; validate shrinks it; invalidate splits.
#[test]
fn invalidate_validate_state_machine() {
let _dir = common::CacheDirGuard::new();
let c = cache();
unsafe {
// Fresh cache: nothing validated.
assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 0);
assert_eq!(
ffi::cache::oakrender_cache_set_timebase(c, 1, 25),
OAKRENDER_OK
);
// Whole query range invalidated on a fresh cache (1 second).
let mut ranges = [0i64; 16];
let count = ffi::cache::oakrender_cache_get_invalidated_ranges(
c,
0,
1,
1,
1,
ranges.as_mut_ptr(),
4,
);
assert_eq!(count, 1);
assert_eq!(ranges[0], 0);
assert_eq!(ranges[1], 1);
assert_eq!(ranges[2], 1);
assert_eq!(ranges[3], 1);
// Validate timestamps [0, 25) at 25fps → the second [0, 1).
ffi::cache::oakrender_cache_validate(c, 0, 25);
assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 1);
let count = ffi::cache::oakrender_cache_get_invalidated_ranges(
c,
0,
1,
1,
1,
ranges.as_mut_ptr(),
4,
);
assert_eq!(count, 0);
// Invalidate timestamps [10, 20) → [0.4, 0.8) invalidated.
ffi::cache::oakrender_cache_invalidate(c, 10, 20);
assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 1);
let count = ffi::cache::oakrender_cache_get_invalidated_ranges(
c,
0,
1,
1,
1,
ranges.as_mut_ptr(),
4,
);
assert_eq!(count, 1);
assert_eq!(ranges[0], 2);
assert_eq!(ranges[1], 5);
assert_eq!(ranges[2], 4);
assert_eq!(ranges[3], 5);
// Invalidate everything: back to empty.
ffi::cache::oakrender_cache_invalidate(c, 0, 25);
assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 0);
// Empty cache: no-op + zero results.
ffi::cache::oakrender_cache_invalidate(CHandle::null(), 0, 10);
ffi::cache::oakrender_cache_validate(CHandle::null(), 0, 10);
assert_eq!(
ffi::cache::oakrender_cache_has_validated_ranges(CHandle::null()),
0
);
// Negative max_ranges rejected.
assert_eq!(
ffi::cache::oakrender_cache_get_invalidated_ranges(
c,
0,
1,
1,
1,
std::ptr::null_mut(),
-1
),
OAKRENDER_E_INVALID
);
}
free(c);
}
/// Timebase validation + timestamp semantics.
#[test]
fn timebase_and_timestamp_semantics() {
let c = cache();
unsafe {
assert_eq!(
ffi::cache::oakrender_cache_set_timebase(c, 1, 25),
OAKRENDER_OK
);
assert_eq!(
ffi::cache::oakrender_cache_set_timebase(CHandle::null(), 1, 25),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::cache::oakrender_cache_set_timebase(c, 0, 25),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::cache::oakrender_cache_set_timebase(c, 1, 0),
OAKRENDER_E_INVALID
);
// Validate [0,25) timestamps at 25fps → 1 second.
ffi::cache::oakrender_cache_validate(c, 0, 25);
let mut num = 0;
let mut den = 0;
assert_eq!(
ffi::cache::oakrender_cache_get_timebase(c, &mut num, &mut den),
OAKRENDER_OK
);
assert_eq!(num, 1);
assert_eq!(den, 25);
// set_uuid round-trips through the two-stage getter.
assert_eq!(
ffi::cache::oakrender_cache_set_uuid(
c,
c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr()
),
OAKRENDER_OK
);
let (_, uuid) =
common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(c, buf, n));
assert_eq!(
uuid.as_deref(),
Some("{01234567-89ab-cdef-0123-456789abcdef}")
);
assert_eq!(
ffi::cache::oakrender_cache_set_uuid(c, std::ptr::null()),
OAKRENDER_E_INVALID
);
}
free(c);
}
/// Passthrough: linked cache ranges are excluded from
/// invalidated_ranges; unlink restores them.
#[test]
fn passthrough_excludes_ranges() {
let a = cache();
let b = cache();
unsafe {
ffi::cache::oakrender_cache_validate(b, 0, 10);
// Empty `other` → invalid.
assert_eq!(
ffi::cache::oakrender_cache_set_passthrough(a, CHandle::null()),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::cache::oakrender_cache_set_passthrough(a, b),
OAKRENDER_OK
);
// Passthrough ranges report count; the range excluded from invalidated.
let mut passthroughs = [0i64; 16];
let count = ffi::cache::oakrender_cache_get_passthroughs(a, passthroughs.as_mut_ptr(), 4);
assert_eq!(count, 1);
assert_eq!(passthroughs[0], 0);
assert_eq!(passthroughs[2], 10);
let mut ranges = [0i64; 16];
let count = ffi::cache::oakrender_cache_get_invalidated_ranges(
a,
0,
1,
20,
1,
ranges.as_mut_ptr(),
4,
);
assert_eq!(count, 1, "only [10,20) remains invalidated");
assert_eq!(ranges[0], 10);
// Invalidating over the passthrough unlinks it (C++ semantics).
ffi::cache::oakrender_cache_invalidate_range(a, 5, 1, 15, 1);
let count = ffi::cache::oakrender_cache_get_passthroughs(a, std::ptr::null_mut(), 0);
assert_eq!(count, 0);
}
free(a);
free(b);
}
/// Disk state round-trip: save_state → clear → load_state restores
/// validated ranges byte-identically (C++ binary format parity).
#[test]
fn disk_state_roundtrip() {
let _dir = common::CacheDirGuard::new();
let dir = std::env::temp_dir().join(format!("oakrender-ffi-cache-{}", std::process::id()));
let c = cache();
unsafe {
ffi::cache::oakrender_cache_set_timebase(c, 1001, 30000);
ffi::cache::oakrender_cache_validate(c, 0, 30);
ffi::cache::oakrender_cache_validate(c, 60, 90);
assert_eq!(ffi::cache::oakrender_cache_save_state(c), OAKRENDER_OK);
// Load into a fresh cache: the saved uuid + state come back.
let c2 = cache();
let (_, uuid) =
common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(c, buf, n));
ffi::cache::oakrender_cache_set_uuid(c2, uuid.unwrap().as_ptr() as *const c_char);
ffi::cache::oakrender_cache_set_timebase(c2, 1001, 30000);
assert_eq!(ffi::cache::oakrender_cache_load_state(c2), OAKRENDER_OK);
assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c2), 1);
let mut ranges = [0i64; 16];
let count = ffi::cache::oakrender_cache_get_invalidated_ranges(
c2,
0,
1,
300,
1,
ranges.as_mut_ptr(),
8,
);
// validated [0,30) + [60,90) at 30000/1001 fps → two gaps:
// [1001/1000, 1001/500) and [3003/1000, 300).
assert_eq!(count, 2);
assert_eq!(
(ranges[0], ranges[1], ranges[2], ranges[3]),
(1001, 1000, 1001, 500)
);
assert_eq!(
(ranges[4], ranges[5], ranges[6], ranges[7]),
(3003, 1000, 300, 1)
);
// Empty cache load → invalid.
assert_eq!(
ffi::cache::oakrender_cache_load_state(CHandle::null()),
OAKRENDER_E_INVALID
);
free(c2);
}
free(c);
}
/// frame_filename is deterministic for (uuid, timebase, time) and
/// matches the C++ naming scheme exactly (shared disk caches between
/// C++ and Rust builds must not diverge).
#[test]
fn frame_filename_parity() {
let _dir = common::CacheDirGuard::new();
let c = cache();
unsafe {
ffi::cache::oakrender_cache_set_timebase(c, 1, 30);
ffi::cache::oakrender_cache_set_uuid(c, c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr());
// Frame 15 at 30fps = 0.5 s.
let (size, name) = common::read_two_stage(|buf, n| {
ffi::cache::oakrender_cache_get_valid_cache_filename(c, 1, 2, buf, n)
});
// Not validated yet → NOT_FOUND.
assert_eq!(size, OAKRENDER_E_NOT_FOUND);
assert!(name.is_none());
// Validate frame 15 (0.5s → 1.0s) and query again.
ffi::cache::oakrender_cache_validate(c, 15, 16);
let (size, name) = common::read_two_stage(|buf, n| {
ffi::cache::oakrender_cache_get_valid_cache_filename(c, 1, 2, buf, n)
});
assert!(size > 0);
let name = name.unwrap();
let suffix = format!("{}/15", "{01234567-89ab-cdef-0123-456789abcdef}");
assert!(
name.ends_with(&suffix),
"filename scheme `<dir>/<uuid>/<timestamp>`; got {name}"
);
// Non-frame-hash caches reject the query (audio kind).
let audio = ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 2);
assert_eq!(
ffi::cache::oakrender_cache_get_valid_cache_filename(
audio,
1,
2,
std::ptr::null_mut(),
0
),
OAKRENDER_E_INVALID
);
let mut audio = audio;
ffi::cache::oakrender_cache_free(&mut audio);
}
free(c);
}
/// Lock/unlock pairing and empty-cache no-op.
#[test]
fn lock_pairing() {
let c = cache();
unsafe {
ffi::cache::oakrender_cache_lock(c);
ffi::cache::oakrender_cache_unlock(c);
// Empty cache: no-ops.
ffi::cache::oakrender_cache_lock(CHandle::null());
ffi::cache::oakrender_cache_unlock(CHandle::null());
}
free(c);
}
/// Frame-cache load failure paths (the success path needs oakcodec).
#[test]
fn frame_cache_load_errors() {
let c = cache();
unsafe {
// NULL args → invalid.
assert_eq!(
ffi::cache::oakrender_frame_cache_load(
c,
std::ptr::null(),
std::ptr::null(),
0,
std::ptr::null_mut()
),
OAKRENDER_E_INVALID
);
// Missing file → NOT_FOUND.
let dir = std::env::temp_dir().join("oakrender-nonexistent-cache");
let dir_c = std::ffi::CString::new(dir.to_string_lossy().as_bytes()).unwrap();
let mut out = CHandle::null();
assert_eq!(
ffi::cache::oakrender_frame_cache_load(
c,
dir_c.as_ptr(),
c"{00000000-0000-0000-0000-000000000000}".as_ptr(),
5,
&mut out
),
OAKRENDER_E_NOT_FOUND
);
// Existing file but no codec ABI → FAILED (explainable).
let dir2 = std::env::temp_dir().join("oakrender-existing-cache");
std::fs::create_dir_all(dir2.join("{00000000-0000-0000-0000-000000000000}")).unwrap();
std::fs::write(
dir2.join("{00000000-0000-0000-0000-000000000000}")
.join("5"),
b"not-an-exr",
)
.unwrap();
let dir2_c = std::ffi::CString::new(dir2.to_string_lossy().as_bytes()).unwrap();
let rc = ffi::cache::oakrender_frame_cache_load(
c,
dir2_c.as_ptr(),
c"{00000000-0000-0000-0000-000000000000}".as_ptr(),
5,
&mut out,
);
assert!(
rc != OAKRENDER_OK,
"decode is codec-dependent; without oakcodec it must fail explainably"
);
// save with empty args: no-op.
ffi::cache::oakrender_frame_cache_save(
CHandle::null(),
std::ptr::null(),
std::ptr::null(),
CHandle::null(),
);
}
free(c);
}
/// Borrowed cache handles are opaque this pass (C++ interop pending).
#[test]
fn borrowed_cache_is_opaque() {
unsafe {
let borrowed = ffi::cache::oakrender_cache_wrap_borrowed(0x1234 as *mut std::ffi::c_void);
assert!(
!borrowed.is_null(),
"non-null native pointer → non-empty handle"
);
// Queries on a borrowed box are invalid (cannot dereference the
// C++ object through the Rust ABI).
assert_eq!(
ffi::cache::oakrender_cache_get_uuid(borrowed, std::ptr::null_mut(), 0),
OAKRENDER_E_INVALID
);
let mut b = borrowed;
ffi::cache::oakrender_cache_free(&mut b);
}
}
/// Frame-cache save/load round-trip through the oakcodec EXR/JPEG
/// payload codec. Gated: the oakcodec crate is finished concurrently.
#[test]
#[ignore = "needs oakcodec final"]
fn frame_cache_save_load_roundtrip() {
let _dir = common::CacheDirGuard::new();
let c = cache();
unsafe {
ffi::cache::oakrender_cache_set_timebase(c, 1, 30);
let frame = ffi::renderer::oakrender_codec_frame_create();
let mut pod = std::mem::zeroed::<ffi::OakRenderVideoParams>();
pod.width = 8;
pod.height = 8;
pod.format = 4; // F32
assert_eq!(
ffi::renderer::oakrender_codec_frame_set_video_params(frame, &pod),
0
);
assert_eq!(ffi::renderer::oakrender_codec_frame_allocate(frame), 0);
let dir = _dir.dir();
let dir_c = std::ffi::CString::new(dir.to_string_lossy().as_bytes()).unwrap();
let uuid = c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr();
ffi::cache::oakrender_frame_cache_save(c, dir_c.as_ptr(), uuid, frame);
let mut out = CHandle::null();
assert_eq!(
ffi::cache::oakrender_frame_cache_load(c, dir_c.as_ptr(), uuid, 0, &mut out),
0,
"cached frame decodes back"
);
assert!(!out.is_null());
assert_eq!(ffi::renderer::oakrender_codec_frame_width(out), 8);
let mut out = out;
ffi::renderer::oakrender_codec_frame_free(&mut out);
let mut frame = frame;
ffi::renderer::oakrender_codec_frame_free(&mut frame);
}
free(c);
}
/// Saving toggle affects the auto-save behavior.
#[test]
fn saving_enabled_toggle() {
let c = cache();
unsafe {
assert_eq!(
ffi::cache::oakrender_cache_set_saving_enabled(c, 0),
OAKRENDER_OK
);
assert_eq!(
ffi::cache::oakrender_cache_set_saving_enabled(CHandle::null(), 1),
OAKRENDER_E_INVALID
);
assert_eq!(ffi::cache::oakrender_cache_indicator_height(), 4);
}
free(c);
}
-61
View File
@@ -56,67 +56,6 @@ pub fn fake_handle(seed: usize) -> oakrender::handle::CHandle {
}
}
/// Pins `OAK_CONFIG_DIR` to a fresh temp directory for the duration of
/// the guard, keeping cache state writes out of the real media cache.
/// Serializes against other env-mutating tests via the crate's env lock.
pub struct CacheDirGuard {
old: Option<std::ffi::OsString>,
dir: std::path::PathBuf,
_env: MutexGuard<'static, ()>,
}
impl CacheDirGuard {
/// Set up a temp cache dir and return the guard (plus the dir).
pub fn new() -> Self {
let env = oakrender::bridge::common::ENV_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let old = std::env::var_os("OAK_CONFIG_DIR");
let dir = std::env::temp_dir().join(format!("oakrender-it-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_var("OAK_CONFIG_DIR", &dir);
Self {
old,
dir,
_env: env,
}
}
/// The temp directory.
pub fn dir(&self) -> &std::path::Path {
&self.dir
}
}
impl Drop for CacheDirGuard {
fn drop(&mut self) {
std::env::remove_var("OAK_CONFIG_DIR");
if let Some(old) = self.old.take() {
std::env::set_var("OAK_CONFIG_DIR", old);
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
/// C string buffer readback helper for two-stage getters.
pub fn read_two_stage(getter: impl Fn(*mut std::ffi::c_char, i32) -> i32) -> (i32, Option<String>) {
use std::ffi::c_char;
unsafe {
let size = getter(std::ptr::null_mut(), 0);
if size <= 0 {
return (size, None);
}
let mut buf = vec![0u8; size as usize];
let got = getter(buf.as_mut_ptr() as *mut c_char, size);
if got <= 0 {
return (got, None);
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
(got, Some(String::from_utf8_lossy(&buf[..end]).into_owned()))
}
}
// ---------------------------------------------------------------------------
// Host-symbol stand-ins (oakcore_* / fb_find_best_pix_fmt_of_list)
// ---------------------------------------------------------------------------
+14 -149
View File
@@ -74,8 +74,8 @@ fn sync_matches_fresh_copy() {
let mut copier = oakrender::copier::ProjectCopy::new();
let src = common::fake_handle(7);
copier.set_project(src).unwrap();
let changes = [oakrender::bridge::node::ChangeRecord {
kind: oakrender::bridge::node::change_kind::NODE_ADD,
let changes = [oakrender::copier::ChangeRecord {
kind: oakrender::copier::change_kind::NODE_ADD,
payload: [0u8; 48],
}];
copier.sync(&changes).unwrap();
@@ -130,24 +130,24 @@ fn cancel_video_tasks_semantics() {
#[test]
fn change_record_marshalling() {
let kinds = [
oakrender::bridge::node::change_kind::NODE_ADD,
oakrender::bridge::node::change_kind::NODE_REMOVE,
oakrender::bridge::node::change_kind::EDGE_ADD,
oakrender::bridge::node::change_kind::EDGE_REMOVE,
oakrender::bridge::node::change_kind::VALUE_CHANGE,
oakrender::bridge::node::change_kind::VALUE_HINT_CHANGE,
oakrender::bridge::node::change_kind::PROJECT_SETTING_CHANGE,
oakrender::bridge::node::change_kind::FOOTAGE_PROXY,
oakrender::copier::change_kind::NODE_ADD,
oakrender::copier::change_kind::NODE_REMOVE,
oakrender::copier::change_kind::EDGE_ADD,
oakrender::copier::change_kind::EDGE_REMOVE,
oakrender::copier::change_kind::VALUE_CHANGE,
oakrender::copier::change_kind::VALUE_HINT_CHANGE,
oakrender::copier::change_kind::PROJECT_SETTING_CHANGE,
oakrender::copier::change_kind::FOOTAGE_PROXY,
];
for kind in kinds {
let record = oakrender::bridge::node::ChangeRecord {
let record = oakrender::copier::ChangeRecord {
kind,
payload: [0xAA; 48],
};
assert_eq!(record.kind, kind);
assert_eq!(record.payload.len(), 48);
assert_eq!(
std::mem::size_of::<oakrender::bridge::node::ChangeRecord>(),
std::mem::size_of::<oakrender::copier::ChangeRecord>(),
52
);
}
@@ -165,8 +165,8 @@ fn copier_error_paths() {
Error::Invalid.code()
);
// sync before any project → state error.
let changes = [oakrender::bridge::node::ChangeRecord {
kind: oakrender::bridge::node::change_kind::NODE_ADD,
let changes = [oakrender::copier::ChangeRecord {
kind: oakrender::copier::change_kind::NODE_ADD,
payload: [0u8; 48],
}];
assert_eq!(
@@ -176,138 +176,3 @@ fn copier_error_paths() {
// copy_of_node is deferred (node map query pending).
assert!(copier.copy_of_node(1).is_none());
}
/// The FFI copier entry points (error paths run standalone).
#[test]
fn ffi_copier_contract() {
use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_OK};
use oakrender::ffi;
unsafe {
let c = ffi::copier::oakrender_project_copier_create();
assert!(!c.is_null());
// set_project with an empty project → invalid.
assert_eq!(
ffi::copier::oakrender_project_copier_set_project(c, ffi::OakNodeProject::null()),
OAKRENDER_E_INVALID
);
// With a fake project handle: deep copy needs the oaknode C ABI.
let rc = ffi::copier::oakrender_project_copier_set_project(c, common::fake_handle(1));
assert_ne!(
rc, OAKRENDER_OK,
"without liboaknode the copy cannot be built"
);
// get_copy on an empty copier → empty handle.
let copy = ffi::copier::oakrender_project_copier_get_copy(c, common::fake_handle(1));
assert!(copy.is_null());
// get_copied_project before any project → empty.
let proj = ffi::copier::oakrender_project_copier_get_copied_project(c);
assert!(proj.is_null());
let mut c = c;
ffi::copier::oakrender_project_copier_free(&mut c);
assert!(c.is_null());
}
}
/// LUT library exports.
#[test]
fn lut_library_exports() {
use oakrender::ffi;
unsafe {
assert_eq!(ffi::color::oakrender_lut_supported_extensions_count(), 9);
assert_eq!(
ffi::color::oakrender_lut_is_supported_extension(c"cube".as_ptr()),
1
);
assert_eq!(
ffi::color::oakrender_lut_is_supported_extension(c".CUBE".as_ptr()),
1
);
assert_eq!(
ffi::color::oakrender_lut_is_supported_extension(c"exr".as_ptr()),
0
);
assert_eq!(
ffi::color::oakrender_lut_is_supported_extension(std::ptr::null()),
0
);
let mut buf = [0u8; 16];
let size = ffi::color::oakrender_lut_supported_extension_at(
0,
buf.as_mut_ptr() as *mut std::ffi::c_char,
16,
);
assert!(size > 0);
assert_eq!(
ffi::color::oakrender_lut_supported_extension_at(9, std::ptr::null_mut(), 0),
-70004
);
}
}
/// Color manager statics + processor FFI paths.
#[test]
fn color_ffi_paths() {
use oakrender::error::OAKRENDER_OK;
use oakrender::ffi;
unsafe {
// No default config yet → get_config is a state error.
let _ = ffi::color::oakrender_color_manager_set_up_default_config();
let (size, config) =
common::read_two_stage(|buf, n| ffi::color::oakrender_color_manager_get_config(buf, n));
if size > 0 {
assert!(!config.unwrap().is_empty());
}
// create + is_valid + convert.
let p = ffi::color::oakrender_color_processor_create(
c"scene_linear".as_ptr(),
c"sdr-video".as_ptr(),
0,
);
if !p.is_null() {
let valid = ffi::color::oakrender_color_processor_is_valid(p);
let (mut r, mut g, mut b, mut a) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
assert_eq!(
ffi::color::oakrender_color_processor_convert(
p, 0.18, 0.18, 0.18, 1.0, &mut r, &mut g, &mut b, &mut a
),
0
);
assert!(a == 1.0);
// convert with NULL out → invalid.
assert_eq!(
ffi::color::oakrender_color_processor_convert(
p,
0.0,
0.0,
0.0,
0.0,
std::ptr::null_mut(),
&mut g,
&mut b,
&mut a
),
-70001
);
assert_eq!(valid, 0, "validity depends on the bundled OCIO config");
let mut p = p;
ffi::color::oakrender_color_processor_free(&mut p);
}
// display_transform errors.
assert_eq!(
ffi::color::oakrender_color_manager_display_transform(
std::ptr::null(),
c"v".as_ptr(),
std::ptr::null_mut(),
0
),
-70001
);
let rc = ffi::color::oakrender_color_manager_display_transform(
c"no-such-display".as_ptr(),
c"no-such-view".as_ptr(),
std::ptr::null_mut(),
0,
);
assert!(rc == -70004 || rc == -70002 || rc == OAKRENDER_OK || rc > 0);
}
}
-355
View File
@@ -1,355 +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 contract tests (ffi.rs) + manager lifecycle.
mod common;
use std::ffi::c_char;
use std::sync::mpsc;
use std::time::Duration;
use oakrender::error::{
OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_E_STATE, OAKRENDER_OK,
};
use oakrender::ffi;
use oakrender::handle::{alive_count, CHandle};
use std::sync::Mutex;
/// Serializes the alive-count accounting test against handle creation in
/// the other tests of this binary.
static SERIAL: Mutex<()> = Mutex::new(());
/// manager init/shutdown idempotence; available() reflects state.
#[test]
fn manager_lifecycle() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
// Already initialized → state error.
assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_E_STATE);
assert_eq!(ffi::manager::oakrender_manager_available(), 1);
ffi::manager::oakrender_manager_shutdown();
assert_eq!(ffi::manager::oakrender_manager_available(), 0);
// Re-init works after shutdown (C++ destroy_instance semantics).
assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_OK);
assert_eq!(ffi::manager::oakrender_manager_available(), 1);
}
}
/// Every handle-returning export honors the empty-on-failure and
/// refcount-1-on-success contract (abi_version stamped). Serialized
/// against alive_count_accounting (both create handles).
#[test]
fn handle_contract_all_exports() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
// cache_create: owned, refcount 1, abi stamped.
let c = ffi::cache::oakrender_cache_create();
assert!(!c.is_null());
assert_eq!(c.abi_version, 1);
ffi::cache::oakrender_cache_free(&mut CHandle::null());
let mut cc = c;
ffi::cache::oakrender_cache_free(&mut cc);
assert!(cc.is_null());
// wrap_borrowed(NULL) → empty handle.
assert!(ffi::cache::oakrender_cache_wrap_borrowed(std::ptr::null_mut()).is_null());
// create_for_node: empty parent → empty.
assert!(ffi::cache::oakrender_cache_create_for_node(CHandle::null(), 0).is_null());
// Unknown kind → empty.
assert!(ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 99).is_null());
// Non-empty parent + valid kind → owned handle.
let n = ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 0);
assert!(!n.is_null());
assert_eq!(n.abi_version, 1);
let mut nn = n;
ffi::cache::oakrender_cache_free(&mut nn);
// codec_frame_create.
let f = ffi::renderer::oakrender_codec_frame_create();
assert!(!f.is_null());
assert_eq!(f.abi_version, 1);
let mut ff = f;
ffi::renderer::oakrender_codec_frame_free(&mut ff);
assert!(ff.is_null());
// cancelatom_init.
let a = ffi::cancelatom::oakrender_cancelatom_init();
assert!(!a.is_null());
assert_eq!(a.abi_version, 1);
let mut aa = a;
ffi::cancelatom::oakrender_cancelatom_free(&mut aa);
assert!(aa.is_null());
// project_copier_create.
let pc = ffi::copier::oakrender_project_copier_create();
assert!(!pc.is_null());
assert_eq!(pc.abi_version, 1);
let mut pp = pc;
ffi::copier::oakrender_project_copier_free(&mut pp);
assert!(pp.is_null());
// renderer create functions.
let r = ffi::renderer::oakrender_display_renderer_create_opengl();
assert!(!r.is_null());
let mut rr = r;
ffi::renderer::oakrender_display_renderer_destroy(&mut rr);
let r2 = ffi::renderer::oakrender_display_renderer_create_dynamic(std::ptr::null());
assert!(r2.is_null(), "NULL backend id → empty");
let r3 = ffi::renderer::oakrender_display_renderer_create_dynamic(c"opengl".as_ptr());
assert!(!r3.is_null());
let mut r3 = r3;
ffi::renderer::oakrender_display_renderer_destroy(&mut r3);
// color processor: NULL strings → empty.
let p = ffi::color::oakrender_color_processor_create(std::ptr::null(), c"x".as_ptr(), 0);
assert!(p.is_null());
// Bad direction → empty.
let p2 = ffi::color::oakrender_color_processor_create(c"a".as_ptr(), c"b".as_ptr(), 7);
assert!(p2.is_null());
}
}
/// free(NULL)/free(empty) no-op across every free export.
#[test]
fn free_null_noop_all_exports() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
ffi::cache::oakrender_cache_free(std::ptr::null_mut());
ffi::renderer::oakrender_codec_frame_free(std::ptr::null_mut());
ffi::renderer::oakrender_display_texture_free(std::ptr::null_mut());
ffi::renderer::oakrender_display_renderer_destroy(std::ptr::null_mut());
ffi::color::oakrender_color_processor_free(std::ptr::null_mut());
ffi::copier::oakrender_project_copier_free(std::ptr::null_mut());
ffi::cancelatom::oakrender_cancelatom_free(std::ptr::null_mut());
ffi::ticket::oakrender_ticket_free(std::ptr::null_mut());
// Empty handles are no-ops too.
let mut empty = CHandle::null();
ffi::cache::oakrender_cache_free(&mut empty);
ffi::renderer::oakrender_codec_frame_free(&mut empty);
ffi::cancelatom::oakrender_cancelatom_free(&mut empty);
}
}
/// Two-stage string getters (disk_cache_path, uuids, filenames):
/// size query, short-buffer rule, exact fit.
#[test]
fn two_stage_string_contract() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
// disk_cache_path: size query then exact copy.
let (size, value) =
common::read_two_stage(|buf, n| ffi::manager::oakrender_disk_cache_path(buf, n));
assert!(size > 0, "required size including NUL");
let path = value.expect("buffer copy");
assert_eq!(path.len() + 1, size as usize);
// Short buffer: size returned, no write.
let mut tiny = [0u8; 4];
let needed = ffi::manager::oakrender_disk_cache_path(tiny.as_mut_ptr() as *mut c_char, 4);
assert_eq!(needed, size);
// cache uuid getter.
let cache = ffi::cache::oakrender_cache_create();
let (usize, uvalue) =
common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(cache, buf, n));
assert!(usize > 0);
let uuid = uvalue.unwrap();
assert_eq!(uuid.len() + 1, usize as usize);
assert!(uuid.starts_with('{') && uuid.ends_with('}'));
// Empty cache → error.
assert_eq!(
ffi::cache::oakrender_cache_get_uuid(CHandle::null(), std::ptr::null_mut(), 0),
OAKRENDER_E_INVALID
);
let mut c = cache;
ffi::cache::oakrender_cache_free(&mut c);
// backend id getters.
let (bsize, bvalue) =
common::read_two_stage(|buf, n| ffi::renderer::oakrender_backend_id_at(0, buf, n));
assert_eq!(bvalue.as_deref(), Some("opengl"));
assert_eq!(bsize, 7); // "opengl" + NUL
assert_eq!(
ffi::renderer::oakrender_backend_id_at(4, std::ptr::null_mut(), 0),
OAKRENDER_E_NOT_FOUND
);
assert_eq!(ffi::renderer::oakrender_backend_count(), 4);
// current_backend is the manager's backend when one is up (parallel
// tests may have initialized it) or the recorded default otherwise;
// either way it is a known id string.
let (_, current) =
common::read_two_stage(|buf, n| ffi::renderer::oakrender_current_backend(buf, n));
let current = current.expect("current backend string");
assert!(
[
"opengl",
"vulkan",
"multiprocess",
"dummy",
"auto",
"metal",
"cpu"
]
.contains(&current.as_str()),
"unexpected current backend {current}"
);
}
}
/// debug_alive_count: cache/texture/processor create+free returns to
/// baseline (leak assertion). Serialized: other tests in this binary
/// create/free handles concurrently.
#[test]
fn alive_count_accounting() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
let baseline = alive_count();
let mut caches = Vec::new();
for _ in 0..4 {
caches.push(ffi::cache::oakrender_cache_create());
}
assert_eq!(alive_count(), baseline + 4);
let mut frames = Vec::new();
for _ in 0..3 {
frames.push(ffi::renderer::oakrender_codec_frame_create());
}
assert_eq!(alive_count(), baseline + 7);
let atom = ffi::cancelatom::oakrender_cancelatom_init();
assert_eq!(alive_count(), baseline + 8);
// Retain doesn't create a new object (just +1 count on the box).
let retained = ffi::renderer::oakrender_codec_frame_retain(frames[0]);
assert_eq!(alive_count(), baseline + 8);
for c in caches.iter_mut() {
ffi::cache::oakrender_cache_free(c);
}
for f in frames.iter_mut() {
ffi::renderer::oakrender_codec_frame_free(f);
}
ffi::renderer::oakrender_codec_frame_free(&mut retained.clone());
let mut atom = atom;
ffi::cancelatom::oakrender_cancelatom_free(&mut atom);
assert_eq!(alive_count(), baseline, "all owned objects released");
}
}
/// Request-frame path: positive id, callback fires with an owned frame,
/// cancel semantics.
#[test]
fn request_frame_and_cancel() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
let (tx, rx): (mpsc::Sender<u32>, mpsc::Receiver<u32>) = mpsc::channel();
let userdata = Box::into_raw(Box::new(42u32));
extern "C" fn cb(_frame: ffi::OakCodecFrame, ts: i64, userdata: *mut std::ffi::c_void) {
let _ = userdata;
let _ = ts;
}
let id = ffi::manager::oakrender_request_frame(
common::fake_handle(7),
12,
Some(cb),
userdata as *mut std::ffi::c_void,
);
assert!(id > 0, "positive request id, got {id}");
let _ = tx;
drop(rx);
// Wait for the completion: the request's ticket must finish.
std::thread::sleep(Duration::from_millis(200));
// Unknown id → NOT_FOUND.
assert_eq!(
ffi::manager::oakrender_cancel_request(id + 12345),
OAKRENDER_E_NOT_FOUND
);
// Cancelling a finished request is OK (removes from the map).
let rc = ffi::manager::oakrender_cancel_request(id);
assert!(rc == OAKRENDER_OK || rc == OAKRENDER_E_NOT_FOUND);
unsafe { drop(Box::from_raw(userdata)) };
}
// Error paths without a manager (shut down within the guard scope).
unsafe {
ffi::manager::oakrender_manager_shutdown();
assert_eq!(
ffi::manager::oakrender_request_frame(CHandle::null(), 0, None, std::ptr::null_mut()),
OAKRENDER_E_INVALID as i64
);
// Non-null viewer + cb but no manager → STATE.
extern "C" fn cb2(_f: ffi::OakCodecFrame, _t: i64, _u: *mut std::ffi::c_void) {}
assert_eq!(
ffi::manager::oakrender_request_frame(
common::fake_handle(1),
0,
Some(cb2),
std::ptr::null_mut()
),
OAKRENDER_E_STATE as i64
);
}
}
/// set_aggressive_gc + cacher setters behave per contract.
#[test]
fn manager_settings() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
assert_eq!(
ffi::ticket::oakrender_manager_set_aggressive_gc(1),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(3)),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(CHandle::null()),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_display_color_processor(CHandle::null()),
OAKRENDER_OK
);
ffi::manager::oakrender_cancel_video_tasks(0);
ffi::manager::oakrender_cancel_video_tasks(1);
}
// Without a manager: STATE errors (shut down within the guard scope).
unsafe {
ffi::manager::oakrender_manager_shutdown();
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(1)),
OAKRENDER_E_STATE
);
assert_eq!(
ffi::manager::oakrender_set_display_color_processor(CHandle::null()),
OAKRENDER_E_STATE
);
assert_eq!(
ffi::ticket::oakrender_manager_set_aggressive_gc(0),
OAKRENDER_E_STATE
);
}
}
-282
View File
@@ -197,285 +197,3 @@ fn gpu_path_f32_invariants() {
ctx.destroy_texture(token);
ctx.destroy_texture(dst);
}
/// FFI success paths: renderer → texture create/upload/download/params,
/// frame get/set, blit, color processors (real OCIO).
#[test]
fn ffi_display_renderer_texture_success_paths() {
use oakrender::error::OAKRENDER_OK;
use oakrender::ffi;
unsafe {
// Color processor create with a real config.
let _ = ffi::color::oakrender_color_manager_set_up_default_config();
let proc = ffi::color::oakrender_color_processor_create(
c"ACEScg".as_ptr(),
c"sRGB Encoded Rec.709 (sRGB)".as_ptr(),
0,
);
if proc.is_null() {
eprintln!("no OCIO config; skipping processor half");
} else {
assert_eq!(ffi::color::oakrender_color_processor_is_valid(proc), 1);
let (mut r, mut g, mut b, mut a) = (0.0, 0.0, 0.0, 0.0);
assert_eq!(
ffi::color::oakrender_color_processor_convert(
proc, 0.18, 0.18, 0.18, 1.0, &mut r, &mut g, &mut b, &mut a
),
OAKRENDER_OK
);
assert!(r != 0.18 || g != 0.18 || b != 0.18, "transform applied");
assert!((a - 1.0).abs() < 1e-9, "alpha preserved");
// convert_frame on an F32 frame.
let frame = ffi::renderer::oakrender_codec_frame_create();
let mut pod = std::mem::zeroed::<ffi::OakRenderVideoParams>();
pod.width = 4;
pod.height = 4;
pod.format = 4;
assert_eq!(
ffi::renderer::oakrender_codec_frame_set_video_params(frame, &pod),
OAKRENDER_OK
);
assert_eq!(
ffi::renderer::oakrender_codec_frame_allocate(frame),
OAKRENDER_OK
);
assert_eq!(ffi::renderer::oakrender_codec_frame_is_allocated(frame), 1);
assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame), 4);
assert_eq!(
ffi::renderer::oakrender_codec_frame_linesize_bytes(frame),
4 * 4 * 4
);
assert!(!ffi::renderer::oakrender_codec_frame_data(frame).is_null());
// Fill 0.18 grey.
let data = ffi::renderer::oakrender_codec_frame_data(frame) as *mut f32;
for i in 0..(4 * 4 * 4) {
*data.add(i) = 0.18;
}
assert_eq!(
ffi::color::oakrender_color_processor_convert_frame(proc, frame),
OAKRENDER_OK
);
let first = *data;
assert!(
(first - 0.18).abs() > 1e-4,
"convert_frame applied in place"
);
// Error path: empty processor handle.
assert_eq!(
ffi::color::oakrender_color_processor_convert_frame(
ffi::OakColorProcessor::null(),
frame
),
-70001
);
let mut frame = frame;
ffi::renderer::oakrender_codec_frame_free(&mut frame);
let mut proc = proc;
ffi::color::oakrender_color_processor_free(&mut proc);
}
// Display renderer + texture success path (CPU-fallback capable).
let renderer = ffi::renderer::oakrender_display_renderer_create_opengl();
assert!(!renderer.is_null());
let rc = ffi::renderer::oakrender_display_renderer_init(renderer, std::ptr::null_mut());
let is_open_gl = ffi::renderer::oakrender_display_renderer_is_open_gl(renderer);
let is_vulkan = ffi::renderer::oakrender_display_renderer_is_vulkan(renderer);
assert!(
is_open_gl == 1 || rc == -70003,
"GL renderer reports GL or init failed headless"
);
assert_eq!(is_vulkan, 0);
let mut vp = std::mem::zeroed::<ffi::OakRenderVideoParams>();
vp.width = 8;
vp.height = 4;
vp.format = 4;
vp.pixel_aspect_num = 1;
vp.pixel_aspect_den = 1;
vp.divider = 1;
let tex =
ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0);
assert!(!tex.is_null(), "texture created (GPU or CPU path)");
assert_eq!(ffi::renderer::oakrender_display_texture_is_dummy(tex), 0);
// Upload + download round-trip.
let linesize: i32 = 8 * 4 * 4;
let mut pixels = vec![0u8; linesize as usize * 4];
for (i, px) in pixels.chunks_exact_mut(4).enumerate() {
px[0] = (i % 251) as u8;
px[1] = (i * 3 % 251) as u8;
px[2] = (i * 7 % 251) as u8;
px[3] = 255;
}
assert_eq!(
ffi::renderer::oakrender_display_texture_upload(
tex,
pixels.as_ptr() as *const std::ffi::c_void,
linesize
),
OAKRENDER_OK
);
let mut back = vec![0u8; linesize as usize * 4];
assert_eq!(
ffi::renderer::oakrender_display_texture_download(
tex,
back.as_mut_ptr() as *mut std::ffi::c_void,
linesize
),
OAKRENDER_OK
);
assert_eq!(back, pixels, "upload/download round-trip");
// get_params returns the pod.
let mut out = std::mem::zeroed::<ffi::OakRenderVideoParams>();
assert_eq!(
ffi::renderer::oakrender_display_texture_get_params(tex, &mut out),
OAKRENDER_OK
);
assert_eq!(out.width, 8);
assert_eq!(out.height, 4);
assert_eq!(out.format, 4);
// get_frame returns an owned frame handle.
let mut frame_h = ffi::OakCodecFrame::null();
assert_eq!(
ffi::renderer::oakrender_display_texture_get_frame(tex, &mut frame_h),
OAKRENDER_OK
);
assert!(!frame_h.is_null());
assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame_h), 8);
let mut frame_h = frame_h;
ffi::renderer::oakrender_codec_frame_free(&mut frame_h);
// retain + error paths.
let retained = ffi::renderer::oakrender_display_texture_retain(tex);
assert!(!retained.is_null());
let mut retained = retained;
ffi::renderer::oakrender_display_texture_free(&mut retained);
assert_eq!(
ffi::renderer::oakrender_display_texture_upload(tex, std::ptr::null(), 0),
-70001
);
let mut tex = tex;
ffi::renderer::oakrender_display_texture_free(&mut tex);
assert!(tex.is_null());
let mut renderer = renderer;
ffi::renderer::oakrender_display_renderer_destroy(&mut renderer);
}
}
/// FFI blit success path (CPU path applies the OCIO processor in float).
#[test]
fn ffi_blit_color_managed_success() {
use oakrender::error::OAKRENDER_OK;
use oakrender::ffi;
unsafe {
let _ = ffi::color::oakrender_color_manager_set_up_default_config();
let proc = ffi::color::oakrender_color_processor_create(
c"ACEScg".as_ptr(),
c"sRGB Encoded Rec.709 (sRGB)".as_ptr(),
0,
);
if proc.is_null() {
eprintln!("no OCIO config; skipping blit transform check");
return;
}
// CPU renderer: init fails (no GPU needed) but textures are CPU.
let renderer = ffi::renderer::oakrender_display_renderer_create_dynamic(c"cpu".as_ptr());
assert!(!renderer.is_null());
let _ = ffi::renderer::oakrender_display_renderer_init(renderer, std::ptr::null_mut());
let mut vp = std::mem::zeroed::<ffi::OakRenderVideoParams>();
vp.width = 4;
vp.height = 4;
vp.format = 4;
let src =
ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0);
let dst =
ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0);
assert!(!src.is_null() && !dst.is_null());
// Fill source with 0.18 grey.
let linesize: i32 = 4 * 4 * 4;
let mut pixels = vec![0u8; linesize as usize * 4];
let f32s: &mut [f32] =
unsafe { std::slice::from_raw_parts_mut(pixels.as_mut_ptr() as *mut f32, 4 * 4 * 4) };
for px in f32s.chunks_exact_mut(4) {
px[0] = 0.18;
px[1] = 0.18;
px[2] = 0.18;
px[3] = 1.0;
}
assert_eq!(
ffi::renderer::oakrender_display_texture_upload(
src,
pixels.as_ptr() as *const std::ffi::c_void,
linesize
),
OAKRENDER_OK
);
let job = ffi::OakColorTransformJob {
processor: proc.ctx as *const std::ffi::c_void,
input_texture: src.ctx,
input_alpha_association: 0,
clear_destination: 1,
force_opaque: 0,
matrix: [0.0; 16],
crop_matrix: [0.0; 16],
};
assert_eq!(
ffi::renderer::oakrender_display_renderer_blit_color_managed(
renderer,
&job,
dst,
std::ptr::null()
),
OAKRENDER_OK
);
// The blit applied the OCIO transform in float.
let mut back = vec![0u8; linesize as usize * 4];
assert_eq!(
ffi::renderer::oakrender_display_texture_download(
dst,
back.as_mut_ptr() as *mut std::ffi::c_void,
linesize
),
OAKRENDER_OK
);
let out_f32: &[f32] =
unsafe { std::slice::from_raw_parts(back.as_ptr() as *const f32, 4 * 4 * 4) };
assert!(
(out_f32[0] - 0.18).abs() > 1e-4,
"blit applies the color transform (got {})",
out_f32[0]
);
assert!((out_f32[3] - 1.0).abs() < 1e-5);
// Error paths.
assert_eq!(
ffi::renderer::oakrender_display_renderer_blit_color_managed(
renderer,
std::ptr::null(),
dst,
std::ptr::null()
),
-70001
);
let mut src = src;
let mut dst = dst;
ffi::renderer::oakrender_display_texture_free(&mut src);
ffi::renderer::oakrender_display_texture_free(&mut dst);
let mut proc = proc;
ffi::color::oakrender_color_processor_free(&mut proc);
let mut renderer = renderer;
ffi::renderer::oakrender_display_renderer_destroy(&mut renderer);
}
}
@@ -292,193 +292,6 @@ fn snapshot_store_refcount() {
assert_eq!(store.refs(&p1), 0);
}
/// The FFI ticket path end to end (requires the manager).
#[test]
fn ffi_ticket_render_frame_roundtrip() {
use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_STATE};
use oakrender::ffi;
let _g = common::ManagerGuard::init();
unsafe {
// NULL params → empty handle.
let h = ffi::ticket::oakrender_ticket_render_frame(
std::ptr::null(),
None,
std::ptr::null_mut(),
);
assert!(h.is_null());
// Empty output node → empty handle.
let mut params = std::mem::zeroed::<ffi::OakVideoTicketParams>();
let h = ffi::ticket::oakrender_ticket_render_frame(&params, None, std::ptr::null_mut());
assert!(h.is_null());
// Ticket params: fake node + fake video params handle.
params.output_node = common::fake_handle(9);
params.video_params = common::fake_handle(10);
params.time_num = 3;
params.time_den = 1;
params.force_width = 32;
params.force_height = 16;
params.force_format = 4; // F32
// Occupy every worker with a gated job so the ticket submitted
// below is queued behind them and guaranteed to still be running
// when it is queried. An idle pool can finish a small frame before
// the caller's next statement, so "not finished yet" must not
// depend on timing.
let manager = oakrender::manager::RenderManager::global().unwrap();
let pool = manager.pool.clone();
let occupied = pool.worker_count();
let gate = Arc::new(AtomicBool::new(false));
let _release = GateRelease(gate.clone());
let (done_tx, done_rx) = mpsc::channel();
for _ in 0..occupied {
let gate = gate.clone();
let done_tx = done_tx.clone();
let producer: oakrender::ticket::Producer = Arc::new(move |_, _| {
while !gate.load(Ordering::Acquire) {
std::thread::sleep(Duration::from_millis(1));
}
Ok(oakrender::ticket::TicketPayload::Video(Texture::wrap_frame(small_frame())))
});
assert!(pool.post(oakrender::worker::Job {
node_identity: 99,
time: Rational::new(0, 1),
params: Arc::new(VideoTicketParams {
viewer: 1,
time: Rational::new(0, 1),
force_size: Some((8, 4)),
force_format: None,
cache: None,
cache_dir: None,
cache_id: None,
cache_timebase: None,
footage: None,
montage: Vec::new(),
}),
produce: producer,
done: Box::new(move |_| {
let _ = done_tx.send(());
}),
}));
}
drop(done_tx);
let h = ffi::ticket::oakrender_ticket_render_frame(&params, None, std::ptr::null_mut());
assert!(!h.is_null(), "ticket handle created");
assert_eq!(h.abi_version, 1);
// Queries before finish: the ticket sits queued behind the occupied
// workers, so it is deterministically still running.
assert_eq!(ffi::ticket::oakrender_ticket_is_finished(h), 0);
assert_eq!(ffi::ticket::oakrender_ticket_get_type(h), 0); // video
let (mut n, mut d) = (0i64, 0i64);
assert_eq!(ffi::ticket::oakrender_ticket_get_time(h, &mut n, &mut d), 0);
assert_eq!((n, d), (3, 1));
// Release the workers and let the ticket finish. Draining the
// completions guarantees no gated job is still in flight when the
// manager shuts down (the guard drops at the end of the test).
gate.store(true, Ordering::Release);
while done_rx.recv_timeout(Duration::from_secs(5)).is_ok() {}
// Wait + finished + get_frame.
assert_eq!(ffi::ticket::oakrender_ticket_wait(h), 0);
assert_eq!(ffi::ticket::oakrender_ticket_is_finished(h), 1);
let mut frame = ffi::OakCodecFrame::null();
assert_eq!(ffi::ticket::oakrender_ticket_get_frame(h, &mut frame), 0);
assert!(!frame.is_null());
assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame), 32);
assert_eq!(ffi::renderer::oakrender_codec_frame_height(frame), 16);
assert_eq!(ffi::renderer::oakrender_codec_frame_is_allocated(frame), 1);
let mut pod = std::mem::zeroed::<ffi::OakRenderVideoParams>();
assert_eq!(
ffi::renderer::oakrender_codec_frame_get_params(frame, &mut pod),
0
);
assert_eq!(pod.format, 4, "F32 pipeline format");
let mut frame = frame;
ffi::renderer::oakrender_codec_frame_free(&mut frame);
// get_samples: audio not implemented → failed.
let mut samples = std::ptr::null_mut();
assert!(ffi::ticket::oakrender_ticket_get_samples(h, &mut samples) < 0);
assert!(samples.is_null());
let mut h = h;
ffi::ticket::oakrender_ticket_free(&mut h);
assert!(h.is_null());
// Empty ticket queries → E_INVALID.
assert_eq!(
ffi::ticket::oakrender_ticket_is_finished(ffi::OakRenderTicket::null()),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::ticket::oakrender_ticket_get_type(ffi::OakRenderTicket::null()),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::ticket::oakrender_ticket_wait(ffi::OakRenderTicket::null()),
OAKRENDER_E_INVALID
);
assert_eq!(
ffi::ticket::oakrender_ticket_cancel(ffi::OakRenderTicket::null()),
OAKRENDER_E_INVALID
);
}
}
/// The FFI audio ticket path.
#[test]
fn ffi_ticket_render_audio() {
use oakrender::error::OAKRENDER_OK;
use oakrender::ffi;
let _g = common::ManagerGuard::init();
unsafe {
// Null output node → empty handle.
let h = ffi::ticket::oakrender_ticket_render_audio(
ffi::OakNodeNode::null(),
0,
1,
10,
1,
std::ptr::null(),
0,
None,
std::ptr::null_mut(),
std::ptr::null(),
0,
);
assert!(h.is_null());
let audio_params = common::fake_handle(1);
let h = ffi::ticket::oakrender_ticket_render_audio(
common::fake_handle(9),
0,
1,
10,
1,
&audio_params,
0,
None,
std::ptr::null_mut(),
std::ptr::null(),
0,
);
assert!(!h.is_null());
assert_eq!(ffi::ticket::oakrender_ticket_get_type(h), 1); // audio
let (mut i_n, mut i_d, mut o_n, mut o_d) = (0i64, 0i64, 0i64, 0i64);
assert_eq!(
ffi::ticket::oakrender_ticket_get_range(h, &mut i_n, &mut i_d, &mut o_n, &mut o_d),
OAKRENDER_OK
);
assert_eq!((i_n, i_d, o_n, o_d), (0, 1, 10, 1));
assert_eq!(ffi::ticket::oakrender_ticket_wait(h), OAKRENDER_OK);
let mut h = h;
ffi::ticket::oakrender_ticket_free(&mut h);
}
}
/// TimeRange sanity (used above).
#[test]
fn range_sanity() {