fix(web): feed full canvas to compositor — transparent margins reveal layers beneath (take-21)

This commit is contained in:
2026-09-06 12:56:34 -07:00
parent 70db34455f
commit e002847777
+15 -11
View File
@@ -30,13 +30,13 @@ public sealed class WebView2Manager : IDisposable
public bool Disposed;
public bool Initialized;
// Buffer reuse (2026-09-04, same lesson as the capture/camera rings): every
// capture tick used to mint a fresh 8.3MB canvas array + a fresh crop array
// (~83MB/s of LOH at 10Hz → gen2 pauses surfaced as the pump's 35-40ms
// "worst render" spikes). Canvas scratch is reused while the size holds;
// the frame handed to the compositor rotates an 8-deep ring with a monotonic
// Epoch (identity-keyed consumers must see each generation). The preview
// WriteableBitmap is recreated only when the cropped size changes.
// Buffer reuse (2026-09-04): every capture tick used to mint a fresh
// 8.3MB canvas array + a fresh crop array (~83MB/s of LOH at 10Hz →
// gen2 pauses surfaced as the pump's 35-40ms "worst render" spikes).
// CanvasScratch is reused while the size holds. The frame handed to the
// compositor is always the FULL canvas (transparent margins reveal layers
// beneath); the crop is only for the preview WriteableBitmap. Epoch is
// monotonic per canvas generation (identity-keyed consumers see each frame).
public byte[]? CanvasScratch;
public readonly byte[]?[] OutRing = new byte[8][];
public int OutRingNext;
@@ -192,9 +192,13 @@ public sealed class WebView2Manager : IDisposable
// bounding box of non-transparent pixels (the background is injected
// transparent, so the alpha channel IS the widget). This is immune to
// DOM timing (fonts/images/animations), needs no JS round-trip, and
// CANNOT break rendering: if the widget fills the canvas the box equals
// the full frame (identical to the confirmed-good uncropped display); if
// it floats in transparent margin, the crop maps it flush under the box.
// CANNOT break the render: if the widget fills the canvas the box equals
// the full frame; if it floats in transparent margin the box is the
// measured content extent — used ONLY for the preview bitmap / selection.
// The frame handed to the compositor is always the FULL canvas, so the
// page's transparent margins keep revealing the layers beneath (the
// "black square over webcam" was the crop fed to the compositor:
// UniformToFill zoomed the opaque content box to cover the whole element).
internal static (int X, int Y, int W, int H) FindContentBounds(byte[] pixels, int pixW, int pixH, int stride)
{
int minX = pixW, minY = pixH, maxX = -1, maxY = -1;
@@ -266,7 +270,7 @@ public sealed class WebView2Manager : IDisposable
session.PreviewBitmap = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
session.PreviewBitmap.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0);
session.LatestFrame = new VideoFrame(cropW, cropH, outPixels) { Epoch = ++session.Epoch };
session.LatestFrame = new VideoFrame(pixW, pixH, pixels) { Epoch = ++session.Epoch };
// The preview bitmap is now MUTABLE and reused (CameraManager precedent):
// WPF re-renders it after WritePixels; handlers must not Freeze it.