[GPU] Draw non-indexed indirect draws from their arguments on the GPU (#1036)

Indexed indirect draws already let vkCmdDrawIndexedIndirect read their
counts, but a non-indexed DRAW_INDIRECT read its four arguments on the
CPU. When a compute pass had just written them (UE writes rectangle-list
draw arguments this way every frame), the read downloaded the buffer and
waited for the whole GPU: about one 20-50 ms stall per frame in Silent
Hill: The Short Message's menu.

The interpreter now hands the argument address to the host when it draws
non-indexed indirect draws on the GPU (deferring the instance count read
as the indexed path does), and the executor records vkCmdDrawIndirect.
The guest layout (vertexCount, instanceCount, firstVertex, firstInstance)
is VkDrawIndirectCommand. Rectangle lists use the general rectangle-list
variant, since the count is unknown; fans, polygons, legacy lists and
targetless draws (which may be retained and replayed) still read the
arguments on the CPU. SHARPEMU_CPU_INDIRECT_DRAW=1 keeps the CPU path.

(cherry picked from commit f2b91befb7dd27d65a35546f1e57dc88f04a5c82)
This commit is contained in:
kuba 2026-10-07 10:48:47 +02:00 committed by GitHub
parent 435bbd4844
commit 0aa611e317
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 154 additions and 4 deletions

View File

@ -103,6 +103,16 @@ public sealed partial class GpuCommandInterpreter
private void DrawIndirectArguments(ulong packetAddress, uint opcode, ulong argumentsAddress, bool indexed)
{
if (!indexed && _host.ResolvesNonIndexedIndirectDrawOnGpu)
{
// The host reads the four arguments on the GPU, so a GPU-written argument buffer
// costs no wait for the GPU here; it reads them itself when the draw needs counts.
_deferredInstanceCountAddress = argumentsAddress + 4;
_host.DrawAuto(SubmitId, new DrawAutoArguments(packetAddress, opcode, 1, 1, 0, 0,
DrawOffsetSource.IndirectArguments, argumentsAddress));
return;
}
if (!indexed)
{
var vertexCount = ReadDword(argumentsAddress);

View File

@ -38,7 +38,10 @@ public readonly record struct DrawAutoArguments(
uint InstanceCount,
uint FirstVertex,
uint FirstInstance,
DrawOffsetSource OffsetSource);
DrawOffsetSource OffsetSource,
// Nonzero when the counts are still in guest memory at this address, laid out as
// (vertexCount, instanceCount, firstVertex, firstInstance); the other counts are then 1/0.
ulong IndirectArgumentsAddress = 0);
public enum EndOfPipeWriteKind
{
@ -136,6 +139,9 @@ public interface ICommandStreamHost
// guest memory (DrawIndexedArguments.IndirectArgumentsAddress).
bool ResolvesIndirectDrawOnGpu => false;
// The host also draws non-indexed indirect draws from their arguments in guest memory.
bool ResolvesNonIndexedIndirectDrawOnGpu => false;
// Called when a queue reset packet clears the processor.
void OnQueueReset(int queueId);

View File

@ -153,6 +153,10 @@ public interface IRenderHost
void DrawIndexedIndirect(BufferBinding arguments) =>
throw new NotSupportedException("The render host does not draw from indirect arguments.");
// One draw whose counts the GPU reads from the buffer (VkDrawIndirectCommand layout).
void DrawIndirect(BufferBinding arguments) =>
throw new NotSupportedException("The render host does not draw from indirect arguments.");
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
// Executes a dispatch from three uint32 group counts in guest memory when the backend can

View File

@ -251,7 +251,7 @@ public sealed partial class RenderExecutor
var vertexBuffers = AcquireVertexBuffers(vertexInput);
var indexBuffer = AcquireIndexBuffer(in indexSource);
var indirectArguments = emission.IndirectArgumentsAddress != 0
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, emission.Indexed ? IndexedIndirectArgumentsSize : AutoIndirectArgumentsSize, isWritten: false)
: default;
DropUnwrittenColorTargets(context, ref state, pixelProgram);
state.Rendering = AcquireAttachments(ref state);
@ -314,7 +314,14 @@ public sealed partial class RenderExecutor
{
// Uploads and shader writes end with barriers to all commands, so the
// indirect read sees them.
_host.DrawIndexedIndirect(indirectArguments);
if (emission.Indexed)
{
_host.DrawIndexedIndirect(indirectArguments);
}
else
{
_host.DrawIndirect(indirectArguments);
}
}
else
{

View File

@ -309,6 +309,48 @@ public sealed partial class RenderExecutor
}
public void DrawAuto(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawAutoIndirectOnGpu(banks))
{
DrawAutoWithCpuArguments(submitId, banks, in arguments);
return;
}
DrawAutoCore(submitId, banks, in arguments);
}
// Fans, polygons and legacy rectangle/quad lists are drawn from their vertex counts, so
// those read the indirect arguments on the CPU. Rectangle lists draw through the host's
// general rectangle-list variant, as indexed indirect draws do.
private static bool CanDrawAutoIndirectOnGpu(RegisterBanks banks) =>
(GuestPrimitiveType)banks.UserConfig.PrimitiveType is
GuestPrimitiveType.PointList or GuestPrimitiveType.LineList or GuestPrimitiveType.LineStrip or
GuestPrimitiveType.TriangleList or GuestPrimitiveType.TriangleStrip or GuestPrimitiveType.RectangleList;
private void DrawAutoWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
Span<byte> bytes = stackalloc byte[(int)AutoIndirectArgumentsSize];
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
{
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
}
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
var resolved = arguments with
{
VertexCount = words[0],
InstanceCount = words[1],
FirstVertex = words[2],
FirstInstance = words[3],
IndirectArgumentsAddress = 0,
};
DrawAutoCore(submitId, banks, in resolved);
}
private const ulong AutoIndirectArgumentsSize = 16;
private void DrawAutoCore(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
if (!_host.IsRecording)
@ -364,6 +406,13 @@ public sealed partial class RenderExecutor
return;
}
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
{
// A targetless draw may be retained and replayed later; it needs its counts.
DrawAutoWithCpuArguments(submitId, banks, in arguments);
return;
}
ResolveShaderPrograms(banks, ref state);
var vertexInput = state.Programs.VertexInput;
var pixelInput = state.Programs.PixelInput;
@ -396,7 +445,8 @@ public sealed partial class RenderExecutor
false,
0,
(uint)vertexOffset,
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput),
arguments.IndirectArgumentsAddress);
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
_host.ResetBindings();
}

