Merge #941 (pr/gpu-fewer-drains)

This commit is contained in:
kuba 2026-09-30 00:12:23 +02:00
commit e702daeb59
5 changed files with 106 additions and 22 deletions

View File

@ -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;
}
}

View File

@ -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

View File

@ -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
{

View File

@ -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);

View File

@ -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);