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:
@@ -211,6 +211,49 @@ public class SceneCompositorTests
|
||||
AssertColor(third, 101, 101, 255, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>The ONE integration test for the capture buffer ring + paste-cache epoch
|
||||
/// (take-11 spike fix): a recycled capture buffer hands out the SAME byte[] with
|
||||
/// different content; only VideoFrame.Epoch can keep the paste cache honest. Without
|
||||
/// the epoch in the key, frame 2 false-hits frame 1's raster and the recording shows
|
||||
/// stale pixels for as long as the array keeps cycling — the exact bug this pins.</summary>
|
||||
[Fact]
|
||||
public void PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits()
|
||||
{
|
||||
var pixels = new byte[64 * 32 * 4];
|
||||
for (var i = 0; i < pixels.Length; i += 4)
|
||||
{
|
||||
pixels[i] = 0; pixels[i + 1] = 0; pixels[i + 2] = 255; pixels[i + 3] = 255; // red
|
||||
}
|
||||
var gen1 = new VideoFrame(64, 32, pixels) { IsOpaque = true, Epoch = 1 };
|
||||
|
||||
var webcam = new WebcamSceneConfig { X = 4, Y = 4, Width = 32, Height = 16 };
|
||||
var scene = new Scene { Name = "Live" };
|
||||
scene.Elements.Add(webcam);
|
||||
var options = new CompositorOptions
|
||||
{
|
||||
SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 128, SourceRectHeight = 64,
|
||||
OutputWidth = 128, OutputHeight = 64,
|
||||
};
|
||||
|
||||
var compositor = new SceneCompositor();
|
||||
var out1 = compositor.Render(scene, _ => gen1, null, options);
|
||||
AssertColor(out1, 20, 12, 255, 0, 0);
|
||||
|
||||
// Generation 2: the SAME array, new content, bumped epoch (what the capture
|
||||
// ring does 60 times a second).
|
||||
for (var i = 0; i < pixels.Length; i += 4)
|
||||
{
|
||||
pixels[i] = 0; pixels[i + 1] = 255; pixels[i + 2] = 0; pixels[i + 3] = 255; // green
|
||||
}
|
||||
var gen2 = new VideoFrame(64, 32, pixels) { IsOpaque = true, Epoch = 2 };
|
||||
var out2 = compositor.Render(scene, _ => gen2, null, options);
|
||||
AssertColor(out2, 20, 12, 0, 255, 0); // a stale hit would still be red
|
||||
|
||||
// And the same epoch twice still hits the cache (idempotent paste).
|
||||
var out3 = compositor.Render(scene, _ => gen2, null, options);
|
||||
Assert.Equal(out2.BgraPixels, out3.BgraPixels);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user