Reported as "the default video and the green screen video are playing at a hideously slow rate… I thought the playback timing issues were resolved?!" They were not: two independent faults, both in the shared clip pipe. 1. QUADRATIC COPY IN THE FRAME READER. `RawVideoFrameReader` re-concatenated the whole pending buffer on every `Feed`, and the pipe feeds it 64 KB at a time. A 1080p BGRA frame is 8 MB, so one frame cost ~127 copies of a growing buffer — roughly half a gigabyte of memcpy per frame. Measured on a 30 fps clip: 6 frames in 3 seconds (2 fps). The reader now fills one preallocated frame buffer and hands that array to the VideoFrame, so a frame is copied exactly once. Pipe reads are 256 KB. Same clip: 29 fps. 2. PACING SLEPT ON TOP OF THE WORK. A fixed 1/fps sleep after each frame adds the frame's own cost, so 20 ms of work on a 30 fps clip yields 1/(33+20) ≈ 19 fps — measured 20.9. Pacing is now deadline-based (`Helpers/FramePacer.cs`): wait only what is left of the frame's interval, resync when a frame overruns instead of bursting, never sleep longer than one interval. The alert clip shares this pipe, so it gained both fixes. Tests: `FramePacerTests` (subtract work, no drift, late → resync, zero duration, reset); `ChromaKeyRealClipTests.RealClip_PlaysAtItsNativeRate` (throughput floor of 15 fps on the real 30 fps clip — the old code sat at 2); the two pacing assertions now check "at most one interval" rather than an exact sleep, which is what deadline pacing means. Full suite: 574 passed, 0 failed, clean build.
75 lines
2.7 KiB
C#
75 lines
2.7 KiB
C#
using System;
|
|
using Xunit;
|
|
using ytLive.Helpers;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// 2026-11-05: a 1080p video layer played at 20.9 fps on a 30 fps clip because pacing slept a full
|
|
/// frame interval ON TOP of the frame's own work. These pin deadline pacing: sleep the remainder of
|
|
/// the interval, resync when work overruns, never burst to catch up.
|
|
/// </summary>
|
|
public class FramePacerTests
|
|
{
|
|
private static readonly TimeSpan Frame = TimeSpan.FromMilliseconds(33);
|
|
|
|
[Fact]
|
|
public void FirstFrame_WaitsAFullInterval()
|
|
=> Assert.Equal(Frame, new FramePacer().DelayFor(Frame, TimeSpan.Zero));
|
|
|
|
[Fact]
|
|
public void WorkInsideTheInterval_IsSubtractedFromTheWait()
|
|
{
|
|
var pacer = new FramePacer();
|
|
pacer.DelayFor(Frame, TimeSpan.Zero); // schedules the next frame at 33ms
|
|
|
|
// The next frame is emitted 20ms later: only 13ms of waiting is left, not another 33ms.
|
|
var wait = pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(20));
|
|
|
|
Assert.Equal(TimeSpan.FromMilliseconds(13), wait);
|
|
}
|
|
|
|
[Fact]
|
|
public void WorkOnSchedule_DoesNotAccumulateDrift()
|
|
{
|
|
var pacer = new FramePacer();
|
|
Assert.Equal(Frame, pacer.DelayFor(Frame, TimeSpan.Zero));
|
|
|
|
// A frame arriving exactly on its deadline waits not at all…
|
|
Assert.Equal(TimeSpan.Zero, pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(33)));
|
|
|
|
// …and a frame arriving 3ms early waits exactly the 3ms that is left, every time: the
|
|
// deadline advances one interval per frame, so no drift accumulates.
|
|
Assert.Equal(TimeSpan.FromMilliseconds(3), pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(63)));
|
|
Assert.Equal(TimeSpan.FromMilliseconds(3), pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(96)));
|
|
}
|
|
|
|
[Fact]
|
|
public void FallingBehind_ReturnsZeroAndResyncs()
|
|
{
|
|
var pacer = new FramePacer();
|
|
pacer.DelayFor(Frame, TimeSpan.Zero);
|
|
|
|
// The frame took 200ms: it is late, so no sleep — and the schedule restarts from now.
|
|
var late = pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(200));
|
|
Assert.Equal(TimeSpan.Zero, late);
|
|
|
|
var next = pacer.DelayFor(Frame, TimeSpan.FromMilliseconds(200));
|
|
Assert.Equal(Frame, next);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroFrameDuration_NeverWaits()
|
|
=> Assert.Equal(TimeSpan.Zero, new FramePacer().DelayFor(TimeSpan.Zero, TimeSpan.Zero));
|
|
|
|
[Fact]
|
|
public void Reset_RestartsTheSchedule()
|
|
{
|
|
var pacer = new FramePacer();
|
|
pacer.DelayFor(Frame, TimeSpan.Zero);
|
|
pacer.Reset();
|
|
|
|
Assert.Equal(Frame, pacer.DelayFor(Frame, TimeSpan.FromSeconds(10)));
|
|
}
|
|
}
|