Files
oak-editor/crates/oak-task
Mike-Solar 012659bf57 export: fix render-ticket classification; export any sequence
Two export killers fixed and the feature surfaced properly:

- TicketArena::allocate() reaps finished fire-and-forget slots on every
  new submit, but RenderTask classified finished tickets by asking the
  arena for their kind/time afterwards - any ticket that completed
  before the next submit (all of them on the inline backend) failed
  with 'Render ticket reported an unexpected timestamp' and the export
  died after writing a header-only file. RenderTask now records each
  ticket's delivery key at submit time instead.
- File > Export becomes 导出序列 (all 8 locales): the dialog offers a
  sequence picker (any sequence in the project, the current one
  preselected) instead of silently exporting the current one, and the
  project bin's sequence context menu gains 导出序列 opening the same
  dialog. Engines expose sequence_entries / current_sequence_id /
  start_export_of.
- The export-project dialog drops its dead hand-typed path field (OK
  already asked the platform save dialog).
- New end-to-end test: a one-second sequence exports to a real
  H.264/AAC MP4 that probes and decodes back to the clip's content.
2026-09-10 16:54:15 +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         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:

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.