using XFESystem.Desktop; using Xunit; namespace XFESystem.Core.Tests; public sealed class FramePacerTests { [Fact] public void CleanDesktopDoesNotScheduleOrCountFrames() { var pacer = new FramePacer(10_000_000, 240000); Assert.False(pacer.ShouldRender(0, false)); Assert.True(pacer.ShouldRender(0, true)); pacer.Rendered(0, 100); long deadline = pacer.NextDeadlineTicks; Assert.False(pacer.ShouldRender(10_000_000, false)); Assert.Equal(1, pacer.FramesRendered); Assert.Equal(deadline, pacer.NextDeadlineTicks); Assert.True(pacer.ShouldRender(10_000_000, true)); } [Fact] public void TwoHundredFortyHzUsesTicksRatherThanTruncatedMilliseconds() { var pacer = new FramePacer(10_000_000, 240000); pacer.Rendered(0, 100); Assert.Equal(41667, pacer.NextDeadlineTicks); Assert.False(pacer.ShouldRender(41666, true)); Assert.True(pacer.ShouldRender(41667, true)); Assert.Equal(240000, pacer.TargetMilliHz); } [Theory] [InlineData(160000)] [InlineData(239760)] [InlineData(240000)] [InlineData(360000)] public void FractionalDeadlinesKeepExactPhaseOverTenThousandFrames(int target) { const long frequency = 10_000_000; var pacer = new FramePacer(frequency, target); long begin = 123; long origin = begin; for (int index = 1; index <= 10000; index++) { Assert.True(pacer.ShouldRender(begin, true)); pacer.Rendered(begin, begin); long expected = origin + (long)(((UInt128)(uint)index * (ulong)frequency * 1000 + (uint)target - 1) / (uint)target); Assert.Equal(expected, pacer.NextDeadlineTicks); begin = pacer.NextDeadlineTicks; } Assert.Equal(0, pacer.Overruns); Assert.Equal(10000, pacer.FramesRendered); } [Fact] public void LowFrequencyClockAccumulatesFractionRatherThanRoundingEveryPeriod() { var pacer = new FramePacer(1000, 240000); long[] expected = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42, 46, 50]; long begin = 0; foreach (long deadline in expected) { pacer.Rendered(begin, begin); Assert.Equal(deadline, pacer.NextDeadlineTicks); begin = deadline; } } [Fact] public void LateRenderCanPresentImmediatelyRatherThanWaitingAnotherFullPeriod() { var pacer = new FramePacer(1000, 100000); pacer.Rendered(0, 15); Assert.Equal(1, pacer.Overruns); Assert.Equal(15, pacer.LastRenderDurationTicks); Assert.Equal(10, pacer.NextDeadlineTicks); Assert.True(pacer.ShouldRender(15, true)); pacer.Rendered(15, 16); Assert.Equal(20, pacer.NextDeadlineTicks); Assert.False(pacer.ShouldRender(16, true)); } [Fact] public void ManyMissedDeadlinesAreSkippedWithoutBurstOfObsoleteFrames() { var pacer = new FramePacer(1000, 100000); pacer.Rendered(0, 95); Assert.Equal(90, pacer.NextDeadlineTicks); Assert.True(pacer.ShouldRender(95, true)); pacer.Rendered(95, 96); Assert.Equal(100, pacer.NextDeadlineTicks); Assert.False(pacer.ShouldRender(96, true)); Assert.Equal(2, pacer.FramesRendered); Assert.Equal(1, pacer.Overruns); } [Fact] public void ResumingAfterIdleJumpsToCurrentPhase() { var pacer = new FramePacer(1000, 100000); pacer.Rendered(0, 1); Assert.True(pacer.ShouldRender(100000, true)); pacer.Rendered(100000, 100001); Assert.Equal(100010, pacer.NextDeadlineTicks); Assert.Equal(2, pacer.FramesRendered); Assert.False(pacer.ShouldRender(100001, true)); } [Fact] public void ActualRendererCadenceDoesNotClaimTargetOrMonitorScanRate() { var pacer = new FramePacer(1000, 240000); Assert.Equal(0, pacer.ActualRendererFps); pacer.Rendered(0, 1); Assert.Equal(0, pacer.ActualRendererFps); pacer.Rendered(10, 11); pacer.Rendered(20, 21); Assert.Equal(100, pacer.ActualRendererFps); Assert.Equal(240000, pacer.TargetMilliHz); Assert.Equal(1, pacer.LastRenderDurationTicks); } [Fact] public void ExactlyOnePeriodIsNotCountedAsAnOverrun() { var pacer = new FramePacer(1000, 100000); pacer.Rendered(0, 10); Assert.Equal(0, pacer.Overruns); Assert.True(pacer.ShouldRender(10, true)); } [Fact] public void HugeFrequencyAndDeadlineAreHandledWithoutOverflow() { var pacer = new FramePacer(long.MaxValue, 1000); pacer.Rendered(10, 11); Assert.Equal(long.MaxValue, pacer.NextDeadlineTicks); Assert.False(pacer.ShouldRender(100, true)); } [Theory] [InlineData(0, 60000)] [InlineData(-1, 60000)] [InlineData(1000, 0)] [InlineData(1000, -1)] [InlineData(1, 2000)] public void InvalidClockOrTargetCannotCreateAZeroTickLoop(long frequency, int target) => Assert.Throws(() => new FramePacer(frequency, target)); [Fact] public void ReversedAndOverlappingTimestampsAreRejected() { var pacer = new FramePacer(1000, 60000); Assert.Throws(() => pacer.ShouldRender(-1, true)); Assert.Throws(() => pacer.Rendered(-1, 0)); Assert.Throws(() => pacer.Rendered(10, 9)); pacer.Rendered(10, 20); Assert.Throws(() => pacer.Rendered(19, 21)); Assert.Equal(1, pacer.FramesRendered); } }