From b93e4d642ae986c9adf073f2af9b895575e16e21 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 00:24:04 +0200 Subject: [PATCH 1/2] [Gpu] Stop draining the GPU for predication that asked not to wait PredicationZPassWaitOp is 0 for kWaitForQueryResults and 1 for kDoNotPredicateIfQueryResultsNotReady, so the wait bit was being read inverted. Every SET_PREDICATION packet that asked the command processor not to stall drained the whole host queue instead, and the packets that did ask to wait read the predication label without waiting for the results at all, so they could act on a stale value. The drain now happens only for kWaitForQueryResults, and only on the branch that actually reads the label. A packet tagged kDoNotPredicateIfQueryResultsNotReady leaves its work unpredicated, which is what the hardware does while the results are still outstanding, and clearing predication reads no memory at all so it never drains. --- .../GpuCommands/GpuCommandInterpreter.Waits.cs | 18 +++++++++++++----- .../GpuCommandInterpreterWaitTests.cs | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) 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/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); From d1c6fd14bf98f460e616ab658df4b963f47a6d61 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 00:47:16 +0200 Subject: [PATCH 2/2] [Agc] Queue the suspend point instead of draining the command stream A suspend point inserts GPU commands that drain the graphics pipe. The call that inserts it does not wait for those commands, and the hardware allows one suspend point to be in flight, but the queue implemented it by blocking the calling thread until every accepted submission had been processed. The frame boundary is now a queued submission of its own, ordered behind the graphics work it ends, and processing it flushes what the frame recorded. The caller returns immediately for the first boundary and only waits when a second one arrives while the first is still pending. Shutdown still drains, because no slice will run a queued boundary once the queue stops accepting work, and a cancelled queue drops pending boundaries along with the rest. Measured on Octopath Traveler II: the call went from 223 ms to 72 ms, and the guest render thread spent 38 s instead of 107 s of its 170 s inside it. --- .../Gpu/GpuCommands/CommandStreamQueue.cs | 75 ++++++++++++++++--- .../Gpu/GpuCommands/CommandSubmission.cs | 2 + .../GpuCommands/CommandStreamQueueTests.cs | 19 +++-- 3 files changed, 81 insertions(+), 15 deletions(-) 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/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);