fix(pump): slice 7 — take the loop off the UI thread (the 'wait 10ms after render 22ms' contradiction resolved)
Take 10 (59a02a5b, slice 6) finally produced a self-contradicting stat: render 22.4ms + submit 2.5 against a 16.7ms deadline, yet avg wait 10ms — a rebasing pacer CANNOT sleep after a blown deadline. The wait was queue time: StartAsync fires from a UI command handler, and async continuations re-capture the current SynchronizationContext — the 'WPF-free, hermetic' frame pump had been rendering ON THE DISPATCHER behind the live preview the entire starvation saga. OBS keeps obs_graphics_thread/video_thread off-UI for exactly this reason (dedicated threads; see docs.obsproject.com/backend-design 'Libobs Threads'). - FramePump: _pumpTask = Task.Run(() => PumpAsync(...)) — null context inside, every continuation stays on the pool. - Audited, not ignored, what that exposes: StaticPixelCache.Get now locks (pool miss-decodes raced UI callers); ChatOverlayLayer.RenderFrame checks its cache off-thread but marshals the rare raster MISS to the dispatcher (DrawingVisual + RenderTargetBitmap are UI-thread objects) and re-validates there; pump events already marshal in the VM. - GCLatencyMode.SustainedLowLatency for the pump's life (restored in finally). - Stats gained 'worst render Xms' — bimodal averages hid per-tick spikes. - Webcam routes through the paste cache (the IsOpaque bypass re-sampled ~156k px every tick even between identical device frames). ONE integration test: Pump_Produces_OffTheStartingContext — an inline-pumping SynchronizationContext makes the old construction run the resolver on the starting thread by capture; the loop must never. 70/70 per-class green, clean build 0 warnings. Docs same commit (ai.md slice 7, TASKS take-11 gate, MyMistakes #6, HANDOFF). take 11: ~300/300 + honest wait -> saga closed, Unit B (two-line top bar spec, fully captured) starts.
This commit is contained in:
@@ -777,6 +777,23 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
bypass was removed (it re-sampled ~156k px every tick even between identical device frames; the
|
||||
cached paste beats the sampler on hits and costs the same on misses). Take 10 verdict: `≈300/300`
|
||||
honest fps — if short, the wait/render split names the remaining term with no ambiguity left.
|
||||
- **Slice 7 — the loop was ON the UI thread the whole time (take-8 numbers, 2026-09-04):** with
|
||||
the wait finally measured, take 8 (59a02a5b) showed the impossible pair — `render 22ms, wait 10ms`
|
||||
against a 16.7ms deadline: a blown deadline rebases with NO wait. The wait was the producer
|
||||
**queued behind the live preview on the dispatcher**: `PumpAsync` starts from a UI command handler
|
||||
and its `await` continuations inherit the UI `SynchronizationContext`, so the "WPF-free" compositor
|
||||
actually rendered on the UI thread and ran only when WPF let it. OBS runs its media loops on
|
||||
dedicated threads for exactly this reason. Fix: `_pumpTask = Task.Run(() => PumpAsync(...))`
|
||||
(no sync context inside → continuations stay on the pool). Side effects handled, not ignored:
|
||||
`StaticPixelCache.Get` now locks (pool miss-decodes race UI callers); `ChatOverlayLayer.RenderFrame`
|
||||
checks its cache off-thread but MARSHALS the rare raster miss to the dispatcher (DrawingVisual/
|
||||
RenderTargetBitmap are UI-thread objects) and re-validates there; pump events already marshalled.
|
||||
Also: `GCLatencyMode.SustainedLowLatency` for the pump's life, stats gained `worst render Xms`
|
||||
(spike visibility — bimodal averages hid them), webcam now routes through the paste cache (bypass
|
||||
re-sampled 156k px even between identical device frames). Tests: `Pump_Produces_OffTheStartingContext`
|
||||
(the ONE — inline-pumping SyncContext proves continuations never return to the starting thread) +
|
||||
70/70. Take 9 verdict: wait ≈ true remainder (period → 16.7, n → ~300); `worst render` names any
|
||||
remaining raster-miss spikes; render+submit+wait still == period — the accounting holds.
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder (closes stdin → EOF → ffmpeg finalizes+exits)
|
||||
**before** awaiting the loop, because closing stdin unblocks a write stuck on pipe backpressure — the
|
||||
reverse order would deadlock. `ProcessFailed` self-stops the pump. `Failed` while live flips
|
||||
|
||||
Reference in New Issue
Block a user