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];
}
}
}