500 lines
23 KiB
C#
500 lines
23 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Diagnostics;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using SharpEmu.Libs.Kernel;
|
|
using SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
|
|
// Completion (counters, events) and presentation (the frame is done) are independent
|
|
// responsibilities; the request leaves the table only after both, in either order.
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class VideoOutFlipRequestTests : IDisposable
|
|
{
|
|
private const ulong MemoryBase = 0x1_0000_0000;
|
|
private const ulong StatusAddress = MemoryBase + 0x100;
|
|
|
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
|
private readonly CpuContext _context;
|
|
private readonly int _handle;
|
|
|
|
public VideoOutFlipRequestTests()
|
|
{
|
|
_context = new CpuContext(_memory, Generation.Gen5);
|
|
_context[CpuRegister.Rdi] = 0;
|
|
_context[CpuRegister.Rsi] = 0;
|
|
_context[CpuRegister.Rdx] = 0;
|
|
_context[CpuRegister.Rcx] = 0;
|
|
_handle = VideoOutExports.VideoOutOpen(_context);
|
|
Assert.True(_handle > 0);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_context[CpuRegister.Rdi] = unchecked((ulong)_handle);
|
|
_ = VideoOutExports.VideoOutClose(_context);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChangeBufferAttribute2UpdatesTheRegisteredGroupAndValidatesArguments()
|
|
{
|
|
var manager = new ModuleManager();
|
|
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
|
Assert.True(manager.TryGetExport("HuViW4HnrOw", out var export));
|
|
Assert.Equal("sceVideoOutSubmitChangeBufferAttribute2", export.Name);
|
|
|
|
var ports = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
var port = ports[_handle]!;
|
|
// Seed registration without starting a native presenter in this state test.
|
|
var groups = (Array)port.GetType().GetProperty("Groups")!.GetValue(port)!;
|
|
groups.SetValue(Activator.CreateInstance(groups.GetType().GetElementType()!, nonPublic: true), 0);
|
|
var slots = (Array)port.GetType().GetProperty("BufferSlots")!.GetValue(port)!;
|
|
var slot = slots.GetValue(0)!;
|
|
slot.GetType().GetProperty("GroupIndex")!.SetValue(slot, 0);
|
|
slot.GetType().GetProperty("AddressLeft")!.SetValue(slot, MemoryBase);
|
|
|
|
const ulong pqFormat = 0x8100070422000000;
|
|
var attribute = new byte[0x50];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x0C), 3840);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x10), 2160);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(attribute.AsSpan(0x18), 8);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(attribute.AsSpan(0x20), pqFormat);
|
|
Assert.True(_memory.TryWrite(StatusAddress, attribute));
|
|
|
|
ulong Change(int setIndex = 0, ulong address = StatusAddress, ulong option = 0, int? handle = null)
|
|
{
|
|
_context[CpuRegister.Rdi] = unchecked((ulong)(handle ?? _handle));
|
|
_context[CpuRegister.Rsi] = unchecked((ulong)setIndex);
|
|
_context[CpuRegister.Rdx] = address;
|
|
_context[CpuRegister.Rcx] = option;
|
|
Assert.True(manager.TryDispatch("HuViW4HnrOw", _context, out _));
|
|
return _context[CpuRegister.Rax];
|
|
}
|
|
|
|
Assert.Equal(0UL, Change());
|
|
Assert.True(VideoOutExports.TryGetDisplayBufferInfo(_handle, 0, out var info));
|
|
Assert.Equal(new VideoOutExports.DisplayBufferInfo(MemoryBase, pqFormat, 0, 3840, 2160, 3840, 8), info);
|
|
Assert.Equal(3840u, port.GetType().GetProperty("OutputWidth")!.GetValue(port));
|
|
Assert.Equal(2160u, port.GetType().GetProperty("OutputHeight")!.GetValue(port));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029000B), Change(handle: -1));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029001A), Change(address: 0));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029001A), Change(option: 1));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: -1));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: 4));
|
|
Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: 1));
|
|
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT), Change(address: MemoryBase + 0x1000));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x0C), 0);
|
|
Assert.True(_memory.TryWrite(StatusAddress, attribute));
|
|
Assert.Equal(unchecked((ulong)(int)0x80290001), Change());
|
|
Assert.True(VideoOutExports.TryGetDisplayBufferInfo(_handle, 0, out var afterFailure));
|
|
Assert.Equal(info, afterFailure);
|
|
}
|
|
|
|
[Fact]
|
|
public void CleanupPreservesAnotherPortsPendingFlip()
|
|
{
|
|
var request = Reserve(42);
|
|
using (var otherPort = new VideoOutFlipRequestTests())
|
|
{
|
|
otherPort.Reserve(43);
|
|
}
|
|
|
|
Assert.Equal(VideoOutExports.FlipOutcome.Pending, VideoOutExports.GetFlipOutcomeForTests(request));
|
|
var before = FlipCount();
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before + 1, FlipCount());
|
|
VideoOutExports.MarkFlipPresented(request);
|
|
}
|
|
|
|
private ulong Reserve(long flipArg)
|
|
{
|
|
Assert.Equal(0, VideoOutExports.TryReserveFlipRequest(_handle, -1, 0, flipArg, gpuQueued: true, out var requestId));
|
|
return requestId;
|
|
}
|
|
|
|
private ulong FlipCount()
|
|
{
|
|
_context[CpuRegister.Rdi] = unchecked((ulong)_handle);
|
|
_context[CpuRegister.Rsi] = StatusAddress;
|
|
Assert.Equal(0, VideoOutExports.VideoOutGetFlipStatus(_context));
|
|
Span<byte> status = stackalloc byte[8];
|
|
Assert.True(_memory.TryRead(StatusAddress, status));
|
|
return BinaryPrimitives.ReadUInt64LittleEndian(status);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, false)]
|
|
[InlineData(2, true)]
|
|
[InlineData(4, false)]
|
|
public void PresentationEligibilityUsesTheRequestedFlipMode(int flipMode, bool immediatelyEligible)
|
|
{
|
|
Assert.Equal(0, VideoOutExports.TryReserveFlipRequest(_handle, -1, flipMode, 21, true, out var request));
|
|
// Fix the previous presentation time to avoid a wall-clock boundary race.
|
|
var ports = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
var port = ports[_handle]!;
|
|
var timestamp = Stopwatch.GetTimestamp();
|
|
port.GetType().GetField("LastPresentationTimestamp")!.SetValue(port, timestamp);
|
|
Assert.Equal(immediatelyEligible, VideoOutExports.CanPresentFlip(request, timestamp));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public void MultipleGpuFlipsWaitForReadinessAndShareOneVblank(int flipRate)
|
|
{
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
_context[CpuRegister.Rsi] = (ulong)flipRate;
|
|
Assert.Equal(0, VideoOutExports.VideoOutSetFlipRate(_context));
|
|
Assert.Equal(0, VideoOutExports.TryReserveFlipRequest(_handle, -1, 4, 31, true, out var firstRequest));
|
|
Assert.Equal(0, VideoOutExports.TryReserveFlipRequest(_handle, -1, 4, 32, true, out var secondRequest));
|
|
|
|
var ports = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
var port = ports[_handle]!;
|
|
var openedAt = (long)port.GetType().GetField("OpenTimestamp")!.GetValue(port)!;
|
|
var refreshInterval = VideoOutDisplayClock.RefreshInterval(60);
|
|
var readyAt = openedAt + refreshInterval / 2;
|
|
var boundary = openedAt + refreshInterval;
|
|
Assert.False(VideoOutExports.CanPresentFlip(firstRequest, boundary + refreshInterval));
|
|
Assert.False(VideoOutExports.CanPresentFlip(secondRequest, boundary + refreshInterval));
|
|
|
|
var requests = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_flipRequests", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
foreach (var requestId in new[] { firstRequest, secondRequest })
|
|
{
|
|
var beforeCompletion = Stopwatch.GetTimestamp();
|
|
VideoOutExports.CompleteFlip(requestId);
|
|
var request = requests[requestId]!;
|
|
var readyTimestampField = request.GetType().GetField("ReadyTimestamp")!;
|
|
var completionTimestamp = Assert.IsType<long>(readyTimestampField.GetValue(request));
|
|
Assert.InRange(completionTimestamp, beforeCompletion, Stopwatch.GetTimestamp());
|
|
// Fixed readiness avoids a wall-clock race between the two completion calls.
|
|
readyTimestampField.SetValue(request, (long?)readyAt);
|
|
VideoOutExports.CompleteFlip(requestId);
|
|
Assert.Equal(readyAt, Assert.IsType<long>(readyTimestampField.GetValue(request)));
|
|
Assert.False(VideoOutExports.CanPresentFlip(requestId, boundary - 1));
|
|
Assert.True(VideoOutExports.CanPresentFlip(requestId, boundary));
|
|
}
|
|
|
|
VideoOutExports.MarkFlipPresented(firstRequest);
|
|
Assert.True(VideoOutExports.CanPresentFlip(secondRequest, boundary));
|
|
Assert.True(VideoOutExports.CanPresentFlip(secondRequest, boundary + 10 * refreshInterval));
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
Assert.Equal(0, VideoOutExports.VideoOutClose(_context));
|
|
Assert.False(VideoOutExports.CanPresentFlip(secondRequest, boundary));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MultipleCpuFlipWaitReachesTheBoundaryWithoutMovingItsDeadline()
|
|
{
|
|
var ports = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
var port = ports[_handle]!;
|
|
var openedAt = (long)port.GetType().GetField("OpenTimestamp")!.GetValue(port)!;
|
|
var refreshInterval = VideoOutDisplayClock.RefreshInterval(60);
|
|
var beforeWait = Stopwatch.GetTimestamp();
|
|
var firstBoundary = openedAt + ((beforeWait - openedAt) / refreshInterval + 1) * refreshInterval;
|
|
var paceFlip = typeof(VideoOutExports).GetMethod("PaceFlip",
|
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;
|
|
var completion = Task.Run(() => (bool)paceFlip.Invoke(null, new object[] { _handle, 4 })!);
|
|
try
|
|
{
|
|
Assert.True(await completion.WaitAsync(TimeSpan.FromSeconds(5)));
|
|
Assert.True(Stopwatch.GetTimestamp() >= firstBoundary);
|
|
}
|
|
finally
|
|
{
|
|
if (!completion.IsCompleted)
|
|
{
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
VideoOutExports.VideoOutClose(_context);
|
|
await completion.WaitAsync(TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void VblankStatusContainsTheLastEventProcessCounter()
|
|
{
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
Assert.Equal(0, VideoOutExports.VideoOutWaitVblank(_context));
|
|
_context[CpuRegister.Rsi] = StatusAddress;
|
|
Assert.Equal(0, VideoOutExports.VideoOutGetVblankStatus(_context));
|
|
Span<byte> status = stackalloc byte[0x28];
|
|
Assert.True(_memory.TryRead(StatusAddress, status));
|
|
Assert.True(BinaryPrimitives.ReadUInt64LittleEndian(status) >= 1);
|
|
var processCounter = BinaryPrimitives.ReadUInt64LittleEndian(status[0x18..]);
|
|
Assert.NotEqual(0UL, processCounter);
|
|
Assert.Equal((ulong)((UInt128)processCounter * 1_000_000 / (ulong)Stopwatch.Frequency),
|
|
BinaryPrimitives.ReadUInt64LittleEndian(status[0x08..]));
|
|
Assert.NotEqual(0UL, BinaryPrimitives.ReadUInt64LittleEndian(status[0x10..]));
|
|
}
|
|
|
|
[Fact]
|
|
public void VblankEventsAndWaitsUseTheSameDisplayCount()
|
|
{
|
|
_context[CpuRegister.Rdi] = StatusAddress;
|
|
_context[CpuRegister.Rsi] = 0;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(_context));
|
|
var bytes = new byte[8];
|
|
Assert.True(_memory.TryRead(StatusAddress, bytes));
|
|
var queue = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
try
|
|
{
|
|
_context[CpuRegister.Rdi] = queue;
|
|
_context[CpuRegister.Rsi] = (ulong)_handle;
|
|
_context[CpuRegister.Rdx] = 0;
|
|
Assert.Equal(0, VideoOutExports.VideoOutAddVblankEvent(_context));
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
Assert.Equal(0, VideoOutExports.VideoOutWaitVblank(_context));
|
|
var timeout = BitConverter.GetBytes(1_000_000u);
|
|
Assert.True(_memory.TryWrite(MemoryBase + 0x400, timeout));
|
|
_context[CpuRegister.Rdi] = queue;
|
|
_context[CpuRegister.Rsi] = MemoryBase + 0x200;
|
|
_context[CpuRegister.Rdx] = 1;
|
|
_context[CpuRegister.Rcx] = MemoryBase + 0x300;
|
|
_context[CpuRegister.R8] = MemoryBase + 0x400;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelWaitEqueue(_context));
|
|
Assert.True(_memory.TryRead(MemoryBase + 0x210, bytes));
|
|
var eventCount = BinaryPrimitives.ReadUInt64LittleEndian(bytes) >> 16;
|
|
Assert.True(eventCount >= 1);
|
|
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
_context[CpuRegister.Rsi] = StatusAddress;
|
|
Assert.Equal(0, VideoOutExports.VideoOutGetVblankStatus(_context));
|
|
Assert.True(_memory.TryRead(StatusAddress, bytes));
|
|
var statusCount = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
Assert.True(statusCount >= eventCount);
|
|
var ports = (System.Collections.IDictionary)typeof(VideoOutExports)
|
|
.GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!;
|
|
var port = ports[_handle]!;
|
|
var openedAt = (long)port.GetType().GetField("OpenTimestamp")!.GetValue(port)!;
|
|
var elapsedCount = (ulong)((Stopwatch.GetTimestamp() - openedAt) / VideoOutDisplayClock.RefreshInterval(60));
|
|
Assert.True(statusCount <= elapsedCount);
|
|
}
|
|
finally
|
|
{
|
|
_context[CpuRegister.Rdi] = queue;
|
|
_context[CpuRegister.Rsi] = (ulong)_handle;
|
|
Assert.Equal(0, VideoOutExports.VideoOutDeleteVblankEvent(_context));
|
|
_context[CpuRegister.Rdi] = queue;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(_context));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OutputModeEventIsImmediatelyAvailableAndTracksConfiguration()
|
|
{
|
|
_context[CpuRegister.Rdi] = StatusAddress;
|
|
_context[CpuRegister.Rsi] = 0;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(_context));
|
|
var bytes = new byte[8];
|
|
Assert.True(_memory.TryRead(StatusAddress, bytes));
|
|
var queue = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
try
|
|
{
|
|
const ulong userData = 0x1234_5678_9ABC_DEF0;
|
|
_context[CpuRegister.Rdi] = queue;
|
|
_context[CpuRegister.Rsi] = (ulong)_handle;
|
|
_context[CpuRegister.Rdx] = userData;
|
|
Assert.Equal(0, VideoOutExports.VideoOutAddOutputModeEvent(_context));
|
|
|
|
AssertOutputModeEvent(queue, expectedMode: 1, userData);
|
|
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
_context[CpuRegister.Rsi] = 1;
|
|
_context[CpuRegister.Rdx] = 0;
|
|
_context[CpuRegister.Rcx] = 0;
|
|
_context[CpuRegister.R8] = 0;
|
|
Assert.Equal(0, VideoOutExports.VideoOutConfigureOutput(_context));
|
|
|
|
AssertOutputModeEvent(queue, expectedMode: 1, userData);
|
|
}
|
|
finally
|
|
{
|
|
_context[CpuRegister.Rdi] = queue;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(_context));
|
|
}
|
|
}
|
|
|
|
private void AssertOutputModeEvent(ulong queue, ulong expectedMode, ulong userData)
|
|
{
|
|
var timeoutAddress = MemoryBase + 0x400;
|
|
Assert.True(_memory.TryWrite(timeoutAddress, BitConverter.GetBytes(1_000_000u)));
|
|
_context[CpuRegister.Rdi] = queue;
|
|
_context[CpuRegister.Rsi] = MemoryBase + 0x200;
|
|
_context[CpuRegister.Rdx] = 1;
|
|
_context[CpuRegister.Rcx] = MemoryBase + 0x300;
|
|
_context[CpuRegister.R8] = timeoutAddress;
|
|
Assert.Equal(0, KernelEventQueueCompatExports.KernelWaitEqueue(_context));
|
|
|
|
var eventBytes = new byte[0x20];
|
|
Assert.True(_memory.TryRead(MemoryBase + 0x200, eventBytes));
|
|
Assert.Equal(8UL, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes));
|
|
Assert.Equal(-13, BinaryPrimitives.ReadInt16LittleEndian(eventBytes.AsSpan(0x08)));
|
|
Assert.Equal(expectedMode, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x10)) >> 16);
|
|
Assert.Equal(userData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x18)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public void RefreshEligibility_UsesDisplayIntervalsWithoutAccumulatingSubmissionDebt(int flipRate)
|
|
{
|
|
var openedAt = Stopwatch.Frequency;
|
|
var interval = Stopwatch.Frequency / 60 * (flipRate + 1);
|
|
Assert.True(VideoOutExports.IsRefreshAvailable(openedAt, -1, openedAt, 60, flipRate));
|
|
Assert.False(VideoOutExports.IsRefreshAvailable(openedAt, openedAt, openedAt + interval - 1, 60, flipRate));
|
|
Assert.True(VideoOutExports.IsRefreshAvailable(openedAt, openedAt, openedAt + interval, 60, flipRate));
|
|
|
|
var latePresentation = openedAt + 100 * interval + interval / 2;
|
|
Assert.True(VideoOutExports.IsRefreshAvailable(openedAt, openedAt, latePresentation, 60, flipRate));
|
|
Assert.False(VideoOutExports.IsRefreshAvailable(openedAt, latePresentation, latePresentation + 1, 60, flipRate));
|
|
Assert.True(VideoOutExports.IsRefreshAvailable(openedAt, latePresentation, openedAt + 101 * interval, 60, flipRate));
|
|
}
|
|
|
|
[Fact]
|
|
public void FastSubmissionAndDiscard_DoNotDelayTheFirstPresentation()
|
|
{
|
|
for (var index = 0; index < 1000; index++)
|
|
{
|
|
var request = Reserve(index);
|
|
Assert.True(VideoOutExports.CanPresentFlip(request, Stopwatch.GetTimestamp()));
|
|
VideoOutExports.DiscardFlip(request);
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.False(VideoOutExports.CanPresentFlip(request, Stopwatch.GetTimestamp()));
|
|
}
|
|
var pending = Reserve(1000);
|
|
Assert.True(VideoOutExports.CanPresentFlip(pending, Stopwatch.GetTimestamp()));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void ClosePort_CancelsPendingPresentationAndIgnoresLateCompletion(bool completed)
|
|
{
|
|
var request = Reserve(123);
|
|
if (completed)
|
|
{
|
|
VideoOutExports.CompleteFlip(request);
|
|
}
|
|
Assert.True(VideoOutExports.IsFlipPresentationPending(request));
|
|
_context[CpuRegister.Rdi] = (ulong)_handle;
|
|
Assert.Equal(0, VideoOutExports.VideoOutClose(_context));
|
|
Assert.False(VideoOutExports.IsFlipPresentationPending(request));
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
VideoOutExports.CompleteFlip(request);
|
|
VideoOutExports.MarkFlipPresented(request);
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
}
|
|
|
|
[Fact]
|
|
public void CompleteThenPresent_CountsTheFlipAndReleasesTheBuffer()
|
|
{
|
|
var before = FlipCount();
|
|
var request = Reserve(11);
|
|
Assert.False(VideoOutExports.IsFlipDone(_handle, -1));
|
|
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before + 1, FlipCount());
|
|
Assert.False(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Equal(VideoOutExports.FlipOutcome.Pending, VideoOutExports.GetFlipOutcomeForTests(request));
|
|
|
|
VideoOutExports.MarkFlipPresented(request);
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
}
|
|
|
|
// Update completion counters even if the presenter has already finished the frame.
|
|
[Fact]
|
|
public void PresentThenComplete_StillCountsTheFlip()
|
|
{
|
|
var before = FlipCount();
|
|
var request = Reserve(12);
|
|
|
|
VideoOutExports.MarkFlipPresented(request);
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Equal(VideoOutExports.FlipOutcome.Presented, VideoOutExports.GetFlipOutcomeForTests(request));
|
|
Assert.Equal(before, FlipCount());
|
|
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before + 1, FlipCount());
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
}
|
|
|
|
// A discarded frame releases the buffer but its retirement is not suppressed.
|
|
[Fact]
|
|
public void DiscardThenComplete_ReleasesTheBufferAndStillCountsTheFlip()
|
|
{
|
|
var before = FlipCount();
|
|
var discardedBefore = VideoOutExports.DiscardedFlipCount;
|
|
var request = Reserve(13);
|
|
|
|
VideoOutExports.DiscardFlip(request);
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Equal(VideoOutExports.FlipOutcome.Discarded, VideoOutExports.GetFlipOutcomeForTests(request));
|
|
Assert.Equal(discardedBefore + 1, VideoOutExports.DiscardedFlipCount);
|
|
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before + 1, FlipCount());
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
|
|
// A second outcome does not overwrite the first.
|
|
var presented = Reserve(14);
|
|
VideoOutExports.MarkFlipPresented(presented);
|
|
VideoOutExports.DiscardFlip(presented);
|
|
Assert.Equal(discardedBefore + 1, VideoOutExports.DiscardedFlipCount);
|
|
Assert.Equal(VideoOutExports.FlipOutcome.Presented, VideoOutExports.GetFlipOutcomeForTests(presented));
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_ReleasesPendingFlipsAndKeepsCompletedOnesOutOfTheTable()
|
|
{
|
|
var pending = Reserve(15);
|
|
var completed = Reserve(16);
|
|
VideoOutExports.CompleteFlip(completed);
|
|
Assert.False(VideoOutExports.IsFlipDone(_handle, -1));
|
|
|
|
VideoOutExports.CancelOutstandingFlips();
|
|
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(pending));
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(completed));
|
|
}
|
|
|
|
[Fact]
|
|
public void NoBufferFlipWaitsForRetirementBeforeCountingCompletion()
|
|
{
|
|
var before = FlipCount();
|
|
var request = Reserve(17);
|
|
Assert.True(VideoOutExports.ReleaseNoBufferFlip(-1, request));
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Equal(before, FlipCount());
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before + 1, FlipCount());
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
}
|
|
|
|
[Fact]
|
|
public void AbandonedReservationDoesNotPublishCompletion()
|
|
{
|
|
var before = FlipCount();
|
|
var request = Reserve(18);
|
|
VideoOutExports.CancelFlip(request);
|
|
VideoOutExports.CancelFlip(request);
|
|
VideoOutExports.CompleteFlip(request);
|
|
Assert.Equal(before, FlipCount());
|
|
Assert.True(VideoOutExports.IsFlipDone(_handle, -1));
|
|
Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request));
|
|
}
|
|
}
|