Compare commits

...

1 Commits

Author SHA1 Message Date
kuba 164a9918a3 [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.
2026-09-29 23:55:09 +02:00
1 changed files with 221 additions and 0 deletions

View File

@ -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<ShaderBlock> 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<ShaderBlock> 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<ShaderBlock> 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<ShaderBlock> 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<ShaderBlock> 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<ShaderBlock> blocks)
{
const int maxBlocks = 32;