preview_window_capacity used the *configured* worker count, so a window opened while workers were still handshaking (or after a crash) could claim every slot of the smaller live pool — the synchronous render ticket then never gets a free slot, and since the slot-releasing cleanup runs on the same UI thread that is blocked in TicketArena::wait, playback deadlocks permanently. Intermittent on Linux CI (the playback_display_tracks_the_playhead hang, caught by the new test watchdog): depends on how many workers had handshaken when playback started. Count only Alive workers (fall back to the configured count while none are alive, keeping the existing unit test semantics).
oakrender Rust crate
Status: implemented (M7 render wave). Every
todo!()from the declaration draft is implemented; the crate builds, tests green (cargo test), and the C ABI surface ininclude/render/*.his exported fromsrc/ffi.rs. Deferred items are documented inline and in the Deferred section below.
Scope
Replaces the C++ oakrender module (src/render/src): render manager,
ticket system + worker pool, textures and GPU backend dispatch, playback/
frame-hash caches, color processing (OCIO), the preview auto-cacher, and
the blit/display path.
Public contract: include/render/*.h (8 headers, ~165 functions) —
frozen, implemented verbatim by src/ffi.rs.
Key architectural decisions (C++ → Rust mapping)
- The ProjectCopier inversion disappears. C++ render deep-copied
the node project with raw C++ calls (the biggest render→node
coupling). In Rust this is impossible by construction: the copier
calls
oaknode_project_deep_copy/sync_copy(designed in the oaknode crate) through the C ABI.copier.rshere is a thin client. - RenderProcessor's inheritance disappears. C++
RenderProcessor : NodeTraverserbecomeseval.rs(the closedJobSpecset + the CPU-side hook implementations; graph traversal stays in oaknode). - Ticket/watchers. C++ RenderTicket/RenderTicketWatcher (Qt
signals) become a ticket arena with completion callbacks —
exactly-once delivery (
ticket.rs),FnOnceboxes fired on the worker thread. - GPU backend = wgpu (v25). The C++ tree's backend plugin split
(liboakgl2/liboakvulkan behind
renderbackend_c.h) exists because C++ had no portable GPU abstraction. Rust haswgpu(Metal/Vulkan/ GL/DX12 in one safe API), so the Rust crate useswgpudirectly — no backend plugins, norenderbackend_c.h, no dlopen.backend.rsowns the wgpu instance/device/queue, the texture registry and the WGSL blit pipeline. Headless status: verified — texture create/upload/download and the plain-copy blit run without any surface or event loop on macOS Metal (the GPU tests exercise them and skip gracefully when no adapter is available). - Threading. The worker pool is scoped threads with a job
channel; every shared structure is
Mutex/RwLock. Process isolation (ProcessPool) is preserved as a documented stub: it needs the oakengine_ipc worker binary, which is not wired this pass. - OFX disappears from render. pluginrenderer.cpp's functionality moves to the oakplugin crate; this crate only sees plugin jobs as opaque C ABI calls.
Dependencies (registered)
| Crate | Version | Reason |
|---|---|---|
oakcore-rs |
path | Rational/TimeRange/PixelFormat value types (crate-internal) |
wgpu |
25 | portable GPU backend — the direct replacement for the C++ GL/Vulkan backend plugins |
ocio-rs |
0.2 | safe Rust bindings for OpenColorIO v2.5.2 (bundled real-OCIO build); the ColorProcessor implementation — OCIO is never rewritten |
Layout
src/
lib.rs crate doc + module map
error.rs error codes (mirrors include/render/error.h)
handle.rs refcounted-handle scaffolding (facade entry points only)
texture.rs Texture value type (wraps backend textures / CPU frames)
frame.rs VideoParamsPod + Frame helpers
cache.rs PlaybackCache / FrameHashCache family + C++-parity disk state
color.rs ColorProcessor over ocio-rs + default config + LUT library
manager.rs RenderManager singleton + lifecycle + disk cache
ticket.rs Ticket arena, params, exactly-once completion delivery
worker.rs JobDispatch seam + thread-free InlineDispatcher (audio
fallback / test backend) + graph snapshot store
scheduler.rs M15 PreviewScheduler: interleaved shard claiming,
priority lanes (seek/playback/background), crash reclaim,
per-request slot-bytes capacity filtering (S3)
ipc.rs M15 render-worker IPC (moved from oak-worker): NDJSON
control protocol (v1 + v2 messages) + the POSIX
shared-memory frame-slot transport both pipe ends link
procpool.rs M15 ProcessDispatcher: spawn/handshake oak-worker
processes, main-assigned slot batches, crash detection +
restart, zero-copy ShmFrameRef / ShmAudioRef completions,
grow-on-demand segment geometry (S3)
autocacher.rs PreviewAutoCacher
eval.rs RenderHooks impl: the CPU evaluation seam
backend.rs wgpu device/queue/texture management + DisplayRenderer
copier.rs Render-side project copy client (bridge::node)
cancelatom.rs the cancellation primitive
bridge/ C ABI imports: node.rs, common.rs, codec.rs (dlsym-resolved)
ffi.rs include/render/*.h export layer
tests/ contract + golden tests (common/ has shared helpers)
Hard rules
CHandleonly appears at the facade boundary: the crate's internal calls pass Rust types directly;handle::make_owned/get/get_mutare the facade entry points the oakengine stubs call.- No
unsafeoutsidebackend.rs(GPU FFI),bridge/, and the M15 process-isolation transport (ipc.rs/procpool.rs: POSIX shm + SPSC rings; every block carries its own SAFETY comment). - F32 + ACEScg pipeline invariants are asserted in tests, not in comments (see tests/pipeline_test.rs).
Deferred (documented; tests gated with #[ignore])
- oakcodec frame payload I/O — disk frame-cache read/write
(
oakrender_frame_cache_load/save) and footage decode go through thebridge::codecC ABI (EXR/JPEG). The oakcodec crate is a concurrent wave; until it lands these fail explainably and the success-path tests are#[ignore = "needs oakcodec final"]. - oaknode C ABI —
oakrender_project_copier_set_project/get_copysuccess paths needoaknode_project_deep_copy; the success-path copier tests are#[ignore = "needs oaknode C ABI"]. - Color-managed GPU blit — the OCIO→WGSL shader generation is not
in this pass:
GpuContext::blithandles the plain copy and returnsError::Failedfor a processor; the CPU path applies the processor in float.oakrender_color_processor_create_transformresolves the destination transform against the default config's reference role until the oakcommon color-transform bridge lands. - Worker process isolation — landed in M15:
procpool.rs(ProcessDispatcher) +scheduler.rs+ipc.rsdrive real oak-worker processes (spawn, handshake, batched renders into main-assigned shm slots, crash restart, zero-copy completions); the end-to-end and crash-isolation tests live incrates/oak-worker/tests/procpool_integration.rs. M15 S2 made the process backend theRenderManagerdefault, removed the in-process thread pool (worker::WorkerPool) and the frozen pre-M15worker::ProcessPoolfacade stub, and wired the app's onscreen path to read the shm slots zero-copy. M15 S3 added audio over shm (render_audio_batch→TicketPayload::ShmAudio, with the main-process inlineInlineDispatcheras the fallback when the process dispatcher is unavailable), per-ticket slot formats (forced-F32 tickets get F32 slots; the export path reads them directly, eliminating the BGRA8→F32 round trip; segments grow on demand), and adaptive pool tuning (default_slots_per_worker/default_batch_size/default_worker_countpolicies plus thebench_processexample that reports 1080p throughput and adjacent-frame completion deltas). - Audio rendering — M15 S3 migrated audio tickets onto the process
dispatcher (
render_audio_batch; the worker mixes viaeval::render_audio_samples_intointoSLOT_FORMAT_AUDIO_F32slots; the main process readsShmAudioRefand releases). The app's real-time pull (pull_audio_tick) renders chunks ahead asynchronously (prefetch depth 4 ≈ 66 ms) so it never blocks on a busy worker;worker::InlineDispatcher::syncremains the fallback (design §3.7). - Borrowed caches —
oakrender_cache_wrap_borrowedboxes an opaque marker; queries on borrowed caches returnOAKRENDER_E_INVALIDuntil the C++ interop layer lands. RenderManager::global()returnsOption<Arc<…>>instead of the draft'sOption<&'static …>— a resettable singleton cannot hand out stable references safely.
Coverage
COVERAGE.md maps every C++ class of src/render/src to its Rust
home. cargo tarpaulin ≥ 80% excluding the deferred areas listed
above.