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:
@@ -29,8 +29,6 @@ ocio-rs = { version = "0.2", features = ["bundled"] }
|
||||
thiserror = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
# Test binaries that pull oakcodec's decoder (the direct-call bridge) link
|
||||
# oakcodec's ffmpeg unit, which references the host `oakcore_audioparams_*`
|
||||
# symbols no Rust crate provides — the `test-stubs` feature compiles
|
||||
# oakcodec's host-mocks (same pattern as oaknode's dev-dependencies).
|
||||
oakcodec = { path = "../oakcodec", features = ["test-stubs"] }
|
||||
# oakcodec is also a plain dependency above; the dev-dependency re-entry
|
||||
# used to enable oakcodec's `test-stubs` host-mocks, which no longer exist
|
||||
# (single-lib unification removed the oakcore_audioparams_* C ABI calls).
|
||||
|
||||
@@ -99,7 +99,7 @@ impl BackendKind {
|
||||
}
|
||||
}
|
||||
let configured =
|
||||
crate::bridge::common::config_get_string(None, "GraphicsBackend").unwrap_or_default();
|
||||
crate::commonutil::config_get_string(None, "GraphicsBackend").unwrap_or_default();
|
||||
BackendKind::from_config_string(&configured)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,317 +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
|
||||
//! (M12 P0: the footage decode path).
|
||||
//!
|
||||
//! 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`] (`oakcore_rs::handle::CHandle`). There is no
|
||||
//! runtime ABI probe: the crate is a path dependency, so the functions are
|
||||
//! always present; failure semantics are oakcodec's own (empty handle /
|
||||
//! negative error code).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakcodec_video_stream_info` — POD probe output (one video stream).
|
||||
///
|
||||
/// Layout-identical to `oakcodec::decoder::OakCodecVideoStreamInfo` (both
|
||||
/// `#[repr(C)]`); kept as a local mirror so the bridge API is self-contained.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct VideoStreamInfo {
|
||||
/// Stream index.
|
||||
pub stream_index: c_int,
|
||||
/// Width in pixels.
|
||||
pub width: c_int,
|
||||
/// Height in pixels.
|
||||
pub height: c_int,
|
||||
/// Frame-rate numerator.
|
||||
pub frame_rate_num: c_int,
|
||||
/// Frame-rate denominator.
|
||||
pub frame_rate_den: c_int,
|
||||
/// Stream length in time-base units.
|
||||
pub duration_ts: i64,
|
||||
/// Time-base numerator (seconds per time-base unit).
|
||||
pub time_base_num: c_int,
|
||||
/// Time-base denominator.
|
||||
pub time_base_den: c_int,
|
||||
/// Native delivery `OakPixelFormat`.
|
||||
pub format: c_int,
|
||||
/// Plane channel count.
|
||||
pub channel_count: c_int,
|
||||
/// ISO/IEC 23001-8 color-primaries code point (0 = unknown).
|
||||
pub color_primaries: c_int,
|
||||
/// ISO/IEC 23001-8 color-transfer code point (0 = unknown).
|
||||
pub color_trc: c_int,
|
||||
/// 1 when the stream is interlaced.
|
||||
pub interlaced: c_int,
|
||||
}
|
||||
|
||||
/// Decoder handle (owned session).
|
||||
pub type DecoderHandle = CHandle;
|
||||
/// Probe handle (owned).
|
||||
pub type ProbeHandle = CHandle;
|
||||
/// Decoded frame handle (owned).
|
||||
pub type CodecFrameHandle = CHandle;
|
||||
|
||||
/// `oakcodec_decoder_probe(filename)` — owned probe handle; null on
|
||||
/// failure.
|
||||
pub fn decoder_probe(filename: &str) -> ProbeHandle {
|
||||
let c = std::ffi::CString::new(filename).unwrap_or_default();
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(c.as_ptr()) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_probe_video_stream_count(probe)`; 0 on an empty
|
||||
/// probe.
|
||||
pub fn probe_video_stream_count(probe: ProbeHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_video_stream_count(probe) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_probe_get_video_stream(probe, index, out)`.
|
||||
pub fn probe_get_video_stream(probe: ProbeHandle, index: c_int, out: &mut VideoStreamInfo) -> Result<()> {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI). `VideoStreamInfo`
|
||||
// is layout-identical to oakcodec's `OakCodecVideoStreamInfo`, so the
|
||||
// pointer crosses via a cast.
|
||||
let rc = unsafe {
|
||||
oakcodec::ffi::decoder::oakcodec_decoder_probe_get_video_stream(
|
||||
probe,
|
||||
index,
|
||||
out as *mut VideoStreamInfo as *mut oakcodec::decoder::OakCodecVideoStreamInfo,
|
||||
)
|
||||
};
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed(format!("probe_get_video_stream rc={rc}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_init()` — owned decoder session; null on failure.
|
||||
pub fn decoder_init() -> DecoderHandle {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_init() }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_free(decoder: *mut CHandle)` — NULL no-op.
|
||||
pub fn decoder_free(decoder: &mut DecoderHandle) {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_open(decoder, filename, stream_index)`.
|
||||
pub fn decoder_open(decoder: DecoderHandle, filename: &str, stream_index: c_int) -> Result<()> {
|
||||
let c = std::ffi::CString::new(filename).unwrap_or_default();
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let rc = unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, c.as_ptr(), stream_index) };
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed(format!("decoder_open rc={rc}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_close(decoder)`.
|
||||
pub fn decoder_close(decoder: DecoderHandle) -> Result<()> {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let rc = unsafe { oakcodec::ffi::decoder::oakcodec_decoder_close(decoder) };
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed(format!("decoder_close rc={rc}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_decode_video(decoder, num, den)` — owned frame
|
||||
/// handle; null when not decodable at `time`.
|
||||
pub fn decoder_decode_video(decoder: DecoderHandle, num: i64, den: i64) -> CodecFrameHandle {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_decode_video(decoder, num as c_int, den as c_int) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_decode_audio` — decode interleaved f32 covering
|
||||
/// `[in, out)` into `buf` (at least `buf_frames` frames, interleaved by
|
||||
/// `channel_layout`). Returns the number of frames written or a negative
|
||||
/// error.
|
||||
pub fn decoder_decode_audio(
|
||||
decoder: DecoderHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
buf: *mut f32,
|
||||
buf_frames: c_int,
|
||||
) -> Result<c_int> {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let rc = unsafe {
|
||||
oakcodec::ffi::decoder::oakcodec_decoder_decode_audio(
|
||||
decoder,
|
||||
in_num as c_int,
|
||||
in_den as c_int,
|
||||
out_num as c_int,
|
||||
out_den as c_int,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
buf,
|
||||
buf_frames,
|
||||
)
|
||||
};
|
||||
if rc < 0 {
|
||||
return Err(Error::Failed(format!("decode_audio rc={rc}")));
|
||||
}
|
||||
Ok(rc)
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_last_error(decoder, buf, size)` — error detail.
|
||||
pub fn decoder_last_error(decoder: DecoderHandle) -> String {
|
||||
let mut buf = [0 as c_char; 512];
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let n = unsafe {
|
||||
oakcodec::ffi::decoder::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as c_int)
|
||||
};
|
||||
if n <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let len = (n as usize).min(buf.len());
|
||||
let bytes: Vec<u8> = buf[..len]
|
||||
.iter()
|
||||
.take_while(|&&b| b != 0)
|
||||
.map(|&b| b as u8)
|
||||
.collect();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decoded frame accessors (`oakcodec_frame_*`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakcodec_frame_width(frame)`; 0 on an empty frame.
|
||||
pub fn frame_width(frame: CodecFrameHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_width(frame) }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_height(frame)`; 0 on an empty frame.
|
||||
pub fn frame_height(frame: CodecFrameHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_height(frame) }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_format(frame)`; `OAKCOMMON_PIXEL_FORMAT_INVALID` on an
|
||||
/// empty frame.
|
||||
pub fn frame_format(frame: CodecFrameHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_format(frame) }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_linesize_bytes(frame)`; 0 on an empty frame.
|
||||
pub fn frame_linesize_bytes(frame: CodecFrameHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_linesize_bytes(frame) }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_is_allocated(frame)`; 0 on an empty frame.
|
||||
pub fn frame_is_allocated(frame: CodecFrameHandle) -> c_int {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_is_allocated(frame) }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_const_data(frame)` — read-only pixel buffer pointer.
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned pointer is valid while `frame` is alive and allocated.
|
||||
pub unsafe fn frame_const_data(frame: CodecFrameHandle) -> *const u8 {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_const_data(frame) as *const u8 }
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_get_timestamp(frame, num, den)`.
|
||||
pub fn frame_timestamp(frame: CodecFrameHandle) -> Option<(i64, i64)> {
|
||||
let mut num: c_int = 0;
|
||||
let mut den: c_int = 0;
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let rc = unsafe { oakcodec::ffi::frame::oakcodec_frame_get_timestamp(frame, &mut num, &mut den) };
|
||||
if rc == 0 && den != 0 {
|
||||
Some((num as i64, den as i64))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_free(frame: *mut CHandle)` — NULL no-op.
|
||||
pub fn frame_free(frame: &mut CodecFrameHandle) {
|
||||
// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
unsafe { oakcodec::ffi::frame::oakcodec_frame_free(frame) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bad_inputs_fail_explainably() {
|
||||
// Wrapper contract with bad input: every call returns the
|
||||
// documented fallback rather than panicking (the real oakcodec
|
||||
// guards each export against empty handles / missing files).
|
||||
assert!(decoder_probe("/nonexistent_oak_test.mp4").is_null());
|
||||
|
||||
// decoder_init always succeeds (a closed session handle) and must
|
||||
// be released.
|
||||
let mut d = decoder_init();
|
||||
assert!(!d.is_null());
|
||||
assert!(decoder_open(d, "f", 0).is_err());
|
||||
// Closing a session that is not open is a successful no-op.
|
||||
assert!(decoder_close(d).is_ok());
|
||||
assert!(decoder_decode_video(d, 0, 1).is_null());
|
||||
decoder_free(&mut d);
|
||||
assert!(d.is_null());
|
||||
|
||||
// Frame accessors on an empty handle hit the documented fallbacks.
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(frame_width(empty), 0);
|
||||
assert_eq!(frame_height(empty), 0);
|
||||
assert_eq!(frame_linesize_bytes(empty), 0);
|
||||
assert_eq!(frame_format(empty), -1);
|
||||
assert!(frame_timestamp(empty).is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,148 +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 calls (config, file functions, strings) — now direct
|
||||
//! Rust calls into the oakcommon crate (single-lib unification, see
|
||||
//! `docs/zh/plans/riir/single-lib.md`). The configuration-location and
|
||||
//! disk-cache helpers delegate to oakcommon's own implementation.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// `oakcore_audioparams_sample_rate` (host-provided; M12 P1 — the audio
|
||||
/// ticket reads the output format from the params handle).
|
||||
pub fn audioparams_sample_rate(params: *const c_void) -> c_int {
|
||||
unsafe extern "C" {
|
||||
fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int;
|
||||
}
|
||||
unsafe { oakcore_audioparams_sample_rate(params) }
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_channel_layout` (host-provided).
|
||||
pub fn audioparams_channel_layout(params: *const c_void) -> u64 {
|
||||
unsafe extern "C" {
|
||||
fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64;
|
||||
}
|
||||
unsafe { oakcore_audioparams_channel_layout(params) }
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_channel_count` (host-provided).
|
||||
pub fn audioparams_channel_count(params: *const c_void) -> c_int {
|
||||
unsafe extern "C" {
|
||||
fn oakcore_audioparams_channel_count(params: *const c_void) -> c_int;
|
||||
}
|
||||
unsafe { oakcore_audioparams_channel_count(params) }
|
||||
}
|
||||
|
||||
/// Read a config string via the two-stage C ABI
|
||||
/// (`oakcommon_config_get(group, key, buf, n)`, C++ `Config::current()
|
||||
/// [key].toString()`).
|
||||
pub fn config_get_string(group: Option<&str>, key: &str) -> Option<String> {
|
||||
let group_c = group.and_then(|g| std::ffi::CString::new(g).ok());
|
||||
let key_c = std::ffi::CString::new(key).ok()?;
|
||||
let group_ptr = || group_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
|
||||
let size = unsafe {
|
||||
oakcommon::ffi::config::oakcommon_config_get(
|
||||
group_ptr(),
|
||||
key_c.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return None; // missing or empty
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
let got = unsafe {
|
||||
oakcommon::ffi::config::oakcommon_config_get(
|
||||
group_ptr(),
|
||||
key_c.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
)
|
||||
};
|
||||
if got <= 0 {
|
||||
return None;
|
||||
}
|
||||
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int(group, key, default)`.
|
||||
pub fn config_get_int(group: Option<&str>, key: &str, default: i32) -> i32 {
|
||||
let group_c = group.and_then(|g| std::ffi::CString::new(g).ok());
|
||||
let key_c = match std::ffi::CString::new(key) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return default,
|
||||
};
|
||||
let group_ptr = group_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group_ptr, key_c.as_ptr(), default) }
|
||||
}
|
||||
|
||||
/// The configuration directory — oakcommon's implementation
|
||||
/// (`FileFunctions::get_configuration_location`, honoring `OAK_CONFIG_DIR`
|
||||
/// and the platform fallbacks).
|
||||
pub fn configuration_location() -> String {
|
||||
oakcommon::filefunctions::FileFunctions::new()
|
||||
.get_configuration_location()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate `OAK_CONFIG_DIR` / `OAK_RENDER_BACKEND`
|
||||
/// (env is process-global; the manager tests share this lock too).
|
||||
pub static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// The default disk cache directory (C++ `DiskManager::
|
||||
/// get_default_disk_cache_path`): `<configuration_location>/mediacache`.
|
||||
pub fn default_disk_cache_path() -> String {
|
||||
oakcommon::filefunctions::default_disk_cache_path()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_missing_key_falls_back() {
|
||||
// The real config store is empty under cargo test: defaults apply.
|
||||
assert_eq!(config_get_int(None, "GraphicsBackend", 7), 7);
|
||||
assert_eq!(config_get_string(None, "missing"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_location_uses_env_override() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-config-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let loc = configuration_location();
|
||||
assert_eq!(loc, dir.to_string_lossy());
|
||||
std::env::remove_var("OAK_CONFIG_DIR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_disk_cache_path_is_under_config() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-cache-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let p = default_disk_cache_path();
|
||||
assert!(p.ends_with("/mediacache") || p.ends_with("\\mediacache"));
|
||||
std::env::remove_var("OAK_CONFIG_DIR");
|
||||
}
|
||||
}
|
||||
@@ -1,29 +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 bridges to sibling oak modules.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! submodule is a compile-time Rust call into the target crate's `ffi`
|
||||
//! (the `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! `oakcore_rs::handle::CHandle`. The only exception is the copier
|
||||
//! direction in [`node`], which targets symbols oaknode never implemented
|
||||
//! and is documented there.
|
||||
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod node;
|
||||
@@ -1,182 +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/>.
|
||||
|
||||
//! oaknode C ABI bridge: direct Rust calls into the `oaknode` crate
|
||||
//! (project copies, node queries).
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the
|
||||
//! live node query below is a compile-time Rust call into `oaknode`'s
|
||||
//! `ffi`; handles cross as the shared [`crate::handle::CHandle`]
|
||||
//! (`oakcore_rs::handle::CHandle`).
|
||||
//!
|
||||
//! The copier direction (`project_deep_copy` / `project_sync_copy`) has
|
||||
//! **no oaknode implementation**: the `oaknode_project_*` symbols were
|
||||
//! declared by the old render bridge but never existed in any crate
|
||||
//! (single-lib plan §4.1 — dead direction). They stay as documented
|
||||
//! always-fail stubs with the exact previous fallback semantics (empty
|
||||
//! handle / explainable error / `node_abi_available() == false`).
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// oaknode project handle.
|
||||
pub type ProjectHandle = CHandle;
|
||||
/// oaknode node handle.
|
||||
pub type NodeHandle = CHandle;
|
||||
|
||||
/// Mirror of oaknode's change record (marshalled as plain C structs;
|
||||
/// layout per include/node/project.h).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ChangeRecord {
|
||||
/// Discriminant (see oaknode `ChangeRecord`).
|
||||
pub kind: u32,
|
||||
/// Payload bytes (per-kind layout documented in project.h).
|
||||
pub payload: [u8; 48],
|
||||
}
|
||||
|
||||
/// Change-record discriminants (oaknode project.h).
|
||||
pub mod change_kind {
|
||||
/// Node added.
|
||||
pub const NODE_ADD: u32 = 0;
|
||||
/// Node removed.
|
||||
pub const NODE_REMOVE: u32 = 1;
|
||||
/// Edge added.
|
||||
pub const EDGE_ADD: u32 = 2;
|
||||
/// Edge removed.
|
||||
pub const EDGE_REMOVE: u32 = 3;
|
||||
/// Value change.
|
||||
pub const VALUE_CHANGE: u32 = 4;
|
||||
/// Value hint change.
|
||||
pub const VALUE_HINT_CHANGE: u32 = 5;
|
||||
/// Project setting change.
|
||||
pub const PROJECT_SETTING_CHANGE: u32 = 6;
|
||||
/// Footage proxy change.
|
||||
pub const FOOTAGE_PROXY: u32 = 7;
|
||||
}
|
||||
|
||||
/// `oaknode_project_deep_copy(project)` — would return an owned
|
||||
/// copied-project handle.
|
||||
///
|
||||
/// Never implemented: oaknode has no such C ABI export (single-lib plan
|
||||
/// §4.1 — dead direction), so this always yields the empty handle, exactly
|
||||
/// as the previous runtime-symbol lookup did when the symbol was absent.
|
||||
pub fn project_deep_copy(_project: ProjectHandle) -> CHandle {
|
||||
CHandle::null()
|
||||
}
|
||||
|
||||
/// `oaknode_project_sync_copy(source, copy, changes, count)` — would push
|
||||
/// a recorded change set into the copy.
|
||||
///
|
||||
/// Never implemented: oaknode has no such C ABI export (single-lib plan
|
||||
/// §4.1 — dead direction), so this always fails explainably, exactly as
|
||||
/// the previous runtime-symbol lookup did when the symbol was absent.
|
||||
pub fn project_sync_copy(
|
||||
_source: ProjectHandle,
|
||||
_copy: ProjectHandle,
|
||||
_changes: &[ChangeRecord],
|
||||
) -> Result<()> {
|
||||
Err(Error::Failed(
|
||||
"oaknode_project_sync_copy missing (not implemented in oaknode)".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// `oaknode_node_get_video_frame_cache(node, out)` — borrowed cache
|
||||
/// handle of a node. `OAKNODE_OK` (0) on success with `*out` set;
|
||||
/// otherwise a negative error.
|
||||
///
|
||||
/// # Safety
|
||||
/// `node` must be a valid handle; `out` a valid pointer.
|
||||
pub unsafe fn node_get_video_frame_cache(node: NodeHandle, out: *mut CHandle) -> Result<()> {
|
||||
// Direct call into the `oaknode` crate (single-lib unification; the
|
||||
// `#[no_mangle]` export stays for the external C ABI).
|
||||
let rc = unsafe {
|
||||
oaknode::ffi::node::oaknode_node_get_video_frame_cache(node, out as *mut std::ffi::c_void)
|
||||
};
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed(format!(
|
||||
"oaknode_node_get_video_frame_cache rc={rc}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the oaknode deep-copy C ABI is available.
|
||||
///
|
||||
/// Constant `false`: `oaknode_project_deep_copy` was never implemented
|
||||
/// (single-lib plan §4.1), so the copier success-path tests stay gated
|
||||
/// exactly as before.
|
||||
pub fn node_abi_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Node identity of a handle (the box pointer; matches the copier's
|
||||
/// identity tracking).
|
||||
pub fn node_identity(node: &NodeHandle) -> u64 {
|
||||
node.ctx as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deep_copy_unavailable_yields_empty_handle() {
|
||||
// oaknode never implemented the deep-copy ABI (dead direction).
|
||||
let h = project_deep_copy(CHandle::null());
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_copy_unavailable_fails_explainably() {
|
||||
let changes = [ChangeRecord {
|
||||
kind: change_kind::NODE_ADD,
|
||||
payload: [0u8; 48],
|
||||
}];
|
||||
let rc = project_sync_copy(CHandle::null(), CHandle::null(), &changes);
|
||||
assert!(rc.is_err(), "not implemented → explainable failure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_record_layout_is_c_stable() {
|
||||
// kind first, then 48 payload bytes (include/node/project.h).
|
||||
let c = ChangeRecord {
|
||||
kind: change_kind::VALUE_CHANGE,
|
||||
payload: [7u8; 48],
|
||||
};
|
||||
assert_eq!(std::mem::size_of::<ChangeRecord>(), 4 + 48);
|
||||
let bytes: [u8; 52] = unsafe { std::mem::transmute(c) };
|
||||
assert_eq!(
|
||||
u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
|
||||
change_kind::VALUE_CHANGE
|
||||
);
|
||||
assert_eq!(bytes[4], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_identity_is_ctx_value() {
|
||||
let h = CHandle {
|
||||
ctx: 0x1234 as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 1,
|
||||
};
|
||||
assert_eq!(node_identity(&h), 0x1234);
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ pub struct PlaybackCache {
|
||||
impl PlaybackCache {
|
||||
/// New cache for `owner` (C++ `PlaybackCache(parent)`).
|
||||
pub fn new(kind: CacheKind, owner: OwnerIdentity) -> Self {
|
||||
let disk_dir = crate::bridge::common::default_disk_cache_path();
|
||||
let disk_dir = crate::commonutil::default_disk_cache_path();
|
||||
Self {
|
||||
kind,
|
||||
owner,
|
||||
|
||||
@@ -391,7 +391,7 @@ pub fn config_path() -> Option<String> {
|
||||
}
|
||||
Some(format!(
|
||||
"{}/ocioconf/config.ocio",
|
||||
crate::bridge::common::configuration_location()
|
||||
crate::commonutil::configuration_location()
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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 helpers (config, file functions) — direct Rust calls into
|
||||
//! the oakcommon crate (single-lib unification; the former
|
||||
//! `bridge/common.rs`). The configuration-location and disk-cache
|
||||
//! helpers delegate to oakcommon's own implementation.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Read a config string via the domain store
|
||||
/// (`ConfigStore::get(group, key)`); `None` when missing or empty.
|
||||
pub fn config_get_string(group: Option<&str>, key: &str) -> Option<String> {
|
||||
oakcommon::configstore::ConfigStore::instance()
|
||||
.get(group, key)
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int(group, key, default)`.
|
||||
pub fn config_get_int(group: Option<&str>, key: &str, default: i32) -> i32 {
|
||||
oakcommon::configstore::ConfigStore::instance().get_int(group, key, default)
|
||||
}
|
||||
|
||||
/// The configuration directory — oakcommon's implementation
|
||||
/// (`FileFunctions::get_configuration_location`, honoring `OAK_CONFIG_DIR`
|
||||
/// and the platform fallbacks).
|
||||
pub fn configuration_location() -> String {
|
||||
oakcommon::filefunctions::FileFunctions::new()
|
||||
.get_configuration_location()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate `OAK_CONFIG_DIR` / `OAK_RENDER_BACKEND`
|
||||
/// (env is process-global; the manager tests share this lock too).
|
||||
pub static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// The default disk cache directory (C++ `DiskManager::
|
||||
/// get_default_disk_cache_path`): `<configuration_location>/mediacache`.
|
||||
pub fn default_disk_cache_path() -> String {
|
||||
oakcommon::filefunctions::default_disk_cache_path()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_missing_key_falls_back() {
|
||||
// The real config store is empty under cargo test: defaults apply.
|
||||
assert_eq!(config_get_int(None, "GraphicsBackend", 7), 7);
|
||||
assert_eq!(config_get_string(None, "missing"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_location_uses_env_override() {
|
||||
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-config-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let loc = configuration_location();
|
||||
assert_eq!(loc, dir.to_string_lossy());
|
||||
std::env::remove_var("OAK_CONFIG_DIR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_disk_cache_path_is_under_config() {
|
||||
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-cache-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let p = default_disk_cache_path();
|
||||
assert!(p.ends_with("/mediacache") || p.ends_with("\\mediacache"));
|
||||
std::env::remove_var("OAK_CONFIG_DIR");
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,68 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Render-side project copy client (the C++ ProjectCopier, inverted):
|
||||
//! all copying happens inside oaknode
|
||||
//! (`oaknode_project_deep_copy` / `sync_copy`); this module only tracks
|
||||
//! which copy belongs to which viewer and when to re-sync.
|
||||
//!
|
||||
//! The oaknode C ABI functions go through [`crate::bridge::node`]; oaknode
|
||||
//! never implemented the deep-copy symbols (single-lib plan §4.1 — dead
|
||||
//! direction), so the copy operations fail explainably and the
|
||||
//! success-path tests are `#[ignore]`d.
|
||||
//! all copying happens inside oaknode. oaknode never implemented the
|
||||
//! deep-copy direction (single-lib plan §4.1 — dead direction), so the
|
||||
//! copy operations fail explainably and the success-path tests are
|
||||
//! `#[ignore]`d.
|
||||
|
||||
use crate::bridge::node::{ChangeRecord, ProjectHandle};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// A project handle (oaknode-owned; the shared canonical handle type).
|
||||
pub type ProjectHandle = crate::handle::CHandle;
|
||||
|
||||
/// One change record (see oaknode `ChangeRecord`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ChangeRecord {
|
||||
/// Discriminant (see oaknode `ChangeRecord`).
|
||||
pub kind: u32,
|
||||
/// Payload bytes (per-kind layout documented in project.h).
|
||||
pub payload: [u8; 48],
|
||||
}
|
||||
|
||||
/// Change-record discriminants (oaknode project.h).
|
||||
pub mod change_kind {
|
||||
/// Node added.
|
||||
pub const NODE_ADD: u32 = 0;
|
||||
/// Node removed.
|
||||
pub const NODE_REMOVE: u32 = 1;
|
||||
/// Edge added.
|
||||
pub const EDGE_ADD: u32 = 2;
|
||||
/// Edge removed.
|
||||
pub const EDGE_REMOVE: u32 = 3;
|
||||
/// Value change.
|
||||
pub const VALUE_CHANGE: u32 = 4;
|
||||
/// Value hint change.
|
||||
pub const VALUE_HINT_CHANGE: u32 = 5;
|
||||
/// Project setting change.
|
||||
pub const PROJECT_SETTING_CHANGE: u32 = 6;
|
||||
/// Footage proxy change.
|
||||
pub const FOOTAGE_PROXY: u32 = 7;
|
||||
}
|
||||
|
||||
/// `oaknode_project_deep_copy(project)` — would return an owned
|
||||
/// copied-project handle.
|
||||
///
|
||||
/// Never implemented: oaknode has no such Rust function (single-lib plan
|
||||
/// §4.1 — dead direction), so this always yields the empty handle, exactly
|
||||
/// as the previous runtime-symbol lookup did when the symbol was absent.
|
||||
pub fn project_deep_copy(_project: ProjectHandle) -> ProjectHandle {
|
||||
ProjectHandle::null()
|
||||
}
|
||||
|
||||
/// `oaknode_project_sync_copy` — never implemented (dead direction); the
|
||||
/// sync always fails explainably.
|
||||
pub fn project_sync_copy(
|
||||
_source: ProjectHandle,
|
||||
_copy: ProjectHandle,
|
||||
_changes: &[ChangeRecord],
|
||||
) -> Result<()> {
|
||||
Err(Error::Failed(
|
||||
"oaknode_project_sync_copy missing (not implemented in oaknode)".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// A handle to a render-side project copy.
|
||||
pub struct ProjectCopy {
|
||||
/// Identity of the source project.
|
||||
@@ -62,7 +112,7 @@ impl ProjectCopy {
|
||||
}
|
||||
// Release any previous copy.
|
||||
self.release_copy();
|
||||
let copy = crate::bridge::node::project_deep_copy(source);
|
||||
let copy = crate::copier::project_deep_copy(source);
|
||||
if copy.is_null() {
|
||||
return Err(Error::Failed(
|
||||
"oaknode_project_deep_copy failed (symbol missing or copy error)".into(),
|
||||
@@ -89,7 +139,7 @@ impl ProjectCopy {
|
||||
if copy.is_null() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
crate::bridge::node::project_sync_copy(source, copy, changes)?;
|
||||
crate::copier::project_sync_copy(source, copy, changes)?;
|
||||
self.last_sync_generation += 1;
|
||||
self.has_pending_updates = false;
|
||||
Ok(())
|
||||
@@ -163,7 +213,7 @@ mod tests {
|
||||
fn sync_without_project_is_state_error() {
|
||||
let mut pc = ProjectCopy::new();
|
||||
let changes = [ChangeRecord {
|
||||
kind: crate::bridge::node::change_kind::NODE_ADD,
|
||||
kind: crate::copier::change_kind::NODE_ADD,
|
||||
payload: [0u8; 48],
|
||||
}];
|
||||
assert_eq!(pc.sync(&changes).unwrap_err().code(), Error::State.code());
|
||||
@@ -177,9 +227,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs oaknode C ABI (oaknode_project_deep_copy)"]
|
||||
#[ignore = "needs oaknode deep-copy (not implemented)"]
|
||||
fn deep_copy_roundtrip_with_real_node() {
|
||||
// Requires a live liboaknode; run with the app linked.
|
||||
// oaknode never implemented the deep-copy direction; the copy
|
||||
// always fails explainably, which is what the live path checks.
|
||||
let mut pc = ProjectCopy::new();
|
||||
let src = ProjectHandle {
|
||||
ctx: 1 as *mut std::ffi::c_void,
|
||||
@@ -187,10 +238,8 @@ mod tests {
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKRENDER_ABI_VERSION,
|
||||
};
|
||||
if crate::bridge::node::node_abi_available() {
|
||||
pc.set_project(src).unwrap();
|
||||
assert_ne!(pc.copy, 0);
|
||||
assert!(pc.copied_project().is_some());
|
||||
}
|
||||
pc.set_project(src).unwrap();
|
||||
assert_ne!(pc.copy, 0);
|
||||
assert!(pc.copied_project().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,14 @@
|
||||
//! depend on the oakcodec / oaknode / oakplugin C ABIs and fail with
|
||||
//! explainable errors (their success-path tests are `#[ignore]`d).
|
||||
|
||||
use std::ffi::c_int;
|
||||
use std::sync::Arc;
|
||||
|
||||
use oakcore_rs::{PixelFormat, Rational};
|
||||
use oakcodec::decoder::{
|
||||
CodecStream, Decoder as _, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
|
||||
K_COLOR_RANGE_DEFAULT,
|
||||
};
|
||||
use oakcodec::ffmpeg::FFmpegDecoder;
|
||||
use oakcore_rs::{PixelFormat, Rational, TimeRange};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::frame::VideoParamsPod;
|
||||
@@ -261,11 +266,13 @@ pub fn render_produced_frame(
|
||||
/// Process-wide open decoder sessions, keyed by (filename, stream).
|
||||
/// Sessions are mutex-serialized inside the oakcodec box, so sharing
|
||||
/// one handle across worker threads is safe.
|
||||
static DECODERS: std::sync::OnceLock<std::sync::Mutex<
|
||||
std::collections::HashMap<(String, i32), crate::handle::CHandle>,
|
||||
>> = std::sync::OnceLock::new();
|
||||
static DECODERS: std::sync::OnceLock<
|
||||
std::sync::Mutex<std::collections::HashMap<(String, i32), Arc<dyn oakcodec::decoder::Decoder>>>,
|
||||
> = std::sync::OnceLock::new();
|
||||
|
||||
fn decoders() -> std::sync::MutexGuard<'static, std::collections::HashMap<(String, i32), crate::handle::CHandle>> {
|
||||
fn decoders(
|
||||
) -> std::sync::MutexGuard<'static, std::collections::HashMap<(String, i32), Arc<dyn oakcodec::decoder::Decoder>>>
|
||||
{
|
||||
DECODERS
|
||||
.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
|
||||
.lock()
|
||||
@@ -273,23 +280,20 @@ fn decoders() -> std::sync::MutexGuard<'static, std::collections::HashMap<(Strin
|
||||
}
|
||||
|
||||
/// Open (or reuse) the decoder session for `(filename, stream_index)`.
|
||||
fn open_decoder(filename: &str, stream_index: i32) -> Result<crate::handle::CHandle> {
|
||||
fn open_decoder(filename: &str, stream_index: i32) -> Result<Arc<dyn oakcodec::decoder::Decoder>> {
|
||||
{
|
||||
let cache = decoders();
|
||||
if let Some(h) = cache.get(&(filename.to_string(), stream_index)) {
|
||||
if !h.is_null() {
|
||||
return Ok(*h);
|
||||
}
|
||||
if let Some(d) = cache.get(&(filename.to_string(), stream_index)) {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
}
|
||||
let decoder = crate::bridge::codec::decoder_init();
|
||||
if decoder.is_null() {
|
||||
return Err(Error::Failed("footage decode: decoder_init failed".into()));
|
||||
}
|
||||
crate::bridge::codec::decoder_open(decoder, filename, stream_index)
|
||||
let decoder: Arc<dyn oakcodec::decoder::Decoder> = Arc::new(FFmpegDecoder::new());
|
||||
let stream = CodecStream::with_block(filename.to_string(), stream_index, None);
|
||||
decoder
|
||||
.open(&stream)
|
||||
.map_err(|e| Error::Failed(format!("footage decode open: {e:?}")))?;
|
||||
let mut cache = decoders();
|
||||
cache.insert((filename.to_string(), stream_index), decoder);
|
||||
cache.insert((filename.to_string(), stream_index), decoder.clone());
|
||||
Ok(decoder)
|
||||
}
|
||||
|
||||
@@ -303,49 +307,44 @@ pub fn render_footage_frame(
|
||||
format: PixelFormat,
|
||||
) -> Result<Texture> {
|
||||
let decoder = open_decoder(filename, stream_index)?;
|
||||
let frame_handle = crate::bridge::codec::decoder_decode_video(
|
||||
decoder,
|
||||
time.numerator(),
|
||||
time.denominator(),
|
||||
);
|
||||
if frame_handle.is_null() {
|
||||
let detail = crate::bridge::codec::decoder_last_error(decoder);
|
||||
return Err(Error::Failed(format!(
|
||||
"footage decode at {time:?}: {detail}"
|
||||
)));
|
||||
}
|
||||
let params = RetrieveVideoParams {
|
||||
stream: CodecStream::with_block(filename.to_string(), stream_index, None),
|
||||
time,
|
||||
length: TimeRange::default(),
|
||||
force_range: K_COLOR_RANGE_DEFAULT,
|
||||
is_image_sequence: false,
|
||||
image_sequence_digits: 0,
|
||||
image_sequence_number: 0,
|
||||
mode: RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
};
|
||||
let decoded = decoder
|
||||
.retrieve_video_frame(¶ms)
|
||||
.map_err(|e| Error::Failed(format!("footage decode at {time:?}: {e:?}")))?;
|
||||
|
||||
let src_w = crate::bridge::codec::frame_width(frame_handle);
|
||||
let src_h = crate::bridge::codec::frame_height(frame_handle);
|
||||
let src_linesize = crate::bridge::codec::frame_linesize_bytes(frame_handle);
|
||||
let _alloc = crate::bridge::codec::frame_is_allocated(frame_handle);
|
||||
let src_w = decoded.width();
|
||||
let src_h = decoded.height();
|
||||
let src_linesize = decoded.linesize_bytes();
|
||||
let (w, h) = size;
|
||||
if src_w <= 0
|
||||
|| src_h <= 0
|
||||
|| src_linesize <= 0
|
||||
|| crate::bridge::codec::frame_is_allocated(frame_handle) == 0
|
||||
{
|
||||
frame_free(frame_handle);
|
||||
if src_w <= 0 || src_h <= 0 || src_linesize <= 0 || !decoded.is_allocated() {
|
||||
return Err(Error::Failed("footage decode: bad decoded frame".into()));
|
||||
}
|
||||
|
||||
let mut dst = generate_frame(time, (w, h), format)?;
|
||||
let dst_linesize = dst.linesize_bytes() as i32;
|
||||
let src_data = unsafe { crate::bridge::codec::frame_const_data(frame_handle) };
|
||||
if src_data.is_null() {
|
||||
frame_free(frame_handle);
|
||||
return Err(Error::Failed("footage decode: no frame data".into()));
|
||||
}
|
||||
let src_data = match decoded.data() {
|
||||
Some(d) => d,
|
||||
None => return Err(Error::Failed("footage decode: no frame data".into())),
|
||||
};
|
||||
|
||||
if src_w == w && src_h == h && src_linesize == dst_linesize {
|
||||
let bytes = (src_h as usize)
|
||||
.checked_mul(src_linesize as usize)
|
||||
.ok_or(Error::NoMem)?;
|
||||
let src_slice = unsafe { std::slice::from_raw_parts(src_data, bytes) };
|
||||
dst.data[..bytes].copy_from_slice(src_slice);
|
||||
dst.data[..bytes].copy_from_slice(&src_data[..bytes]);
|
||||
} else {
|
||||
scale_rgba_f32(
|
||||
src_data,
|
||||
src_data.as_ptr(),
|
||||
src_linesize,
|
||||
src_w,
|
||||
src_h,
|
||||
@@ -355,7 +354,6 @@ pub fn render_footage_frame(
|
||||
h,
|
||||
);
|
||||
}
|
||||
frame_free(frame_handle);
|
||||
Ok(Texture::wrap_frame(dst))
|
||||
}
|
||||
|
||||
@@ -402,19 +400,14 @@ pub fn render_audio_samples(
|
||||
let media_end = media_start + (out_time - in_time);
|
||||
let mut buf = vec![0.0f32; frames * channels as usize];
|
||||
let decoder = open_decoder(&clip.filename, clip.stream_index)?;
|
||||
let rc = crate::bridge::codec::decoder_decode_audio(
|
||||
decoder,
|
||||
media_start.numerator(),
|
||||
media_start.denominator(),
|
||||
media_end.numerator(),
|
||||
media_end.denominator(),
|
||||
rate,
|
||||
params.channel_layout,
|
||||
buf.as_mut_ptr(),
|
||||
frames as c_int,
|
||||
);
|
||||
let written = rc.unwrap_or(0).max(0) as usize;
|
||||
let written = written.min(frames);
|
||||
let range = TimeRange::new(media_start, media_end);
|
||||
let status = decoder
|
||||
.retrieve_audio(&mut buf, &range, rate, params.channel_layout)
|
||||
.map_err(|e| Error::Failed(format!("footage audio decode: {e:?}")))?;
|
||||
let written = match status {
|
||||
RetrieveAudioStatus::Success => frames,
|
||||
_ => 0,
|
||||
};
|
||||
// Mix into the accumulator (per-channel gain).
|
||||
for i in 0..written * channels as usize {
|
||||
acc[start_frame * channels as usize + i] += buf[i] * clip.gain;
|
||||
@@ -429,11 +422,6 @@ pub fn render_audio_samples(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Free a codec frame handle (Copy-handle dance).
|
||||
fn frame_free(mut h: crate::handle::CHandle) {
|
||||
crate::bridge::codec::frame_free(&mut h);
|
||||
}
|
||||
|
||||
/// Bilinear scale an F32-RGBA image (row-major with per-row strides).
|
||||
fn scale_rgba_f32(
|
||||
src: *const u8,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,14 +40,13 @@
|
||||
|
||||
pub mod autocacher;
|
||||
pub mod backend;
|
||||
pub mod bridge;
|
||||
pub mod cache;
|
||||
pub mod cancelatom;
|
||||
pub mod color;
|
||||
pub mod commonutil;
|
||||
pub mod copier;
|
||||
pub mod error;
|
||||
pub mod eval;
|
||||
pub mod ffi;
|
||||
pub mod frame;
|
||||
pub mod handle;
|
||||
pub mod manager;
|
||||
|
||||
@@ -243,7 +243,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn disk_cache_size_and_clear() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
let _guard = crate::commonutil::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-diskcache-test");
|
||||
@@ -260,7 +260,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn disk_cache_size_missing_dir_is_zero() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
let _guard = crate::commonutil::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-diskcache-missing");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t.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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms, 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(¶ms, 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() {
|
||||
|
||||
Reference in New Issue
Block a user