From d1c6fd14bf98f460e616ab658df4b963f47a6d61 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 00:47:16 +0200 Subject: [PATCH] [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);