216 lines
8.3 KiB
C#
216 lines
8.3 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using SharpEmu.Libs.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class PresenterStartupTests
|
|
{
|
|
private sealed class StartupState : IDisposable
|
|
{
|
|
private static readonly Type PresenterType = typeof(VulkanVideoPresenter);
|
|
private readonly Dictionary<FieldInfo, object?> _saved = new();
|
|
private readonly object _gate = Read("_gate")!;
|
|
private readonly Func<SharpEmu.HLE.ICpuMemory, SharpEmu.Libs.Agc.AgcExports.HeadlessCommandStream>? _factory =
|
|
VulkanVideoPresenter.TestCommandStreamFactory;
|
|
|
|
public StartupState()
|
|
{
|
|
VulkanVideoPresenter.TestCommandStreamFactory = null;
|
|
Set("_closed", false);
|
|
Set("_presenterCloseRequested", false);
|
|
Set("_presenterStartupFailure", null);
|
|
Set("_activePresenter", null);
|
|
Set("_thread", Thread.CurrentThread);
|
|
Set("_shaderPrewarmDecided", true);
|
|
Set("_shaderPrewarmRunning", false);
|
|
Set("_shaderPrewarmReleased", false);
|
|
Set("_shaderPrewarmHoldReported", 0);
|
|
}
|
|
|
|
private static FieldInfo Field(string name) => PresenterType.GetField(name, BindingFlags.Static | BindingFlags.NonPublic)!;
|
|
private static object? Read(string name) => Field(name).GetValue(null);
|
|
|
|
public void Set(string name, object? value)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
var field = Field(name);
|
|
_saved.TryAdd(field, field.GetValue(null));
|
|
field.SetValue(null, value);
|
|
Monitor.PulseAll(_gate);
|
|
}
|
|
}
|
|
|
|
public void Publish(CommandStreamQueue queue)
|
|
{
|
|
var type = PresenterType.GetNestedType("Presenter", BindingFlags.NonPublic)!;
|
|
var presenter = RuntimeHelpers.GetUninitializedObject(type);
|
|
type.GetField("_commandStream", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(presenter, queue);
|
|
Set("_activePresenter", presenter);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
foreach (var pair in _saved)
|
|
pair.Key.SetValue(null, pair.Value);
|
|
VulkanVideoPresenter.TestCommandStreamFactory = _factory;
|
|
Monitor.PulseAll(_gate);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void EarlySubmissionWaitsAndUsesThePublishedQueue()
|
|
{
|
|
var runner = new StreamRunner();
|
|
var queue = new CommandStreamQueue(runner.Host);
|
|
Assert.True(runner.Host.Memory.TryWrite(StreamRunner.CommandAddress, new byte[] { 0, 16, 0, 192, 0, 0, 0, 0 }));
|
|
Assert.True(runner.Host.Memory.TryWrite(StreamRunner.CommandAddress + 8, new byte[] { 0, 16, 0, 192, 0, 0, 0, 0 }));
|
|
using var state = new StartupState();
|
|
Exception? failure = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { VulkanVideoPresenter.SubmitCommandStream(runner.Host.Memory, 0, StreamRunner.CommandAddress, 2, 1, null); }
|
|
catch (Exception exception) { failure = exception; }
|
|
});
|
|
thread.Start();
|
|
try
|
|
{
|
|
Assert.True(SpinWait.SpinUntil(() => (thread.ThreadState & ThreadState.WaitSleepJoin) != 0, 5000));
|
|
Assert.Equal(0, queue.PendingSubmissionCount);
|
|
state.Publish(queue);
|
|
Assert.True(thread.Join(5000));
|
|
Assert.Null(failure);
|
|
Assert.Equal(1, queue.PendingSubmissionCount);
|
|
VulkanVideoPresenter.SubmitCommandStream(runner.Host.Memory, 0, StreamRunner.CommandAddress + 8, 2, 2, null);
|
|
Assert.Equal(2, queue.PendingSubmissionCount);
|
|
Assert.Equal(0UL, queue.SubmissionsStarted);
|
|
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
|
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
|
Assert.Equal(new[] { "begin 0 1", "begin 0 2" }, runner.Host.Calls.Where(call => call.StartsWith("begin ", StringComparison.Ordinal)));
|
|
}
|
|
finally
|
|
{
|
|
if (thread.IsAlive)
|
|
{
|
|
VulkanVideoPresenter.RequestClose();
|
|
Assert.True(thread.Join(5000));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void ASubmissionWaitsUntilTheShaderPrewarmIsDecidedAndDone(bool undecided)
|
|
{
|
|
var runner = new StreamRunner();
|
|
var queue = new CommandStreamQueue(runner.Host);
|
|
Assert.True(runner.Host.Memory.TryWrite(StreamRunner.CommandAddress, new byte[] { 0, 16, 0, 192, 0, 0, 0, 0 }));
|
|
using var state = new StartupState();
|
|
state.Set(undecided ? "_shaderPrewarmDecided" : "_shaderPrewarmRunning", !undecided);
|
|
state.Publish(queue);
|
|
Exception? failure = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { VulkanVideoPresenter.SubmitCommandStream(runner.Host.Memory, 0, StreamRunner.CommandAddress, 2, 1, null); }
|
|
catch (Exception exception) { failure = exception; }
|
|
});
|
|
thread.Start();
|
|
try
|
|
{
|
|
Assert.True(SpinWait.SpinUntil(() => (thread.ThreadState & ThreadState.WaitSleepJoin) != 0, 5000));
|
|
Assert.False(thread.Join(200));
|
|
Assert.Equal(0, queue.PendingSubmissionCount);
|
|
state.Set("_shaderPrewarmDecided", true);
|
|
state.Set("_shaderPrewarmRunning", false);
|
|
Assert.True(thread.Join(5000));
|
|
Assert.Null(failure);
|
|
Assert.Equal(1, queue.PendingSubmissionCount);
|
|
}
|
|
finally
|
|
{
|
|
if (thread.IsAlive)
|
|
{
|
|
VulkanVideoPresenter.RequestClose();
|
|
Assert.True(thread.Join(5000));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void StartupFailureRejectsTheSubmission()
|
|
{
|
|
var memory = new FakeCpuMemory(0x1000, 0x100);
|
|
using var state = new StartupState();
|
|
var failure = new InvalidOperationException("Device initialization failed.");
|
|
state.Set("_presenterStartupFailure", failure);
|
|
state.Set("_closed", true);
|
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
|
VulkanVideoPresenter.SubmitCommandStream(memory, 0, 0x1000, 2, 1, null));
|
|
Assert.Same(failure, exception.InnerException);
|
|
Assert.Equal(IdleOutcome.Failed, VulkanVideoPresenter.SubmitDone(memory));
|
|
}
|
|
|
|
[Fact]
|
|
public void CloseRejectsAnEarlySubmission()
|
|
{
|
|
var memory = new FakeCpuMemory(0x1000, 0x100);
|
|
using var state = new StartupState();
|
|
VulkanVideoPresenter.RequestClose();
|
|
Assert.Throws<OperationCanceledException>(() =>
|
|
VulkanVideoPresenter.SubmitCommandStream(memory, 0, 0x1000, 2, 1, null));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void StartupTerminationReleasesAWaitingSubmission(bool failed)
|
|
{
|
|
var memory = new FakeCpuMemory(0x1000, 0x100);
|
|
using var state = new StartupState();
|
|
Exception? failure = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { VulkanVideoPresenter.SubmitCommandStream(memory, 0, 0x1000, 2, 1, null); }
|
|
catch (Exception exception) { failure = exception; }
|
|
});
|
|
thread.Start();
|
|
try
|
|
{
|
|
Assert.True(SpinWait.SpinUntil(() => (thread.ThreadState & ThreadState.WaitSleepJoin) != 0, 5000));
|
|
if (failed)
|
|
{
|
|
state.Set("_presenterStartupFailure", new InvalidOperationException("Device initialization failed."));
|
|
state.Set("_closed", true);
|
|
}
|
|
else
|
|
{
|
|
VulkanVideoPresenter.RequestClose();
|
|
}
|
|
Assert.True(thread.Join(5000));
|
|
if (failed) Assert.IsType<InvalidOperationException>(failure);
|
|
else Assert.IsType<OperationCanceledException>(failure);
|
|
}
|
|
finally
|
|
{
|
|
if (thread.IsAlive)
|
|
{
|
|
VulkanVideoPresenter.RequestClose();
|
|
Assert.True(thread.Join(5000));
|
|
}
|
|
}
|
|
}
|
|
}
|