refactor: drop internal bridge/ffi layers; exporter family lands

Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

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

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
This commit is contained in:
2026-08-16 00:33:45 +08:00
parent 2248be8567
commit ab1a2e9c7b
293 changed files with 27826 additions and 90128 deletions
+1 -1
View File
@@ -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)
}
-317
View File
@@ -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());
}
}
-148
View File
@@ -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");
}
}
-29
View File
@@ -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;
-182
View File
@@ -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);
}
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -391,7 +391,7 @@ pub fn config_path() -> Option<String> {
}
Some(format!(
"{}/ocioconf/config.ocio",
crate::bridge::common::configuration_location()
crate::commonutil::configuration_location()
))
}
+87
View File
@@ -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");
}
}
+68 -19
View File
@@ -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());
}
}
+53 -65
View File
@@ -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(&params)
.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
+1 -2
View File
@@ -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;
+2 -2
View File
@@ -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");