b37b8a30f9
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.
72 lines
3.1 KiB
C#
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());
|
|
}
|
|
} |