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<ArgumentOutOfRangeException>(() => new FramePacer(frequency, target));
[Fact]
public void ReversedAndOverlappingTimestampsAreRejected()
{
var pacer = new FramePacer(1000, 60000);
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.ShouldRender(-1, true));
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(-1, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(10, 9));
pacer.Rendered(10, 20);
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(19, 21));
Assert.Equal(1, pacer.FramesRendered);
}
}
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<ArgumentOutOfRangeException>(() => new FramePacer(frequency, target));
[Fact]
public void ReversedAndOverlappingTimestampsAreRejected()
{
var pacer = new FramePacer(1000, 60000);
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.ShouldRender(-1, true));
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(-1, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(10, 9));
pacer.Rendered(10, 20);
Assert.Throws<ArgumentOutOfRangeException>(() => pacer.Rendered(19, 21));
Assert.Equal(1, pacer.FramesRendered);
}
}