319 lines
10 KiB
C#
319 lines
10 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.HLE;
|
|
using SharpEmu.Libs.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
|
|
|
internal sealed class CommandStreamFatalException(string message) : Exception(message);
|
|
|
|
// Records every host call in order and serves guest memory from a fake.
|
|
internal sealed class RecordingCommandStreamHost : ICommandStreamHost
|
|
{
|
|
public const ulong MemoryBase = 0x1000_0000;
|
|
public const int MemorySize = 0x10_0000;
|
|
|
|
public RecordingCommandStreamHost()
|
|
{
|
|
GuestMemory = new FakeCpuMemory(MemoryBase, MemorySize);
|
|
}
|
|
|
|
public FakeCpuMemory GuestMemory { get; }
|
|
|
|
public ICpuMemory Memory => GuestMemory;
|
|
|
|
public List<string> Calls { get; } = new();
|
|
|
|
// When set, the host counts occlusion samples itself and records each dump address here.
|
|
public List<ulong>? OcclusionDumps { get; set; }
|
|
|
|
public bool TryRecordOcclusionCounterDump(ulong address)
|
|
{
|
|
if (OcclusionDumps is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
OcclusionDumps.Add(address);
|
|
return true;
|
|
}
|
|
|
|
public List<EndOfPipeWrite> EndOfPipeWrites { get; } = new();
|
|
|
|
// Satisfied waits and command-processor memory writes reported to the host.
|
|
public int VisibilityPoints { get; private set; }
|
|
|
|
public void NoteMemoryVisibilityPoint() => VisibilityPoints++;
|
|
|
|
public List<ulong> GuestReads { get; } = new();
|
|
|
|
public Action<ulong>? BeforeGuestRead { get; set; }
|
|
|
|
public List<DrawIndexedArguments> IndexedDraws { get; } = new();
|
|
|
|
public List<DrawAutoArguments> AutoDraws { get; } = new();
|
|
|
|
public Queue<Action> PendingCommands { get; } = new();
|
|
|
|
public int PendingCommandRuns { get; private set; }
|
|
|
|
public bool FlipSlot { get; set; } = true;
|
|
|
|
public bool FlipDone { get; set; } = true;
|
|
|
|
public ulong NextFlipRequestId { get; set; } = 1;
|
|
|
|
public byte[] Gds { get; } = new byte[ManagedCommandStreamHost.GdsBytes];
|
|
|
|
// Qword values the GPU owns; the synchronized read lands them in memory first, as a download would.
|
|
public Dictionary<ulong, ulong> PendingGpuValues { get; } = new();
|
|
|
|
public bool TryReadGuest(ulong address, Span<byte> destination)
|
|
{
|
|
GuestReads.Add(address);
|
|
BeforeGuestRead?.Invoke(address);
|
|
if (PendingGpuValues.Remove(address, out var pending))
|
|
{
|
|
WriteQword(address, pending);
|
|
}
|
|
|
|
return GuestMemory.TryRead(address, destination);
|
|
}
|
|
|
|
public void RunPendingCommands()
|
|
{
|
|
PendingCommandRuns++;
|
|
while (PendingCommands.Count != 0)
|
|
{
|
|
PendingCommands.Dequeue()();
|
|
}
|
|
}
|
|
|
|
public void BeginSubmission(int queueId, ulong submissionId, object? geometrySnapshots) => Calls.Add($"begin {queueId} {submissionId}");
|
|
|
|
public void Flush() => Calls.Add("flush");
|
|
|
|
public void FlushAndWait() => Calls.Add("flush_and_wait");
|
|
|
|
public void SynchronizeGpu() => Calls.Add("synchronize");
|
|
|
|
public void RunGarbageCollector() => Calls.Add("gc");
|
|
|
|
public void EmitGlobalBarrier() => Calls.Add("barrier");
|
|
|
|
public void FillBuffer(ulong address, ulong size, uint value, bool isGds)
|
|
{
|
|
Calls.Add($"fill {address:X} {size:X} {value:X8} gds={isGds}");
|
|
var pattern = new byte[size];
|
|
for (var offset = 0; offset + 4 <= pattern.Length; offset += 4)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(pattern.AsSpan(offset), value);
|
|
}
|
|
|
|
if (isGds)
|
|
{
|
|
pattern.CopyTo(Gds.AsSpan((int)address));
|
|
}
|
|
else if (!GuestMemory.TryWrite(address, pattern))
|
|
{
|
|
throw Fatal($"fill outside memory {address:X}");
|
|
}
|
|
}
|
|
|
|
public void CopyBuffer(ulong destination, ulong source, ulong size, bool destinationIsGds, bool sourceIsGds)
|
|
{
|
|
Calls.Add($"copy {destination:X} {source:X} {size:X} dstGds={destinationIsGds} srcGds={sourceIsGds}");
|
|
var bytes = new byte[size];
|
|
if (sourceIsGds)
|
|
{
|
|
Gds.AsSpan((int)source, (int)size).CopyTo(bytes);
|
|
}
|
|
else if (!GuestMemory.TryRead(source, bytes))
|
|
{
|
|
throw Fatal($"copy source outside memory {source:X}");
|
|
}
|
|
|
|
if (destinationIsGds)
|
|
{
|
|
bytes.CopyTo(Gds.AsSpan((int)destination));
|
|
}
|
|
else if (!GuestMemory.TryWrite(destination, bytes))
|
|
{
|
|
throw Fatal($"copy destination outside memory {destination:X}");
|
|
}
|
|
}
|
|
|
|
public void ReadGds(Span<uint> destination, uint wordOffset, uint wordCount)
|
|
{
|
|
Calls.Add($"read_gds {wordOffset} {wordCount}");
|
|
EndOfPipe.ReadGdsWords(Gds, destination, wordOffset, wordCount);
|
|
}
|
|
|
|
public void RecordEndOfPipe(in EndOfPipeWrite write)
|
|
{
|
|
EndOfPipeWrites.Add(write);
|
|
Calls.Add($"eop {write.Kind}");
|
|
}
|
|
|
|
public void TriggerInterrupt(int eventId, uint contextId) => Calls.Add($"interrupt {eventId} {contextId}");
|
|
|
|
public bool HasFlipSlot() => FlipSlot;
|
|
|
|
public ulong PrepareFlip(int handle, int index, int flipMode, long flipArgument)
|
|
{
|
|
Calls.Add($"prepare_flip {handle} {index} {flipMode} {flipArgument}");
|
|
return NextFlipRequestId++;
|
|
}
|
|
|
|
public void PrepareCpuFlip(int handle, int index, ulong requestId) => Calls.Add($"cpu_flip {handle} {index} {requestId}");
|
|
|
|
public bool IsFlipDone(int handle, int index)
|
|
{
|
|
Calls.Add($"flip_done? {handle} {index}");
|
|
return FlipDone;
|
|
}
|
|
|
|
public void DrawIndexed(ulong submitId, in DrawIndexedArguments arguments)
|
|
{
|
|
IndexedDraws.Add(arguments);
|
|
Calls.Add($"draw_indexed {submitId} {arguments.IndexCount}");
|
|
}
|
|
|
|
public void DrawAuto(ulong submitId, in DrawAutoArguments arguments)
|
|
{
|
|
AutoDraws.Add(arguments);
|
|
Calls.Add($"draw_auto {submitId} {arguments.VertexCount}");
|
|
}
|
|
|
|
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
|
|
Calls.Add(indirectArgumentsAddress == 0
|
|
? $"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}"
|
|
: $"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X} @{indirectArgumentsAddress:X}");
|
|
|
|
public bool ResolvesIndirectDispatchOnGpu { get; set; }
|
|
|
|
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);
|
|
|
|
public uint ReadDword(ulong address)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[4];
|
|
Assert.True(GuestMemory.TryRead(address, bytes));
|
|
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
|
}
|
|
|
|
public ulong ReadQword(ulong address)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[8];
|
|
Assert.True(GuestMemory.TryRead(address, bytes));
|
|
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
}
|
|
|
|
public void WriteDword(ulong address, uint value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[4];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
|
Assert.True(GuestMemory.TryWrite(address, bytes));
|
|
}
|
|
|
|
public void WriteQword(ulong address, ulong value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[8];
|
|
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
|
Assert.True(GuestMemory.TryWrite(address, bytes));
|
|
}
|
|
|
|
public void WriteWords(ulong address, ReadOnlySpan<uint> words) =>
|
|
Assert.True(GuestMemory.TryWrite(address, MemoryMarshal.AsBytes(words)));
|
|
}
|
|
|
|
// Builds packet words and runs them through one processor, keeping the cursor stack for retries.
|
|
internal sealed class StreamRunner
|
|
{
|
|
public const ulong CommandAddress = RecordingCommandStreamHost.MemoryBase + 0x1000;
|
|
public const ulong LabelAddress = RecordingCommandStreamHost.MemoryBase + 0x8000;
|
|
public const ulong TableAddress = RecordingCommandStreamHost.MemoryBase + 0x9000;
|
|
public const ulong DataAddress = RecordingCommandStreamHost.MemoryBase + 0xA000;
|
|
|
|
public StreamRunner(int queueId = 0)
|
|
{
|
|
Host = new RecordingCommandStreamHost();
|
|
Interpreter = new GpuCommandInterpreter(Host, queueId, queueId == 0 ? 0 : GpuCommandInterpreter.ComputeQueueBase + queueId - 1);
|
|
}
|
|
|
|
public RecordingCommandStreamHost Host { get; }
|
|
|
|
public GpuCommandInterpreter Interpreter { get; }
|
|
|
|
public PacketCursorStack Commands { get; private set; } = new();
|
|
|
|
public uint Dwords { get; private set; }
|
|
|
|
public static uint[] CustomPacket(uint opcode, uint customCode, params uint[] payload)
|
|
{
|
|
var words = new uint[payload.Length + 1];
|
|
words[0] = PacketHeader.Make((uint)words.Length, opcode, customCode);
|
|
payload.CopyTo(words, 1);
|
|
return words;
|
|
}
|
|
|
|
public static uint[] Packet(uint opcode, params uint[] payload) => CustomPacket(opcode, 0, payload);
|
|
|
|
public static uint[] Concat(params uint[][] packets)
|
|
{
|
|
var length = 0;
|
|
foreach (var packet in packets)
|
|
{
|
|
length += packet.Length;
|
|
}
|
|
|
|
var words = new uint[length];
|
|
var offset = 0;
|
|
foreach (var packet in packets)
|
|
{
|
|
packet.CopyTo(words, offset);
|
|
offset += packet.Length;
|
|
}
|
|
|
|
return words;
|
|
}
|
|
|
|
public void Load(params uint[][] packets) => Load(CommandAddress, Concat(packets));
|
|
|
|
// Loading new packets starts a new stream; Run() without packets continues the current one.
|
|
public void Load(ulong address, uint[] words)
|
|
{
|
|
Host.WriteWords(address, words);
|
|
Dwords = (uint)words.Length;
|
|
Commands = new PacketCursorStack();
|
|
}
|
|
|
|
public SubmissionProgress Run() => Interpreter.Process(Commands, CommandAddress, Dwords);
|
|
|
|
public SubmissionProgress Run(params uint[][] packets)
|
|
{
|
|
Load(packets);
|
|
return Run();
|
|
}
|
|
|
|
public CommandStreamFatalException RunExpectingFatal(params uint[][] packets)
|
|
{
|
|
Load(packets);
|
|
return Assert.Throws<CommandStreamFatalException>(() => Run());
|
|
}
|
|
|
|
public static uint Low(ulong value) => (uint)value;
|
|
|
|
public static uint High(ulong value) => (uint)(value >> 32);
|
|
}
|