render: the M1 thread pipeline — one render thread, one decode thread

docs/zh/plans/render-pipeline-threads.md M1: an in-process
alternative to the worker-process pool, behind OAK_PIPELINE=threads
(processes stays the default and is fully retained).

- pipeline.rs: PipelineBackend implements JobDispatch over a single
  render thread draining a bounded FIFO (cap 8; blocking post with
  condvar backpressure and a one-ahead exception for re-posts from
  the render thread itself; shutdown drains with Error::State like
  the inline dispatcher). The DecodeService is a single decode
  thread behind a bounded command queue with a real LRU (tick-based
  eviction), rendezvous requests (None on shutdown -> the caller
  decodes inline), prefetch gated on render-queue room, and a Sync
  barrier; it installs into a process-wide slot that eval's footage
  path consults per frame (no service -> the synchronous decode it
  always was).
- The manager gains RenderBackendChoice::Pipeline; init() reads
  OAK_PIPELINE (threads -> pipeline, anything else -> the process
  pool), audio stays deliberately inline.
- Present mapping: the UI thread consumes through the ticket
  completion, unchanged — no fourth thread is invented.
- Tests: decode-service unit tests (rendezvous, LRU hit/eviction,
  error propagation, backpressure gate) plus a six-case integration
  suite matrixed over inline vs pipeline — consecutive-frame and
  out-of-order seek pixel equality asserted byte for byte, with
  decode counters proving the service (not the caller) did the
  codec work.
This commit is contained in:
2026-09-11 19:37:27 +08:00
parent 23174512c0
commit a5b0b2a1b1
7 changed files with 1789 additions and 15 deletions
+4 -1
View File
@@ -232,7 +232,10 @@ impl InlineDispatcher {
}
}
fn execute_job(job: Job) {
/// Run one job on the calling thread: the producer, then its completion
/// (used by the inline dispatcher and by [`crate::pipeline::PipelineBackend`],
/// which runs the same producer on its render thread).
pub(crate) fn execute_job(job: Job) {
let result = catch_unwind(AssertUnwindSafe(|| (job.produce)(job.time, &job.params)))
.unwrap_or_else(|_| Err(Error::Failed("frame producer panicked".into())));
(job.done)(result);