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:
@@ -209,6 +209,35 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer recycling (same lesson as the screen capture): a fresh ~3.7MB byte[]
|
||||
// per device frame at 30-60fps is 110-220MB/s of LOH churn — gen2 pauses land
|
||||
// on the frame pump as 35-40ms render spikes. An 8-deep ring (take-14 rule:
|
||||
// depth × source period must exceed the worst consumer hold), rotated on the
|
||||
// single frame-reader thread, with per-frame Epoch so the identity-keyed paste
|
||||
// cache can never serve a recycled array as unchanged.
|
||||
private readonly byte[]?[] _frameRing = new byte[8][];
|
||||
private int _ringNext;
|
||||
private long _epoch;
|
||||
|
||||
private byte[] RentRingBuffer(int size)
|
||||
{
|
||||
for (var tries = 0; tries < _frameRing.Length; tries++)
|
||||
{
|
||||
var idx = (_ringNext + tries) % _frameRing.Length;
|
||||
var buf = _frameRing[idx];
|
||||
if (buf != null && buf.Length == size)
|
||||
{
|
||||
_ringNext = (idx + 1) % _frameRing.Length;
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
var slot = _ringNext;
|
||||
_ringNext = (slot + 1) % _frameRing.Length;
|
||||
var fresh = new byte[size];
|
||||
_frameRing[slot] = fresh;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
private void OnFrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
|
||||
{
|
||||
using var frame = sender.TryAcquireLatestFrame();
|
||||
@@ -225,10 +254,11 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
|
||||
using var reference = buffer.CreateReference();
|
||||
if (WindowsRuntimeMarshal.TryGetDataUnsafe(reference, out var pixelsPtr, out var capacity))
|
||||
{
|
||||
var pixels = new byte[capacity];
|
||||
var pixels = RentRingBuffer((int)capacity);
|
||||
Marshal.Copy(pixelsPtr, pixels, 0, (int)capacity);
|
||||
// MediaCapture video carries no alpha plane — the OS fills 255.
|
||||
FrameAvailable?.Invoke(new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels) { IsOpaque = true });
|
||||
FrameAvailable?.Invoke(new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels)
|
||||
{ IsOpaque = true, Epoch = ++_epoch });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user