feat(ux)+fix(rings): release counter #N in the wordmark; all shared-frame rings 4->8 (#11)
Roll-forward of today-slices 6fd1d9c onto the slice-8 base, two-loop hunk dropped. Release counter (per-build GUID read as noise; +1 per commit from git rev-list, baseline 241 -> #13, generated by GenerateBuildStamp; GUID demotes to startup.log). Tests pinned to Label/#N >= 13; wordmark display test asserts the Label. Flash fix (take 14 finding): consumer holds must never outlive depth x source period — 4 slots at high refresh lap ~27ms vs a <=50ms compositor read, so a recycled slot flashed its new frame over the lagged old one. All shared rings 4->8 (OBS/overlay precedent for ring discipline). Camera producer now rotates an 8-deep ring + Epoch instead of a fresh ~3.7MB array per device frame (110-220MB/s LOH churn); WebView2 capture reuses a canvas scratch + 8-deep output ring + a reused WriteableBitmap instead of two fresh arrays + a fresh bitmap per 10Hz tick. Paste cache stays identity-keyed (Epoch).
This commit is contained in:
+50
-13
@@ -29,6 +29,38 @@ public sealed class WebView2Manager : IDisposable
|
||||
public VideoFrame? LatestFrame;
|
||||
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.
|
||||
public byte[]? CanvasScratch;
|
||||
public readonly byte[]?[] OutRing = new byte[8][];
|
||||
public int OutRingNext;
|
||||
public long Epoch;
|
||||
public WriteableBitmap? PreviewBitmap;
|
||||
|
||||
public byte[] RentOutBuffer(int size)
|
||||
{
|
||||
for (var tries = 0; tries < OutRing.Length; tries++)
|
||||
{
|
||||
var idx = (OutRingNext + tries) % OutRing.Length;
|
||||
var buf = OutRing[idx];
|
||||
if (buf != null && buf.Length == size)
|
||||
{
|
||||
OutRingNext = (idx + 1) % OutRing.Length;
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
var slot = OutRingNext;
|
||||
OutRingNext = (slot + 1) % OutRing.Length;
|
||||
var fresh = new byte[size];
|
||||
OutRing[slot] = fresh;
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
||||
public WebView2Manager(Panel hostPanel, Dispatcher dispatcher)
|
||||
@@ -212,31 +244,36 @@ public sealed class WebView2Manager : IDisposable
|
||||
|
||||
if (pixW < 1 || pixH < 1) return;
|
||||
|
||||
var pixels = new byte[pixW * pixH * 4];
|
||||
var need = pixW * pixH * 4;
|
||||
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;
|
||||
|
||||
var (cropX, cropY, cropW, cropH) = FindContentBounds(pixels, pixW, pixH, stride);
|
||||
|
||||
var outPixels = pixels;
|
||||
if (cropW != pixW || cropH != pixH || cropX != 0 || cropY != 0)
|
||||
{
|
||||
var dstStride = cropW * 4;
|
||||
outPixels = new byte[dstStride * cropH];
|
||||
var outPixels = session.RentOutBuffer(cropW * cropH * 4);
|
||||
if (cropW == pixW && cropH == pixH && cropX == 0 && cropY == 0)
|
||||
Buffer.BlockCopy(pixels, 0, outPixels, 0, outPixels.Length);
|
||||
else
|
||||
for (var r = 0; r < cropH; r++)
|
||||
Buffer.BlockCopy(pixels, (cropY + r) * stride + cropX * 4, outPixels, r * dstStride, dstStride);
|
||||
}
|
||||
Buffer.BlockCopy(pixels, (cropY + r) * stride + cropX * 4, outPixels, r * cropW * 4, cropW * 4);
|
||||
|
||||
var wb = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
|
||||
wb.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0);
|
||||
wb.Freeze();
|
||||
if (session.PreviewBitmap == null
|
||||
|| session.PreviewBitmap.PixelWidth != cropW || session.PreviewBitmap.PixelHeight != cropH)
|
||||
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);
|
||||
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.
|
||||
var preview = session.PreviewBitmap;
|
||||
_ = _dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
PreviewBitmapChanged?.Invoke(sourceId, wb);
|
||||
PreviewBitmapChanged?.Invoke(sourceId, preview);
|
||||
});
|
||||
}
|
||||
catch (ObjectDisposedException) { }
|
||||
|
||||
Reference in New Issue
Block a user