Files
oak-editor/crates/oaktask/README.md
T
Mike-Solar b36cbd6b6f refactor: purge CHandle from module internals (M14 R5)
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.
2026-08-17 16:40:15 +08:00

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.