refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
// 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/>.
|
||||
|
||||
//! Background task submission hook (`include/codec/task.h`).
|
||||
//!
|
||||
//! The codec module needs occasional background work (audio conforms,
|
||||
//! proxy transcodes). The task system itself splits out at milestone M8;
|
||||
//! until then oakcodec exposes a single global submit callback. A host
|
||||
//! (M8: oaktask) registers with [`set_task_submit_cb`]; the conform/proxy
|
||||
//! managers call it whenever they need a task. With no callback, managers
|
||||
//! report work as unavailable — they never crash and never block.
|
||||
|
||||
use std::ffi::{c_void, CString};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::error::{Error, OAKCODEC_OK};
|
||||
|
||||
/// Kinds of background tasks oakcodec can request.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum TaskKind {
|
||||
/// Audio conform to pcm cache files.
|
||||
Conform = 0,
|
||||
/// Video proxy transcode.
|
||||
Proxy = 1,
|
||||
}
|
||||
|
||||
/// Description of one background task request.
|
||||
///
|
||||
/// All strings are borrowed and only valid for the duration of the submit
|
||||
/// call; the callback must copy anything it retains.
|
||||
#[repr(C)]
|
||||
pub struct TaskRequest<'a> {
|
||||
/// `TaskKind`.
|
||||
pub kind: TaskKind,
|
||||
/// Source media filename.
|
||||
pub input_filename: &'a str,
|
||||
/// Final destination path (see field docs in include/codec/task.h).
|
||||
pub output_filename: &'a str,
|
||||
/// Stream inside the source media.
|
||||
pub stream_index: i32,
|
||||
/// Conform: target sample rate.
|
||||
pub sample_rate: i32,
|
||||
/// Conform: target channel layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Conform: target sample format (enum as int).
|
||||
pub sample_format: i32,
|
||||
/// Proxy: target width (0 = unspecified/divider-based).
|
||||
pub proxy_width: i32,
|
||||
/// Proxy: target height (0 = unspecified/divider-based).
|
||||
pub proxy_height: i32,
|
||||
}
|
||||
|
||||
/// Task submit callback signature.
|
||||
///
|
||||
/// Returns `Ok(())` if the task was accepted (completed synchronously or
|
||||
/// queued), `Err` if the request was rejected.
|
||||
pub type TaskSubmitFn = dyn Fn(&TaskRequest, *mut std::ffi::c_void) -> crate::error::Result<()>;
|
||||
|
||||
/// `OakCodecTaskRequest` — C ABI mirror of [`TaskRequest`] for the submit
|
||||
/// callback; see `include/codec/task.h`. Strings are borrowed C pointers,
|
||||
/// valid only for the duration of the call.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecTaskRequest {
|
||||
/// `TaskKind` value.
|
||||
pub kind: i32,
|
||||
/// Source media filename.
|
||||
pub input_filename: *const std::ffi::c_char,
|
||||
/// Final destination path.
|
||||
pub output_filename: *const std::ffi::c_char,
|
||||
/// Stream inside the source media.
|
||||
pub stream_index: i32,
|
||||
/// Conform: target sample rate.
|
||||
pub sample_rate: i32,
|
||||
/// Conform: target channel-layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Conform: target sample format (enum as int).
|
||||
pub sample_format: i32,
|
||||
/// Proxy: target width (0 = unspecified/divider-based).
|
||||
pub proxy_width: i32,
|
||||
/// Proxy: target height (0 = unspecified/divider-based).
|
||||
pub proxy_height: i32,
|
||||
}
|
||||
|
||||
/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see
|
||||
/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a
|
||||
/// negative `OAKCODEC_E_*` code.
|
||||
pub type OakCodecTaskSubmitFn = unsafe extern "C" fn(
|
||||
req: *const OakCodecTaskRequest,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> i32;
|
||||
|
||||
/// One registered submit callback (extern-C from the host, or a crate
|
||||
/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.
|
||||
enum SubmitCb {
|
||||
/// No callback registered.
|
||||
None,
|
||||
/// Extern-C callback registered via `oakcodec_set_task_submit_cb`.
|
||||
Extern {
|
||||
/// The C function pointer.
|
||||
cb: OakCodecTaskSubmitFn,
|
||||
/// Opaque userdata passed back on each call.
|
||||
userdata: *mut c_void,
|
||||
},
|
||||
/// Crate-internal Rust closure registered via [`set_task_submit_cb`].
|
||||
Rust {
|
||||
/// Raw fat-pointer to the `&'static TaskSubmitFn` (kept `*const` so
|
||||
/// the registry is `Send`).
|
||||
cb: *const TaskSubmitFn,
|
||||
/// Opaque userdata passed back on each call.
|
||||
userdata: *mut c_void,
|
||||
},
|
||||
}
|
||||
|
||||
// # Safety: the stored pointers (extern-C fn pointer, fat pointer to a
|
||||
// 'static closure, userdata) are only dereferenced/called while holding the
|
||||
// registry mutex; the Rust closure is 'static and the extern-C fn outlives
|
||||
// registration by contract. Moving the enum between threads under the lock
|
||||
// therefore cannot alias.
|
||||
unsafe impl Send for SubmitCb {}
|
||||
|
||||
/// The global task submit callback registry. Only one callback is held at
|
||||
/// a time; registering replaces it, `None` clears it. Thread-safe.
|
||||
static TASK_SUBMIT: Mutex<SubmitCb> = Mutex::new(SubmitCb::None);
|
||||
|
||||
/// Registers (or replaces) the global task submit callback. Pass `None` to
|
||||
/// unregister. Thread-safe. Interim state (pre-M8): nobody registers and all
|
||||
/// task-dependent work reports unavailable.
|
||||
pub fn set_task_submit_cb(cb: Option<&'static TaskSubmitFn>, userdata: *mut std::ffi::c_void) {
|
||||
let mut g = TASK_SUBMIT.lock().unwrap();
|
||||
*g = match cb {
|
||||
Some(cb) => SubmitCb::Rust {
|
||||
cb: cb as *const TaskSubmitFn,
|
||||
userdata,
|
||||
},
|
||||
None => SubmitCb::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Register an extern-C submit callback (used by `ffi::task`).
|
||||
///
|
||||
/// Mirrors `oakcodec_set_task_submit_cb`: a `None` pointer clears it.
|
||||
pub(crate) fn set_task_submit_cb_extern(
|
||||
cb: Option<OakCodecTaskSubmitFn>,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) {
|
||||
let mut g = TASK_SUBMIT.lock().unwrap();
|
||||
*g = match cb {
|
||||
Some(cb) => SubmitCb::Extern { cb, userdata },
|
||||
None => SubmitCb::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns 1 if a submit callback is currently registered, else 0.
|
||||
/// Thread-safe.
|
||||
pub fn task_submit_is_registered() -> bool {
|
||||
let g = TASK_SUBMIT.lock().unwrap();
|
||||
!matches!(&*g, SubmitCb::None)
|
||||
}
|
||||
|
||||
/// Submit a task through the registered callback, if any.
|
||||
///
|
||||
/// Returns `Ok(false)` when no callback is registered (nothing submitted),
|
||||
/// `Ok(true)` when accepted, or `Err` when the callback rejected it.
|
||||
pub fn submit_task(
|
||||
req: &TaskRequest,
|
||||
) -> crate::error::Result<bool> {
|
||||
let g = TASK_SUBMIT.lock().unwrap();
|
||||
match &*g {
|
||||
SubmitCb::None => Ok(false),
|
||||
SubmitCb::Extern { cb, userdata } => {
|
||||
// Bind the C strings to locals so the temporaries outlive the
|
||||
// callback call (their pointers feed the request struct).
|
||||
let in_c = cstring_or_empty(req.input_filename);
|
||||
let out_c = cstring_or_empty(req.output_filename);
|
||||
let creq = OakCodecTaskRequest {
|
||||
kind: req.kind as i32,
|
||||
input_filename: in_c.as_ptr(),
|
||||
output_filename: out_c.as_ptr(),
|
||||
stream_index: req.stream_index,
|
||||
sample_rate: req.sample_rate,
|
||||
channel_layout: req.channel_layout,
|
||||
sample_format: req.sample_format,
|
||||
proxy_width: req.proxy_width,
|
||||
proxy_height: req.proxy_height,
|
||||
};
|
||||
// # Safety: the callback is a C function we registered; passing a
|
||||
// request whose string pointers are alive for the call duration.
|
||||
let ret = unsafe { cb(&creq, *userdata) };
|
||||
if ret == OAKCODEC_OK {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(Error::Failed(format!("task submit rejected (code {})", ret)))
|
||||
}
|
||||
}
|
||||
SubmitCb::Rust { cb, userdata } => {
|
||||
// # Safety: the fat pointer was stored by set_task_submit_cb and
|
||||
// points to a 'static closure that outlives this call.
|
||||
let cb = unsafe { &**cb };
|
||||
match cb(req, *userdata) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a NUL-terminated C string from a Rust string; empty on embedded
|
||||
/// NUL (callers pass sane filenames, so this is defensive only).
|
||||
fn cstring_or_empty(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// Same registry lock the conform/proxy/ffi tests use: the submit
|
||||
// callback is process-global and every test that mutates it must
|
||||
// serialize on the same mutex.
|
||||
use crate::conformmanager::test_util::REG_LOCK;
|
||||
|
||||
unsafe extern "C" fn reject_cb(
|
||||
_req: *const OakCodecTaskRequest,
|
||||
_ud: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
-1 // rejected
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_via_rust_closure_and_clear() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
|
||||
// A Rust closure that accepts and records the request.
|
||||
let accepted = std::sync::Arc::new(std::sync::Mutex::new(false));
|
||||
let recorded = std::sync::Arc::new(std::sync::Mutex::new(None::<String>));
|
||||
let acc = accepted.clone();
|
||||
let rec = recorded.clone();
|
||||
let cb: &'static TaskSubmitFn = Box::leak(Box::new(
|
||||
move |req: &TaskRequest, _ud: *mut std::ffi::c_void| {
|
||||
*acc.lock().unwrap() = true;
|
||||
*rec.lock().unwrap() = Some(req.output_filename.to_string());
|
||||
Ok(())
|
||||
},
|
||||
));
|
||||
set_task_submit_cb(Some(cb), std::ptr::null_mut());
|
||||
assert!(task_submit_is_registered());
|
||||
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Conform,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.pcm",
|
||||
stream_index: 1,
|
||||
sample_rate: 48000,
|
||||
channel_layout: 0x3,
|
||||
sample_format: 10,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
assert!(submit_task(&req).unwrap());
|
||||
assert!(*accepted.lock().unwrap());
|
||||
assert_eq!(recorded.lock().unwrap().as_deref(), Some("out.pcm"));
|
||||
|
||||
// Clearing the callback: nothing submitted.
|
||||
set_task_submit_cb(None, std::ptr::null_mut());
|
||||
assert!(!task_submit_is_registered());
|
||||
assert!(!submit_task(&req).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extern_cb_reject_maps_to_err() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(Some(reject_cb), std::ptr::null_mut());
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Proxy,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.mp4",
|
||||
stream_index: 0,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: 1280,
|
||||
proxy_height: 720,
|
||||
};
|
||||
assert!(submit_task(&req).is_err());
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extern_cb_accept_returns_ok() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Conform,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.pcm",
|
||||
stream_index: 0,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
assert!(submit_task(&req).unwrap());
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cstring_or_empty_handles_embedded_nul() {
|
||||
// Embedded NUL -> empty string (defensive).
|
||||
let c = cstring_or_empty("a\0b");
|
||||
assert_eq!(c.as_c_str().to_bytes(), b"");
|
||||
assert_eq!(cstring_or_empty("ok").as_c_str().to_bytes(), b"ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_kind_values_match_abi() {
|
||||
assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32);
|
||||
assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32);
|
||||
}
|
||||
|
||||
// ABI constants mirrored from include/codec/task.h.
|
||||
const OAKCODEC_TASK_CONFORM: i32 = 0;
|
||||
const OAKCODEC_TASK_PROXY: i32 = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user