mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 05:30:57 +08:00
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:
@@ -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>
|
||||
Reference in New Issue
Block a user