Files
LlamaCasty/ytLive.Tests/ScreenCaptureFrameSourceTests.cs
gramps b37b8a30f9 perf(capture): overlap GPU readbacks with monotonic publish gate (slice 17)
Measure take ty-1824 on the slice-16 build: the downscale fix worked (conv
~47ms, ring allocs 0) but desktop band was still 88% frozen at 6.8 updates/s.
Telemetry isolated the real wall — CreateCopyFromSurfaceAsync readback ~45ms
of each conversion, serialized one-in-flight => ~17/s capture cap. Docs fact:
pool-sized surfaces CLIP, not scale (Microsoft Learn), so readback stays
native; the lever is concurrency.

- MaxConcurrentConversions=3 with pool 2->5 buffers (in-flight frames fit)
- new MonotonicGate (Interlocked compare-exchange): stale OLDER completions
  are dropped, never overwrite a newer LatestFrame (mirror of 1742 tear)
- FrameRingBuffer.Rent/ConsumeAllocations now lock; downscale row scratch is
  per-conversion locals
- Good Dog test PublishGate_TryPublish_OnlyStrictlyNewerWins; 296/296 green,
  0 warnings; docs cited Microsoft screen-capture page + libyuv fixed-point.

Local only, no push.
2026-09-14 18:40:20 -07:00

72 lines
3.1 KiB
C#

using System;
using Xunit;
using ytLive.Services;
namespace ytLive.Tests;
/// <summary>
/// The capture conversion ring (<see cref="FrameRingBuffer"/>) is a pure,
/// deterministic piece of <see cref="ScreenCaptureFrameSource"/> — the WGC
/// pool/session layer is WinRT and exercised only on Windows at runtime. These tests
/// pin the slice-16 no-lap contract: a slot is never rewritten within redLine rents
/// of its last hand-out (the 1742 tear — a ring slot rewritten under the consumer's
/// read handed the compositor one new-top/old-bottom frame).
/// </summary>
public class ScreenCaptureFrameSourceTests
{
[Fact]
public void PublishGate_TryPublish_OnlyStrictlyNewerWins()
{
// Slice 17: overlapping conversions can finish out of order; the monotonic
// gate must keep a slow OLDER completion from overwriting a newer LatestFrame.
var gate = new MonotonicGate();
Assert.True(gate.TryPublish(1));
Assert.True(gate.TryPublish(2));
Assert.False(gate.TryPublish(1)); // stale replay (older than 2)
Assert.True(gate.TryPublish(3));
Assert.False(gate.TryPublish(3)); // duplicate — never accepted twice
Assert.False(gate.TryPublish(2)); // late older completion
Assert.False(gate.TryPublish(long.MinValue));
Assert.True(gate.TryPublish(long.MaxValue));
}
[Fact]
public void Ring_NoLap_ReusesOnlyAfterRedLineRents()
{
// Depth 3 / redLine 4: the ring is narrower than its safety distance, so the
// red-line skip is actually exercised (the production 8/4 config can never
// block a rotation — the ring cycles before any slot comes due).
var ring = new FrameRingBuffer(depth: 3, redLine: 4);
const int size = 100;
var a = ring.Rent(size);
var b = ring.Rent(size);
var c = ring.Rent(size);
Assert.NotSame(a, b);
Assert.NotSame(b, c);
Assert.NotSame(a, c);
Assert.Equal(3, ring.ConsumeAllocations());
// 4th rent arrives while all three slots are inside their red line: the ring
// must hand out a fresh buffer instead of rewriting a still-loaned slot.
var d = ring.Rent(size);
Assert.NotSame(d, a);
Assert.NotSame(d, b);
Assert.NotSame(d, c);
Assert.Equal(1, ring.ConsumeAllocations());
// 5th rent: slot a (handed at seq 1, revisited at seq 5 = exactly redLine)
// is reusable, and reuse is an in-place recycle, not a fresh allocation.
Assert.Same(a, ring.Rent(size));
Assert.Equal(0, ring.ConsumeAllocations());
// The production-sized ring (8/4, what ScreenCaptureFrameSource uses) settles
// at 8 buffers and recycles them forever — no growth under steady capture.
var prod = new FrameRingBuffer(depth: 8, redLine: 4);
var first = new byte[8][];
for (var i = 0; i < 8; i++) first[i] = prod.Rent(size);
for (var i = 8; i < 200; i++)
Assert.Same(first[i % 8], prod.Rent(size));
Assert.Equal(8, prod.ConsumeAllocations());
}
}