using System; namespace ytLive.Services; /// /// 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 /// 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): ScreenCaptureManager keeps each delivered frame as /// session.LatestFrame 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. /// internal sealed class FrameRingBuffer { private readonly byte[]?[] _slots; private readonly long[] _lastHandout; private readonly int _redLine; private readonly object _lock = new(); 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; } /// Buffers freshly allocated after the last . public long Allocations { get { lock (_lock) return _allocations; } } /// Returns the allocation count since the last call and resets it. public long ConsumeAllocations() { lock (_lock) { var count = _allocations; _allocations = 0; return count; } } /// Hands out a scratch buffer of bytes. public byte[] Rent(int size) { lock (_lock) { 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]; } } }