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:
+13
-1
@@ -93,7 +93,19 @@ Both halves were solved by OBS/libyuv long ago; do not re-derive:
|
||||
(the chat log) silently arm the per-tick cost even in flows that never touch the
|
||||
feature (signed-out record-only takes paid chat rendering!).
|
||||
|
||||
6. **Prove the stage, then the fix — and re-prove after every slice (2026-09-04, takes 6-8).**
|
||||
6. **An async loop started from a UI handler runs ON THE UI THREAD until you take it off.**
|
||||
`await` continuations re-capture the current `SynchronizationContext` — the frame pump was started
|
||||
from a WPF command handler, so the "WPF-free, hermetic" compositor rendered and read capture state
|
||||
ON THE DISPATCHER, serialized behind the live preview itself, for the whole starvation saga. The
|
||||
`wait` stat caught it only when the numbers became self-contradictory (render 22 + wait 10 > any
|
||||
rebasing deadline — a blown deadline cannot sleep). OBS runs `obs_graphics_thread`/`video_thread`
|
||||
as dedicated threads for exactly this reason. Pattern: `_task = Task.Run(() => Loop())` (null
|
||||
context inside), then audit EVERY object the loop touches for UI affinity (RenderTargetBitmap /
|
||||
DrawingVisual / WriteableBitmap: marshal the work or the rare miss; plain locked byte[] lookups:
|
||||
fine) and pin it with a context test (`Pump_Produces_OffTheStartingContext`, inline-pumping
|
||||
SynchronizationContext that the old code failed by construction). Cost: takes 3–10.
|
||||
|
||||
7. **Prove the stage, then the fix — and re-prove after every slice (2026-09-04, takes 6-8).**
|
||||
The chat raster fix was REAL but the composer blamed it for the residual slowness it did not
|
||||
own; two takes burned before the render/resolve split showed `resolve ≈ 0` and pointed at the
|
||||
compositor pasting static layers per tick (`BlitCachedLayer` finished the job OBS-style). Before
|
||||
|
||||
Reference in New Issue
Block a user