feat(oakrender): render-process isolation S1 - dispatcher, scheduler, real worker

Per the M15 design (docs/zh/plans/riir/M15-render-process-isolation.md):

- ipc.rs moved into oakrender with protocol v2: hello_caps,
  render_batch, batch_accepted, frame_failed; main-process-assigned
  slots; BGRA8 slot format. POSIX shm verified to 1GiB on macOS.
- ProcessDispatcher: spawns oak-worker processes, handshake, stdio
  NDJSON control, shm segment lifecycle with generation-tagged keys,
  crash detection with bounded restart and frame redispatch, zero-copy
  ShmFrameRef delivery and copy counters.
- PreviewScheduler: interleaved batch claiming (frame % W per worker,
  no work stealing), seek > playback-distance > background priority,
  credit-based flow control, crash recovery.
- oak-worker renders for real: graph snapshot deserialization, montage
  decode+composite straight into the assigned shm slot, F32->BGRA8
  final conversion in-worker, OFX plugin executor installed in-worker,
  crash hooks for isolation testing.

Thread pool coexists for now (S2 removes it). Integration tests cover
two-worker zero-copy rendering, crash isolation with redelivery, and
real H.264 footage decode into slots.
This commit is contained in:
2026-08-18 18:58:38 +08:00
parent f2af92958a
commit 431b9ed2b1
22 changed files with 5162 additions and 1615 deletions
+38 -5
View File
@@ -16,13 +16,15 @@
//! The worker layer (C++ RenderWorkerPool + RenderThread +
//! workerprocess/workerjson): thread pool AND process-isolated pool
//! behind one enum.
//! behind one dispatch seam.
//!
//! This pass ships the in-process [`WorkerPool`] fully. The
//! [`ProcessPool`] (crash isolation via oakengine_ipc worker processes)
//! is a documented stub: the oakengine_ipc C ABI worker binary is not
//! wired into the Rust world yet, so `start`/`post` fail with
//! `Error::Failed` and the crash-isolation tests are `#[ignore]`d.
//! process-isolated backend landed in M15 S1 as
//! [`crate::procpool::ProcessDispatcher`] (spawn/handshake/crash-restart
//! of oak-worker binaries over NDJSON + shared memory); both backends
//! implement the [`JobDispatch`] seam the ticket arena posts through.
//! [`ProcessPool`] below is the frozen pre-M15 facade stub kept for C
//! ABI parity.
use std::collections::{HashMap, VecDeque};
use std::panic::{catch_unwind, AssertUnwindSafe};
@@ -52,6 +54,21 @@ fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
/// The job-dispatch seam (M15 S1): the ticket arena posts [`Job`]s
/// through this interface without knowing the backend. Implemented by
/// the in-process [`WorkerPool`] (threads) and the process-isolated
/// [`crate::procpool::ProcessDispatcher`] (oak-worker children); S2
/// removes the thread pool and this seam becomes process-only.
pub trait JobDispatch: Send + Sync {
/// Enqueue a job; false when the backend is gone (the arena then
/// delivers the completion itself with `Error::State`).
fn post(&self, job: Job) -> bool;
/// Stop accepting work, deliver the queued completions (cancelled)
/// and release the backend. Idempotent.
fn shutdown(&self);
}
/// Thread-pool backend (C++ RenderThread model). Cheap to clone (all
/// state is behind an `Arc`); the manager and the ticket arena share one
/// pool.
@@ -132,6 +149,12 @@ impl WorkerPool {
/// without running); running jobs are joined so no completion fires
/// after shutdown returns.
pub fn shutdown(&mut self) {
self.shutdown_ref();
}
/// [`Self::shutdown`] on a shared reference (the [`JobDispatch`]
/// seam; all state is interior-mutable). Idempotent.
pub fn shutdown_ref(&self) {
// Set the flag and wake the workers while holding the queue lock.
// Workers decide whether to block in `cv.wait` while holding that
// lock, so a flag set outside it could land between a worker's
@@ -158,6 +181,16 @@ impl WorkerPool {
}
}
impl JobDispatch for WorkerPool {
fn post(&self, job: Job) -> bool {
WorkerPool::post(self, job)
}
fn shutdown(&self) {
self.shutdown_ref();
}
}
fn worker_loop(inner: Arc<PoolInner>) {
loop {
let job = {