sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs

390 lines
17 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.Pipelines;
// The vertex tables: registers from the header's direct resources, attributes by semantic, buffers merged by stream.
[Collection(SchedulingStateCollection.Name)]
public sealed class VertexInputResolverTests : IDisposable
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong ShaderAddress = MemoryBase + 0x100;
private const ulong UserDataBlock = MemoryBase + 0x1000;
private const ulong DirectOffsets = MemoryBase + 0x1100;
private const ulong Semantics = MemoryBase + 0x1200;
private const ulong AttributeTable = MemoryBase + 0x1300;
private const ulong BufferTable = MemoryBase + 0x1400;
private const ulong StreamBase = MemoryBase + 0x8000;
private const uint DirectResourceCount = 11;
private const uint VertexBufferTableType = 8;
private const uint VertexAttributeTableType = 10;
private const uint AttributeFormat32x2Float = 257;
private const uint BufferFormat32x2Float = 64;
private const uint BufferFormat32x4Float = 77;
private const uint IdentitySelect = 4 | (5 << 3) | (6 << 6) | (7 << 9);
private const uint X001Select = 4 | (0 << 3) | (0 << 6) | (1 << 9);
private const uint XY01Select = 4 | (5 << 3) | (0 << 6) | (1 << 9);
private const uint XYZ1Select = 4 | (5 << 3) | (6 << 6) | (1 << 9);
private readonly FatalScope _fatal = new();
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1_0000);
private readonly CpuContext _context;
public VertexInputResolverTests() => _context = new CpuContext(_memory, Generation.Gen5);
public void Dispose() => _fatal.Dispose();
private void WriteWord(ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Assert.True(_memory.TryWrite(address, bytes));
}
private void WriteQword(ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
Assert.True(_memory.TryWrite(address, bytes));
}
private void WriteHalf(ulong address, ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
Assert.True(_memory.TryWrite(address, bytes));
}
private static uint SemanticWord(uint semantic, uint hardwareMapping, uint size) => semantic | (hardwareMapping << 8) | (size << 16);
private static uint AttributeWord(uint bufferIndex, uint format, uint offset, uint fetchIndex = 0) =>
bufferIndex | (format << 5) | (offset << 14) | (fetchIndex << 26);
private void WriteBuffer(uint index, ulong address, uint stride, uint records, uint format = BufferFormat32x4Float)
{
var entry = BufferTable + index * 16u;
WriteWord(entry, (uint)address);
WriteWord(entry + 4, (uint)((address >> 32) & 0xFFFF) | (stride << 16));
WriteWord(entry + 8, records);
WriteWord(entry + 12, IdentitySelect | (format << 12));
}
// A user data block whose direct resources name the buffer table at s[10:11] and the attribute table at s[8:9].
private RegisteredShader RegisterTables(uint directCount = DirectResourceCount, ushort bufferRegister = 10, ushort attributeRegister = 8, uint semanticCount = 1, ulong semanticsAddress = Semantics)
{
WriteQword(UserDataBlock, DirectOffsets);
WriteHalf(UserDataBlock + 0x2C, (ushort)directCount);
for (uint type = 0; type < DirectResourceCount; type++)
{
WriteHalf(DirectOffsets + type * 2, 0xFFFF);
}
WriteHalf(DirectOffsets + VertexBufferTableType * 2, bufferRegister);
WriteHalf(DirectOffsets + VertexAttributeTableType * 2, attributeRegister);
return new RegisteredShader(ShaderAddress, MemoryBase, 16, 0, UserDataBlock, semanticsAddress, semanticCount, 0, 0);
}
[Fact]
public void TryReadTables_ReadsTheRegistersAndSemanticsFromTheHeader()
{
var shader = RegisterTables(semanticCount: 2);
WriteWord(Semantics, SemanticWord(1, 4, 2));
WriteWord(Semantics + 4, SemanticWord(0, 6, 4));
Assert.True(VertexInputResolver.TryReadTables(_context, shader, 32, out var metadata, out var error), error);
Assert.Equal(10, metadata.VertexBufferRegister);
Assert.Equal(8, metadata.VertexAttributeRegister);
Assert.Equal([SemanticWord(1, 4, 2), SemanticWord(0, 6, 4)], metadata.InputSemantics);
}
[Fact]
public void ResolveUsesStageRelativePointersAndMergedBufferLayout()
{
var shader = RegisterTables(semanticCount: 2);
WriteWord(Semantics, SemanticWord(1, 4, 2));
WriteWord(Semantics + 4, SemanticWord(0, 0, 2));
WriteWord(AttributeTable, AttributeWord(0, AttributeFormat32x2Float, 0));
WriteWord(AttributeTable + 4, AttributeWord(0, AttributeFormat32x2Float, 8));
WriteBuffer(0, StreamBase, 16, 3);
var userData = new uint[12];
userData[8] = unchecked((uint)AttributeTable);
userData[9] = (uint)(AttributeTable >> 32);
userData[10] = unchecked((uint)BufferTable);
userData[11] = (uint)(BufferTable >> 32);
var input = VertexInputResolver.ResolveVertexInputs(_context, shader, userData);
Assert.True(input.FetchEmbedded);
Assert.Equal(8, input.FetchAttributeRegister);
Assert.Equal(10, input.FetchBufferRegister);
Assert.Equal(new VertexInputBuffer(StreamBase, 16, 3), Assert.Single(input.Buffers));
Assert.Equal([8u, 0u], input.Attributes.Select(attribute => attribute.OffsetBytes));
Assert.Equal([4, 0], input.Attributes.Select(attribute => attribute.RegisterStart));
}
[Fact]
public void TryReadTables_WithoutDirectResources_HasNoTables()
{
var shader = RegisterTables(directCount: 0);
Assert.True(VertexInputResolver.TryReadTables(_context, shader, 32, out var metadata, out _));
Assert.Equal(VertexTableMetadata.NoRegister, metadata.VertexBufferRegister);
Assert.Equal(VertexTableMetadata.NoRegister, metadata.VertexAttributeRegister);
Assert.Empty(metadata.InputSemantics);
}
[Fact]
public void TryReadTables_IllegalOffsetSkipsTheTable()
{
var shader = RegisterTables(attributeRegister: 0xFFFF);
WriteWord(Semantics, SemanticWord(0, 0, 4));
Assert.True(VertexInputResolver.TryReadTables(_context, shader, 32, out var metadata, out _));
Assert.Equal(10, metadata.VertexBufferRegister);
Assert.Equal(VertexTableMetadata.NoRegister, metadata.VertexAttributeRegister);
}
[Fact]
public void TryReadTables_AttributeTableWithoutBufferTable_Fails()
{
var shader = RegisterTables(bufferRegister: 0xFFFF);
Assert.False(VertexInputResolver.TryReadTables(_context, shader, 32, out _, out var error));
Assert.Contains("needs a vertex buffer table", error);
}
[Theory]
[InlineData(31, 8)]
[InlineData(10, 31)]
public void TryReadTables_PointerOutsideTheUserRegisters_Fails(ushort bufferRegister, ushort attributeRegister)
{
var shader = RegisterTables(bufferRegister: bufferRegister, attributeRegister: attributeRegister);
WriteWord(Semantics, SemanticWord(0, 0, 4));
Assert.False(VertexInputResolver.TryReadTables(_context, shader, 32, out _, out var error));
Assert.Contains("outside the user register domain", error);
}
[Fact]
public void TryReadTables_SemanticsOutsideTheDomain_Fail()
{
Assert.False(VertexInputResolver.TryReadTables(_context, RegisterTables(semanticCount: 0), 32, out _, out var zero));
Assert.Contains("semantic count", zero);
Assert.False(VertexInputResolver.TryReadTables(_context, RegisterTables(semanticCount: 1, semanticsAddress: 0), 32, out _, out var missing));
Assert.Contains("semantics are missing", missing);
Assert.False(VertexInputResolver.TryReadTables(_context, RegisterTables(directCount: 12), 32, out _, out var domain));
Assert.Contains("outside the known resource domain", domain);
}
[Fact]
public void ApplySemantics_UsesTheSemanticAsTheAttributeIndexAndOverridesTheFormat()
{
// attribute 1: buffer 0, a known 32x2 float attribute format, byte offset 8.
WriteWord(AttributeTable, 0xDEAD_BEEF);
WriteWord(AttributeTable + 4, AttributeWord(0, AttributeFormat32x2Float, 8));
WriteBuffer(0, StreamBase, 16, 3, BufferFormat32x4Float);
var resources = VertexInputResolver.ApplySemantics(_context, [SemanticWord(1, 4, 2)], AttributeTable, BufferTable, ShaderAddress);
var resource = Assert.Single(resources);
Assert.Equal(1, resource.AttributeId);
Assert.Equal(4, resource.RegisterStart);
Assert.Equal(2, resource.RegisterCount);
Assert.Equal(0u, resource.FetchIndex);
Assert.Equal(StreamBase + 8, resource.Descriptor.Address);
Assert.Equal(16u, resource.Descriptor.Stride);
Assert.Equal(3u, resource.Descriptor.RecordCount);
Assert.Equal(BufferFormat32x2Float, resource.Descriptor.Format);
Assert.Equal(XY01Select, resource.Descriptor.DestinationSelectXYZW);
Assert.Equal(-1, resource.BufferIndex);
}
[Fact]
public void ApplySemantics_KeepsTheBufferFormatWhenTheAttributeFormatIsZero()
{
WriteWord(AttributeTable, AttributeWord(0, 0, 0, fetchIndex: 1));
WriteBuffer(0, StreamBase, 32, 5, BufferFormat32x4Float);
var resource = Assert.Single(VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 4)], AttributeTable, BufferTable, ShaderAddress));
Assert.Equal(BufferFormat32x4Float, resource.Descriptor.Format);
Assert.Equal(StreamBase, resource.Descriptor.Address);
Assert.Equal(1u, resource.FetchIndex);
}
[Fact]
public void ApplySemantics_KeepsEachDescriptorWhenScratchStorageIsReused()
{
WriteWord(AttributeTable, AttributeWord(0, 0, 0));
WriteWord(AttributeTable + 4, AttributeWord(1, 0, 0));
WriteBuffer(0, StreamBase, 32, 5, BufferFormat32x4Float);
WriteBuffer(1, StreamBase + 0x100, 8, 2, BufferFormat32x2Float);
var resources = VertexInputResolver.ApplySemantics(_context,
[SemanticWord(0, 0, 4), SemanticWord(1, 4, 2)], AttributeTable, BufferTable, ShaderAddress);
Assert.Equal(2, resources.Count);
Assert.Equal(StreamBase, resources[0].Descriptor.Address);
Assert.Equal(32u, resources[0].Descriptor.Stride);
Assert.Equal(5u, resources[0].Descriptor.RecordCount);
Assert.Equal(BufferFormat32x4Float, resources[0].Descriptor.Format);
Assert.Equal(StreamBase + 0x100, resources[1].Descriptor.Address);
Assert.Equal(8u, resources[1].Descriptor.Stride);
Assert.Equal(2u, resources[1].Descriptor.RecordCount);
Assert.Equal(BufferFormat32x2Float, resources[1].Descriptor.Format);
}
[Theory]
[InlineData(88u, 22u, X001Select)]
[InlineData(257u, 64u, XY01Select)]
[InlineData(298u, 74u, XYZ1Select)]
[InlineData(311u, 77u, IdentitySelect)]
public void ApplySemantics_DecodesPackedFormatsWithChannelSwizzle(uint attributeFormat, uint bufferFormat, uint destinationSelect)
{
WriteWord(AttributeTable, AttributeWord(0, attributeFormat, 0));
WriteBuffer(0, StreamBase, 8, 1, BufferFormat32x4Float);
var resource = Assert.Single(VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 2)], AttributeTable, BufferTable, ShaderAddress));
Assert.Equal(bufferFormat, resource.Descriptor.Format);
Assert.Equal(destinationSelect, resource.Descriptor.DestinationSelectXYZW);
}
[Theory]
[InlineData(1u << 25)]
[InlineData(1u << 26)]
public void ApplySemantics_StaticAttributes_AreFatal(uint staticBit)
{
var fatal = Assert.Throws<SchedulerFatalException>(() =>
VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 4) | staticBit], AttributeTable, BufferTable, ShaderAddress));
Assert.Contains("static vertex attribute", fatal.Message);
Assert.Contains($"shader=0x{ShaderAddress:X16}", fatal.Message);
}
[Fact]
public void ApplySemantics_BufferIndexOutsideTheTable_IsFatal()
{
WriteWord(AttributeTable, 0x1Fu | (0u << 5));
WriteBuffer(0, StreamBase, 8, 1);
WriteWord(AttributeTable, 32u & 0x1Fu);
// Index 31 is the highest table slot; the resolver reads it, so the fatal needs the reserved bit pattern.
var resources = VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 4)], AttributeTable, BufferTable, ShaderAddress);
Assert.Single(resources);
}
private static VertexAttributeResource Attribute(ulong address, uint stride, uint records, int attributeId, uint fetchIndex = 0) =>
new(new BufferDescriptorWords((uint)address, (uint)((address >> 32) & 0xFFFF) | (stride << 16), records, IdentitySelect | (BufferFormat32x4Float << 12)), 0, 4, attributeId, fetchIndex, -1, 0);
[Fact]
public void MergeBuffers_SharesOneBufferForAttributesWithinOneStride()
{
var resources = new List<VertexAttributeResource>
{
Attribute(StreamBase + 12, 32, 10, 0),
Attribute(StreamBase, 32, 10, 1),
Attribute(StreamBase + 16, 32, 10, 2),
};
var buffers = VertexInputResolver.MergeBuffers(resources, ShaderAddress);
var buffer = Assert.Single(buffers);
Assert.Equal(new VertexInputBuffer(StreamBase, 32, 10), buffer);
Assert.Equal([0, 0, 0], resources.Select(resource => resource.BufferIndex));
Assert.Equal([12u, 0u, 16u], resources.Select(resource => resource.OffsetBytes));
}
[Fact]
public void MergeBuffers_SeparatesDifferentStridesAndFetchRates()
{
var resources = new List<VertexAttributeResource>
{
Attribute(StreamBase, 16, 4, 0),
Attribute(StreamBase + 4, 32, 4, 1),
Attribute(StreamBase + 8, 16, 4, 2, fetchIndex: 1),
};
var buffers = VertexInputResolver.MergeBuffers(resources, ShaderAddress);
Assert.Equal(3, buffers.Length);
Assert.False(buffers[0].PerInstance);
Assert.True(buffers[2].PerInstance);
Assert.Equal([0, 1, 2], resources.Select(resource => resource.BufferIndex));
}
[Fact]
public void MergeBuffers_SeparatesBasesFurtherThanOneStride()
{
var resources = new List<VertexAttributeResource>
{
Attribute(StreamBase, 16, 4, 0),
Attribute(StreamBase + 16, 16, 4, 1),
};
var buffers = VertexInputResolver.MergeBuffers(resources, ShaderAddress);
Assert.Equal(2, buffers.Length);
Assert.Equal(StreamBase + 16, buffers[1].Address);
Assert.Equal(0u, resources[1].OffsetBytes);
}
[Fact]
public void MergeBuffers_RecordCountMismatch_IsFatal()
{
var resources = new List<VertexAttributeResource>
{
Attribute(StreamBase, 16, 4, 0),
Attribute(StreamBase + 4, 16, 5, 1),
};
var fatal = Assert.Throws<SchedulerFatalException>(() => VertexInputResolver.MergeBuffers(resources, ShaderAddress));
Assert.Contains("disagree on the record count", fatal.Message);
Assert.Contains("records=4/5", fatal.Message);
}
[Theory]
[InlineData(74u, 3, 12ul)]
[InlineData(77u, 4, 16ul)]
[InlineData(77u, 3, 16ul)]
[InlineData(64u, 2, 8ul)]
public void MergeBuffers_ConstantUncheckedStreamIncludesFullFormattedFetch(uint format, int components, ulong expectedSize)
{
var resource = Attribute(StreamBase, 0, 1, 0);
resource = resource with
{
Descriptor = resource.Descriptor with { Word3 = IdentitySelect | (format << 12) | (2u << 28) },
RegisterCount = components,
};
var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress));
Assert.Equal(1u, buffer.RecordCount);
Assert.Equal(0u, buffer.Stride);
Assert.Equal(expectedSize, buffer.Size);
}
[Theory]
[InlineData(0u, 1u)]
[InlineData(1u, 1u)]
[InlineData(3u, 1u)]
[InlineData(2u, 0u)]
public void MergeBuffers_BoundedOrUnboundConstantStreamKeepsDeclaredFootprint(uint outOfBounds, uint records)
{
var resource = Attribute(StreamBase, 0, records, 0);
resource = resource with { Descriptor = resource.Descriptor with { Word3 = resource.Descriptor.Word3 | (outOfBounds << 28) } };
var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress));
Assert.Equal((ulong)records, buffer.Size);
}
}