From 164a9918a3f3f49381f142b5c5fc45ca77b84d98 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 18:14:33 +0200 Subject: [PATCH] [SpirvTranslator] Emit forward, nested control flow as structured selections Every program with more than one basic block ran as a loop around a switch on the guest PC. That shape keeps every guest register live across iterations and stops the host compiler from optimizing across blocks, so on Metal the register files spill. A program whose branches only skip forward over properly nested ranges -- an s_cbranch_* skip, an if/else with an s_branch over the else range, a skip to the end of the program -- is exactly a nest of SPIR-V selections, and is now emitted as one. The structuring is attempted first without emitting: if any branch goes backwards, leaves a range it entered, or targets an instruction that is not a block boundary, nothing is emitted and the program keeps the PC dispatcher, so no program loses a shape the dispatcher handled. Setting SHARPEMU_STRUCTURED_CF=0 forces the dispatcher for every program. Measured on two replayed programs, with identical output: an instanced vertex shader 8.1 -> 4.8 ms and a per-pixel compute program 10.7 -> 6.6 ms. --- .../Gen5SpirvTranslator.cs | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 4c4a7b9..dffe73b 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -314,6 +314,25 @@ public static partial class Gen5SpirvTranslator } EmitInitialState(); + if (StructuredControlFlow && blocks.Count > 1 && 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(); + var doneLabel = _module.AllocateId(); + var isActive = Load(_boolType, _programActive); + _module.AddStatement(SpirvOp.SelectionMerge, doneLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, isActive, runLabel, doneLabel); + _module.AddLabel(runLabel); + if (!TryStructureRange(blocks, 0, blocks.Count, blocks.Count, emit: true, out error)) + { + return false; + } + + _module.AddStatement(SpirvOp.Branch, doneLabel); + _module.AddLabel(doneLabel); + } + else + { var loopHeader = _module.AllocateId(); var switchHeader = _module.AllocateId(); var switchMerge = _module.AllocateId(); @@ -388,6 +407,7 @@ public static partial class Gen5SpirvTranslator loopHeader, loopMerge); _module.AddLabel(loopMerge); + } if (_stage == Gen5SpirvStage.Pixel && Environment.GetEnvironmentVariable( "SHARPEMU_TRACE_TITLE_SHADER_STATE") == "1" && @@ -1295,6 +1315,20 @@ public static partial class Gen5SpirvTranslator IReadOnlyList blocks, int blockIndex, out string error) + { + if (!TryEmitBlockBody(blocks, blockIndex, out error)) + { + return false; + } + + return TryEmitBlockTerminator(blocks, blockIndex, out error); + } + + // Every instruction of a block except its terminating branch or s_endpgm. + private bool TryEmitBlockBody( + IReadOnlyList blocks, + int blockIndex, + out string error) { error = string.Empty; var block = blocks[blockIndex]; @@ -1337,6 +1371,17 @@ public static partial class Gen5SpirvTranslator } if (synchronizeSharedMemory && sharedMemoryPhase != SharedMemoryPhase.None) EmitWave64Barrier(); + return true; + } + + // Stores the next dispatcher block (or ends the program) for a block's terminator. + private bool TryEmitBlockTerminator( + IReadOnlyList blocks, + int blockIndex, + out string error) + { + error = string.Empty; + var block = blocks[blockIndex]; var terminator = _request.Program.Instructions[block.EndIndex - 1]; if (terminator.Opcode == "SEndpgm") { @@ -1414,6 +1459,182 @@ public static partial class Gen5SpirvTranslator return true; } + // Forward-only, properly nested control flow is emitted as structured selections instead of + // the PC-dispatch loop: a loop around a switch keeps every register live across iterations, + // blocks cannot be optimized together, and the Metal compiler spills. SHARPEMU_STRUCTURED_CF=0 + // keeps the dispatcher for every program. + private static readonly bool StructuredControlFlow = + Environment.GetEnvironmentVariable("SHARPEMU_STRUCTURED_CF") != "0"; + + private static bool IsStructuredCondition(string opcode) => opcode is + "SCbranchScc0" or "SCbranchScc1" or "SCbranchVccz" or "SCbranchVccnz" or + "SCbranchExecz" or "SCbranchExecnz" or + "SCbranchCdbgsys" or "SCbranchCdbguser" or "SCbranchCdbgsysOrUser" or "SCbranchCdbgsysAndUser"; + + // The block a branch continues at, or blocks.Count for a branch past the last instruction. + private bool TryResolveBranchBlock(IReadOnlyList blocks, Gen5ShaderInstruction branch, out int target) + { + target = -1; + if (!TryGetBranchTargetPc(branch, out var targetPc)) + { + return false; + } + + if (IsExitBranchTarget(_request.Program.Instructions, targetPc)) + { + target = blocks.Count; + return true; + } + + return TryFindBlock(blocks, targetPc, out target); + } + + // 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. + private bool TryStructureRange( + IReadOnlyList blocks, + int begin, + int end, + int leave, + bool emit, + out string error) + { + error = string.Empty; + var instructions = _request.Program.Instructions; + var index = begin; + while (index < end) + { + if (emit && !TryEmitBlockBody(blocks, index, out error)) + { + error = $"block=0x{blocks[index].StartPc:X}: {error}"; + return false; + } + + var terminator = instructions[blocks[index].EndIndex - 1]; + var next = index + 1; + if (terminator.Opcode == "SEndpgm") + { + if (next != blocks.Count) + { + error = "s_endpgm before the last block"; + return false; + } + + if (emit) + { + Store(_programActive, _module.ConstantBool(false)); + } + + index = next; + continue; + } + + if (terminator.Opcode == "SBranch") + { + if (!TryResolveBranchBlock(blocks, terminator, out var branchTarget) || + !(branchTarget == next || (next == end && branchTarget == leave))) + { + error = $"unstructured s_branch at 0x{terminator.Pc:X}"; + return false; + } + + index = next; + continue; + } + + if (!terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal)) + { + index = next; + continue; + } + + if (!IsStructuredCondition(terminator.Opcode) || + !TryResolveBranchBlock(blocks, terminator, out var target) || + target <= index || target > end) + { + error = $"unstructured {terminator.Opcode} at 0x{terminator.Pc:X}"; + return false; + } + + if (target == next) + { + index = next; + continue; + } + + // 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) + { + if (!emit) + { + if (!TryStructureRange(blocks, next, target, joinTarget, emit: false, out error) || + !TryStructureRange(blocks, target, joinTarget, joinTarget, emit: false, out error)) + { + return false; + } + } + else + { + TryGetBranchCondition(terminator.Opcode, out var taken); + var thenLabel = _module.AllocateId(); + var elseLabel = _module.AllocateId(); + var joinLabel = _module.AllocateId(); + _module.AddStatement(SpirvOp.SelectionMerge, joinLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, taken, elseLabel, thenLabel); + _module.AddLabel(thenLabel); + if (!TryStructureRange(blocks, next, target, joinTarget, emit: true, out error)) + { + return false; + } + + _module.AddStatement(SpirvOp.Branch, joinLabel); + _module.AddLabel(elseLabel); + if (!TryStructureRange(blocks, target, joinTarget, joinTarget, emit: true, out error)) + { + return false; + } + + _module.AddStatement(SpirvOp.Branch, joinLabel); + _module.AddLabel(joinLabel); + } + + index = joinTarget; + continue; + } + + // if (!taken) { skipped range } + if (!emit) + { + if (!TryStructureRange(blocks, next, target, target, emit: false, out error)) + { + return false; + } + } + else + { + TryGetBranchCondition(terminator.Opcode, out var taken); + var bodyLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, taken, mergeLabel, bodyLabel); + _module.AddLabel(bodyLabel); + if (!TryStructureRange(blocks, next, target, target, emit: true, out error)) + { + return false; + } + + _module.AddStatement(SpirvOp.Branch, mergeLabel); + _module.AddLabel(mergeLabel); + } + + index = target; + } + + return true; + } + private static string FormatBlockStarts(IReadOnlyList blocks) { const int maxBlocks = 32;