perf(capture): slice 8 — buffer ring + paste-cache Epoch; gen2 visibility (take-11 spikes)

Take 11 (c10ce06c) validated the off-UI architecture: typical frames land
work ~10ms + wait ~6.8ms = 16.7 exactly on the deadline; 212/300 best yet.
The ENTIRE remaining gap is periodic 35-65ms render spikes that WORSENED
across the take (189 -> 147) — the signature of gen2 GC pauses. Biggest
churner is structural: the screen capture minted a fresh ~8.3MB byte[] per
DWM frame (~500MB/s of LOH), a producer OBS never does (it owns fixed
surface pools).

- ScreenCaptureFrameSource: 4-deep buffer ring with size-matched slots (a
  <=17ms consumer cannot be lapped at 60Hz) + reused downscale row scratch.
- VideoFrame.Epoch: monotonic per producer frame. The paste cache keys on
  array IDENTITY, so recycled arrays MUST be distinguished — epoch joins the
  PasteKey. Producers handing fresh arrays leave it 0 (key unchanged effect).
- Stats print 'gen2 +N' per 5s window: next take acquits or convicts GC
  without another guess (rule: prove the stage).
- Test (the ONE): PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits
  — same array, new content, bumped epoch; fails on the old key by
  construction. 37/37 compositor/pump, clean build.
- Next suspect if gen2 stays hot: the 10Hz WebView2 capture (full-canvas PNG
  decode + fresh arrays on the UI thread) — recorded, untouched.

Creator audio ask queued in the same working session (+40% post-mix master
gain before the -1dBFS limiter) lands as its own commit next.
This commit is contained in:
2026-09-04 12:36:14 -07:00
parent eb4c379b91
commit fbc8562cf4
7 changed files with 128 additions and 20 deletions
+6 -5
View File
@@ -19,10 +19,11 @@ public sealed class SceneCompositor
// chat/web/image layers are identical frame after frame. Same shape as OBS (the
// source surface is cached, the compositor blits): a scaled/round-clipped/mirrored
// layer rasterizes once and is pasted (row-copy/blend) on every later tick.
// Keyed by the SOURCE FRAME IDENTITY — every producer (capture, webcam, chat
// renderer, web capture, static cache) hands out immutable byte[], so "same pixels
// array + same target rect" ⇒ same raster. Bounds-checked eviction clears wholesale.
private sealed record PasteKey(byte[] Pixels, int SrcW, int SrcH, int DstW, int DstH, bool Round, bool Mirror);
// Keyed by the SOURCE FRAME IDENTITY (array + Epoch) — producers hand out fresh
// immutable byte[], or recycled-ring arrays whose monotonic Epoch distinguishes
// each generation (screen capture, take-11 fix), so "same array+epoch + same
// target rect" ⇒ same raster. Bounds-checked eviction clears wholesale.
private sealed record PasteKey(byte[] Pixels, long Epoch, int SrcW, int SrcH, int DstW, int DstH, bool Round, bool Mirror);
private static readonly Dictionary<PasteKey, VideoFrame> _pasteCache = new();
private static readonly object _pasteGate = new();
private const int MaxPasteEntries = 48;
@@ -284,7 +285,7 @@ public sealed class SceneCompositor
VideoFrame? raster = null;
lock (_pasteGate)
{
key = new PasteKey(frame.BgraPixels, frame.Width, frame.Height, dw, dh, isRound, mirror);
key = new PasteKey(frame.BgraPixels, frame.Epoch, frame.Width, frame.Height, dw, dh, isRound, mirror);
_pasteCache.TryGetValue(key, out raster);
}