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:
@@ -117,8 +117,32 @@ public sealed class ChatOverlayLayer
|
||||
if (_frameKey == key && _frameVersion == _contentVersion)
|
||||
return _cachedFrame; // same content + same config — the cached raster stands
|
||||
|
||||
// The raster is WPF (DrawingVisual + RenderTargetBitmap = UI-thread objects)
|
||||
// and the frame pump now renders from its own thread (take-8 off-UI fix) —
|
||||
// so only the cache CHECK runs off-thread; the rare miss marshals to the
|
||||
// dispatcher and re-validates there (a second message landing mid-hop must
|
||||
// not be answered by a snapshot taken before it).
|
||||
var app = System.Windows.Application.Current;
|
||||
if (app != null && !app.Dispatcher.CheckAccess())
|
||||
{
|
||||
var capturedKey = key;
|
||||
var capturedVersion = _contentVersion;
|
||||
var frame = app.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_frameKey == capturedKey && _frameVersion == capturedVersion)
|
||||
return _cachedFrame; // someone already re-rasterized on the UI thread
|
||||
return RenderCore(chatBox, width, height, capturedKey, capturedVersion);
|
||||
});
|
||||
return frame;
|
||||
}
|
||||
|
||||
return RenderCore(chatBox, width, height, key, _contentVersion);
|
||||
}
|
||||
|
||||
private VideoFrame? RenderCore(Source chatBox, int width, int height, string key, int version)
|
||||
{
|
||||
_frameKey = key;
|
||||
_frameVersion = _contentVersion;
|
||||
_frameVersion = version;
|
||||
if (Messages.Count == 0) return _cachedFrame = null;
|
||||
|
||||
return _cachedFrame = _renderer.Render(
|
||||
|
||||
Reference in New Issue
Block a user