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 Disposed;
public bool Initialized; public bool Initialized;
// Buffer reuse (2026-09-04, same lesson as the capture/camera rings): every // Buffer reuse (2026-09-04): every capture tick used to mint a fresh
// capture tick used to mint a fresh 8.3MB canvas array + a fresh crop array // 8.3MB canvas array + a fresh crop array (~83MB/s of LOH at 10Hz →
// (~83MB/s of LOH at 10Hz → gen2 pauses surfaced as the pump's 35-40ms // gen2 pauses surfaced as the pump's 35-40ms "worst render" spikes).
// "worst render" spikes). Canvas scratch is reused while the size holds; // CanvasScratch is reused while the size holds. The frame handed to the
// the frame handed to the compositor rotates an 8-deep ring with a monotonic // compositor is always the FULL canvas (transparent margins reveal layers
// Epoch (identity-keyed consumers must see each generation). The preview // beneath); the crop is only for the preview WriteableBitmap. Epoch is
// WriteableBitmap is recreated only when the cropped size changes. // monotonic per canvas generation (identity-keyed consumers see each frame).
public byte[]? CanvasScratch; public byte[]? CanvasScratch;
public readonly byte[]?[] OutRing = new byte[8][]; public readonly byte[]?[] OutRing = new byte[8][];
public int OutRingNext; public int OutRingNext;
@@ -192,9 +192,13 @@ public sealed class WebView2Manager : IDisposable
// bounding box of non-transparent pixels (the background is injected // bounding box of non-transparent pixels (the background is injected
// transparent, so the alpha channel IS the widget). This is immune to // transparent, so the alpha channel IS the widget). This is immune to
// DOM timing (fonts/images/animations), needs no JS round-trip, and // DOM timing (fonts/images/animations), needs no JS round-trip, and
// CANNOT break rendering: if the widget fills the canvas the box equals // CANNOT break the render: if the widget fills the canvas the box equals
// the full frame (identical to the confirmed-good uncropped display); if // the full frame; if it floats in transparent margin the box is the
// it floats in transparent margin, the crop maps it flush under the box. // 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) 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; 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 = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
session.PreviewBitmap.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0); 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): // The preview bitmap is now MUTABLE and reused (CameraManager precedent):
// WPF re-renders it after WritePixels; handlers must not Freeze it. // WPF re-renders it after WritePixels; handlers must not Freeze it.