View File

@ -513,6 +513,9 @@ internal static unsafe partial class VulkanVideoPresenter
// SHARPEMU_CPU_INDIRECT_DRAW=1 reads the arguments back on the CPU as before.
public bool ResolvesIndirectDrawOnGpu => !_cpuIndirectDraw;
// RenderExecutor records non-indexed indirect draws with vkCmdDrawIndirect.
public bool ResolvesNonIndexedIndirectDrawOnGpu => !_cpuIndirectDraw;
private static readonly bool _cpuIndirectDraw = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DRAW"), "1", StringComparison.Ordinal);

View File

@ -1041,6 +1041,31 @@ internal static unsafe partial class VulkanVideoPresenter
CountDraw();
}
void IRenderHost.DrawIndirect(BufferBinding arguments)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
var command = BeginBatchedGuestCommands();
if (_boundGraphicsPipeline is { RectangleList: true } entry)
{
// The count is unknown here, so neither the single-rectangle strip nor a
// count check for native rectangles applies.
if (_supportsFillRectangle)
{
BindNativeRectangleList(entry, command);
}
else
{
BindRectangleListVariant(entry, strip: false, command);
}
}
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Preparation);
_vk.CmdDrawIndirect(command, new VkBuffer(arguments.Handle), arguments.Offset, 1, 16);
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Draw,
_boundGraphicsPipeline?.Id ?? 0, 0, 0);
CountDraw();
}
public void Dispatch(uint groupsX, uint groupsY, uint groupsZ)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);

