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