[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:
parent
435bbd4844
commit
0aa611e317
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue