sharpemu/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs

396 lines
14 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.VideoOut;
namespace SharpEmu.Libs.Gpu.GpuCommands;
// The packet a handler executes: header fields plus its position in the cursor.
public readonly record struct PacketContext(uint Header, ulong PacketAddress, uint Offset, uint Remaining, uint Total)
{
public uint Length => PacketHeader.Length(Header);
public uint Opcode => PacketHeader.Opcode(Header);
public uint CustomCode => PacketHeader.CustomCode(Header);
}
public readonly record struct FlipRequest(int Handle, int Index, int FlipMode, long FlipArgument);
// Parses one queue's command stream and keeps that queue's register and packet state.
public sealed partial class GpuCommandInterpreter
{
public const int ComputeQueueBase = 0x20;
public const uint ConstantRamDwords = 0x3000;
public const uint RingChunkBytes = 0x10000;
private readonly ICommandStreamHost _host;
private readonly List<uint[]> _payloadScratchByDepth = new();
private readonly uint[] _constantRam = new uint[ConstantRamDwords];
private PacketCursorStack? _execution;
private bool _chainRequested;
public GpuCommandInterpreter(ICommandStreamHost host, int queueId, int interruptEventId)
{
_host = host;
QueueId = queueId;
InterruptEventId = interruptEventId;
TypedRegisters = new RegisterBanks(host.Fatal);
}
public int QueueId { get; }
public int InterruptEventId { get; }
public bool IsComputeQueue => InterruptEventId >= ComputeQueueBase;
public CommandRegisterBanks Registers { get; } = new();
// The typed banks the render executor reads; the dictionaries feed the translation they replace.
public RegisterBanks TypedRegisters { get; }
public ulong SubmitId { get; set; }
public uint IndexTypeAndSize { get; private set; }
public uint IndexBufferSize { get; private set; }
public ulong IndexBaseAddress { get; private set; }
public ulong DrawIndirectArgumentsBase { get; private set; }
public ulong DispatchIndirectArgumentsBase { get; private set; }
// Persistent draw state: indirect draws update it for later draws.
public uint InstanceCount { get; private set; } = 1;
public uint DrawIndexOffset { get; private set; }
public uint UserDataMarker { get; private set; }
public bool PredicateSkip { get; private set; }
public bool ConditionalWaitEnabled { get; private set; }
public FlipRequest PendingFlip { get; private set; }
public uint ConstantEngineCount { get; private set; }
public uint DrawEngineCount { get; private set; }
public bool ConstantEngineComplete { get; set; }
public ulong SyntheticOcclusionCounter { get; private set; }
public ulong AtomicReturnMeData { get; private set; }
public bool AtomicReturnMeValid { get; private set; }
public ulong AtomicReturnPfpData { get; private set; }
public bool AtomicReturnPfpValid { get; private set; }
public ReadOnlySpan<uint> ConstantRam => _constantRam;
public void Reset()
{
Registers.Reset();
TypedRegisters.Reset();
IndexTypeAndSize = 0;
IndexBufferSize = 0;
UserDataMarker = 0;
DrawIndirectArgumentsBase = 0;
DispatchIndirectArgumentsBase = 0;
Array.Clear(_constantRam);
}
// The queue-reset packets also drop the index base, the instance count and the wait condition.
public void ResetForQueueReset()
{
Reset();
IndexBaseAddress = 0;
InstanceCount = 1;
DrawIndexOffset = 0;
ConditionalWaitEnabled = false;
_host.OnQueueReset(QueueId);
}
public void ResetCounters()
{
DrawEngineCount = 0;
ConstantEngineCount = 0;
ConstantEngineComplete = false;
}
public void SetFlip(FlipRequest flip) => PendingFlip = flip;
public SubmissionProgress Process(PacketCursorStack execution, ulong address, uint dwordCount)
{
if (_execution is not null)
{
throw _host.Fatal($"The command interpreter is already running a stream: queue={QueueId}.");
}
if (execution.IsEmpty && dwordCount != 0)
{
execution.Push(new PacketCursor(address, dwordCount, ringChunkBase: address));
}
execution.Suspended = false;
execution.MadeProgress = false;
_execution = execution;
try
{
RunPackets(execution, 0);
}
finally
{
_execution = null;
}
return execution.IsEmpty ? SubmissionProgress.Complete : SubmissionProgress.Blocked;
}
// Runs a nested buffer to completion before the calling packet finishes.
public void RunIndirectBuffer(ulong address, uint dwordCount)
{
var execution = RequireExecution();
if (dwordCount == 0)
{
return;
}
if (execution.Depth >= PacketCursorStack.MaxDepth)
{
throw _host.Fatal($"The indirect buffer nesting is too deep: depth={execution.Depth} address=0x{address:X16} dwords={dwordCount}.");
}
var stopDepth = execution.Depth;
execution.Push(new PacketCursor(address, dwordCount, ringChunkBase: address));
RunPackets(execution, stopDepth);
}
// Replaces the current buffer with another one at the same depth; the chaining packet ends its buffer.
private void ChainToBuffer(ulong address, uint dwordCount, bool followedChunkAdvance = false)
{
var execution = RequireExecution();
execution.Pop();
execution.Push(new PacketCursor(address, dwordCount, ringChunkBase: address, followedChunkAdvance));
_chainRequested = true;
}
public void Suspend() => RequireExecution().Suspended = true;
private PacketCursorStack RequireExecution() =>
_execution ?? throw _host.Fatal($"No command stream is running: queue={QueueId}.");
private void RunPackets(PacketCursorStack execution, int stopDepth)
{
while (execution.Depth > stopDepth)
{
_host.RunPendingCommands();
var cursorIndex = execution.Depth - 1;
ref var cursor = ref execution.At(cursorIndex);
if (cursor.Offset > cursor.DwordCount)
{
throw _host.Fatal($"The packet cursor is past its buffer: offset={cursor.Offset} dwords={cursor.DwordCount} address=0x{cursor.Address:X16}.");
}
if (cursor.DeferredAdvance != 0)
{
if (cursor.DeferredAdvance > cursor.Remaining)
{
throw _host.Fatal($"The deferred advance is past the buffer: advance={cursor.DeferredAdvance} remaining={cursor.Remaining} address=0x{cursor.Address:X16}.");
}
cursor.Offset += cursor.DeferredAdvance;
cursor.DeferredAdvance = 0;
execution.MadeProgress = true;
continue;
}
if (cursor.Offset == cursor.DwordCount)
{
execution.Pop();
continue;
}
var packetAddress = cursor.Address + ((ulong)cursor.Offset * sizeof(uint));
var header = ReadDword(packetAddress, RenderPhaseProfile.CommandReadKind.Header);
var total = cursor.DwordCount;
var remaining = cursor.Remaining;
var offset = cursor.Offset;
if (header == PacketHeader.FillerHeader)
{
cursor.Offset++;
execution.MadeProgress = true;
continue;
}
// Ring memory the guest has not written yet; retry after it appends more.
if (header == 0 && cursor.FollowedChunkAdvance)
{
execution.Suspended = true;
return;
}
if (remaining < 2)
{
throw _host.Fatal($"The packet is shorter than two dwords: offset=0x{offset:X5} header=0x{header:X8} address=0x{packetAddress:X16}.");
}
var length = PacketHeader.Length(header);
if (length > remaining)
{
throw _host.Fatal($"The packet is longer than the buffer: offset=0x{offset:X5} header=0x{header:X8} length={length} remaining={remaining} address=0x{packetAddress:X16}.");
}
if (PacketHeader.IsPredicated(header) && PredicateSkip)
{
cursor.Offset += length;
execution.MadeProgress = true;
continue;
}
var opcode = PacketHeader.Opcode(header);
var handler = PacketDispatchTable.Opcodes[opcode];
if (handler is null)
{
DumpUnknownPacket(cursor, offset);
throw _host.Fatal($"The packet opcode is unknown: offset=0x{offset:X5} header=0x{header:X8} opcode=0x{opcode:X2} address=0x{packetAddress:X16}.");
}
var payload = ReadPayload(cursorIndex, packetAddress, length - 1);
var packet = new PacketContext(header & ~1u, packetAddress, offset, remaining, total);
var consumed = handler(this, in packet, payload) + 1;
if (consumed > remaining)
{
throw _host.Fatal($"The handler consumed more than the buffer holds: consumed={consumed} remaining={remaining} header=0x{header:X8} address=0x{packetAddress:X16}.");
}
if (execution.Suspended)
{
if (execution.Depth > cursorIndex + 1 && !_chainRequested)
{
execution.At(cursorIndex).DeferredAdvance = consumed;
}
_chainRequested = false;
return;
}
if (_chainRequested)
{
_chainRequested = false;
execution.MadeProgress = true;
continue;
}
if (execution.Depth != cursorIndex + 1)
{
throw _host.Fatal($"The cursor stack changed under a packet: depth={execution.Depth} expected={cursorIndex + 1} header=0x{header:X8}.");
}
execution.At(cursorIndex).Offset += consumed;
execution.MadeProgress = true;
}
}
// The payload lives in a per-depth scratch buffer for the handler call only.
private ReadOnlySpan<uint> ReadPayload(int depth, ulong packetAddress, uint payloadDwords)
{
while (_payloadScratchByDepth.Count <= depth)
{
_payloadScratchByDepth.Add(new uint[PacketHeader.MaxLength]);
}
var scratch = _payloadScratchByDepth[depth].AsSpan(0, (int)payloadDwords);
if (payloadDwords != 0)
{
RenderPhaseProfile.RecordCommandRead(RenderPhaseProfile.CommandReadKind.Payload, scratch.Length * sizeof(uint));
}
if (payloadDwords != 0 && !_host.TryReadGuest(packetAddress + sizeof(uint), MemoryMarshal.AsBytes(scratch)))
{
throw _host.Fatal($"The command stream cannot read a packet: address=0x{packetAddress:X16} dwords={payloadDwords + 1}.");
}
return scratch;
}
private void DumpUnknownPacket(in PacketCursor cursor, uint offset)
{
var begin = offset > 8 ? offset - 8 : 0;
var end = Math.Min(cursor.DwordCount, offset + 16);
Console.Error.WriteLine($"[GPU][FATAL] The packet stream near the unknown packet: buffer=0x{cursor.Address:X16} dwords={cursor.DwordCount} offset=0x{offset:X5}");
Span<byte> word = stackalloc byte[sizeof(uint)];
for (var index = begin; index < end; index++)
{
var text = _host.TryReadGuest(cursor.Address + ((ulong)index * sizeof(uint)), word)
? BinaryPrimitives.ReadUInt32LittleEndian(word).ToString("X8")
: "????????";
Console.Error.WriteLine($"\t{index:X5}{(index == offset ? ":" : " ")} {text}");
}
}
internal uint ReadDword(ulong address, RenderPhaseProfile.CommandReadKind readKind = RenderPhaseProfile.CommandReadKind.Operand32)
{
RenderPhaseProfile.RecordCommandRead(readKind, sizeof(uint));
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!_host.TryReadGuest(address, bytes))
{
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size=4.");
}
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
internal ulong ReadQword(ulong address)
{
RenderPhaseProfile.RecordCommandRead(RenderPhaseProfile.CommandReadKind.Operand64, sizeof(ulong));
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!_host.TryReadGuest(address, bytes))
{
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size=8.");
}
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
internal void ReadBytes(ulong address, Span<byte> destination)
{
RenderPhaseProfile.RecordCommandRead(RenderPhaseProfile.CommandReadKind.Other, destination.Length);
if (!_host.TryReadGuest(address, destination))
{
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size={destination.Length}.");
}
}
internal void WriteDword(ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
WriteBytes(address, bytes);
}
internal void WriteQword(ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
WriteBytes(address, bytes);
}
internal void WriteBytes(ulong address, ReadOnlySpan<byte> source)
{
if (!_host.Memory.TryWrite(address, source))
{
throw _host.Fatal($"The command stream cannot write guest memory: address=0x{address:X16} size={source.Length}.");
}
}
private static ulong Address(uint low, uint high) => low | ((ulong)high << 32);
}