diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs index f8b7944..0313ece 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs @@ -29,6 +29,8 @@ public sealed class CommandStreamQueue public const int ComputeQueueCount = 56; public const int QueueCount = 1 + ComputeQueueCount; public const int AllBlockedRetryMilliseconds = 100; + // The hardware lets one suspend point be in flight; a second one waits for the first. + public const int MaxBoundariesInFlight = 1; private readonly ICommandStreamHost _host; private readonly object _gate = new(); @@ -43,6 +45,7 @@ public sealed class CommandStreamQueue private ulong _submitId; private ulong _lastCompletedGpuTick; private int _doneCount; + private int _pendingBoundaries; private IdleOutcome _outcome = IdleOutcome.Completed; private Thread? _processingThread; @@ -191,28 +194,57 @@ public sealed class CommandStreamQueue _queues[submission.QueueId].AddLast(submission); _submissionCount++; - if (submission.Kind != CommandSubmissionKind.FlipPreparation) + if (submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary)) SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.Enqueued, submission.QueueId, submission.SubmissionId, submission.Address, submission.DwordCount, _submissionCount); Monitor.PulseAll(_gate); } - // Marks the frame boundary; from another thread it first waits for the queue to drain. + // Marks the frame boundary. A suspend point inserts commands that drain the graphics + // pipe; it does not wait for them, and the hardware allows one to be in flight, so the + // boundary is queued behind the submissions it ends and only a second one waits. public IdleOutcome Done() { - var outcome = IdleOutcome.Completed; - if (_processingThread != Thread.CurrentThread) + if (_processingThread == Thread.CurrentThread) { - outcome = WaitForIdle(); + lock (_gate) + { + _graphicsDone = true; + _doneCount++; + } + + return IdleOutcome.Completed; } lock (_gate) { - _graphicsDone = true; - _doneCount++; - } + if (_outcome != IdleOutcome.Completed) + { + return _outcome; + } - return outcome; + _graphicsDone = true; + if (!_accepting) + { + // Shutdown: no slice will run a queued boundary, so drain as before. + while (_outcome == IdleOutcome.Completed && (_processing || _submissionCount != 0)) + { + Monitor.Wait(_gate); + } + + _doneCount++; + return _outcome; + } + + _pendingBoundaries++; + EnqueueLocked(new CommandSubmission(CommandSubmissionKind.FrameBoundary, 0, 0, 0, 0, null)); + while (_outcome == IdleOutcome.Completed && _pendingBoundaries > MaxBoundariesInFlight) + { + Monitor.Wait(_gate); + } + + return _outcome; + } } // Returns once nothing is pending; a cancelled or failed queue reports that instead. @@ -348,7 +380,7 @@ public sealed class CommandStreamQueue _nextQueue = (selected + 1) % QueueCount; _processing = true; _processingThread = Thread.CurrentThread; - if (!submission.Started && submission.Kind != CommandSubmissionKind.FlipPreparation) + if (!submission.Started && submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary)) SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.ProcessingStarted, submission.QueueId, submission.SubmissionId, submission.Address, submission.DwordCount, _submissionCount); } @@ -388,6 +420,12 @@ public sealed class CommandStreamQueue } else { + if (submission.Kind == CommandSubmissionKind.FrameBoundary) + { + _pendingBoundaries--; + _doneCount++; + } + foreach (var queue in _queues) { if (queue.First is { } head) @@ -415,6 +453,14 @@ public sealed class CommandStreamQueue return true; } + // A suspend point drains the graphics pipe: submit what the frame recorded. + if (submission.Kind == CommandSubmissionKind.FrameBoundary) + { + SlicesRun++; + _host.Flush(); + return true; + } + var firstSlice = !submission.Started; var processor = GetInterpreter(submission.QueueId); if (firstSlice && submission.ResetInterpreter) @@ -526,6 +572,14 @@ public sealed class CommandStreamQueue if (queue.First is { } head && head.Value.Blocked) { _submissionCount -= queue.Count; + foreach (var dropped in queue) + { + if (dropped.Kind == CommandSubmissionKind.FrameBoundary) + { + _pendingBoundaries--; + } + } + queue.Clear(); _outcome = IdleOutcome.Cancelled; } @@ -630,5 +684,6 @@ public sealed class CommandStreamQueue } _submissionCount = 0; + _pendingBoundaries = 0; } } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs index b935cd5..7b0903d 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs @@ -9,6 +9,8 @@ public enum CommandSubmissionKind Compute, // A video-out export flip queued behind the graphics submissions before it. FlipPreparation, + // A suspend point queued behind the graphics submissions of the frame it ends. + FrameBoundary, } public enum SubmissionProgress diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs index 98ee995..3d276c5 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs @@ -40,16 +40,15 @@ public sealed partial class GpuCommandInterpreter } } + // The wait bit is PredicationZPassWaitOp: 0 (kWaitForQueryResults) stalls the command + // processor until the results arrive, 1 (kDoNotPredicateIfQueryResultsNotReady) never + // stalls and leaves the tagged packets unpredicated while the results are outstanding. internal void SetPredication(uint condition, uint operation, uint waitOperation, ulong address) { - if (waitOperation != 0) - { - _host.FlushAndWait(); - } - switch (operation) { case 0: + // Clearing predication reads no memory, so no GPU result can be outstanding. PredicateSkip = false; break; case 3: @@ -59,6 +58,15 @@ public sealed partial class GpuCommandInterpreter throw _host.Fatal("The predication address is zero."); } + if (waitOperation != 0) + { + // The results are outstanding while work is queued behind this packet, and + // the game asked for the packets to run unpredicated in that case. + PredicateSkip = false; + break; + } + + _host.FlushAndWait(); var value = ReadQword(address); PredicateSkip = condition switch { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs index a45f185..9090db2 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs @@ -318,19 +318,28 @@ public sealed class CommandStreamQueueTests } [Fact] - public async Task Done_FromAnotherThreadWaitsForTheQueueAndCountsFrames() + public async Task Done_QueuesTheBoundaryAndOnlyWaitsForASecondOne() { var (host, queue) = NewQueue(); Enqueue(host, queue, Graphics, 1, CreateInstanceCountPacket(1)); - var done = Task.Run(queue.Done); - await Task.WhenAny(done, Task.Delay(100)); - Assert.False(done.IsCompleted); + // The first suspend point returns without draining the accepted submissions. + Assert.Equal(IdleOutcome.Completed, await Task.Run(queue.Done).WaitAsync(TimeSpan.FromSeconds(2))); + Assert.Equal(0, queue.FrameNumber); + Assert.True(queue.HasPending); + + // The second one waits until the first boundary has been processed. + var second = Task.Run(queue.Done); + await Task.WhenAny(second, Task.Delay(100)); + Assert.False(second.IsCompleted); Assert.Equal(SliceResult.Completed, queue.ProcessOne()); - Assert.Equal(IdleOutcome.Completed, await done.WaitAsync(TimeSpan.FromSeconds(2))); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(IdleOutcome.Completed, await second.WaitAsync(TimeSpan.FromSeconds(2))); Assert.Equal(1, queue.FrameNumber); // The next graphics submission starts from a reset processor. + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(2, queue.FrameNumber); Enqueue(host, queue, Graphics, 2, CreateInstanceCountPacket(2)); Assert.Equal(SliceResult.Completed, queue.ProcessOne()); Assert.Equal(2UL, queue.GetInterpreter(0).SubmitId); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterWaitTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterWaitTests.cs index 58a41e9..87aee95 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterWaitTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterWaitTests.cs @@ -169,14 +169,24 @@ public sealed class GpuCommandInterpreterWaitTests var skipped = CreateInstanceCountPacket(2); skipped[0] |= 1u; - var newLayout = StreamRunner.Packet(PacketOpcode.SetPredication, (3u << 16) | (1u << 8) | (1u << 12), StreamRunner.Low(Label), StreamRunner.High(Label)); - runner.Run(newLayout, skipped); + // kWaitForQueryResults (0) drains the GPU and predicates on the label. + var waitingLayout = StreamRunner.Packet(PacketOpcode.SetPredication, (3u << 16) | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)); + runner.Run(waitingLayout, skipped); Assert.Equal(new[] { "flush_and_wait" }, runner.Host.Calls); Assert.True(runner.Interpreter.PredicateSkip); Assert.Equal(1u, runner.Interpreter.InstanceCount); + // kDoNotPredicateIfQueryResultsNotReady (1) never drains and never skips. + runner.Host.Calls.Clear(); + var nonWaitingLayout = StreamRunner.Packet(PacketOpcode.SetPredication, (3u << 16) | (1u << 8) | (1u << 12), StreamRunner.Low(Label), StreamRunner.High(Label)); + runner.Run(nonWaitingLayout, skipped); + Assert.Empty(runner.Host.Calls); + Assert.False(runner.Interpreter.PredicateSkip); + Assert.Equal(2u, runner.Interpreter.InstanceCount); + var oldLayout = StreamRunner.Packet(PacketOpcode.SetPredication, StreamRunner.Low(Label), StreamRunner.High(Label) | (3u << 16)); runner.Run(oldLayout, skipped); + Assert.Equal(new[] { "flush_and_wait" }, runner.Host.Calls); Assert.False(runner.Interpreter.PredicateSkip); Assert.Equal(2u, runner.Interpreter.InstanceCount);