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:
+27
-32
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user