Files
oak-editor/crates/oaktask
Mike-Solar 013a175707 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)
2026-08-10 20:24:25 +08:00
..

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

  1. C++ inheritance → one module per class + trait objects. olive::Task is an abstract base with a virtual run(). In Rust each concrete task is a struct and the shared behavior lives on a common Task base that exposes a run: Box<dyn FnMut() -> Result<()>>-style field (or an equivalent TaskBehavior trait object). RenderTask and its subclasses (ExportTask, PreCacheTask) use the same trait-object approach: the abstract base's download_frame / frame_downloaded / audio_downloaded / encode_subtitle virtuals 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.

  2. Cancellation goes through the oakrender C ABI. The C++ Task holds an olive::CancelAtom (from render/cancelatom.h). Until oakrender is rewritten, the Rust crate reaches it through bridge::render (oakrender_cancelatom_*), with a borrowed OakCancelAtom stored on the base task. No oakrender type is reimplemented here.

  3. Event listeners are the one async return channel. The C++ Task exposes 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 a Mutex, matching the oaktask_task_subscribe C ABI contract (event ids STARTED/PROGRESS/ FINISHED, progress in 0..1). This is the single deliberate async exception to the otherwise fully synchronous model.

  4. Render-driven tasks reuse oakrender tickets. RenderTask::render() drives oakrender tickets (bridge::render, oakrender_ticket_render_frame / oakrender_ticket_render_audio). The ForceParams struct maps to the oakrender_video_ticket_params force fields. The ticket's oakrender_ticket_finished_fn callback (fired on the render thread) is the only other async return channel.

  5. Project tasks borrow oaknode handles, take ownership only on take*. ProjectSaveTask borrows its OakNodeProject; ProjectImportTask borrows the folder/project and produces an OakUndoCommand (oaktask_import_take_command) and a list of imported OakNodeFootage. ProjectLoadTask / LoadOTIOTask transfer ownership on take_project(). This mirrors the C ABI's borrow/take split exactly.

  6. OTIO load/save parses with the pure-Rust oakotio binding. SaveOTIOTask / LoadOTIOTask parse and serialize OpenTimelineIO JSON (.otio) and FCPXML (.fcpxml) through the oakotio crate (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 the oakotio parse/serialize call — the track/clip/footage building (load) and the serialize_* 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 write oakotio::Timeline / SerializableCollection documents (save), mirroring the C++ serialize_* helpers. ExportTask/PreCacheTask use oakcore_rs::{Rational, TimeRange} for their frame/audio timing (the reason this crate depends on oakcore-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* / explicit catch_unwind; panics never cross FFI.
  • Handles are opaque refcounted boxes; ctx never points at Rust data directly except through RefBox.
  • All C++ coupling comments carry a // CPP-PARITY: <file> marker so a C++ change that breaks parity is greppable.
  • Free functions are no-op on null handles; free(NULL) is always safe.
  • Error codes mirror include/task/error.h verbatim (module 08, plus OAKTASK_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/TimeRange for frame/audio timing).
  • oakotio (path dep, crates/oakotio — pure-Rust serde model of the OpenTimelineIO JSON format used by LoadOTIOTask/SaveOTIOTask; brings in serde/serde_json transitively).
  • oakrender (path dep, optional, enabled only by the real-oakrender feature): links the real oakrender crate into the render_real_integration_test binary. The library never references it — all render access stays on the bridge::render C ABI.