253 lines
12 KiB
C#
253 lines
12 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 GpuCommandInterpreterDrawTests
|
|
{
|
|
private const uint Nop = PacketOpcode.Nop;
|
|
private const ulong Indices = StreamRunner.DataAddress;
|
|
private const ulong Arguments = StreamRunner.TableAddress;
|
|
|
|
private static uint[] SetBase(ulong address, bool dispatch)
|
|
{
|
|
var packet = StreamRunner.Packet(PacketOpcode.SetBase, 1, StreamRunner.Low(address), StreamRunner.High(address));
|
|
if (dispatch)
|
|
{
|
|
packet[0] |= 2u;
|
|
}
|
|
|
|
return packet;
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawIndex2_PassesTheIndexStateAndInstances()
|
|
{
|
|
var runner = new StreamRunner();
|
|
|
|
runner.Run(
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 1),
|
|
StreamRunner.Packet(PacketOpcode.NumInstances, 4),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndex2, 100, StreamRunner.Low(Indices), StreamRunner.High(Indices), 60, 0x20));
|
|
|
|
var draw = Assert.Single(runner.Host.IndexedDraws);
|
|
Assert.Equal(60u, draw.IndexCount);
|
|
Assert.Equal(Indices, draw.IndexAddress);
|
|
Assert.Equal(1u, draw.IndexTypeAndSize);
|
|
Assert.Equal(4u, draw.InstanceCount);
|
|
Assert.Equal(PacketOpcode.DrawIndex2, draw.Opcode);
|
|
Assert.Equal(StreamRunner.CommandAddress + 16, draw.PacketAddress);
|
|
Assert.Contains("indexed draw is not supported", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndex2, 10, 0, 0, 60, 0)).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawPreamble_UsesTheLongForm()
|
|
{
|
|
var runner = new StreamRunner();
|
|
|
|
runner.Run(StreamRunner.Packet(PacketOpcode.DispatchDrawPreamble, 30, StreamRunner.Low(Indices), StreamRunner.High(Indices), 8, 0, 0, 3, 0xA0));
|
|
|
|
var draw = Assert.Single(runner.Host.IndexedDraws);
|
|
Assert.Equal(30u, draw.IndexCount);
|
|
Assert.Equal(3u, draw.InstanceCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawIndexOffset_AddsTheScaledOffsetToTheIndexBase()
|
|
{
|
|
var runner = new StreamRunner();
|
|
|
|
runner.Run(
|
|
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 1),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexOffset2, 100, 5, 20, 0));
|
|
|
|
var draw = Assert.Single(runner.Host.IndexedDraws);
|
|
Assert.Equal(Indices + 20, draw.IndexAddress);
|
|
Assert.Equal(5u, runner.Interpreter.DrawIndexOffset);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(3u, 3u, 0x40000000u)]
|
|
[InlineData(3u, 3u, 1u)]
|
|
[InlineData(2u, 3u, 0u)]
|
|
public void DrawIndexOffsetRejectsInvalidPacketFlagsAndCounts(uint maximumIndexCount, uint indexCount, uint initiator)
|
|
{
|
|
var runner = new StreamRunner();
|
|
var failure = runner.RunExpectingFatal(StreamRunner.Packet(
|
|
PacketOpcode.DrawIndexOffset2, maximumIndexCount, 0, indexCount, initiator));
|
|
Assert.Contains("offset draw is not supported", failure.Message);
|
|
Assert.Empty(runner.Host.IndexedDraws);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawAuto_NativeWrappedAndMultiAuto()
|
|
{
|
|
var runner = new StreamRunner();
|
|
|
|
runner.Run(
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 12, 0x22),
|
|
StreamRunner.CustomPacket(Nop, PacketCustomCode.DrawIndexAuto, 0, 0, 0, 0, 0, 0),
|
|
StreamRunner.CustomPacket(Nop, PacketCustomCode.DrawIndexAuto, 7, 0, 0, 0, 0, 0),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexMultiAuto, 0, 0, 3u << 21));
|
|
|
|
Assert.Equal(new uint[] { 12, 7, 3 }, runner.Host.AutoDraws.Select(draw => draw.VertexCount));
|
|
Assert.Contains("auto draw flags", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 1, 0x1)).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndirectDraws_ReadArgumentsFromMemoryAndKeepTheInstanceCount()
|
|
{
|
|
var runner = new StreamRunner();
|
|
runner.Host.WriteWords(Arguments, new uint[] { 9, 3, 1, 2 });
|
|
runner.Host.WriteWords(Arguments + 16, new uint[] { 50, 2, 4, 7, 1 });
|
|
|
|
runner.Run(
|
|
SetBase(Arguments, dispatch: false),
|
|
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 0),
|
|
StreamRunner.Packet(PacketOpcode.IndexBufferSize, 40),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 5, 0));
|
|
|
|
var auto = runner.Host.AutoDraws[0];
|
|
Assert.Equal((9u, 3u, 1u, 2u, DrawOffsetSource.IndirectArguments), (auto.VertexCount, auto.InstanceCount, auto.FirstVertex, auto.FirstInstance, auto.OffsetSource));
|
|
var indexed = Assert.Single(runner.Host.IndexedDraws);
|
|
Assert.Equal(40u, indexed.IndexCount);
|
|
Assert.Equal(Indices + 8, indexed.IndexAddress);
|
|
Assert.Equal(7, indexed.BaseVertex);
|
|
Assert.Equal(2u, runner.Host.AutoDraws[1].InstanceCount);
|
|
|
|
Assert.Contains("initiator is not supported", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 3)).Message);
|
|
Assert.Contains("arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2)).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan()
|
|
{
|
|
var runner = new StreamRunner();
|
|
runner.Host.ResolvesIndirectDrawOnGpu = true;
|
|
runner.Host.WriteWords(Arguments + 16, new uint[] { 50, 2, 4, 7, 1 });
|
|
|
|
runner.Run(
|
|
SetBase(Arguments, dispatch: false),
|
|
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 0),
|
|
StreamRunner.Packet(PacketOpcode.IndexBufferSize, 40),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22));
|
|
|
|
var indexed = Assert.Single(runner.Host.IndexedDraws);
|
|
Assert.Equal((40u, Indices, Arguments + 16, DrawOffsetSource.IndirectArguments),
|
|
(indexed.IndexCount, indexed.IndexAddress, indexed.IndirectArgumentsAddress, indexed.OffsetSource));
|
|
Assert.DoesNotContain(runner.Host.GuestReads, address => address >= Arguments + 16 && address < Arguments + 36);
|
|
|
|
// A later draw that inherits the instance count reads it then.
|
|
runner.Run(StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 5, 0));
|
|
Assert.Equal(2u, runner.Host.AutoDraws[0].InstanceCount);
|
|
|
|
// 8-bit indices are expanded on the CPU, so their counts are read at once.
|
|
runner.Run(
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 2),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22));
|
|
Assert.Equal(0ul, runner.Host.IndexedDraws[1].IndirectArgumentsAddress);
|
|
Assert.Equal(40u, runner.Host.IndexedDraws[1].IndexCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsizedIndexedIndirectDraws_LearnTheIndexRangeBeforeUsingTheGpu()
|
|
{
|
|
var runner = new StreamRunner();
|
|
runner.Host.ResolvesIndirectDrawOnGpu = true;
|
|
runner.Host.WriteWords(Arguments + 16, new uint[] { 3, 1, 2, 0, 0 });
|
|
var draw = new[]
|
|
{
|
|
SetBase(Arguments, dispatch: false),
|
|
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
|
StreamRunner.Packet(PacketOpcode.IndexType, 0),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22),
|
|
};
|
|
|
|
runner.Run(draw);
|
|
runner.Run(draw);
|
|
|
|
Assert.Equal(2, runner.Host.IndexedDraws.Count);
|
|
Assert.Equal((3u, Indices + 4, 0ul), (runner.Host.IndexedDraws[0].IndexCount, runner.Host.IndexedDraws[0].IndexAddress, runner.Host.IndexedDraws[0].IndirectArgumentsAddress));
|
|
var learned = runner.Host.IndexedDraws[1];
|
|
Assert.Equal((Arguments + 16, Indices, true), (learned.IndirectArgumentsAddress, learned.IndexAddress, learned.UnboundedIndexBuffer));
|
|
Assert.Equal((uint)(((Indices + 10 + 0x3FFF) & ~0x3FFFul) - Indices) / 2, learned.IndexCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndirectMulti_LoopsOverTheStrideAndTheCountAddress()
|
|
{
|
|
var runner = new StreamRunner();
|
|
var countAddress = StreamRunner.LabelAddress;
|
|
runner.Host.WriteWords(Arguments, new uint[] { 1, 1, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0 });
|
|
runner.Host.WriteDword(countAddress, 2);
|
|
|
|
runner.Run(
|
|
SetBase(Arguments, dispatch: false),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndirectMulti, 0, 0, 0, 1u << 30, 5, StreamRunner.Low(countAddress), StreamRunner.High(countAddress), 32, 2),
|
|
StreamRunner.Packet(PacketOpcode.DrawIndirectMulti, 0, 0, 0, 0, 0, 0, 0, 32, 2));
|
|
|
|
Assert.Equal(new uint[] { 1, 2 }, runner.Host.AutoDraws.Select(draw => draw.VertexCount));
|
|
Assert.Contains("stride is smaller", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirectMulti, 0, 0, 0, 0, 1, 0, 0, 8, 2)).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatches_DirectAndBothIndirectEncodings()
|
|
{
|
|
var runner = new StreamRunner();
|
|
runner.Host.WriteWords(Arguments, new uint[] { 4, 5, 6 });
|
|
|
|
runner.Run(
|
|
StreamRunner.Packet(PacketOpcode.DispatchDirect, 1, 2, 3, 0x41),
|
|
SetBase(Arguments, dispatch: true),
|
|
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41),
|
|
StreamRunner.Packet(PacketOpcode.DispatchIndirect, StreamRunner.Low(Arguments), StreamRunner.High(Arguments), 0x43));
|
|
|
|
Assert.Equal(new[] { "dispatch 0 1 2 3 41", $"dispatch 0 4 5 6 41 @{Arguments:X}", $"dispatch 0 4 5 6 43 @{Arguments:X}" }, runner.Host.Calls);
|
|
Assert.Contains("indirect dispatch arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41)).Message);
|
|
Assert.Contains("set-base packet is not supported", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.SetBase, 2, 0, 0)).Message);
|
|
}
|
|
|
|
// A host that dispatches indirectly on the GPU gets the arguments address without the
|
|
// counts, so the interpreter never waits for a GPU readback of them. Thread-unit
|
|
// dispatches (initiator bit 5) still read them to convert threads into groups.
|
|
[Fact]
|
|
public void IndirectDispatches_LeaveWorkgroupCountsOnTheGpuWhenTheHostCan()
|
|
{
|
|
var runner = new StreamRunner();
|
|
runner.Host.ResolvesIndirectDispatchOnGpu = true;
|
|
runner.Host.WriteWords(Arguments, new uint[] { 4, 5, 6 });
|
|
|
|
runner.Run(
|
|
SetBase(Arguments, dispatch: true),
|
|
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41),
|
|
StreamRunner.Packet(PacketOpcode.DispatchIndirect, StreamRunner.Low(Arguments), StreamRunner.High(Arguments), 0x41));
|
|
|
|
Assert.Equal(new[] { $"dispatch 0 0 0 0 41 @{Arguments:X}", $"dispatch 0 0 0 0 41 @{Arguments:X}" }, runner.Host.Calls);
|
|
Assert.DoesNotContain(Arguments, runner.Host.GuestReads);
|
|
|
|
runner.Host.Calls.Clear();
|
|
runner.Run(
|
|
SetBase(Arguments, dispatch: true),
|
|
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x61));
|
|
Assert.Equal(new[] { $"dispatch 0 4 5 6 61 @{Arguments:X}" }, runner.Host.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndexCountPackets_SetTheBufferSize()
|
|
{
|
|
var runner = new StreamRunner();
|
|
|
|
runner.Run(StreamRunner.CustomPacket(Nop, PacketCustomCode.IndexCount, 77), StreamRunner.Packet(PacketOpcode.IndexBufferSize, 88));
|
|
|
|
Assert.Equal(88u, runner.Interpreter.IndexBufferSize);
|
|
}
|
|
}
|