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:
@@ -794,6 +794,18 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
(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.
|
||||
- **Slice 8 — capture buffer ring + paste-cache Epoch (take 11, 2026-09-04):** take 11 validated
|
||||
the architecture — typical frames now land `work ~10ms + wait ~6.8ms = 16.7`, exactly the deadline
|
||||
(212/300). The whole remaining gap is PERIODIC 35-65ms worst-render spikes that worsened across
|
||||
the take (189→147 frames/5s) — the signature of gen2 GC pauses, now provable via `gen2 +N` in
|
||||
every stats window. Biggest churn was structural: `ScreenCaptureFrameSource` minted a fresh
|
||||
~8.3MB `byte[]` per DWM frame (~500MB/s LOH). It now rotates a 4-deep ring (a ≤17ms consumer can
|
||||
never be lapped at 60Hz), size-matched per slot, with reused downscale row scratch. Recycled
|
||||
arrays would poison the paste cache (it keys on array IDENTITY), so `VideoFrame.Epoch` —
|
||||
monotonic per producer frame, 0 for fresh-array producers — joins the key. Regression test
|
||||
`PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits` fails on the old key by
|
||||
construction. Next suspect if gen2 stays hot: the 10Hz WebView2 capture loop (full-canvas PNG
|
||||
decode + fresh arrays on the UI thread) — recorded as follow-up, untouched this slice.
|
||||
- **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