Files
LlamaCasty/Services/FrameRingBuffer.cs
T
gramps 71932b9756 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.
2026-09-14 18:18:34 -07:00

80 lines
3.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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];
}
}