450 lines
18 KiB
C#
450 lines
18 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
namespace SharpEmu.Libs.Gpu.GpuCommands;
|
|
|
|
public sealed partial class GpuCommandInterpreter
|
|
{
|
|
private const uint DrawIndirectArgumentsSize = 16;
|
|
private const uint DrawIndexedIndirectArgumentsSize = 20;
|
|
|
|
internal void SetIndexType(uint indexTypeAndSize) => IndexTypeAndSize = indexTypeAndSize & 0x3u;
|
|
|
|
internal void SetIndexBaseAddress(ulong address) => IndexBaseAddress = address;
|
|
|
|
internal void SetIndexBufferSize(uint size) => IndexBufferSize = size;
|
|
|
|
internal void SetInstanceCount(uint instanceCount) => InstanceCount = instanceCount == 0 ? 1 : instanceCount;
|
|
|
|
private ulong IndexElementSize => IndexTypeAndSize switch
|
|
{
|
|
0 => 2,
|
|
1 => 4,
|
|
2 => 1,
|
|
_ => throw _host.Fatal($"The index type is unknown: type={IndexTypeAndSize}."),
|
|
};
|
|
|
|
internal void DrawIndexed(ulong packetAddress, uint opcode, uint indexCount, ulong indexAddress, uint instanceCount = 0, int baseVertex = 0, uint firstInstance = 0, DrawOffsetSource offsetSource = DrawOffsetSource.Packet)
|
|
{
|
|
if (instanceCount == 0)
|
|
{
|
|
instanceCount = InstanceCount;
|
|
}
|
|
|
|
_host.DrawIndexed(SubmitId, new DrawIndexedArguments(packetAddress, opcode, indexCount, indexAddress, IndexTypeAndSize, instanceCount, baseVertex, firstInstance, offsetSource));
|
|
}
|
|
|
|
internal void DrawIndexedOffset(ulong packetAddress, uint indexOffset, uint indexCount)
|
|
{
|
|
DrawIndexOffset = indexOffset;
|
|
DrawIndexed(packetAddress, PacketOpcode.DrawIndexOffset2, indexCount, IndexBaseAddress + (indexOffset * IndexElementSize));
|
|
}
|
|
|
|
internal void DrawAuto(ulong packetAddress, uint opcode, uint vertexCount, uint instanceCount = 0, uint firstVertex = 0, uint firstInstance = 0, DrawOffsetSource offsetSource = DrawOffsetSource.Packet)
|
|
{
|
|
if (instanceCount == 0)
|
|
{
|
|
instanceCount = InstanceCount;
|
|
}
|
|
|
|
_host.DrawAuto(SubmitId, new DrawAutoArguments(packetAddress, opcode, vertexCount, instanceCount, firstVertex, firstInstance, offsetSource));
|
|
}
|
|
|
|
// Indirect arguments are read from guest memory now, as the packet executes.
|
|
internal void DrawIndirect(ulong packetAddress, uint opcode, uint dataOffset, uint drawInitiator, bool indexed)
|
|
{
|
|
if ((drawInitiator & ~0x20u) != 2u)
|
|
{
|
|
throw _host.Fatal($"The indirect draw initiator is not supported: initiator=0x{drawInitiator:X8} address=0x{packetAddress:X16}.");
|
|
}
|
|
|
|
if (DrawIndirectArgumentsBase == 0)
|
|
{
|
|
throw _host.Fatal($"The indirect draw arguments base is zero: address=0x{packetAddress:X16}.");
|
|
}
|
|
|
|
DrawIndirectArguments(packetAddress, opcode, DrawIndirectArgumentsBase + dataOffset, indexed);
|
|
}
|
|
|
|
internal void DrawIndirectMulti(ulong packetAddress, uint opcode, uint dataOffset, uint maxCountOrCount, ulong countAddress, uint strideInBytes, uint drawInitiator, bool indexed)
|
|
{
|
|
if ((drawInitiator & ~0x20u) != 2u)
|
|
{
|
|
throw _host.Fatal($"The indirect draw initiator is not supported: initiator=0x{drawInitiator:X8} address=0x{packetAddress:X16}.");
|
|
}
|
|
|
|
if (DrawIndirectArgumentsBase == 0)
|
|
{
|
|
throw _host.Fatal($"The indirect draw arguments base is zero: address=0x{packetAddress:X16}.");
|
|
}
|
|
|
|
var drawCount = maxCountOrCount;
|
|
if (countAddress != 0)
|
|
{
|
|
drawCount = Math.Min(ReadDword(countAddress), maxCountOrCount);
|
|
}
|
|
|
|
if (drawCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var argumentsSize = indexed ? DrawIndexedIndirectArgumentsSize : DrawIndirectArgumentsSize;
|
|
if (strideInBytes < argumentsSize)
|
|
{
|
|
throw _host.Fatal($"The indirect draw stride is smaller than the arguments: stride={strideInBytes} size={argumentsSize} address=0x{packetAddress:X16}.");
|
|
}
|
|
|
|
for (var draw = 0u; draw < drawCount; draw++)
|
|
{
|
|
DrawIndirectArguments(packetAddress, opcode, DrawIndirectArgumentsBase + dataOffset + ((ulong)draw * strideInBytes), indexed);
|
|
}
|
|
}
|
|
|
|
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);
|
|
var instanceCount = ReadDword(argumentsAddress + 4);
|
|
var startVertex = ReadDword(argumentsAddress + 8);
|
|
var startInstance = ReadDword(argumentsAddress + 12);
|
|
InstanceCount = instanceCount;
|
|
DrawAuto(packetAddress, opcode, vertexCount, instanceCount, startVertex, startInstance, DrawOffsetSource.IndirectArguments);
|
|
return;
|
|
}
|
|
|
|
if (_host.ResolvesIndirectDrawOnGpu && IndexTypeAndSize is 0 or 1 && TryGetGpuIndexRange(out var gpuIndexCount))
|
|
{
|
|
// The host reads the five arguments on the GPU. It draws from the whole index
|
|
// range; the arguments pick the indices. The host falls back to reading them
|
|
// itself when the draw needs the counts.
|
|
_deferredInstanceCountAddress = argumentsAddress + 4;
|
|
_host.DrawIndexed(SubmitId, new DrawIndexedArguments(
|
|
packetAddress, opcode, gpuIndexCount, IndexBaseAddress, IndexTypeAndSize, 1, 0, 0,
|
|
DrawOffsetSource.IndirectArguments, argumentsAddress, UnboundedIndexBuffer: IndexBufferSize == 0));
|
|
return;
|
|
}
|
|
|
|
var indexCountPerInstance = ReadDword(argumentsAddress);
|
|
var indexedInstanceCount = ReadDword(argumentsAddress + 4);
|
|
var startIndex = ReadDword(argumentsAddress + 8);
|
|
var baseVertex = ReadDword(argumentsAddress + 12);
|
|
var indexedStartInstance = ReadDword(argumentsAddress + 16);
|
|
var indexAddress = IndexBaseAddress + (startIndex * IndexElementSize);
|
|
var indexCount = IndexBufferSize != 0 ? Math.Min(indexCountPerInstance, IndexBufferSize) : indexCountPerInstance;
|
|
InstanceCount = indexedInstanceCount;
|
|
if (IndexBufferSize == 0 && IndexTypeAndSize is 0 or 1)
|
|
{
|
|
LearnIndexExtent(startIndex, indexCountPerInstance);
|
|
}
|
|
|
|
DrawIndexed(packetAddress, opcode, indexCount, indexAddress, indexedInstanceCount, unchecked((int)baseVertex), indexedStartInstance, DrawOffsetSource.IndirectArguments);
|
|
}
|
|
|
|
// An index buffer without a size (INDEX_BUFFER_SIZE never set) has no bound the GPU
|
|
// path could bind. The extent the draws from an index base have used so far, rounded
|
|
// up to the end of its 16 KiB page, stands in for it; every 1024th such draw reads its
|
|
// arguments on the CPU again so the extent can grow.
|
|
private const ulong IndexExtentPageSize = 0x4000;
|
|
private const ulong MaxIndexExtentBytes = 64ul << 20;
|
|
private const int MaxIndexExtents = 4096;
|
|
private readonly Dictionary<ulong, ulong> _indexExtents = new();
|
|
private uint _indexExtentDraws;
|
|
|
|
private bool TryGetGpuIndexRange(out uint indexCount)
|
|
{
|
|
if (IndexBufferSize != 0)
|
|
{
|
|
indexCount = IndexBufferSize;
|
|
return true;
|
|
}
|
|
|
|
indexCount = 0;
|
|
if ((++_indexExtentDraws & 1023) == 0 || !_indexExtents.TryGetValue(IndexBaseAddress, out var extentBytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
indexCount = (uint)(extentBytes / IndexElementSize);
|
|
return indexCount != 0;
|
|
}
|
|
|
|
private void LearnIndexExtent(uint startIndex, uint indexCount)
|
|
{
|
|
if (indexCount == 0 || IndexBaseAddress == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var end = IndexBaseAddress + (((ulong)startIndex + indexCount) * IndexElementSize);
|
|
var extentBytes = ((end + IndexExtentPageSize - 1) & ~(IndexExtentPageSize - 1)) - IndexBaseAddress;
|
|
if (extentBytes > MaxIndexExtentBytes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_indexExtents.TryGetValue(IndexBaseAddress, out var known))
|
|
{
|
|
if (known >= extentBytes)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else if (_indexExtents.Count >= MaxIndexExtents)
|
|
{
|
|
_indexExtents.Clear();
|
|
}
|
|
|
|
_indexExtents[IndexBaseAddress] = extentBytes;
|
|
}
|
|
|
|
internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
|
|
_host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
|
|
|
|
internal void DispatchIndirect(uint dataOffset, uint dispatchInitiator)
|
|
{
|
|
if (DispatchIndirectArgumentsBase == 0)
|
|
{
|
|
throw _host.Fatal($"The indirect dispatch arguments base is zero: offset=0x{dataOffset:X}.");
|
|
}
|
|
|
|
DispatchIndirectAt(DispatchIndirectArgumentsBase + dataOffset, dispatchInitiator);
|
|
}
|
|
|
|
private const uint DispatchInitiatorUseThreadDimensions = 1u << 5;
|
|
|
|
// Group counts written by an earlier shader are only in GPU memory; reading them
|
|
// here waits for the GPU. When the host dispatches indirectly they stay there.
|
|
// Thread-unit dispatches still need the counts to convert them to groups.
|
|
private void DispatchIndirectAt(ulong argumentsAddress, uint dispatchInitiator)
|
|
{
|
|
if (_host.ResolvesIndirectDispatchOnGpu && (dispatchInitiator & DispatchInitiatorUseThreadDimensions) == 0)
|
|
{
|
|
DispatchDirect(0, 0, 0, dispatchInitiator, argumentsAddress);
|
|
return;
|
|
}
|
|
|
|
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator, argumentsAddress);
|
|
}
|
|
|
|
internal uint DrawIndexPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Header == PacketHeader.Make(9, PacketOpcode.DispatchDrawPreamble))
|
|
{
|
|
var indexCount = payload[0];
|
|
var indexAddress = Address(payload[1], payload[2]);
|
|
var maxInstanceCount = payload[3];
|
|
var instanceCount = payload[6];
|
|
var flags = payload[7];
|
|
if (instanceCount > maxInstanceCount || (flags & ~0xA0u) != 0)
|
|
{
|
|
throw _host.Fatal($"The draw preamble is not supported: instances={instanceCount} max={maxInstanceCount} flags=0x{flags:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawIndexed(packet.PacketAddress, packet.Opcode, indexCount, indexAddress, instanceCount);
|
|
return 8;
|
|
}
|
|
|
|
if (packet.Header == PacketHeader.Make(6, PacketOpcode.DrawIndex2))
|
|
{
|
|
var maxIndexCount = payload[0];
|
|
var indexAddress = Address(payload[1], payload[2]);
|
|
var indexCount = payload[3];
|
|
var flags = payload[4];
|
|
if (indexCount > maxIndexCount || (flags & ~0x20u) != 0)
|
|
{
|
|
throw _host.Fatal($"The indexed draw is not supported: count={indexCount} max={maxIndexCount} flags=0x{flags:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawIndexed(packet.PacketAddress, packet.Opcode, indexCount, indexAddress);
|
|
return 5;
|
|
}
|
|
|
|
throw _host.Fatal($"The indexed draw header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
internal uint DrawIndirectPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
var indexed = packet.Header == PacketHeader.Make(5, PacketOpcode.DrawIndexIndirect);
|
|
if (!indexed && packet.Header != PacketHeader.Make(5, PacketOpcode.DrawIndirect))
|
|
{
|
|
throw _host.Fatal($"The indirect draw header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawIndirect(packet.PacketAddress, packet.Opcode, payload[0], payload[3], indexed);
|
|
return 4;
|
|
}
|
|
|
|
internal uint DrawIndirectMultiPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
var indexed = packet.Header == PacketHeader.Make(10, PacketOpcode.DrawIndexIndirectMulti);
|
|
if (!indexed && packet.Header != PacketHeader.Make(10, PacketOpcode.DrawIndirectMulti))
|
|
{
|
|
throw _host.Fatal($"The multi indirect draw header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
var countIndirect = (payload[3] >> 30) & 0x1u;
|
|
var countAddress = countIndirect != 0 ? Address(payload[5], payload[6]) : 0;
|
|
DrawIndirectMulti(packet.PacketAddress, packet.Opcode, payload[0], payload[4], countAddress, payload[7], payload[8], indexed);
|
|
return 9;
|
|
}
|
|
|
|
internal uint DrawIndexOffsetPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Header != PacketHeader.Make(5, PacketOpcode.DrawIndexOffset2))
|
|
{
|
|
throw _host.Fatal($"The offset draw header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
var maxIndexCount = payload[0];
|
|
var indexOffset = payload[1];
|
|
var indexCount = payload[2];
|
|
var flags = payload[3];
|
|
if (indexCount > maxIndexCount || (flags & ~0x20u) != 0)
|
|
{
|
|
throw _host.Fatal($"The offset draw is not supported: count={indexCount} max={maxIndexCount} flags=0x{flags:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawIndexedOffset(packet.PacketAddress, indexOffset, indexCount);
|
|
return 4;
|
|
}
|
|
|
|
internal uint DrawIndexAutoPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Header != PacketHeader.Make(3, PacketOpcode.DrawIndexAuto))
|
|
{
|
|
throw _host.Fatal($"The auto draw header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
var flags = payload[1];
|
|
if ((flags & ~0x22u) != 0)
|
|
{
|
|
throw _host.Fatal($"The auto draw flags are not supported: flags=0x{flags:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawAuto(packet.PacketAddress, packet.Opcode, payload[0]);
|
|
return 2;
|
|
}
|
|
|
|
// The draw count of this packet sits in the control dword; the stream carries no index buffer.
|
|
internal uint DrawIndexMultiAutoPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Length < 4)
|
|
{
|
|
throw _host.Fatal($"The multi auto draw packet is too short: length={packet.Length} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DrawAuto(packet.PacketAddress, packet.Opcode, (payload[2] >> 21) & 0x7FFu);
|
|
return packet.Length - 1;
|
|
}
|
|
|
|
internal uint DispatchDirectPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Opcode != PacketOpcode.DispatchDirect || packet.Length != 5)
|
|
{
|
|
throw _host.Fatal($"The dispatch header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DispatchDirect(payload[0], payload[1], payload[2], payload[3]);
|
|
return 4;
|
|
}
|
|
|
|
internal uint DispatchIndirectPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Header == PacketHeader.Make(4, PacketOpcode.DispatchIndirect))
|
|
{
|
|
var argumentsAddress = Address(payload[0], payload[1]);
|
|
if (argumentsAddress == 0)
|
|
{
|
|
throw _host.Fatal($"The indirect dispatch arguments address is zero: address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DispatchIndirectAt(argumentsAddress, payload[2]);
|
|
return 3;
|
|
}
|
|
|
|
if (packet.Header != PacketHeader.Make(3, PacketOpcode.DispatchIndirect))
|
|
{
|
|
throw _host.Fatal($"The indirect dispatch header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
DispatchIndirect(payload[0], payload[1]);
|
|
return 2;
|
|
}
|
|
|
|
internal uint IndexTypePacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
RequireHeader(packet, PacketHeader.Make(2, PacketOpcode.IndexType));
|
|
SetIndexType(payload[0]);
|
|
return 1;
|
|
}
|
|
|
|
internal uint IndexBufferSizePacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
RequireHeader(packet, PacketHeader.Make(2, PacketOpcode.IndexBufferSize));
|
|
SetIndexBufferSize(payload[0]);
|
|
return 1;
|
|
}
|
|
|
|
internal uint IndexBasePacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
RequireHeader(packet, PacketHeader.Make(3, PacketOpcode.IndexBase));
|
|
SetIndexBaseAddress(Address(payload[0], payload[1]));
|
|
return 2;
|
|
}
|
|
|
|
internal uint SetInstanceCountPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
RequireHeader(packet, PacketHeader.Make(2, PacketOpcode.NumInstances));
|
|
SetInstanceCount(payload[0]);
|
|
return 1;
|
|
}
|
|
|
|
// Header bit 1 selects the draw (0) or dispatch (1) indirect arguments base.
|
|
internal uint SetBasePacket(in PacketContext packet, ReadOnlySpan<uint> payload)
|
|
{
|
|
if (packet.Opcode != PacketOpcode.SetBase || packet.Length != 4)
|
|
{
|
|
throw _host.Fatal($"The set-base header is not supported: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
var baseIndex = payload[0] & 0xFu;
|
|
var baseAddress = (payload[1] & ~0x7UL) | ((ulong)(payload[2] & 0xFFFFu) << 32);
|
|
var shaderType = (packet.Header >> 1) & 0x3u;
|
|
if (baseIndex != 1 || shaderType > 1)
|
|
{
|
|
throw _host.Fatal($"The set-base packet is not supported: index={baseIndex} shaderType={shaderType} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
|
|
if (shaderType == 0)
|
|
{
|
|
DrawIndirectArgumentsBase = baseAddress;
|
|
}
|
|
else
|
|
{
|
|
DispatchIndirectArgumentsBase = baseAddress;
|
|
}
|
|
|
|
return 3;
|
|
}
|
|
|
|
private void RequireHeader(in PacketContext packet, uint header)
|
|
{
|
|
if (packet.Header != header)
|
|
{
|
|
throw _host.Fatal($"The packet header is not supported: header=0x{packet.Header:X8} expected=0x{header:X8} address=0x{packet.PacketAddress:X16}.");
|
|
}
|
|
}
|
|
}
|