From 0aa611e3179be9eeb2cf6ca47249c7e9db9d1510 Mon Sep 17 00:00:00 2001 From: kuba Date: Wed, 7 Oct 2026 10:48:47 +0200 Subject: [PATCH] [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) --- .../GpuCommandInterpreter.Draws.cs | 10 ++++ .../Gpu/GpuCommands/ICommandStreamHost.cs | 8 ++- .../Gpu/Rendering/IRenderHost.cs | 4 ++ .../Gpu/Rendering/RenderExecutor.Resources.cs | 11 +++- .../Gpu/Rendering/RenderExecutor.cs | 52 ++++++++++++++++++- .../VulkanVideoPresenter.CommandStream.cs | 3 ++ .../VulkanVideoPresenter.Rendering.cs | 25 +++++++++ .../GpuCommands/CommandStreamTestSupport.cs | 2 + .../GpuCommandInterpreterDrawTests.cs | 18 +++++++ .../Gpu/Rendering/RenderExecutorDrawTests.cs | 21 ++++++++ .../Rendering/RenderExecutorTestSupport.cs | 4 ++ 11 files changed, 154 insertions(+), 4 deletions(-) diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs index af8fbb6..becffda 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs index 6196265..cdc9461 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs index a4ca4e3..f81efb3 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs @@ -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 diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs index 5831fae..dc570ef 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs @@ -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 { diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs index 8558cf4..ad86efc 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs @@ -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 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(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(); } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 36e741d..b4ded39 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index cafaa4d..dc4d92e 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -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); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs index c61697d..85814a3 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs @@ -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); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs index 707e5b0..298269d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs index aa772f4..7801afb 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs index c50241a..36f7234 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs @@ -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);