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
+27 -32
View File
@@ -20,16 +20,10 @@
//! ONLY its `oakengine_*` C ABI — it never depends on the `oakengine` crate
//! as an rlib. This module declares every exported function the real engine
//! binding uses, with the exact signatures from the facade's `#[no_mangle]`
//! exports (`crates/oakengine/src/*.rs`), plus the two `oaktask_*` module
//! exports the dylib carries alongside the facade (interchange
//! load/save getter and the task event subscription, see the comments
//! below).
//!
//! The facade also exports the module C ABIs (`oakundo_*`, `oakcommon_*`,
//! ...) inside the same dylib; the module functions the app needs beyond
//! the facade's wrapping (`oaktask_load_take_project`,
//! `oaktask_task_subscribe`) are declared here too and resolve from the
//! dylib.
//! exports (`crates/oakengine/src/*.rs`). The facade wraps the module
//! C ABIs that are also embedded in the same dylib (`oakundo_*`,
//! `oakcommon_*`, ...), so everything the app touches resolves through an
//! `oakengine_*` symbol.
//!
//! # Handle layout mirrors
//!
@@ -125,8 +119,8 @@ pub trait HandleBox: Sized {
/// `oakengine_*_free` export.
///
/// # Safety
/// The handle must be a live module handle (e.g. from
/// `oaktask_load_take_project`).
/// The handle must be a live module handle (e.g. from a facade export that
/// hands one over for the app to box).
pub unsafe fn box_handle<T: HandleBox>(handle: CHandle) -> *mut T {
// SAFETY: the caller passes a live handle; the box is managed by the
// C ABI consumers from here on.
@@ -682,9 +676,9 @@ unsafe extern "C" {
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakaudio_waveform_extract` — real waveform extraction (M12 P4):
/// `oakengine_waveform_extract` — real waveform extraction (M12 P4):
/// two-stage min/max extraction of `filename`'s audio stream.
pub fn oakaudio_waveform_extract(
pub fn oakengine_waveform_extract(
filename: *const c_char,
stream_index: c_int,
samples_per_point: c_int,
@@ -856,9 +850,9 @@ unsafe extern "C" {
) -> *mut c_void;
/// `oakcore_audioparams_free` (NULL no-op).
pub fn oakcore_audioparams_free(params: *mut c_void);
/// `oakrender_manager_shutdown` — tear down the render manager
/// `oakengine_render_manager_shutdown` — tear down the render manager
/// (test/tooling; the app keeps it for the process lifetime).
pub fn oakrender_manager_shutdown() -> c_int;
pub fn oakengine_render_manager_shutdown() -> c_int;
/// `oakengine_testmedia_write_clip` — encode the known test pattern
/// into `path` (test/tooling only; M12 P0).
pub fn oakengine_testmedia_write_clip(
@@ -888,27 +882,28 @@ unsafe extern "C" {
/// the channel count (0 = nothing buffered), negative on error.
pub fn oakengine_audio_output_levels(peaks: *mut f32, capacity: c_int) -> c_int;
// -- oakrender module C ABI (carried by the dylib) --
// -- oakengine::render (manager lifecycle) --
/// `oakrender_manager_init` — bring up the module's process-global
/// render manager. Without it `render_frame` fails with NULL +
/// last_error. The facade does not wrap this; like the `oaktask_*`
/// entries below, the symbol is exported by the dylib itself. Fails
/// (nonzero) when the manager is already initialized.
pub fn oakrender_manager_init() -> c_int;
/// `oakrender_manager_available` — 1 when the render manager is up.
pub fn oakrender_manager_available() -> c_int;
/// `oakengine_render_manager_init` — bring up the module's
/// process-global render manager. Without it `render_frame` fails with
/// NULL + last_error. Fails (nonzero) when the manager is already
/// initialized.
pub fn oakengine_render_manager_init() -> c_int;
/// `oakengine_render_manager_available` — 1 when the render manager is up.
pub fn oakengine_render_manager_available() -> c_int;
// -- oaktask module C ABI (carried by the dylib) --
// -- oakengine::task (interchange load result + event subscription) --
/// `oaktask_load_take_project` — take the project an interchange
/// load/load-otio task produced (ownership moves to the caller).
pub fn oaktask_load_take_project(t: CHandle) -> CHandle;
/// `oaktask_task_subscribe` — register the task event callback
/// `oakengine_task_load_take_project` — take the project an
/// interchange load/load-otio task produced (ownership moves to the
/// caller; release with `oakengine_project_free`). NULL when the task
/// is not a load task or has no project yet.
pub fn oakengine_task_load_take_project(task: *mut OakEngineTask) -> *mut OakEngineProject;
/// `oakengine_task_subscribe` — register the task event callback
/// (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1,
/// `OAKTASK_EVENT_FINISHED`=2).
pub fn oaktask_task_subscribe(
t: CHandle,
pub fn oakengine_task_subscribe(
task: *mut OakEngineTask,
cb: Option<OakTaskEventFn>,
userdata: *mut c_void,
) -> i64;
+18 -32
View File
@@ -130,22 +130,15 @@ const UNTITLED: &str = "Untitled Project";
const PIXEL_FORMAT_F32: c_int = 4;
// ---------------------------------------------------------------------------
// oaktask module C ABI entries the facade does not wrap
// Facade task event subscription
// ---------------------------------------------------------------------------
//
// Two module-level exports are needed here that the facade does not wrap:
// `oaktask_load_take_project` (the loaded-project getter for the
// interchange load task) and `oaktask_task_subscribe` (the task event
// callback that delivers export progress). Both take the module `CHandle`,
// which is the same value handle the facade's own boxes wrap (exposed here
// through [`ffi::unbox`]), and both symbols are exported by the dylib
// itself (it carries the module C ABIs). The declarations live in [`ffi`];
// this keeps the *operations* on the facade contract while bridging two
// getter/event gaps the facade intentionally leaves open (see
// `oakengine/src/deferred.rs`).
// The export path subscribes to task events through the facade's
// `oakengine_task_subscribe` (wrapping the module's `oaktask_task_subscribe`);
// the callback fires on the task's own thread.
/// The C callback the module task event subscription invokes on the task's
/// own thread. `userdata` is the raw pointer of a leaked
/// The C callback the facade task subscription invokes on the task's own
/// thread. `userdata` is the raw pointer of a leaked
/// `mpsc::Sender<ExportEvent>` the export thread reclaims after the run.
unsafe extern "C" fn export_event_cb(event_id: c_int, value: f64, userdata: *mut c_void) {
let Some(sender) = (userdata as *const mpsc::Sender<ExportEvent>).as_ref() else {
@@ -708,11 +701,11 @@ impl RealEngine {
/// Returns false when the manager could not be started.
fn ensure_render_manager() -> bool {
unsafe {
if oakrender_manager_available() != 0 {
if oakengine_render_manager_available() != 0 {
return true;
}
oakrender_manager_init();
oakrender_manager_available() != 0
oakengine_render_manager_init();
oakengine_render_manager_available() != 0
}
}
@@ -2331,17 +2324,12 @@ impl AppEngine for RealEngine {
return Err("failed to create the export task".into());
}
// Progress events through the module task callback.
let module_handle =
unsafe { unbox(task) }.ok_or_else(|| "invalid export task handle".to_string())?;
// Progress events through the facade task subscription (the callback
// is invoked on the task's own thread with the raw userdata pointer).
let (tx, rx) = mpsc::channel::<ExportEvent>();
let cb_userdata = SendPtr(Box::into_raw(Box::new(tx.clone())));
unsafe {
oaktask_task_subscribe(
module_handle,
Some(export_event_cb),
cb_userdata.0 as *mut c_void,
);
oakengine_task_subscribe(task, Some(export_event_cb), cb_userdata.0 as *mut c_void);
}
// The task pointer is shared between the cancel handle and the worker
@@ -2456,9 +2444,8 @@ impl RealEngine {
}
/// Opens an `.otio` / `.fcpxml` project through the oaktask interchange
/// loader and adopts the loaded project (the facade exposes no
/// load-result getter, so the module's `oaktask_load_take_project` is
/// called directly — see the module docs).
/// loader and adopts the loaded project (`oakengine_task_load_take_project`
/// hands over the loader's project after a successful run).
fn open_interchange(&mut self, path: &PathBuf, cx: &mut Context<Self>) -> Result<(), String> {
let Some(cpath) = cstr_path(path) else {
return Err("invalid project path".into());
@@ -2469,15 +2456,14 @@ impl RealEngine {
}
let rc = unsafe { oakengine_task_start_sync(task) };
let error = Self::task_error(task);
let module_handle = unsafe { unbox(task) };
let loaded = module_handle.and_then(|h| {
let project = unsafe { oaktask_load_take_project(h) };
let loaded = {
let project = unsafe { oakengine_task_load_take_project(task) };
if project.is_null() {
None
} else {
Some(unsafe { box_handle::<OakEngineProject>(project) })
Some(project)
}
});
};
unsafe { oakengine_task_free(task) };
if rc == 0 {
return Err(format!("failed to load \"{}\": {error}", path.display()));
+15 -7
View File
@@ -17,7 +17,7 @@
//! M12 P4: timeline audio waveforms.
//!
//! The [`WaveformCache`] holds per-clip min/max peak data extracted
//! through `oakaudio_waveform_extract` (the real FFmpeg-backed
//! through `oakengine_waveform_extract` (the real FFmpeg-backed
//! extraction); the engine refreshes it when the timeline rebuilds. The
//! [`OakClipDecorator`] draws the peaks into the timeline's clip body.
//!
@@ -100,13 +100,14 @@ impl WaveformCache {
}
}
/// Extract a clip's waveform (first channel) via the oakaudio C ABI.
/// Extract a clip's waveform (first channel) via the facade's waveform
/// C ABI.
fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option<ClipWaveform> {
let cname = std::ffi::CString::new(filename).ok()?;
const SAMPLES_PER_POINT: c_int = 256;
unsafe {
let mut channels: c_int = 0;
let needed = crate::oakui::ffi::oakaudio_waveform_extract(
let needed = crate::oakui::ffi::oakengine_waveform_extract(
cname.as_ptr(),
0, // audio-stream index (probe numbering)
SAMPLES_PER_POINT,
@@ -117,20 +118,27 @@ impl WaveformCache {
if needed <= 0 || channels <= 0 {
return None;
}
let mut peaks = vec![MinMax::default(); needed as usize];
let rc = crate::oakui::ffi::oakaudio_waveform_extract(
// The extractor's `out_pairs` receives `point_count *
// channel_count` channel-interleaved pairs (capacity is in points),
// so the buffer is sized for the media's channel count.
let channel_count = channels.max(1) as usize;
let mut raw = vec![MinMax::default(); needed as usize * channel_count];
let rc = crate::oakui::ffi::oakengine_waveform_extract(
cname.as_ptr(),
0,
SAMPLES_PER_POINT,
peaks.as_mut_ptr(),
raw.as_mut_ptr(),
needed,
&mut channels,
);
if rc < 0 {
return None;
}
// Keep only the first channel: the pairs are interleaved per point
// (point i channel c at index i * channels + c).
let count = rc.min(needed) as usize;
peaks.truncate(count);
let mut peaks = Vec::with_capacity(count);
peaks.extend((0..count).map(|i| raw[i * channel_count]));
Some(ClipWaveform {
peaks,
channel_count: channels.max(1),