View File

@ -180,6 +180,8 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost
public bool ResolvesIndirectDrawOnGpu { get; set; }
public bool ResolvesNonIndexedIndirectDrawOnGpu { get; set; }
public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}");
public Exception Fatal(string message) => new CommandStreamFatalException(message);

View File

@ -126,6 +126,24 @@ public sealed class GpuCommandInterpreterDrawTests
Assert.Contains("arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2)).Message);
}
[Fact]
public void NonIndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan()
{
var runner = new StreamRunner();
runner.Host.ResolvesNonIndexedIndirectDrawOnGpu = true;
runner.Host.WriteWords(Arguments, new uint[] { 9, 3, 1, 2 });
runner.Run(SetBase(Arguments, dispatch: false), StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2));
var auto = Assert.Single(runner.Host.AutoDraws);
Assert.Equal((Arguments, DrawOffsetSource.IndirectArguments), (auto.IndirectArgumentsAddress, auto.OffsetSource));
Assert.DoesNotContain(runner.Host.GuestReads, address => address >= Arguments && address < Arguments + 16);
// A later draw that inherits the instance count reads it then.
runner.Run(StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 5, 0));
Assert.Equal(3u, runner.Host.AutoDraws[1].InstanceCount);
}
[Fact]
public void IndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan()
{

View File

@ -112,6 +112,27 @@ public sealed class RenderExecutorDrawTests : IDisposable
Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw_indexed_indirect", StringComparison.Ordinal));
}
[Fact]
public void IndirectAutoDraw_LetsTheGpuReadTheArguments()
{
var arguments = Auto(1, source: DrawOffsetSource.IndirectArguments) with { IndirectArgumentsAddress = IndirectArguments };
_executor.DrawAuto(7, Banks(), arguments);
AssertOrder("obtain 100600000 10 written=False", "begin_rendering", "bind_pipeline Graphics A1", "draw_indirect", "reset_bindings");
Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw ", StringComparison.Ordinal));
}
[Fact]
public void IndirectAutoTriangleFan_ReadsTheArgumentsOnTheCpu()
{
WriteIndirectArguments(3, 2, 4, 1);
var arguments = Auto(1, source: DrawOffsetSource.IndirectArguments) with { IndirectArgumentsAddress = IndirectArguments };
_executor.DrawAuto(7, Banks(PrimitiveTriangleFan), arguments);
Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw_indirect", StringComparison.Ordinal));
Assert.Contains(_host.Calls, c => c.StartsWith("draw ", StringComparison.Ordinal));
}
[Fact]
public void AutoDraw_RecordsThePhasesAndTheVertexOffsets()
{

View File

@ -325,6 +325,9 @@ internal sealed class RecordingRenderHost : IRenderHost
public void DrawIndexedIndirect(BufferBinding arguments) =>
Calls.Add($"draw_indexed_indirect {arguments.Handle:X}:{arguments.Offset:X}");
public void DrawIndirect(BufferBinding arguments) =>
Calls.Add($"draw_indirect {arguments.Handle:X}:{arguments.Offset:X}");
public void Dispatch(uint groupsX, uint groupsY, uint groupsZ) => Calls.Add($"dispatch {groupsX} {groupsY} {groupsZ}");
public bool TryDispatchIndirect(ulong argumentsAddress) => false;
@ -497,6 +500,7 @@ internal static class RenderExecutorFixtures
public const ulong ComputeShader = 0x2_0002_0000;
public const uint PrimitiveTriangleList = 4;
public const uint PrimitiveTriangleStrip = 6;
public const uint PrimitiveTriangleFan = 5;
private static Exception Fatal(string message) => new RenderExecutorFatalException(message);