Revert "fix(web): composite consumes the FULL canvas, alpha crop is measure-only — transparent web margins reveal the webcam again"

This reverts commit 0f441d825a.
This commit is contained in:
2026-09-05 16:21:16 -07:00
parent 0f441d825a
commit 1295e0ed80
4 changed files with 21 additions and 151 deletions
+20 -44
View File
@@ -33,36 +33,16 @@ public sealed class WebView2Manager : IDisposable
// 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). The full-canvas slots rotate an 8-deep ring with a
// monotonic Epoch (identity-keyed consumers — the compositor's paste cache —
// must see each generation); the crop slots feed the preview WriteableBitmap,
// recreated only when the cropped size changes.
public readonly byte[]?[] CanvasRing = new byte[8][];
public int CanvasRingNext;
public long Epoch;
// "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.
public byte[]? CanvasScratch;
public readonly byte[]?[] OutRing = new byte[8][];
public int OutRingNext;
public long Epoch;
public WriteableBitmap? PreviewBitmap;
public byte[] RentCanvasBuffer(int size)
{
for (var tries = 0; tries < CanvasRing.Length; tries++)
{
var idx = (CanvasRingNext + tries) % CanvasRing.Length;
var buf = CanvasRing[idx];
if (buf != null && buf.Length == size)
{
CanvasRingNext = (idx + 1) % CanvasRing.Length;
return buf;
}
}
var slot = CanvasRingNext;
CanvasRingNext = (slot + 1) % CanvasRing.Length;
var fresh = new byte[size];
CanvasRing[slot] = fresh;
return fresh;
}
public byte[] RentOutBuffer(int size)
{
for (var tries = 0; tries < OutRing.Length; tries++)
@@ -96,10 +76,11 @@ public sealed class WebView2Manager : IDisposable
// OBS model: the page renders at the MASTER CANVAS size (1920×1080),
// stable, never tracked. Full-bleed widgets (designed for the canvas)
// render fully instead of being clipped to a small box-sized viewport;
// dragging the box never reflows the page. The alpha bounding box
// (FindContentBounds) measures the widget's extent for the UI/selection;
// the COMPOSITE consumes the FULL canvas — the element rect is a viewport
// onto the page, whose transparent margins reveal the layers beneath.
// dragging the box never reflows the page. The capture is tightly
// cropped to the widget's ALPHA bounding box (FindContentBounds) and
// the display (Stretch=Fill) maps it flush under the box, anchored at
// (0,0) — no DOM query, so no timing fragility; worst case is the
// full frame, which is the confirmed-good rendering.
var webView = new WebView2
{
Width = 1920,
@@ -209,15 +190,11 @@ public sealed class WebView2Manager : IDisposable
// The widget's extent is measured from the captured frame itself: the
// bounding box of non-transparent pixels (the background is injected
// transparent, so the alpha channel IS the widget). Immune to DOM timing
// (fonts/images/animations), needs no JS round-trip, and 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 take-19-2/take-20 "black square
// over the webcam" was the CROP fed to the compositor: UniformToFill then
// zoomed the opaque content box to cover the whole element rect).
// 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.
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;
@@ -268,7 +245,9 @@ public sealed class WebView2Manager : IDisposable
if (pixW < 1 || pixH < 1) return;
var need = pixW * pixH * 4;
var pixels = session.RentCanvasBuffer(need);
var pixels = session.CanvasScratch is { } scratch && scratch.Length >= need
? scratch
: (session.CanvasScratch = new byte[need]);
formatted.CopyPixels(pixels, pixW * 4, 0);
var stride = pixW * 4;
@@ -287,10 +266,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);
// The compositor consumes the FULL canvas (alpha intact): the element rect
// scales the page region onto itself and the transparent margins reveal what
// is beneath. The alpha crop above exists only for the preview/selection.
session.LatestFrame = new VideoFrame(pixW, pixH, pixels) { Epoch = ++session.Epoch };
session.LatestFrame = new VideoFrame(cropW, cropH, outPixels) { Epoch = ++session.Epoch };
// The preview bitmap is now MUTABLE and reused (CameraManager precedent):
// WPF re-renders it after WritePixels; handlers must not Freeze it.