fix(gpu): support Gen5 flat memory and 3D images (#587)

Vector-mesh UI text samples type-10 volume LUTs; treat MIMG DIM=2 as
Dim3D and transport depth through AGC and Vulkan so Z slices no longer
collapse into a single 2D plane.
This commit is contained in:
MarcelMediaDev
2026-07-24 13:44:58 +01:00
committed by GitHub
parent 21f964a0dc
commit 5228335f15
14 changed files with 1311 additions and 299 deletions
@@ -0,0 +1,179 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5FlatMemoryTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
[Fact]
public void FlatLoadUbyteInfersScalarBaseAndCompiles()
{
var memory = new TestCpuMemory(ShaderAddress, 0x4000);
uint[] words =
[
// v_add_co_u32 v1, vcc_lo, s12, v6
0xD70F6A01,
0x00020C0C,
// v_add_co_ci_u32_sdwa v2, vcc_lo, 0, s13, vcc_lo
0x50041AF9,
0x86860680,
// flat_load_ubyte v0, v[1:2]
0xDC200000,
0x007D0001,
SEndpgm,
];
var shader = new byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader.AsSpan(index * sizeof(uint)),
words[index]);
}
Assert.True(memory.TryWrite(ShaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
ShaderAddress,
out var program,
out var decodeError),
decodeError);
var instruction = Assert.Single(
program.Instructions,
item => item.Opcode == "FlatLoadUbyte");
var control = Assert.IsType<Gen5GlobalMemoryControl>(
instruction.Control);
Assert.True(control.UsesFlatAddress);
Assert.Equal(1u, control.VectorAddress);
Assert.Equal(0u, control.VectorData);
Assert.Equal(12u, control.ScalarAddress);
Assert.Equal(
[
Gen5Operand.Vector(1),
Gen5Operand.Vector(2),
Gen5Operand.Scalar(12),
],
instruction.Sources);
uint[] userData =
[
unchecked((uint)ShaderAddress),
unchecked((uint)(ShaderAddress >> 32)),
];
var state = new Gen5ShaderState(
program,
userData,
null,
UserDataScalarRegisterBase: 12);
Assert.True(
Gen5ShaderScalarEvaluator.TryEvaluate(
ctx,
state,
out var evaluation,
out var evaluationError),
evaluationError);
var binding = Assert.Single(evaluation.GlobalMemoryBindings);
Assert.Equal(12u, binding.ScalarAddress);
Assert.Contains(instruction.Pc, binding.InstructionPcs);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var compiled,
out var compileError),
compileError);
Assert.Contains(
(ushort)SpirvOp.ISub,
ReadSpirvOpcodes(compiled.Spirv));
}
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
{
Assert.Equal(0, spirv.Length % sizeof(uint));
Assert.True(spirv.Length >= 5 * sizeof(uint));
Assert.Equal(
0x07230203u,
BinaryPrimitives.ReadUInt32LittleEndian(spirv));
var opcodes = new List<ushort>();
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
{
var instruction =
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
var wordCount = checked((int)(instruction >> 16));
Assert.InRange(
wordCount,
1,
(spirv.Length - offset) / sizeof(uint));
opcodes.Add((ushort)instruction);
offset += wordCount * sizeof(uint);
}
return opcodes;
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(
ulong virtualAddress,
ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(
ulong virtualAddress,
int length,
out int offset)
{
offset = 0;
if (virtualAddress < baseAddress)
{
return false;
}
var relative = virtualAddress - baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
}
@@ -0,0 +1,197 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5ImageTests
{
private const ulong ShaderAddress = 0x1_0000_C000;
private const uint SEndpgm = 0xBF810000;
[Theory]
[InlineData(1u, SpirvImageDim.Dim2D, 2u)]
[InlineData(2u, SpirvImageDim.Dim3D, 3u)]
public void ImageStoreDimensionControlsImageAndCoordinateTypes(
uint dimension,
SpirvImageDim expectedImageDimension,
uint expectedCoordinateComponents)
{
var instructions = ReadSpirvInstructions(
CompileImageOperation("ImageStore", dimension));
var imageType = Assert.Single(
instructions,
item => item.Opcode == SpirvOp.TypeImage);
Assert.Equal((uint)expectedImageDimension, imageType.Operands[2]);
Assert.Equal(2u, imageType.Operands[6]);
AssertCoordinateVectorWidth(
instructions,
SpirvOp.ImageWrite,
coordinateOperand: 1,
expectedComponents: expectedCoordinateComponents);
var sizeQuery = Assert.Single(
instructions,
item => item.Opcode == SpirvOp.ImageQuerySize);
AssertVectorTypeWidth(
instructions,
sizeQuery.Operands[0],
expectedCoordinateComponents);
}
[Fact]
public void ImageSampleDim3DUsesThreeComponentSampleCoordinates()
{
var instructions = ReadSpirvInstructions(
CompileImageOperation("ImageSampleLz", dimension: 2));
var imageType = Assert.Single(
instructions,
item => item.Opcode == SpirvOp.TypeImage);
Assert.Equal((uint)SpirvImageDim.Dim3D, imageType.Operands[2]);
Assert.Equal(1u, imageType.Operands[6]);
AssertCoordinateVectorWidth(
instructions,
SpirvOp.ImageSampleExplicitLod,
coordinateOperand: 3,
expectedComponents: 3);
}
private static byte[] CompileImageOperation(string opcode, uint dimension)
{
var addressRegisters = dimension == 2
? new uint[] { 0, 1, 2 }
: [0, 1];
var control = new Gen5ImageControl(
Dmask: 0xF,
VectorAddress: 0,
AddressRegisters: addressRegisters,
VectorData: 4,
ScalarResource: 8,
ScalarSampler: 16,
Dimension: dimension,
IsArray: false,
Glc: false,
Slc: false,
A16: false,
D16: false);
var imageInstruction = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Mimg,
opcode,
[],
[],
[],
control);
var end = new Gen5ShaderInstruction(
8,
Gen5ShaderEncoding.Sopp,
"SEndpgm",
[SEndpgm],
[],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(ShaderAddress, [imageInstruction, end]),
[],
null);
var scalarRegisters = new uint[256];
var descriptor = new uint[8];
descriptor[1] = 71u << 20; // FORMAT_16_16_16_16_FLOAT
descriptor[3] = (dimension == 2 ? 10u : 9u) << 28;
var evaluation = new Gen5ShaderEvaluation(
scalarRegisters,
scalarRegisters,
[
new Gen5ImageBinding(
imageInstruction.Pc,
imageInstruction.Opcode,
control,
descriptor,
new uint[4],
null),
],
[]);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
return shader.Spirv;
}
private static IReadOnlyList<ParsedSpirvInstruction> ReadSpirvInstructions(
byte[] spirv)
{
var instructions = new List<ParsedSpirvInstruction>();
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
{
var instruction = BinaryPrimitives.ReadUInt32LittleEndian(
spirv.AsSpan(offset));
var wordCount = checked((int)(instruction >> 16));
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
var operands = new uint[wordCount - 1];
for (var operand = 0; operand < operands.Length; operand++)
{
operands[operand] = BinaryPrimitives.ReadUInt32LittleEndian(
spirv.AsSpan(offset + (operand + 1) * sizeof(uint)));
}
instructions.Add(
new ParsedSpirvInstruction((SpirvOp)(ushort)instruction, operands));
offset += wordCount * sizeof(uint);
}
return instructions;
}
private static void AssertCoordinateVectorWidth(
IReadOnlyList<ParsedSpirvInstruction> instructions,
SpirvOp operation,
int coordinateOperand,
uint expectedComponents)
{
var imageOperation = Assert.Single(
instructions,
item => item.Opcode == operation);
var coordinateId = imageOperation.Operands[coordinateOperand];
var coordinate = Assert.Single(
instructions,
item =>
item.Opcode == SpirvOp.CompositeConstruct &&
item.Operands.Length >= 2 &&
item.Operands[1] == coordinateId);
AssertVectorTypeWidth(
instructions,
coordinate.Operands[0],
expectedComponents);
}
private static void AssertVectorTypeWidth(
IReadOnlyList<ParsedSpirvInstruction> instructions,
uint vectorTypeId,
uint expectedComponents)
{
var vectorType = Assert.Single(
instructions,
item =>
item.Opcode == SpirvOp.TypeVector &&
item.Operands[0] == vectorTypeId);
Assert.Equal(expectedComponents, vectorType.Operands[2]);
}
private readonly record struct ParsedSpirvInstruction(
SpirvOp Opcode,
uint[] Operands);
}
@@ -0,0 +1,22 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>