From 40c8043a76bd58706482974a4c9532fba3213054 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 21 Aug 2026 20:37:17 +0800 Subject: [PATCH] render: size the preview-window headroom by alive workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit preview_window_capacity used the *configured* worker count, so a window opened while workers were still handshaking (or after a crash) could claim every slot of the smaller live pool — the synchronous render ticket then never gets a free slot, and since the slot-releasing cleanup runs on the same UI thread that is blocked in TicketArena::wait, playback deadlocks permanently. Intermittent on Linux CI (the playback_display_tracks_the_playhead hang, caught by the new test watchdog): depends on how many workers had handshaken when playback started. Count only Alive workers (fall back to the configured count while none are alive, keeping the existing unit test semantics). --- crates/oakrender/src/procpool.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/oakrender/src/procpool.rs b/crates/oakrender/src/procpool.rs index 517faba52..0af926e95 100644 --- a/crates/oakrender/src/procpool.rs +++ b/crates/oakrender/src/procpool.rs @@ -883,9 +883,27 @@ impl ProcessDispatcher { /// display) and audio tickets always keep credit to dispatch. A /// pre-render window larger than the pool exhausted every slot, which /// deadlocked the UI's synchronous frame wait (the playback freeze). + /// + /// The count is over ALIVE workers: with the configured count a window + /// opened during worker startup (or after a crash) could still claim + /// every slot of the smaller live pool — the same deadlock, just + /// timing-dependent (seen as the intermittent Linux CI hang in + /// playback_display_tracks_the_playhead). pub fn preview_window_capacity(&self) -> usize { let inner = lock(&self.inner); - let workers = inner.scheduler.workers(); + let alive = inner + .workers + .iter() + .filter(|w| matches!(w.state, WorkerState::Alive)) + .count(); + // Nobody alive yet: nothing can be claimed right now anyway, so + // reporting the configured pool keeps the window building instead + // of stalling at the 1-frame floor. + let workers = if alive == 0 { + inner.scheduler.workers() + } else { + alive + }; workers .saturating_mul(inner.slots as usize) .saturating_sub(workers)