render: cache worker frames and decouple audio from the video workers

- oak-worker gains an LRU frame cache (default 64 MiB) keyed by the
  render-deterministic spec subset; repeat frames (paused frames,
  scrubs over rendered ranges, re-renders after an effect change) are
  memcpy-cheap, which is what made adding an OFX plugin -- and the
  in-flight batches after removing one -- stall the UI
- audio dispatch on the Processes backend now mixes inline on the UI
  tick instead of queueing behind video batches in the worker pool, so
  video stalls no longer starve the ~100ms cpal output buffer into
  silence
This commit is contained in:
2026-08-27 20:35:50 +08:00
parent c78be39f8b
commit 9c0269bcbb
6 changed files with 447 additions and 54 deletions
+18 -10
View File
@@ -58,9 +58,10 @@ pub enum RenderBackendChoice {
pub struct RenderManager {
/// Video job dispatch (the process dispatcher, M15).
pub dispatch: Arc<dyn JobDispatch>,
/// Audio job dispatch (M15 S3: the process dispatcher, like video —
/// audio renders in oak-worker so plugin crashes are isolated; the
/// inline fallback lives in the arena, design §3.7).
/// Audio job dispatch (M16 S2: deliberately the inline dispatcher —
/// playback audio renders synchronously on the submitting thread so it
/// never queues behind worker video batches; the worker-pool audio path
/// was the M15 S3 default, design §3.7).
pub audio_dispatch: Arc<dyn JobDispatch>,
/// Ticket arena.
pub tickets: Arc<TicketArena>,
@@ -124,13 +125,20 @@ impl RenderManager {
RenderBackendChoice::Processes(config) => {
let dispatcher = ProcessDispatcher::new(config)?;
dispatcher.start()?;
// M15 S3: audio rendering runs in the worker pool too (a
// plugin crash during an audio render must not take down
// the main process — design §3.7). The inline dispatcher
// stays as the fallback when the process dispatcher is
// unavailable (teardown).
let fallback = InlineDispatcher::sync();
(dispatcher.clone(), dispatcher, Some(fallback))
// M16 S2: audio rendering deliberately runs INLINE on the
// submitting (UI) thread instead of the worker pool. Video
// batches — especially OFX plugin frames at ~100-500 ms
// each — can occupy the workers far longer than the cpal
// output buffer (~4 chunks, ≈100 ms) holds; an audio batch
// parked behind them underruns and the audio goes silent
// while the video is merely slow. Playback audio is a short
// synchronous range pull, so inline mixing keeps audio
// flowing no matter how stuck the video workers are
// (design §3.7 prefers inline before S3). Trade: an
// audio-side plugin crash now takes down the main process,
// and the mix cost lands on the UI tick.
let inline = InlineDispatcher::sync();
(dispatcher.clone(), inline, None)
}
};
let tickets = Arc::new(TicketArena::new_with_audio_fallback(