- oak-cli: new engine.rs assembly layer maps every facade call to module Rust APIs (oaknode graph/serializer, oaktimeline commands, oakrender ticket arena, oaktask ExportTask, oakcommon config); the ffi/optional/host layers and build.rs link config are gone - oak-worker: the worker session + POSIX shm transport moved into the crate (oakrender backend + serde_json control plane); no dylib - both binaries carry zero liboakengine references (otool verified); tests green (30 cli / 41 worker)
110 lines
4.9 KiB
Markdown
110 lines
4.9 KiB
Markdown
# oak-worker (Rust)
|
|
|
|
Headless render worker process — the Rust rewrite of `worker/workermain.cpp`
|
|
(contract: `engine/include/oakengine/worker.h` and
|
|
`engine/include/oakengine/ipc.h`).
|
|
|
|
## Build and test
|
|
|
|
```sh
|
|
cargo build --release # binary: target/release/oak-worker
|
|
cargo test # unit + integration tests
|
|
```
|
|
|
|
The worker is **self-contained** (M14 R2): the whole runtime is compiled
|
|
into this binary and links the module crates directly — no `liboakengine`
|
|
dylib is needed at build or run time.
|
|
|
|
- `src/worker.rs` is the port of `engine/src/capi/worker.cpp`
|
|
`oakengine_worker_main()` and owns the whole runtime: render backend
|
|
selection through the oakrender crate's direct Rust API (dynamic →
|
|
OpenGL fallback), the startup handshake and the NDJSON control loop.
|
|
`src/main.rs` only scans argv for `--backend` and forwards.
|
|
- `src/ipc.rs` owns the shared-memory frame-slot transport (the real
|
|
`SpscRingBuffer` + `FrameSlotPool` over POSIX `shm_open`/`mmap`) and the
|
|
NDJSON control-plane message structs.
|
|
|
|
The oakrender module crate (`../oakrender`) is a plain Rust dependency;
|
|
it depends on `ocio-rs` with the `bundled` feature, whose first-time build
|
|
fetches a vendored OpenColorIO dependency (`sse2neon`) from github.com. On
|
|
networks without github access, build with a shared target directory that
|
|
already contains a completed oakrender build tree, e.g.:
|
|
|
|
```sh
|
|
CARGO_TARGET_DIR=/path/to/oak/crates/oakrender/target cargo build --release
|
|
```
|
|
|
|
## What the worker does
|
|
|
|
Same flow as the C++ main, in the same order:
|
|
|
|
1. **parse `--backend <name>`** (default `opengl`; `none` skips
|
|
renderer creation and the process exits 1, like the C++ main).
|
|
2. **initialize the render backend** (inside `src/worker.rs`): the
|
|
oakrender `DisplayRenderer` direct Rust API, falling back to the direct
|
|
OpenGL renderer exactly like the C++ `create_renderer()` fallback
|
|
chain.
|
|
3. **write the startup handshake** (protocol version 1, empty shared-memory
|
|
geometry — same as the C++ worker's startup handshake; the parent
|
|
creates the segments and announces their geometry in its reply).
|
|
4. **serve the NDJSON control loop** on stdin/stdout until a `shutdown`
|
|
message or EOF: `handshake` attaches the announced shared-memory
|
|
frame-slot pools through the real transport; `load_graph` /
|
|
`render_frame` / `cancel` / `shutdown` are dispatched by the session.
|
|
Responses are one compact JSON line per message.
|
|
|
|
## Implemented vs stubbed (nothing is faked)
|
|
|
|
**Real:** argument parsing, render backend initialization (real wgpu
|
|
renderer, dynamic → OpenGL fallback), startup handshake, NDJSON framing,
|
|
message validation (protocol version, handshake geometry, `load_graph`
|
|
file existence/size — the same messages the C++ worker emits), the
|
|
**shared-memory frame-slot transport** (`src/ipc.rs` — POSIX
|
|
`shm_open`/`mmap`/`munmap`/`shm_unlink`, the SPSC ring buffer and the
|
|
frame-slot pool with the exact version-1 shared layout; a `handshake`
|
|
genuinely attaches the output and input pools), unknown-type/
|
|
malformed-message errors, shutdown/EOF termination.
|
|
|
|
**Stubbed (documented in `src/worker.rs`):**
|
|
|
|
| area | reason |
|
|
|---|---|
|
|
| node-graph deserialization (`load_graph` beyond the file checks) | the oaknode crate is a `todo!()` skeleton |
|
|
| frame rendering (`render_frame`) | no graph/render-pipeline backing (the shm frame-slot transport is attached, but there is no graph to render) |
|
|
|
|
Stubbed requests answer with a clear `{"type":"error","message":…}` that
|
|
names the missing piece (a `render_frame` error also carries the ticket,
|
|
mirroring the C++ `error_message()` shape). A real `load_graph` on a
|
|
non-existent/empty file produces the C++-identical error before reaching
|
|
the stub.
|
|
|
|
**Deviation from the C++:** the startup handshake omits `gl_major`/
|
|
`gl_minor` — the oakrender module exposes no GL context version (the C++
|
|
worker reads them off its `QOpenGLContext`).
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/
|
|
main.rs argv --backend scanning (default opengl; last flag wins);
|
|
forwards to worker::worker_main
|
|
worker.rs the real worker runtime: backend selection (oakrender
|
|
DisplayRenderer, dynamic -> OpenGL fallback), WorkerSession,
|
|
handshake + NDJSON loop (M14 R2: the facade's port, owned by
|
|
this binary since the facade C ABI was cut)
|
|
ipc.rs control-plane message structs + NDJSON framing (serde),
|
|
AND the real shared-memory frame-slot transport
|
|
(SpscRingBuffer + FrameSlotPool over POSIX shm)
|
|
tests/worker.rs binary-level tests (--backend none exit 1)
|
|
```
|
|
|
|
The NDJSON control-loop behavior is exercised in-process in `src/worker.rs`
|
|
against the local real shared memory (no GPU needed via `--backend none`
|
|
sessions); a binary-level loop test would require a working GPU backend and
|
|
is deliberately not part of the unit suite. Run the binary against a
|
|
created segment to see the real attach path:
|
|
|
|
```sh
|
|
target/release/oak-worker --backend opengl <<< '{"type":"shutdown"}'
|
|
```
|