diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs index 5e461d1..b942cbf 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs @@ -154,6 +154,83 @@ public sealed class CommandStreamQueueTests Assert.Equal(new[] { "begin 0 1", "begin 1 3", "begin 0 2" }, host.Calls.Where(call => call.StartsWith("begin", StringComparison.Ordinal))); } + [Fact] + public void ControlBarrier_RunsBetweenTheAcceptedSliceAndLaterAdmissions() + { + var (host, queue) = NewQueue(); + Enqueue(host, queue, Graphics, 1, CreateInstanceCountPacket(1)); + var cancelled = false; + host.PendingCommands.Enqueue(() => + { + Assert.True(queue.TryEnqueueControlBarrier( + () => host.Calls.Add("mapping_change"), + () => cancelled = true)); + Enqueue(host, queue, Graphics + 0x100, 2, CreateInstanceCountPacket(2)); + }); + + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.DoesNotContain("mapping_change", host.Calls); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + + var first = host.Calls.IndexOf("begin 0 1"); + var barrier = host.Calls.IndexOf("mapping_change"); + var second = host.Calls.IndexOf("begin 0 2"); + Assert.True(first >= 0 && barrier > first && second > barrier, string.Join(", ", host.Calls)); + Assert.False(cancelled); + } + + [Fact] + public void ControlBarrier_PreservesCrossQueueProgressWhileAnOlderHeadIsBlocked() + { + var (host, queue) = NewQueue(); + host.WriteDword(Label, 0); + Enqueue(host, queue, Graphics, 1, WaitEqual(Label, 1)); + Assert.Equal(SliceResult.BlockedWithoutProgress, queue.ProcessOne()); + Assert.True(queue.TryEnqueueControlBarrier(() => host.Calls.Add("mapping_change"), () => { })); + EnqueueCompute(host, queue, 0x20, Compute, 2, CreateInstanceCountPacket(2)); + + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.DoesNotContain("mapping_change", host.Calls); + host.WriteDword(Label, 1); + queue.RetryBlocked(); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + + var compute = host.Calls.IndexOf("begin 1 2"); + var retriedGraphics = host.Calls.FindLastIndex(call => call == "begin 0 1"); + var barrier = host.Calls.IndexOf("mapping_change"); + Assert.True(compute >= 0 && retriedGraphics > compute && barrier > retriedGraphics, string.Join(", ", host.Calls)); + } + + [Fact] + public void ControlBarrier_IsCancelledWhenTheQueueIsDiscarded() + { + var (_, queue) = NewQueue(); + var ran = false; + var cancelled = false; + Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => cancelled = true)); + + queue.DiscardAll(); + + Assert.False(ran); + Assert.True(cancelled); + Assert.False(queue.HasPending); + } + + [Fact] + public void ShutdownDrain_RunsAnAcceptedControlBarrier() + { + var (_, queue) = NewQueue(); + var ran = false; + Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => { })); + queue.StopAccepting(); + + Assert.Equal(IdleOutcome.Completed, queue.DrainForShutdown(cancelBlockedOnNoProgress: true)); + Assert.True(ran); + Assert.False(queue.HasPending); + } + [Fact] public void BlockedHead_IsRequeuedAndRetriedWithoutStoppingOtherQueues() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Scheduling/GpuWorkerRelayTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Scheduling/GpuWorkerRelayTests.cs index 6fb32ec..16c8477 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Scheduling/GpuWorkerRelayTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Scheduling/GpuWorkerRelayTests.cs @@ -18,9 +18,9 @@ public sealed class GpuWorkerRelayTests : IDisposable private readonly Thread _thread; private bool _stop; - public TestWorker() + public TestWorker(Func? tryEnqueueAfterPendingWork = null) { - Relay = new GpuWorkerRelay(Wake); + Relay = new GpuWorkerRelay(Wake, tryEnqueueAfterPendingWork); _thread = new Thread(Run) { IsBackground = true }; } @@ -160,6 +160,116 @@ public sealed class GpuWorkerRelayTests : IDisposable device.Complete(1); } + [Fact] + public void AfterPendingWork_BypassesTheCommandStreamBarrier() + { + var barrierCalled = false; + using var worker = new TestWorker( + (_, _) => + { + barrierCalled = true; + return true; + }); + worker.Start(); + var workRan = false; + var ranOnWorker = false; + + Assert.True(worker.Relay.TryRunAfterPendingWork(() => + { + workRan = true; + ranOnWorker = worker.Relay.IsGpuQueueThread; + })); + Assert.True(workRan); + Assert.True(ranOnWorker); + Assert.False(barrierCalled); + } + + [Fact] + public void AfterPendingWork_ReturnsFalseWhenTheWorkerIsClosed() + { + var barrierCalled = false; + var relay = new GpuWorkerRelay( + () => { }, + (_, _) => + { + barrierCalled = true; + return true; + }); + relay.StopAcceptingWork(); + + Assert.False(relay.TryRunAfterPendingWork(() => { })); + Assert.False(barrierCalled); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_WaitsForTheControlBarrier() + { + Action? run = null; + Action? cancel = null; + var relay = new GpuWorkerRelay( + () => { }, + (work, cancellation) => + { + run = work; + cancel = cancellation; + return true; + }); + var workRan = false; + + var completion = Task.Run(() => + relay.TryRunAfterAcceptedCommandStreams(() => workRan = true)); + Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null)); + Assert.False(await CompletesWithin(completion, 50)); + + run!(); + + Assert.True(await completion.WaitAsync(TimeSpan.FromSeconds(2))); + Assert.True(workRan); + Assert.NotNull(cancel); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_CancellationReleasesTheCaller() + { + Action? cancel = null; + var relay = new GpuWorkerRelay( + () => { }, + (_, cancellation) => + { + cancel = cancellation; + return true; + }); + + var completion = Task.Run(() => Record.Exception(() => + relay.TryRunAfterAcceptedCommandStreams(() => { }))); + Assert.True(WaitUntil(() => Volatile.Read(ref cancel) is not null)); + cancel!(); + + Assert.IsType( + await completion.WaitAsync(TimeSpan.FromSeconds(2))); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_RethrowsTheWorkException() + { + Action? run = null; + var relay = new GpuWorkerRelay( + () => { }, + (work, _) => + { + run = work; + return true; + }); + var expected = new InvalidOperationException("ordered state change failed"); + + var completion = Task.Run(() => Record.Exception(() => + relay.TryRunAfterAcceptedCommandStreams(() => throw expected))); + Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null)); + run!(); + + Assert.Same(expected, await completion.WaitAsync(TimeSpan.FromSeconds(2))); + } + [Fact] public void CommandsRunBeforeGuestWork() {