perf(capture): fast integer downscale + 10ms cadence floor + reuse-distance ring (slice 16)
The 240Hz monitor delivery + one-in-flight conversions + naive double-per-pixel DownscaleBgra (~150ms/frame under load) froze the desktop layer 90% of take ty-1742 (6.1 fresh content updates/s, freeze runs to 2.8s; decoded raw-frame audit). The render stat (33-36ms) was real but moot — the capture CONVERSION was the wall, and the one torn frame was a ring slot rewritten under the consumer's read. Reference: WGC delivers at DWM/monitor cadence (https://learn.microsoft.com/en-us/windows/apps/develop/media-authoring-processing/screen-capture) and libyuv row-simple/fixed-point scaling (https://chromium.googlesource.com/libyuv/libyuv/) — the repo's own take-4 rule. - DownscaleBgra: integer 8.8 fixed-point, shift-only-at-the-end (same two-stage math as SceneCompositor.Bilinear). ~150ms -> ~5ms per 2.5K->1080p frame. - 10ms MinConvertInterval: the ~4.2ms 240Hz tail stopped queuing ~150ms of serialized conversion/s; capacity sits just above the 60/s the pump can use. - FrameRingBuffer (depth 8, redLine 4): reuse-DISTANCE ring — a buffer is only rewritten >=4 rents after its last hand-out else fresh-allocated, so a frame a consumer still holds (session.LatestFrame survives conversions, dispatcher preview lags) is never read-while-overwritten. Needs no consumer Release API. - 2s startup.log telemetry: frames/s, conv avg/max ms, skip busy/cadence, ring allocs — the device take is judgeable numerically. Good Dog test: Ring_NoLap_ReusesOnlyAfterRedLineRents. 295/295 green, 0 warnings. C4 (composite Epoch-cached downscale) deferred pending the device re-measure. Local only, no push.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
namespace ytLive.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A depth-bounded ring of scratch frame buffers that never writes into memory a live
|
||||
/// consumer may still be reading. A slot's byte[] is only rewritten after at least
|
||||
/// <paramref name="redLine"/> rents have cycled since it was last handed out; a slot
|
||||
/// still inside its red line is skipped, and if the whole ring is inside its red line a
|
||||
/// fresh buffer is handed out rather than lapping a loaned slot. Length-mismatched
|
||||
/// buffers are always replaced by a fresh allocation (the previous array is orphaned,
|
||||
/// never overwritten), so a hand-out keeps its bytes for as long as anyone holds it.
|
||||
///
|
||||
/// The screen capture's consumer contract makes this structural necessity (slice 16,
|
||||
/// 2026-09-14): <c>ScreenCaptureManager</c> keeps each delivered frame as
|
||||
/// <c>session.LatestFrame</c> across conversions and the dispatcher preview copy lags,
|
||||
/// so a held frame can survive several conversions. The 1742 take recorded one torn
|
||||
/// frame (new-top/old-bottom at the webcam split) — a ring slot rewritten under the
|
||||
/// consumer's read. Depth 8 × the conversion interval already exceeded the worst
|
||||
/// observed hold (~50ms, take-14 rule); the red line makes the guarantee structural
|
||||
/// instead of a sizing coincidence.
|
||||
/// </summary>
|
||||
internal sealed class FrameRingBuffer
|
||||
{
|
||||
private readonly byte[]?[] _slots;
|
||||
private readonly long[] _lastHandout;
|
||||
private readonly int _redLine;
|
||||
private int _next;
|
||||
private long _seq;
|
||||
private long _allocations;
|
||||
|
||||
public FrameRingBuffer(int depth, int redLine)
|
||||
{
|
||||
_slots = new byte[depth][];
|
||||
_lastHandout = new long[depth];
|
||||
for (var i = 0; i < depth; i++) _lastHandout[i] = -redLine;
|
||||
_redLine = redLine;
|
||||
}
|
||||
|
||||
/// <summary>Buffers freshly allocated after the last <see cref="ConsumeAllocations"/>.</summary>
|
||||
public long Allocations => _allocations;
|
||||
|
||||
/// <summary>Returns the allocation count since the last call and resets it.</summary>
|
||||
public long ConsumeAllocations()
|
||||
{
|
||||
var count = _allocations;
|
||||
_allocations = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>Hands out a scratch buffer of <paramref name="size"/> bytes.</summary>
|
||||
public byte[] Rent(int size)
|
||||
{
|
||||
var seq = ++_seq;
|
||||
for (var tries = 0; tries < _slots.Length; tries++)
|
||||
{
|
||||
var idx = (_next + tries) % _slots.Length;
|
||||
// Red line: rewriting this slot could hit a frame a consumer still reads.
|
||||
if (seq - _lastHandout[idx] < _redLine) continue;
|
||||
_next = (idx + 1) % _slots.Length;
|
||||
if (_slots[idx] is { Length: var len } buf && len == size)
|
||||
{
|
||||
_lastHandout[idx] = seq;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Length mismatch (or never allocated): a fresh array, never an in-place
|
||||
// overwrite — the previous loan's bytes stay valid for whoever holds it.
|
||||
_allocations++;
|
||||
var fresh = new byte[size];
|
||||
_slots[idx] = fresh;
|
||||
_lastHandout[idx] = seq;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
// Defensive: the whole ring is inside its red line — do not lap a loaned slot.
|
||||
_allocations++;
|
||||
return new byte[size];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user