docs/zh/plans/render-pipeline-threads.md M3 (design 3.2): OpenFX crash isolation moves from "every worker hosts plugins" to a single dedicated host process, served over NDJSON + shared memory. - oak-worker --ofx-host mode (src/ofx_host.rs): loads every plugin once, resolves jobs by the cross-process-stable OFX identifier, and renders through the same in-process executor the workers used to install. - oak-render/ofxhost.rs: the single-host client. The render manager creates and installs it for the Pipeline backend (lazy spawn on the first plugin job); eval::process_plugin_job prefers it and falls back to the in-process executor otherwise, so the process backend keeps its current behavior until M4. - Data plane: input/output FrameSlotPool pairs (the handshake's input_* fields are used for the first time). Named clips and the source frame are written to input slots after the explicit CPU readback; the plugin output returns through an output slot. Pool size/capacity grow by a host restart when a job needs more (safe: submissions are serialized and one job is in flight). - Crash loop: reader EOF fails the in-flight submit, which respawns the host and re-posts the same job (frames are read back once); after three consecutive crashes the client is permanently dead and the evaluator falls back to a purple frame. The dead child is reaped immediately, and a submit mutex enforces the one-job-in-flight contract. - Progress/cancel: the host flushes plugin_progress immediately (live progress), and reads stdin on its own thread so plugin_cancel takes effect mid-render at the plugin's next progressUpdate; the sticky flag resets at progressStart and request_plugin_cancel_all broadcasts to both the worker pool and the host. - JobSpec::Plugin / PluginJobPayload carry the plugin type_id (stable across processes); `--ofx-crash-once` / `--ofx-crash-always` are the deterministic crash hooks, matching the worker's env hooks. - Tests: wire round-trips; host unit tests (crash budget, cancel-flag reset through the factory, source mapping); oak-worker integration tests against the real host + bundled test plugin (render + progress, crash respawn and re-post, three-crash give-up, mid-render cancel on the new slow variant, concurrent submits); eval's purple fallback.
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 closedJobSpecexecutor set + the CPU-side hook implementations; the job payload enum itself lives in oaknode'sjobs.rs, 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 re-exports oak_core::error (the OAKRENDER_* codes stay as
the public-code contract)
handle.rs refcounted-handle scaffolding (facade entry points only)
cache.rs PlaybackCache / FrameHashCache family + C++-parity disk state
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
shaderfx.rs effect GLSL→WGSL translation (naga) + std140 uniform
packing + the effect runner
copier.rs Render-side project copy client (bridge::node)
cancelatom.rs the cancellation primitive
tests/ contract + golden tests (common/ has shared helpers)
The value/GPU types — backend.rs (wgpu device/queue/texture management
- DisplayRenderer),
color.rs(ColorProcessor over ocio-rs + default config + LUT library),texture.rs,frame.rsand thecommonutil.rsconfig helpers — moved tooak-corein the oak-common/oak-core merge; this crate uses them asoak_core::*.
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
unsafeoutsidehandle.rs, the evaluation seam (eval.rs), and the M15 process-isolation transport (ipc.rs/procpool.rs: POSIX shm + SPSC rings; every block carries its own SAFETY comment). GPU unsafe lives inoak-core'sbackend.rs. - 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 oak_core 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.