diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index a9082ab..9da55ea 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -151,6 +151,7 @@ public static partial class Gen5SpirvTranslator private readonly Dictionary _scalarRegisterVariables = new(); private readonly Dictionary _vectorRegisterVariables = new(); private uint _vectorRegisters; + private (uint First, uint Last) _dynamicVectorRange; private uint _packedHalfRegisters; private uint _scc; private uint _vcc; @@ -546,11 +547,11 @@ public static partial class Gen5SpirvTranslator if (_request.Program.Instructions.Any(static instruction => instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal))) { - var vectorArrayType = _module.TypeArray(_uintType, VectorRegisterCount); - _vectorRegisters = _module.AddGlobalVariable( - _module.TypePointer(SpirvStorageClass.Private, vectorArrayType), - SpirvStorageClass.Private, - _module.ConstantNull(vectorArrayType)); + // A dynamically indexed private array cannot live in registers: Metal spills the + // whole 2 KiB file to thread memory and every VGPR access becomes a memory access + // (Octopath's instanced vertex shader: 112 ms per draw). Keep one variable per + // register and resolve V_MOVREL* with a select over the registers the program uses. + _dynamicVectorRange = DynamicVectorRange(_request.Program); } _scc = _module.AddGlobalVariable( @@ -6806,26 +6807,61 @@ public static partial class Gen5SpirvTranslator // takes a computed index instead of a constant. The index is masked to // the array bounds: SPIR-V leaves an out-of-range Private access chain // undefined, and a mask costs nothing next to the surrounding load. - private uint DynamicVectorPointer(uint registerIndex) => - _module.AddInstruction( - SpirvOp.AccessChain, - _privateUintPointer, - _vectorRegisters, - BitwiseAnd(registerIndex, UInt(VectorRegisterCount - 1))); + // Every register a relative access can reach: from the lowest V_MOVREL* base to past the + // highest register the program names (a multi-dword result may extend beyond its listed + // first register). An index outside it reads zero and writes nothing. + private static (uint First, uint Last) DynamicVectorRange(Gen5ShaderProgram program) + { + var first = VectorRegisterCount - 1; + var last = 0u; + foreach (var instruction in program.Instructions) + { + foreach (var operand in instruction.Sources.Concat(instruction.Destinations)) + { + if (operand.Kind == Gen5OperandKind.VectorRegister) + { + last = Math.Max(last, operand.Value); + if (instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal)) + { + first = Math.Min(first, operand.Value); + } + } + } + } - private uint LoadVDynamic(uint registerIndex) => - Load(_uintType, DynamicVectorPointer(registerIndex)); + last = Math.Min(last + 8, VectorRegisterCount - 1); + return (Math.Min(first, last), last); + } + + private uint LoadVDynamic(uint registerIndex) + { + var result = UInt(0); + for (var register = _dynamicVectorRange.First; register <= _dynamicVectorRange.Last; register++) + { + result = _module.AddInstruction( + SpirvOp.Select, + _uintType, + _module.AddInstruction(SpirvOp.IEqual, _boolType, registerIndex, UInt(register)), + Load(_uintType, VectorPointer(register)), + result); + } + + return result; + } private void StoreVDynamic(uint registerIndex, uint value) { - var pointer = DynamicVectorPointer(registerIndex); - value = _module.AddInstruction( - SpirvOp.Select, - _uintType, - Load(_boolType, _exec), - value, - Load(_uintType, pointer)); - Store(pointer, value); + var exec = Load(_boolType, _exec); + for (var register = _dynamicVectorRange.First; register <= _dynamicVectorRange.Last; register++) + { + var pointer = VectorPointer(register); + var selected = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + exec, + _module.AddInstruction(SpirvOp.IEqual, _boolType, registerIndex, UInt(register))); + Store(pointer, _module.AddInstruction(SpirvOp.Select, _uintType, selected, value, Load(_uintType, pointer))); + } } private uint PackedHalfPointer(uint register) => diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5MoveRelativeSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5MoveRelativeSpirvTests.cs index bf81482..b36955b 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5MoveRelativeSpirvTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5MoveRelativeSpirvTests.cs @@ -10,7 +10,8 @@ using Xunit; namespace SharpEmu.Libs.Tests.Agc; -// Checks computed register indices and rejects invalid relative sources. +// Checks that relative register accesses resolve without a dynamically indexed register +// file, and rejects invalid relative sources. public sealed class Gen5MoveRelativeSpirvTests { private const ulong ShaderAddress = 0x1_0000_0000; @@ -24,25 +25,21 @@ public sealed class Gen5MoveRelativeSpirvTests private const uint SMovM0 = 0xBE800000u | (124u << 16) | (0x03u << 8) | 130u; [Fact] - public void MovrelsB32_ReadsTheSourceRegisterThroughADynamicIndex() + public void MovrelsB32_ReadsTheSourceRegisterThroughAComputedIndex() { // s_mov_b32 m0, 2 ; v_movrels_b32 v5, v3 -> v5 = vgpr[3 + m0] var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x43u << 9) | (256u + 3u)]); - Assert.True( - HasDynamicVectorRegisterAccess(spirv), - "V_MOVRELS_B32 must index the VGPR array with a computed index"); + AssertResolvesRelativeRegistersWithSelects(spirv, "V_MOVRELS_B32"); } [Fact] - public void MovreldB32_WritesTheDestinationRegisterThroughADynamicIndex() + public void MovreldB32_WritesTheDestinationRegisterThroughAComputedIndex() { // s_mov_b32 m0, 2 ; v_movreld_b32 v5, v3 -> vgpr[5 + m0] = v3 var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x42u << 9) | (256u + 3u)]); - Assert.True( - HasDynamicVectorRegisterAccess(spirv), - "V_MOVRELD_B32 must index the VGPR array with a computed index"); + AssertResolvesRelativeRegistersWithSelects(spirv, "V_MOVRELD_B32"); } [Fact] @@ -51,9 +48,7 @@ public sealed class Gen5MoveRelativeSpirvTests // s_mov_b32 m0, 2 ; v_movrelsd_b32 v5, v3 -> vgpr[5 + m0] = vgpr[3 + m0] var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x44u << 9) | (256u + 3u)]); - Assert.True( - HasDynamicVectorRegisterAccess(spirv), - "V_MOVRELSD_B32 must index the VGPR array with a computed index"); + AssertResolvesRelativeRegistersWithSelects(spirv, "V_MOVRELSD_B32"); } [Fact] @@ -63,9 +58,7 @@ public sealed class Gen5MoveRelativeSpirvTests // 10-bit halves (source index in [9:0], destination index in [25:16]). var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x48u << 9) | (256u + 3u)]); - Assert.True( - HasDynamicVectorRegisterAccess(spirv), - "V_MOVRELSD_2_B32 must index the VGPR array with a computed index"); + AssertResolvesRelativeRegistersWithSelects(spirv, "V_MOVRELSD_2_B32"); } [Fact] @@ -82,14 +75,14 @@ public sealed class Gen5MoveRelativeSpirvTests Assert.Contains("vector register", error, StringComparison.Ordinal); } - // True when some OpAccessChain into the "vgpr" array uses an index that is - // not an OpConstant — i.e. a register number computed from M0. - private static bool HasDynamicVectorRegisterAccess(byte[] spirv) + // A dynamically indexed private register array is lowered to thread memory on Metal, so no + // access chain may take a computed index; the relative access compares the computed register + // number with each candidate register (OpIEqual) and selects its value (OpSelect). + private static void AssertResolvesRelativeRegistersWithSelects(byte[] spirv, string opcode) { - var vectorRegisters = FindNamedId(spirv, "vgpr"); - Assert.True(vectorRegisters != 0, "the module must name its VGPR array"); - var constants = new HashSet(); + var equalities = 0; + var selects = 0; foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) { // OpConstant = 43, OpConstantNull = 46: (opcode, resultType, resultId, ...). @@ -97,47 +90,29 @@ public sealed class Gen5MoveRelativeSpirvTests { constants.Add(ReadWord(spirv, offset + 8)); } + + // OpIEqual = 170, OpSelect = 169. + equalities += op == 170 ? 1 : 0; + selects += op == 169 ? 1 : 0; } foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) { // OpAccessChain = 65: (opcode, resultType, resultId, base, index...). - if (op != 65 || wordCount < 5 || ReadWord(spirv, offset + 12) != vectorRegisters) + if (op != 65) { continue; } - if (!constants.Contains(ReadWord(spirv, offset + 16))) + for (var index = 4; index < wordCount; index++) { - return true; + Assert.True( + constants.Contains(ReadWord(spirv, offset + index * sizeof(uint))), + $"{opcode} must not index a register array with a computed index"); } } - return false; - } - - // Result id of the OpName whose literal string matches, or 0. - private static uint FindNamedId(byte[] spirv, string name) - { - foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) - { - // OpName = 5: (opcode, target, literal string...). - if (op != 5 || wordCount < 3) - { - continue; - } - - var bytes = spirv.AsSpan(offset + 8, (wordCount - 2) * sizeof(uint)); - var terminator = bytes.IndexOf((byte)0); - var text = System.Text.Encoding.UTF8.GetString( - terminator < 0 ? bytes : bytes[..terminator]); - if (text == name) - { - return ReadWord(spirv, offset + 4); - } - } - - return 0; + Assert.True(equalities > 1 && selects > 1, $"{opcode} must select the register its computed index names"); } private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(