diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index bc781f1..99aed16 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -321,7 +321,7 @@ public static partial class Gen5SpirvTranslator } EmitInitialState(); - if (StructuredControlFlow && blocks.Count > 1 && TryStructureRange(blocks, 0, blocks.Count, blocks.Count, emit: false, out _)) + if (StructuredControlFlow && blocks.Count > 0 && TryStructureRange(blocks, 0, blocks.Count, blocks.Count, emit: false, out _)) { // As the dispatcher does before its first block: an out-of-bounds invocation runs nothing. var runLabel = _module.AllocateId(); @@ -1513,26 +1513,47 @@ public static partial class Gen5SpirvTranslator } // Emits (or, without emit, only checks) blocks [begin, end). A branch from the range's last - // block to leave falls through to the range end. + // block to leave falls through to the range end. Inside a loop body, latch is the block + // whose backward branch the caller emits as the loop's continue, and a branch to end would + // leave the loop, which a structured body cannot express. private bool TryStructureRange( IReadOnlyList blocks, int begin, int end, int leave, bool emit, - out string error) + out string error, + int latch = -1) { error = string.Empty; var instructions = _request.Program.Instructions; var index = begin; while (index < end) { + // A loop body starts at its own header, which must not open the loop again. + if (!(latch >= 0 && index == begin) && index != latch && + TryFindLoopLatch(blocks, index, end, out var loopLatch)) + { + if (!TryStructureLoop(blocks, index, loopLatch, emit, out error)) + { + return false; + } + + index = loopLatch + 1; + continue; + } + if (emit && !TryEmitBlockBody(blocks, index, out error)) { error = $"block=0x{blocks[index].StartPc:X}: {error}"; return false; } + if (index == latch) + { + return true; + } + var terminator = instructions[blocks[index].EndIndex - 1]; var next = index + 1; if (terminator.Opcode == "SEndpgm") @@ -1572,8 +1593,32 @@ public static partial class Gen5SpirvTranslator } if (!IsStructuredCondition(terminator.Opcode) || - !TryResolveBranchBlock(blocks, terminator, out var target) || - target <= index || target > end) + !TryResolveBranchBlock(blocks, terminator, out var target) || target <= index) + { + error = $"unstructured {terminator.Opcode} at 0x{terminator.Pc:X}"; + return false; + } + + // if (taken) break; -- only out of the innermost loop, to the block after its latch. + if (_structuredLoopExit >= 0 && target == _structuredLoopExit) + { + if (emit) + { + TryGetBranchCondition(terminator.Opcode, out var leaveLoop); + var breakLabel = _module.AllocateId(); + var stayLabel = _module.AllocateId(); + _module.AddStatement(SpirvOp.SelectionMerge, stayLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, leaveLoop, breakLabel, stayLabel); + _module.AddLabel(breakLabel); + _module.AddStatement(SpirvOp.Branch, _structuredLoopMerge); + _module.AddLabel(stayLabel); + } + + index = next; + continue; + } + + if (target > end || (latch >= 0 && target == end)) { error = $"unstructured {terminator.Opcode} at 0x{terminator.Pc:X}"; return false; @@ -1588,7 +1633,8 @@ public static partial class Gen5SpirvTranslator // if/else: the skipped range ends with an s_branch over the taken range. var thenLast = instructions[blocks[target - 1].EndIndex - 1]; if (target < blocks.Count && target - 1 > index && thenLast.Opcode == "SBranch" && - TryResolveBranchBlock(blocks, thenLast, out var joinTarget) && joinTarget > target && joinTarget <= end) + TryResolveBranchBlock(blocks, thenLast, out var joinTarget) && joinTarget > target && joinTarget <= end && + !(latch >= 0 && joinTarget == end)) { if (!emit) { @@ -1658,6 +1704,93 @@ public static partial class Gen5SpirvTranslator return true; } + // The innermost structured loop's exit block and merge label, for its breaks; -1 outside loops. + private int _structuredLoopExit = -1; + private uint _structuredLoopMerge; + + // The last block in [header, end) that branches back to header, unconditionally or on a + // wave-uniform condition. + private bool TryFindLoopLatch(IReadOnlyList blocks, int header, int end, out int latch) + { + latch = -1; + var instructions = _request.Program.Instructions; + for (var index = end - 1; index >= header; index--) + { + var terminator = instructions[blocks[index].EndIndex - 1]; + if ((IsStructuredCondition(terminator.Opcode) || terminator.Opcode == "SBranch") && + TryResolveBranchBlock(blocks, terminator, out var target) && target == header) + { + latch = index; + return true; + } + } + + return false; + } + + // do { blocks [header, latch] } while (latch condition), bounded by the same step guard as + // the dispatcher so a guest loop that never ends cannot wedge the GPU queue. + private bool TryStructureLoop(IReadOnlyList blocks, int header, int latch, bool emit, out string error) + { + var outerExit = _structuredLoopExit; + var outerMerge = _structuredLoopMerge; + try + { + return TryStructureLoopCore(blocks, header, latch, emit, out error); + } + finally + { + _structuredLoopExit = outerExit; + _structuredLoopMerge = outerMerge; + } + } + + private bool TryStructureLoopCore(IReadOnlyList blocks, int header, int latch, bool emit, out string error) + { + _structuredLoopExit = latch + 1; + if (!emit) + { + _structuredLoopMerge = 0; + return TryStructureRange(blocks, header, latch + 1, latch + 1, emit: false, out error, latch); + } + + var headerLabel = _module.AllocateId(); + var bodyLabel = _module.AllocateId(); + var continueLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + _structuredLoopMerge = mergeLabel; + _module.AddStatement(SpirvOp.Branch, headerLabel); + _module.AddLabel(headerLabel); + _module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0); + _module.AddStatement(SpirvOp.Branch, bodyLabel); + _module.AddLabel(bodyLabel); + if (!TryStructureRange(blocks, header, latch + 1, latch + 1, emit: true, out error, latch)) + { + return false; + } + + _module.AddStatement(SpirvOp.Branch, continueLabel); + _module.AddLabel(continueLabel); + var terminator = _request.Program.Instructions[blocks[latch].EndIndex - 1]; + var again = _module.ConstantBool(true); + if (terminator.Opcode != "SBranch") + { + TryGetBranchCondition(terminator.Opcode, out again); + } + + if (_maxDispatcherSteps > 0) + { + var steps = IAdd(Load(_uintType, _iterationGuard), UInt(1)); + Store(_iterationGuard, steps); + var withinLimit = _module.AddInstruction(SpirvOp.ULessThan, _boolType, steps, UInt((uint)_maxDispatcherSteps)); + again = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, again, withinLimit); + } + + _module.AddStatement(SpirvOp.BranchConditional, again, headerLabel, mergeLabel); + _module.AddLabel(mergeLabel); + return true; + } + private static string FormatBlockStarts(IReadOnlyList blocks) { const int maxBlocks = 32; diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5StructuredLoopTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5StructuredLoopTests.cs new file mode 100644 index 0000000..f23d6fc --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5StructuredLoopTests.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Diagnostics; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.ShaderCompiler.Tests; + +// A loop closed by a backward branch is emitted as a structured SPIR-V loop instead of the PC +// dispatcher, with conditional exits from its body as breaks. Any other backward edge keeps the +// dispatcher. +public sealed class Gen5StructuredLoopTests +{ + private const uint MoveFour = 0xBE800384; // s_mov_b32 s0, 4 + private const uint SubtractOne = 0x80808100; // s_sub_u32 s0, s0, 1 + private const uint CompareNotZero = 0xBF078000; // s_cmp_lg_u32 s0, 0 + private const uint CompareTwo = 0xBF068200; // s_cmp_eq_u32 s0, 2 + private const uint CompareZero = 0xBF068000; // s_cmp_eq_u32 s0, 0 + private const uint EndProgram = 0xBF810000; // s_endpgm + + [Fact] + public void BackwardConditionalBranchBecomesAStructuredLoop() + { + var spirv = Compile(MoveFour, SubtractOne, CompareNotZero, 0xBF85FFFD /* s_cbranch_scc1 0x04 */, EndProgram); + + var opcodes = Opcodes(spirv); + Assert.Contains(SpirvOp.LoopMerge, opcodes); + Assert.DoesNotContain(SpirvOp.Switch, opcodes); + ValidateWhenAvailable(spirv); + } + + [Fact] + public void ConditionalExitFromTheBodyBecomesABreak() + { + var spirv = Compile( + MoveFour, SubtractOne, CompareTwo, + 0xBF850002, // s_cbranch_scc1 0x18, past the loop + CompareNotZero, + 0xBF85FFFB, // s_cbranch_scc1 0x04 + EndProgram); + + var opcodes = Opcodes(spirv); + Assert.Contains(SpirvOp.LoopMerge, opcodes); + Assert.DoesNotContain(SpirvOp.Switch, opcodes); + ValidateWhenAvailable(spirv); + } + + [Fact] + public void UnconditionalBackBranchLoopLeftByABreakIsStructured() + { + var spirv = Compile( + MoveFour, SubtractOne, CompareZero, + 0xBF850001, // s_cbranch_scc1 0x14, past the loop + 0xBF82FFFC, // s_branch 0x04 + EndProgram); + + var opcodes = Opcodes(spirv); + Assert.Contains(SpirvOp.LoopMerge, opcodes); + Assert.DoesNotContain(SpirvOp.Switch, opcodes); + ValidateWhenAvailable(spirv); + } + + [Fact] + public void SecondBackEdgeToTheHeaderKeepsTheDispatcher() + { + var spirv = Compile( + MoveFour, SubtractOne, CompareTwo, + 0xBF85FFFD, // s_cbranch_scc1 0x04, a continue from the middle of the body + CompareNotZero, + 0xBF85FFFB, // s_cbranch_scc1 0x04 + EndProgram); + + Assert.Contains(SpirvOp.Switch, Opcodes(spirv)); + ValidateWhenAvailable(spirv); + } + + private static byte[] Compile(params uint[] words) + { + var context = new CpuContext(new InstructionMemory(words), Generation.Gen5); + Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out var program, out var decodeError), decodeError); + var (plan, resources, layout) = Prepare(program); + var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + return shader.Spirv; + } + + private static List Opcodes(byte[] code) + { + var result = new List(); + for (var index = 5 * sizeof(uint); index < code.Length;) + { + var word = BinaryPrimitives.ReadUInt32LittleEndian(code.AsSpan(index)); + result.Add((SpirvOp)(word & 0xFFFF)); + index += (int)(word >> 16) * sizeof(uint); + } + + return result; + } + + private static void ValidateWhenAvailable(byte[] code) + { + var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); + var executable = string.IsNullOrWhiteSpace(sdk) ? null + : Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val"); + if (executable is null || !File.Exists(executable)) + { + return; + } + + var path = Path.GetTempFileName(); + try + { + File.WriteAllBytes(path, code); + var start = new ProcessStartInfo(executable) { UseShellExecute = false, RedirectStandardError = true, CreateNoWindow = true }; + start.ArgumentList.Add("--target-env"); + start.ArgumentList.Add("vulkan1.2"); + start.ArgumentList.Add(path); + using var process = Process.Start(start)!; + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + Assert.True(process.ExitCode == 0, error); + } + finally + { + File.Delete(path); + } + } + + private sealed class InstructionMemory(uint[] words) : ICpuMemory + { + public bool TryRead(ulong address, Span destination) + { + var index = (address - 0x1000) / sizeof(uint); + if (address < 0x1000 || (address - 0x1000) % sizeof(uint) != 0 || destination.Length != sizeof(uint) || + index >= (ulong)words.Length) + { + return false; + } + + BinaryPrimitives.WriteUInt32LittleEndian(destination, words[(int)index]); + return true; + } + + public bool TryWrite(ulong address, ReadOnlySpan source) => false; + } +}