Module-internal object references are Rust types now (values, Arc, Mutex); CHandle remains only at the oakengine C-ABI boundary: - oakundo: the global stack holds UndoStack/UndoCommand values directly (stack token is the static's address) - oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands share the same allocation through Arc clones (readers in oakengine stubs and the app's graphops updated to lock) - oaktask/oakstorage: sessions, write-through bindings and the database backend pass ProjectArc; the Session drops its manual release bookkeeping; nodeutil keeps the CHandle<->Arc boundary conversion (release_project restored for the app) - oakcodec: handle.rs deleted outright (no facade entry needed it); texture/block placeholders are unit structs - oakrender: copier's project handle is an identity u64; alive-count machinery removed; handle.rs is make_owned/get/get_mut only - oakplugin: the instance registry is gone (its unregister key never matched, leaking weak entries); handle.rs is the RefBox boundary type - oaknode/oakcommon: only dead guard/borrow helpers removed; external payload handles (texture/processor) documented as the boundary Flake hunts landed along the way: the audio recording test serializes on the shared manager lock with a normalized state; the autocacher cancel test uses a slow producer so cancellation is deterministic.
183 lines
9.4 KiB
Markdown
183 lines
9.4 KiB
Markdown
# 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 owned-handle box + get/get_mut views (facade task boxes)
|
|
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:
|
|
|
|
```text
|
|
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.
|