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)
oaktask — Oak task execution module (Rust)
Reimplements the C++ task module behind its frozen C ABI
(include/task/*.h). Every public type, trait, function and C ABI export is
implemented; the C++ code in src/task/src remains the parity source of
truth (// CPP-PARITY: markers point at the exact C++ file).
Scope
This crate replaces src/task/src (the C++ olive::Task family: the Task
base class, the TaskManager singleton, the codec submitter bridge, and the
concrete task classes ConformTask, CustomCacheTask, ExportTask,
PreCacheTask, ProxyTask, RenderTask, and the project
load/save/import/OTIO tasks). The behavior it reproduces is defined by:
- the frozen C ABI headers
include/task/*.h(authoritative), - the C++ implementation in
src/task/src(parity source of truth), - the C++ unit tests in
src/task/tests/task_test.cpp(golden source).
Architectural decisions
-
C++ inheritance → one module per class + trait objects.
olive::Taskis an abstract base with a virtualrun(). In Rust each concrete task is astructand the shared behavior lives on a commonTaskbase that exposes arun: Box<dyn FnMut() -> Result<()>>-style field (or an equivalentTaskBehaviortrait object).RenderTaskand its subclasses (ExportTask,PreCacheTask) use the same trait-object approach: the abstract base'sdownload_frame/frame_downloaded/audio_downloaded/encode_subtitlevirtuals become a single trait the subclasses implement. Rationale: Rust has no virtual inheritance, and a trait object is the minimal faithful mapping; it keeps each concrete task independent so the crate stays modular. -
Cancellation goes through the oakrender C ABI. The C++
Taskholds anolive::CancelAtom(fromrender/cancelatom.h). Until oakrender is rewritten, the Rust crate reaches it throughbridge::render(oakrender_cancelatom_*), with a borrowedOakCancelAtomstored on the base task. No oakrender type is reimplemented here. -
Event listeners are the one async return channel. The C++
Taskexposes an event listener (k_event_started/k_event_progress/k_event_finished) delivered on the task's own thread. Qt signals are gone; the Rust equivalent is a callback (std::function-shaped closure) invoked under aMutex, matching theoaktask_task_subscribeC ABI contract (event idsSTARTED/PROGRESS/FINISHED, progress in 0..1). This is the single deliberate async exception to the otherwise fully synchronous model. -
Render-driven tasks reuse oakrender tickets.
RenderTask::render()drives oakrender tickets (bridge::render,oakrender_ticket_render_frame/oakrender_ticket_render_audio). TheForceParamsstruct maps to theoakrender_video_ticket_paramsforce fields. The ticket'soakrender_ticket_finished_fncallback (fired on the render thread) is the only other async return channel. -
Project tasks borrow oaknode handles, take ownership only on take*.
ProjectSaveTaskborrows itsOakNodeProject;ProjectImportTaskborrows the folder/project and produces anOakUndoCommand(oaktask_import_take_command) and a list of importedOakNodeFootage.ProjectLoadTask/LoadOTIOTasktransfer ownership ontake_project(). This mirrors the C ABI's borrow/take split exactly. -
OTIO load/save parses with the pure-Rust
oakotiobinding.SaveOTIOTask/LoadOTIOTaskparse and serialize OpenTimelineIO JSON (.otio) and FCPXML (.fcpxml) through theoakotiocrate (crates/oakotio, a self-contained serde model of exactly the object graph the C++ loadotio/saveotio tasks use) instead of the C++ OTIO library. The format is inferred from the filename extension (case-insensitive;src/project/format.rs), so the frozen C ABI (include/task/project.h) needs no format parameter. Format handling ends at theoakotioparse/serialize call — the track/clip/footage building (load) and theserialize_*helpers (save) are shared between both formats, mirroring the C++ tasks. No OTIO or FCPXML type crosses the oaktask C ABI — the tasks build the project through the oaknode/oaktimeline C ABIs (load) and writeoakotio::Timeline/SerializableCollectiondocuments (save), mirroring the C++serialize_*helpers.ExportTask/PreCacheTaskuseoakcore_rs::{Rational, TimeRange}for their frame/audio timing (the reason this crate depends onoakcore-rs).
Layout
src/
lib.rs crate doc + module declarations
error.rs OAKTASK_* codes + Error enum (module number 08)
handle.rs RefBox / CHandle / guard* FFI scaffolding
task.rs Task base class + TaskEvent / EventListener
manager.rs TaskManager singleton
codecbridge.rs codec task submitter registration
conform.rs ConformTask + derive_filenames
proxy.rs ProxyTask + build_arguments / parse_progress
customcache.rs CustomCacheTask
render.rs RenderTask base + ForceParams
export.rs ExportTask
precache.rs PreCacheTask
project.rs project module (declares submodules)
project/load.rs ProjectLoadBaseTask + ProjectLoadTask
project/save.rs ProjectSaveTask
project/import.rs ProjectImportTask
project/format.rs interchange-format dispatch (.otio / .fcpxml)
project/loadotio.rs LoadOTIOTask
project/saveotio.rs SaveOTIOTask
bridge/ extern "C" imports of other modules' C ABIs
mod.rs, codec.rs, common.rs, node.rs, render.rs, timeline.rs, undo.rs
ffi.rs C ABI export layer (one pub mod per include/task header)
tests/ contract + parity tests
Hard rules
- Every C ABI export checks its handle before dereferencing and maps panics
through
handle::guard*/ explicitcatch_unwind; panics never cross FFI. - Handles are opaque refcounted boxes;
ctxnever points at Rust data directly except throughRefBox. - All C++ coupling comments carry a
// CPP-PARITY: <file>marker so a C++ change that breaks parity is greppable. - Free functions are
no-opon null handles;free(NULL)is always safe. - Error codes mirror
include/task/error.hverbatim (module 08, plusOAKTASK_E_CANCELLED = -80006).
Test strategy
cargo test runs the full suite against link-time stubs of the
other-module C ABIs (tests/common/mod.rs, pulled into each integration
test binary via #[path]). The oaktask library declares its cross-module
imports as real extern "C" symbols (production builds link them against
the oak dylibs); the test binaries provide #[no_mangle] stub definitions
so plain cargo test links and runs without any oak dylib. The stubs are
faithful-in-spirit: serializer round-trips write/validate a marker file,
footage validity checks real file existence, the codec submitter routes
conform/proxy requests into the task module, and a fake ffmpeg executable
drives the proxy run end to end. The same scenarios against the real dylibs
are covered by the C++ gtest suite (src/task/tests/task_test.cpp).
Since oakrender's ticket C ABI is live, the render loop is also verified
against the real oakrender arena (no stubs): tests/ render_real_integration_test.rs links the oakrender crate into the test
binary and drives RenderTask::render through bridge::render's
extern "C" declarations, getting real frames back in timestamp order via
the CPU path (no GPU). Both rlibs expose #[no_mangle] exports that cannot
coexist with the stub-based test binaries in one link, so the test is gated
behind the real-oakrender feature and built alone:
cargo test --features real-oakrender --test render_real_integration_test
The render stubs in tests/common/mod.rs are compiled out under the same
feature (#[cfg(not(feature = "real-oakrender"))]), so plain cargo test
is unaffected.
OTIO load/save is implemented end to end: oakotio (path dep,
crates/oakotio) parses the document inside LoadOTIOTask::run and
serializes the project in SaveOTIOTask::run; the tasks keep driving the
oaknode/oaktimeline C ABIs exactly like the C++ implementations. tests/ otio_test.rs builds synthetic documents with the oakotio model, imports
them, and re-parses exports for a round-trip check (README decision #6).
The tasks are format-aware (.otio → OpenTimelineIO JSON, .fcpxml →
FCPXML, dispatched from the extension in src/project/format.rs): the
FCPXML tests build documents with oakotio's fcpxml writer, cover the
extension dispatch matrix (.otio/.fcpxml/.OTIO/.FCPXML/unknown)
and the FCPXML error paths (corrupt XML, unsupported version), and run a
save → load cycle through both tasks.
Dependency policy
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible) over hand-rolling; register each addition (name + reason) here. Large existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten — they are consumed through their C ABI / bridge layers.
Current dependency inventory:
oakcore-rs(path dep,Rational/TimeRangefor frame/audio timing).oakotio(path dep,crates/oakotio— pure-Rust serde model of the OpenTimelineIO JSON format used byLoadOTIOTask/SaveOTIOTask; brings inserde/serde_jsontransitively).oakrender(path dep, optional, enabled only by thereal-oakrenderfeature): links the real oakrender crate into therender_real_integration_testbinary. The library never references it — all render access stays on thebridge::renderC ABI.