Files
oak-editor/crates/oak-render/README.md
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

161 lines
8.4 KiB
Markdown

# 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 in `include/render/*.h` is
> exported from `src/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)
1. **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.rs` here is a thin client.
2. **RenderProcessor's inheritance disappears.** C++
`RenderProcessor : NodeTraverser` becomes `eval.rs` (the closed
`JobSpec` set + the CPU-side hook implementations; graph traversal
stays in oaknode).
3. **Ticket/watchers.** C++ RenderTicket/RenderTicketWatcher (Qt
signals) become a ticket arena with completion callbacks —
exactly-once delivery (`ticket.rs`), `FnOnce` boxes fired on the
worker thread.
4. **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 has `wgpu` (Metal/Vulkan/
GL/DX12 in one safe API), so the Rust crate uses `wgpu` directly —
no backend plugins, no `renderbackend_c.h`, no dlopen. `backend.rs`
owns 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).
5. **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.
6. **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
1. `CHandle` only appears at the facade boundary: the crate's internal
calls pass Rust types directly; `handle::make_owned`/`get`/`get_mut`
are the facade entry points the oakengine stubs call.
2. No `unsafe` outside `backend.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).
3. 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 the
`bridge::codec` C 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_copy` success paths need `oaknode_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::blit` handles the plain copy and returns
`Error::Failed` for a processor; the CPU path applies the processor
in float. `oakrender_color_processor_create_transform` resolves 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.rs` drive 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 in
`crates/oak-worker/tests/procpool_integration.rs`. M15 S2 made the
process backend the `RenderManager` default, removed the in-process
thread pool (`worker::WorkerPool`) and the frozen pre-M15
`worker::ProcessPool` facade 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 inline `InlineDispatcher` as 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_count` policies plus the
`bench_process` example 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 via
`eval::render_audio_samples_into` into `SLOT_FORMAT_AUDIO_F32` slots;
the main process reads `ShmAudioRef` and 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::sync` remains the fallback
(design §3.7).
- **Borrowed caches** — `oakrender_cache_wrap_borrowed` boxes an
opaque marker; queries on borrowed caches return `OAKRENDER_E_INVALID`
until the C++ interop layer lands.
- **`RenderManager::global()` returns `Option<Arc<…>>`** instead of the
draft's `Option<&'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.