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
+4 -1
View File
@@ -47,7 +47,10 @@ fn frame_producer() -> oakrender::ticket::Producer {
fn cacher() -> (oakrender::autocacher::PreviewAutoCacher, WorkerPool) {
let mut pool = WorkerPool::new(2);
pool.start();
let arena = Arc::new(TicketArena::new(pool.clone(), frame_producer()));
let arena = Arc::new(TicketArena::new(
Arc::new(pool.clone()),
frame_producer(),
));
(oakrender::autocacher::PreviewAutoCacher::new(arena), pool)
}
+5 -5
View File
@@ -83,7 +83,7 @@ impl Drop for GateRelease {
fn ticket_completion_once_success() {
let mut pool = WorkerPool::new(2);
pool.start();
let arena = TicketArena::new(pool.clone(), ok_producer());
let arena = TicketArena::new(Arc::new(pool.clone()), ok_producer());
let (tx, rx) = mpsc::channel();
let id = arena.submit_video(
@@ -120,7 +120,7 @@ fn ticket_completion_once_on_cancel() {
}
Ok(oakrender::ticket::TicketPayload::Video(Texture::wrap_frame(small_frame())))
});
let arena = TicketArena::new(pool.clone(), blocking);
let arena = TicketArena::new(Arc::new(pool.clone()), blocking);
let (tx, rx) = mpsc::channel();
let id = arena.submit_video(
@@ -159,7 +159,7 @@ fn shutdown_drains_completions() {
Ok(oakrender::ticket::TicketPayload::Video(Texture::wrap_frame(small_frame())))
})
};
let arena = TicketArena::new(pool.clone(), blocking);
let arena = TicketArena::new(Arc::new(pool.clone()), blocking);
let (tx, rx) = mpsc::channel();
let mut ids = Vec::new();
@@ -197,7 +197,7 @@ fn shutdown_drains_completions() {
fn pool_saturation() {
let mut pool = WorkerPool::new(4);
pool.start();
let arena = TicketArena::new(pool.clone(), ok_producer());
let arena = TicketArena::new(Arc::new(pool.clone()), ok_producer());
let (tx, rx) = mpsc::channel();
let mut ids = Vec::new();
for i in 0..64u64 {
@@ -228,7 +228,7 @@ fn pool_saturation() {
fn ticket_id_monotonic() {
let mut pool = WorkerPool::new(1);
pool.start();
let arena = TicketArena::new(pool.clone(), ok_producer());
let arena = TicketArena::new(Arc::new(pool.clone()), ok_producer());
let a = arena.submit_video(params(Rational::new(0, 1)), Box::new(|_| {}));
let b = arena.submit_video(params(Rational::new(1, 1)), Box::new(|_| {}));
let c = arena.submit_video(params(Rational::new(2, 1)), Box::new(|_| {}));