sharpemu/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelT...

425 lines
18 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.GpuCommands;
public sealed class GpuCommandInterpreterLabelTests
{
private const uint Nop = PacketOpcode.Nop;
private const ulong Label = StreamRunner.LabelAddress;
private static uint[] EventWriteEop(uint eventType, uint eventIndex, uint cacheAction, uint source, uint interruptSelector, ulong destination, ulong value, uint cachePolicy) =>
StreamRunner.Packet(
PacketOpcode.EventWriteEndOfPipe,
eventType | (eventIndex << 8) | (cacheAction << 12) | (cachePolicy << 25),
StreamRunner.Low(destination),
(StreamRunner.High(destination) & 0xFFFF) | (interruptSelector << 24) | (source << 29),
StreamRunner.Low(value),
StreamRunner.High(value));
private static uint[] EventWriteEos(uint eventType, uint eventIndex, uint cacheAction, uint source, uint interruptSelector, ulong destination, uint value)
{
var packet = StreamRunner.Packet(
PacketOpcode.EventWriteEndOfShader,
eventType | (eventIndex << 8) | (cacheAction << 12),
StreamRunner.Low(destination),
(StreamRunner.High(destination) & 0xFFFF) | (interruptSelector << 24) | (source << 29),
value);
packet[0] |= 2u;
return packet;
}
private static uint[] ReleaseMemoryNative(uint eventType, uint eventIndex, uint gcr, uint destinationSelect, uint dataSelection, uint interruptSelector, ulong destination, ulong value, uint contextId) =>
StreamRunner.Packet(
PacketOpcode.ReleaseMemory,
eventType | (eventIndex << 8) | (gcr << 12),
(destinationSelect << 16) | (interruptSelector << 24) | (dataSelection << 29),
StreamRunner.Low(destination), StreamRunner.High(destination),
StreamRunner.Low(value), StreamRunner.High(value),
contextId);
private static uint[] ReleaseMemoryWrapped(uint action, uint gcr, uint dataSelection, uint interruptSelector, ulong destination, ulong value, uint contextId) =>
StreamRunner.CustomPacket(
Nop, PacketCustomCode.ReleaseMemory,
action,
gcr | (dataSelection << 16) | (interruptSelector << 24),
StreamRunner.Low(destination), StreamRunner.High(destination),
StreamRunner.Low(value), StreamRunner.High(value),
contextId);
[Fact]
public void EndOfPipeWrite64_LandsBeforeTheCompletionIsRecorded()
{
var runner = new StreamRunner();
runner.Run(EventWriteEop(0x28, 0, 0x00, 2, 2, Label, 0x1122_3344_5566_7788, 0));
var write = Assert.Single(runner.Host.EndOfPipeWrites);
Assert.Equal(EndOfPipeWriteKind.Interrupt64, write.Kind);
Assert.Equal(Label, write.Destination);
Assert.Equal(0x1122_3344_5566_7788UL, runner.Host.ReadQword(Label));
Assert.Equal(0, write.EventId);
}
[Theory]
[InlineData(0x04u, 5u, 0x00u, 0u, EndOfPipeWriteKind.Write64)]
[InlineData(0x14u, 0u, 0x38u, 0u, EndOfPipeWriteKind.WriteBack64)]
[InlineData(0x2Fu, 0u, 0x00u, 0u, EndOfPipeWriteKind.Write64)]
[InlineData(0x2Fu, 0u, 0x00u, 2u, EndOfPipeWriteKind.Interrupt64)]
[InlineData(0x04u, 5u, 0x3Bu, 2u, EndOfPipeWriteKind.InterruptWriteBack64)]
[InlineData(0x28u, 0u, 0x38u, 2u, EndOfPipeWriteKind.InterruptWriteBack64)]
public void EndOfPipeWrite64_AcceptedCombinations(uint eventType, uint eventIndex, uint cacheAction, uint interruptSelector, EndOfPipeWriteKind expected)
{
var runner = new StreamRunner();
runner.Run(EventWriteEop(eventType, eventIndex, cacheAction, 2, interruptSelector, Label, 0xABCD, 0));
Assert.Equal(expected, Assert.Single(runner.Host.EndOfPipeWrites).Kind);
Assert.Equal(0xABCDUL, runner.Host.ReadQword(Label));
}
[Theory]
[InlineData(0x2Fu, 5u, 0x00u, 2u)]
[InlineData(0x04u, 0u, 0x00u, 0u)]
[InlineData(0x2Fu, 6u, 0x38u, 2u)]
public void EndOfPipeWrite64_RejectedCombinations(uint eventType, uint eventIndex, uint cacheAction, uint interruptSelector)
{
var runner = new StreamRunner();
var fatal = runner.RunExpectingFatal(EventWriteEop(eventType, eventIndex, cacheAction, 2, interruptSelector, Label, 1, 0));
Assert.Contains("event type is unknown", fatal.Message);
Assert.Contains($"type=0x{eventType:X2}", fatal.Message);
}
[Fact]
public void InterruptSelectorOne_OnGraphicsQueuesTheInterruptWithoutAWrite()
{
var runner = new StreamRunner();
runner.Host.WriteQword(Label, 0);
runner.Run(EventWriteEop(0x28, 0, 0x00, 2, 1, Label, 0xFF, 0));
Assert.Equal(0UL, runner.Host.ReadQword(Label));
var write = Assert.Single(runner.Host.EndOfPipeWrites);
Assert.Equal(EndOfPipeWriteKind.InterruptOnly, write.Kind);
}
[Fact]
public void InterruptSelectorOne_OnComputeWritesAndInterrupts()
{
var runner = new StreamRunner(queueId: 3);
runner.Run(EventWriteEop(0x28, 0, 0x00, 2, 1, Label, 0xFF, 0));
Assert.Equal(0xFFUL, runner.Host.ReadQword(Label));
var write = Assert.Single(runner.Host.EndOfPipeWrites);
Assert.Equal(EndOfPipeWriteKind.Interrupt64, write.Kind);
Assert.Equal(GpuCommandInterpreter.ComputeQueueBase + 2, write.EventId);
}
[Fact]
public void ClockSource_WritesTheReferenceClock()
{
var runner = new StreamRunner();
runner.Run(EventWriteEop(0x04, 5, 0x00, 4, 0, Label, 0, 0));
Assert.NotEqual(0UL, runner.Host.ReadQword(Label));
Assert.Equal(EndOfPipeWriteKind.ClockWrite, Assert.Single(runner.Host.EndOfPipeWrites).Kind);
}
[Fact]
public void UnsupportedCachePolicyOrSelector_IsFatal()
{
var runner = new StreamRunner();
Assert.Contains("cache policy", runner.RunExpectingFatal(EventWriteEop(0x28, 0, 0, 2, 0, Label, 1, 1)).Message);
Assert.Contains("interrupt selector is unknown", runner.RunExpectingFatal(EventWriteEop(0x28, 0, 0, 2, 5, Label, 1, 0)).Message);
}
[Fact]
public void EndOfShader_WritesThirtyTwoBits()
{
var runner = new StreamRunner();
runner.Host.WriteQword(Label, ulong.MaxValue);
runner.Run(EventWriteEos(0x2F, 6, 0x38, 2, 0, Label, 0x1234));
Assert.Equal(0xFFFF_FFFF_0000_1234UL, runner.Host.ReadQword(Label));
Assert.Equal(EndOfPipeWriteKind.WriteBack32, Assert.Single(runner.Host.EndOfPipeWrites).Kind);
}
[Fact]
public void GdsSource_SynchronizesReadsAndInterruptsAtOnce()
{
var runner = new StreamRunner(queueId: 1);
runner.Host.Gds[8] = 0x11;
runner.Host.Gds[12] = 0x22;
runner.Run(EventWriteEos(0x2F, 6, 0x00, 1, 2, Label, (2u << 16) | 2u));
Assert.Equal(new[] { "synchronize", "read_gds 2 2", "eop GdsWrite32", $"interrupt {GpuCommandInterpreter.ComputeQueueBase} 0" }, runner.Host.Calls);
Assert.Equal(0x11u, runner.Host.ReadDword(Label));
Assert.Equal(0x22u, runner.Host.ReadDword(Label + 4));
}
// Release-memory packets leave their completions in the recording buffer; the
// command stream queue submits it when the slice ends or blocks (see
// CommandStreamQueueTests), so none of these expect a per-packet flush.
[Fact]
public void ReleaseMemoryNative_DataSelectionOne_WritesAndFollowsTheGcrBarrierRule()
{
var runner = new StreamRunner();
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 1, 0, Label, 0x77, 0));
Assert.Equal(new[] { "eop Write32" }, runner.Host.Calls);
Assert.Equal(0x77u, runner.Host.ReadDword(Label));
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryNative(0x28, 5, 1u << 9, 0, 1, 2, Label, 0x78, 9));
Assert.Equal(new[] { "barrier", "eop InterruptWriteBack32" }, runner.Host.Calls);
Assert.Equal(9u, runner.Host.EndOfPipeWrites[^1].ContextId);
}
[Fact]
public void ReleaseMemoryNative_NoDataOrSelectorFour_OnlyInterrupts()
{
var runner = new StreamRunner();
runner.Host.WriteDword(Label, 5);
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 0, 2, Label, 1, 0));
Assert.Equal(new[] { "eop InterruptOnly" }, runner.Host.Calls);
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryNative(0x04, 5, 0, 0, 1, 4, Label, 1, 0));
Assert.Equal(new[] { "barrier", "eop InterruptOnly" }, runner.Host.Calls);
Assert.Equal(5u, runner.Host.ReadDword(Label));
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 1, 3, Label, 1, 0));
Assert.Equal(new[] { "eop Write32" }, runner.Host.Calls);
Assert.Equal(1u, runner.Host.ReadDword(Label));
}
[Fact]
public void ReleaseMemoryNative_DataSelectionTwoAndThree()
{
var runner = new StreamRunner();
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 2, 0, Label, 0x0102_0304_0506_0708, 0));
Assert.Equal(0x0102_0304_0506_0708UL, runner.Host.ReadQword(Label));
Assert.Equal(EndOfPipeWriteKind.Write64, runner.Host.EndOfPipeWrites[^1].Kind);
Assert.DoesNotContain("flush", runner.Host.Calls);
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 3, 0, Label, 0, 0));
Assert.NotEqual(0UL, runner.Host.ReadQword(Label));
Assert.Equal(EndOfPipeWriteKind.ClockWrite, runner.Host.EndOfPipeWrites[^1].Kind);
}
[Theory]
[InlineData(false, 0)]
[InlineData(true, 0)]
[InlineData(false, 3)]
[InlineData(true, 3)]
public void ReleaseMemory_ComputeDoneWrites64BitsAndDefersTheInterrupt(bool wrapped, int queueId)
{
var runner = new StreamRunner(queueId);
const ulong value = 0x1122_3344_5566_7788;
const uint contextId = 9;
runner.Run(wrapped
? ReleaseMemoryWrapped(0x2F, 0, 2, 2, Label, value, contextId)
: ReleaseMemoryNative(0x2F, 6, 0, 0, 2, 2, Label, value, contextId));
Assert.Equal(value, runner.Host.ReadQword(Label));
var write = Assert.Single(runner.Host.EndOfPipeWrites);
Assert.Equal(EndOfPipeWriteKind.Interrupt64, write.Kind);
Assert.Equal(Label, write.Destination);
Assert.Equal(value, write.Value);
Assert.Equal(contextId, write.ContextId);
Assert.Equal(runner.Interpreter.InterruptEventId, write.EventId);
Assert.Equal(new[] { "barrier", "eop Interrupt64" }, runner.Host.Calls);
}
[Fact]
public void ReleaseMemoryNative_GdsSelectionInterruptsOnlyForSelectorOne()
{
var runner = new StreamRunner(queueId: 2);
runner.Host.Gds[0] = 0x42;
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 5, 1, Label, 1u << 16, 0));
Assert.Equal(new[] { "synchronize", "read_gds 0 1", "eop GdsWrite32", $"interrupt {GpuCommandInterpreter.ComputeQueueBase + 1} 0" }, runner.Host.Calls);
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 5, 0, Label, 1u << 16, 0));
Assert.DoesNotContain("flush", runner.Host.Calls);
}
[Fact]
public void ReleaseMemoryNative_RejectsUnknownSelections()
{
var runner = new StreamRunner();
Assert.Contains("data selection", runner.RunExpectingFatal(ReleaseMemoryNative(0x28, 5, 0, 0, 4, 0, Label, 1, 0)).Message);
Assert.Contains("destination is not supported", runner.RunExpectingFatal(ReleaseMemoryNative(0x28, 5, 0, 2, 1, 0, Label, 1, 0)).Message);
Assert.Contains("interrupt selector is unknown", runner.RunExpectingFatal(ReleaseMemoryNative(0x28, 5, 0, 0, 0, 7, Label, 1, 0)).Message);
}
[Fact]
public void ReleaseMemoryWrapped_DerivesTheEventIndexFromTheAction()
{
var runner = new StreamRunner();
runner.Run(ReleaseMemoryWrapped(0x04, 0, 2, 2, Label, 0x5, 3));
Assert.Equal(0x5UL, runner.Host.ReadQword(Label));
Assert.Equal(new[] { "barrier", "eop Interrupt64" }, runner.Host.Calls);
Assert.Equal(3u, runner.Host.EndOfPipeWrites[^1].ContextId);
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryWrapped(0x2F, 1u << 9, 1, 0, Label, 0x9, 0));
Assert.Equal(0x9u, runner.Host.ReadDword(Label));
Assert.Equal(new[] { "barrier", "eop WriteBack32" }, runner.Host.Calls);
}
[Fact]
public void ReleaseMemoryWrapped_ConditionalInterruptsCompareTheLabel()
{
var runner = new StreamRunner();
runner.Host.WriteDword(Label, 4);
runner.Run(ReleaseMemoryWrapped(0x28, 0, 1, 5, Label, 4, 1));
Assert.Equal(new[] { "eop InterruptOnly" }, runner.Host.Calls);
Assert.Equal(4u, runner.Host.ReadDword(Label));
runner.Host.Calls.Clear();
runner.Run(ReleaseMemoryWrapped(0x28, 0, 1, 5, Label, 3, 1));
Assert.Empty(runner.Host.Calls);
}
[Theory]
[InlineData(0x07u, 0u)]
[InlineData(0x16u, 7u)]
[InlineData(0x2Cu, 0u)]
public void EventWrite_FlushTypesEmitABarrier(uint eventType, uint eventIndex)
{
var runner = new StreamRunner();
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, eventType | (eventIndex << 8)));
Assert.Equal(new[] { "barrier" }, runner.Host.Calls);
}
[Fact]
public void EventWrite_IgnoredTypesUnknownTypesAndOcclusionDump()
{
var runner = new StreamRunner();
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x38));
Assert.Empty(runner.Host.Calls);
Assert.Contains("event type is unknown", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.EventWrite, 0x16 | (1u << 8))).Message);
Assert.Contains("event type is unknown", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.EventWrite, 0x3F)).Message);
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
// CPU-only stores leave GPU query-reduction inputs stale. Require the
// host transfer path for both words of every counter, including ready.
Assert.Equal(32, runner.Host.Calls.Count);
Assert.All(runner.Host.Calls, call => Assert.StartsWith("fill ", call));
Assert.Equal(1UL << 63, runner.Host.ReadQword(Label));
Assert.Equal(1UL << 63, runner.Host.ReadQword(Label + 15 * 16));
Assert.Equal(0UL, runner.Host.ReadQword(Label + 8));
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
Assert.Equal((1UL << 63) | 0x2FFFFFFUL, runner.Host.ReadQword(Label));
}
[Fact]
public void EventWrite_OcclusionDumpUsesHostSampleCountsWhenAvailable()
{
var runner = new StreamRunner();
runner.Host.OcclusionDumps = new List<ulong>();
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label + 8), StreamRunner.High(Label + 8)));
Assert.Equal(new[] { Label, Label + 8 }, runner.Host.OcclusionDumps);
Assert.Empty(runner.Host.Calls);
Assert.Equal(0UL, runner.Host.ReadQword(Label));
}
[Fact]
public void FlipMarkers_PrepareRecordAndFlush()
{
var runner = new StreamRunner();
var setFlip = StreamRunner.CustomPacket(Nop, PacketCustomCode.Flip, 7, 2, 1, 0x44, 0);
runner.Run(setFlip);
Assert.Equal(new[] { "prepare_flip 7 2 1 68", "eop Flip", "flush" }, runner.Host.Calls);
runner.Host.Calls.Clear();
runner.Run(StreamRunner.CustomPacket(Nop, 0, 0x6875_0778, StreamRunner.Low(Label), StreamRunner.High(Label), 0x99));
Assert.Equal(new[] { "prepare_flip 7 2 1 68", "eop FlipWithWrite32", "flush" }, runner.Host.Calls);
Assert.Equal(0x99u, runner.Host.ReadDword(Label));
runner.Host.Calls.Clear();
runner.Run(StreamRunner.CustomPacket(Nop, 0, 0x6875_0781, StreamRunner.Low(Label), StreamRunner.High(Label), 0x9A, 0x04, 0x38));
Assert.Equal(new[] { "prepare_flip 7 2 1 68", "eop FlipWithInterruptWriteBack32", "flush" }, runner.Host.Calls);
Assert.Contains("flip event type is unknown", runner.RunExpectingFatal(StreamRunner.CustomPacket(Nop, 0, 0x6875_0781, 0, 0, 0, 0x28, 0x38)).Message);
// Marker ids with no side effect (Demon's Souls emits 0x6EC and 0x80C) are consumed, not fatal.
runner.Host.Calls.Clear();
runner.Run(StreamRunner.CustomPacket(Nop, 0, 0x6875_06EC), StreamRunner.CustomPacket(Nop, 0, 0x6875_080C));
Assert.Empty(runner.Host.Calls);
}
[Fact]
public void FlipWithoutASlot_SuspendsBeforeWritingTheLabel_AndWritesOnceOnRetry()
{
var runner = new StreamRunner();
runner.Host.WriteDword(Label, 0);
runner.Host.FlipSlot = false;
var progress = runner.Run(
StreamRunner.CustomPacket(Nop, PacketCustomCode.Flip, 1, 0, 0, 0, 0),
StreamRunner.CustomPacket(Nop, 0, 0x6875_0778, StreamRunner.Low(Label), StreamRunner.High(Label), 0x55));
Assert.Equal(SubmissionProgress.Blocked, progress);
Assert.Equal(0u, runner.Host.ReadDword(Label));
runner.Host.FlipSlot = true;
Assert.Equal(SubmissionProgress.Complete, runner.Run());
Assert.Equal(0x55u, runner.Host.ReadDword(Label));
Assert.Equal(2, runner.Host.Calls.Count(call => call.StartsWith("prepare_flip", StringComparison.Ordinal)));
Assert.Equal(1, runner.Host.Calls.Count(call => call == "eop FlipWithWrite32"));
}
[Fact]
public void WaitFlipDone_FlushesThenSuspendsUntilTheBufferIsPresented()
{
var runner = new StreamRunner();
runner.Host.FlipDone = false;
var progress = runner.Run(StreamRunner.CustomPacket(Nop, PacketCustomCode.WaitFlipDone, 3, 1, 0, 0, 0, 0), StreamRunner.Packet(PacketOpcode.NumInstances, 5));
Assert.Equal(SubmissionProgress.Blocked, progress);
Assert.Equal(new[] { "flush", "flip_done? 3 1" }, runner.Host.Calls);
runner.Host.FlipDone = true;
Assert.Equal(SubmissionProgress.Complete, runner.Run());
Assert.Equal(5u, runner.Interpreter.InstanceCount);
}
[Fact]
public void AcquireMemory_IsANoOp()
{
var runner = new StreamRunner();
runner.Run(StreamRunner.CustomPacket(Nop, PacketCustomCode.AcquireMemory, 0, 0, 0, 0, 0, 0, 0), StreamRunner.Packet(PacketOpcode.AcquireMemory, 0, 0, 0, 0, 0, 0));
Assert.Empty(runner.Host.Calls);
}
}