sharpemu/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutDisplayClockTests.cs

129 lines
6.0 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using SharpEmu.Libs.VideoOut;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class VideoOutDisplayClockTests
{
[Theory]
[InlineData(60u)]
[InlineData(120u)]
public void RepeatedReadersDoNotAdvanceCountOrChangeEventTimestamps(uint refreshRate)
{
var openedAt = Stopwatch.Frequency;
var interval = VideoOutDisplayClock.RefreshInterval(refreshRate);
var clock = new VideoOutDisplayClock(openedAt, 1000, 2000, (ulong)Stopwatch.Frequency * 3);
clock.Advance(openedAt + interval - 1, refreshRate);
Assert.Equal(0UL, clock.Count);
Assert.Equal(0UL, clock.ProcessCounter);
Assert.Equal(openedAt + interval, clock.NextTimestamp(refreshRate));
for (var reader = 0; reader < 100; reader++)
{
clock.Advance(openedAt + 5 * interval + reader, refreshRate);
Assert.Equal(5UL, clock.Count);
Assert.Equal(openedAt + 5 * interval, clock.LastTimestamp);
Assert.Equal(1000UL + (ulong)(5 * interval), clock.ProcessCounter);
Assert.Equal(2000UL + (ulong)(15 * interval), clock.TimestampCounter);
Assert.Equal((ulong)((UInt128)clock.ProcessCounter * 1_000_000 / (ulong)Stopwatch.Frequency), clock.ProcessMicroseconds);
}
clock.Advance(openedAt + 2 * interval, refreshRate);
Assert.Equal(5UL, clock.Count);
Assert.Equal(openedAt + 6 * interval, clock.NextTimestamp(refreshRate));
}
[Theory]
[InlineData(60u, 0)]
[InlineData(60u, 1)]
[InlineData(60u, 2)]
[InlineData(120u, 0)]
[InlineData(120u, 1)]
[InlineData(120u, 2)]
public void VsyncDeadlineUsesPortRefreshRateAndFlipInterval(uint refreshRate, int flipRate)
{
var openedAt = Stopwatch.Frequency;
var interval = VideoOutDisplayClock.RefreshInterval(refreshRate) * (flipRate + 1);
Assert.Equal(openedAt + interval, GetNextFlipTimestamp(openedAt, openedAt, openedAt + 1, refreshRate, flipRate, 1));
var late = openedAt + 10 * interval + interval / 2;
Assert.Equal(late, GetNextFlipTimestamp(openedAt, openedAt, late, refreshRate, flipRate, 1));
Assert.Equal(openedAt + 11 * interval, GetNextFlipTimestamp(openedAt, late, late + 1, refreshRate, flipRate, 1));
}
[Fact]
public void AsapDoesNotInheritFlipRateLimit()
{
var openedAt = Stopwatch.Frequency;
Assert.Equal(openedAt + 1, GetNextFlipTimestamp(openedAt, openedAt, openedAt + 1, 60, 2, 2));
}
[Theory]
[InlineData(60u, 0)]
[InlineData(60u, 1)]
[InlineData(60u, 2)]
[InlineData(120u, 0)]
[InlineData(120u, 1)]
[InlineData(120u, 2)]
public void MultipleFlipsShareTheNextVblankWithoutMovingItOnRepeatedChecks(uint refreshRate, int flipRate)
{
var openedAt = Stopwatch.Frequency;
var refreshInterval = VideoOutDisplayClock.RefreshInterval(refreshRate);
var readyAt = openedAt + refreshInterval / 2;
var expectedBoundary = openedAt + refreshInterval;
foreach (var timestamp in new[] { readyAt, expectedBoundary - 1, expectedBoundary, expectedBoundary + 1,
expectedBoundary + 10 * refreshInterval })
{
Assert.Equal(expectedBoundary, VideoOutDisplayClock.NextFlipTimestamp(openedAt, timestamp,
timestamp, refreshRate, flipRate, 4, 1000, 100, 900, readyAt));
}
Assert.Equal(expectedBoundary, VideoOutDisplayClock.NextFlipTimestamp(openedAt, expectedBoundary,
expectedBoundary, refreshRate, flipRate, 4, 1000, 100, 900, readyAt + 1));
Assert.Equal(expectedBoundary + refreshInterval, VideoOutDisplayClock.NextFlipTimestamp(openedAt, expectedBoundary,
expectedBoundary, refreshRate, flipRate, 4, 1000, 100, 900, expectedBoundary));
}
[Fact]
public void WindowModeUsesScanlineMarginsAndDoesNotRepeatWithinOneWindow()
{
var openedAt = Stopwatch.Frequency;
var interval = VideoOutDisplayClock.RefreshInterval(60);
var bottom = interval * 900 / 1000;
var middle = openedAt + interval / 2;
Assert.Equal(openedAt + bottom, GetNextFlipTimestamp(openedAt, -1, middle, 60, 0, 3));
Assert.Equal(openedAt + bottom, GetNextFlipTimestamp(openedAt, openedAt, middle, 60, 0, 3));
var presented = openedAt + bottom + 1;
Assert.Equal(openedAt + interval + bottom, GetNextFlipTimestamp(openedAt, presented, presented + 1, 60, 0, 3));
Assert.Equal(openedAt + interval + 1, GetNextFlipTimestamp(openedAt, openedAt, openedAt + interval + 1, 60, 0, 3));
}
[Fact]
public void WindowModeWithoutMarginsUsesTheNextRefreshWithoutRequiringAnExactTimestamp()
{
var openedAt = Stopwatch.Frequency;
var interval = VideoOutDisplayClock.RefreshInterval(60);
var late = openedAt + interval + 1;
Assert.Equal(late, VideoOutDisplayClock.NextFlipTimestamp(openedAt, openedAt,
late, 60, 0, 3, 1080, 0, int.MaxValue, late));
Assert.Equal(late, VideoOutDisplayClock.NextFlipTimestamp(openedAt, -1,
late, 60, 0, 3, 1080, 0, int.MaxValue, late));
}
[Fact]
public void DifferentPortsHaveIndependentVblankOrigins()
{
var openedAt = Stopwatch.Frequency;
var interval = VideoOutDisplayClock.RefreshInterval(60);
var first = new VideoOutDisplayClock(openedAt, 0, 0, (ulong)Stopwatch.Frequency);
var second = new VideoOutDisplayClock(openedAt + interval / 2, 0, 0, (ulong)Stopwatch.Frequency);
first.Advance(openedAt + interval, 60);
second.Advance(openedAt + interval, 60);
Assert.Equal(1UL, first.Count);
Assert.Equal(0UL, second.Count);
}
private static long GetNextFlipTimestamp(long openedAt, long lastPresentedAt, long timestamp, uint refreshRate, int flipRate, int flipMode) =>
VideoOutDisplayClock.NextFlipTimestamp(openedAt, lastPresentedAt, timestamp, refreshRate, flipRate, flipMode, 1000, 100, 900, timestamp);
}