shader: simplify shared-exit control flow

This commit is contained in:
nmzik
2026-09-27 21:04:57 +04:00
parent 566beaa067
commit 609de519d7
12 changed files with 786 additions and 1180 deletions
@@ -636,6 +636,29 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Arg(inst, 1));
}
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.other_half == nullptr) return ctx.Arg(inst, 0);
// A native scalar branch makes one decision for both emulated wave halves.
if (ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
const auto kind = inst.Flags<CFG::BranchCondition>();
if (kind == CFG::BranchCondition::ScalarInstruction) return ctx.Arg(inst, 0);
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto combined = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero || kind == CFG::BranchCondition::SccZero;
ctx.state.builder.AddFunction(zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr,
TypeU32(ctx.state), combined, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBool(ctx.state), result, combined,
ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
}
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
return ctx.Ballot(predicate);
}
@@ -182,6 +182,7 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
uint32_t EmitLaneId(EmitterState& state);
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -97,30 +97,6 @@ void EmitReturn(ValueEmitContext& ctx) {
ctx.state.builder.AddFunction(spv::OpReturn);
}
uint32_t BranchCondition(ValueEmitContext& ctx, const IR::BlockInfo& info) {
// Scalar-instruction conditions already test the full wave's raw register values.
if (ctx.other_half == nullptr ||
info.terminator.condition == CFG::BranchCondition::ScalarInstruction ||
info.terminator.condition == CFG::BranchCondition::GotoVariable) {
return ctx.Def(info.condition);
}
const auto ballot = ctx.Ballot(info.condition);
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const auto kind = info.terminator.condition;
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero ||
kind == CFG::BranchCondition::SccZero;
const auto combined =
EmitBinaryU32(ctx.state, zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual, TypeBool(ctx.state),
result, combined, ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
}
void EmitStructuredTerminator(ValueEmitContext& ctx, const IR::Block* block,
const IR::BlockInfo& info) {
const auto& program = ctx.state.program;
@@ -160,7 +136,7 @@ void EmitStructuredTerminator(ValueEmitContext& ctx, const IR::Block* block,
EmitReturn(ctx);
return;
}
const auto condition = BranchCondition(ctx, info);
const auto condition = ctx.Def(info.condition);
emit_merge();
ctx.state.builder.AddFunction(spv::OpBranchConditional, condition,
ctx.Label(true_block), ctx.Label(false_block));
@@ -193,7 +169,7 @@ uint32_t EmitDispatcherNextPc(ValueEmitContext& ctx, const DispatcherFunctionSta
EmitDispatcherTarget(ctx, dispatcher, block, term.false_block);
const auto selected = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(
spv::OpSelect, TypeU32(ctx.state), selected, BranchCondition(ctx, info),
spv::OpSelect, TypeU32(ctx.state), selected, ctx.Def(info.condition),
ConstantU32(ctx.state, term.true_block), ConstantU32(ctx.state, term.false_block));
return selected;
}
File diff suppressed because it is too large Load Diff
@@ -18,10 +18,17 @@ enum class BranchCondition {
ExecZero,
ExecNonZero,
ScalarInstruction,
GotoVariable,
Expression,
Unknown
};
struct ConditionExpression {
enum class Op { Constant, Variable, Native, Not, Or };
Op op;
uint32_t lhs;
uint32_t rhs = UINT32_MAX;
};
enum class TerminatorKind { Branch, ConditionalBranch, IndirectBranch, Return, Unsupported };
enum class FailureKind {
@@ -48,8 +55,7 @@ struct Terminator {
std::vector<uint32_t> indirect_targets;
std::vector<uint32_t> indirect_selector_values;
std::vector<uint32_t> indirect_selector_targets;
uint32_t goto_variable = UINT32_MAX;
int32_t goto_value = -1;
uint32_t expression = UINT32_MAX;
bool loop_header = false;
};
@@ -59,6 +65,8 @@ struct BasicBlock {
uint32_t end_pc = 0;
uint32_t inst_begin = 0;
uint32_t inst_end = 0;
struct Assignment { uint32_t variable; uint32_t expression; };
std::vector<Assignment> assignments;
std::vector<uint32_t> predecessors;
std::vector<uint32_t> successors;
std::vector<uint32_t> dominators;
@@ -89,6 +97,7 @@ struct StronglyConnectedComponent {
struct Graph {
std::vector<BasicBlock> blocks;
std::vector<ConditionExpression> expressions;
std::vector<BackEdge> back_edges;
std::vector<NaturalLoop> natural_loops;
std::vector<StronglyConnectedComponent> components;
@@ -746,55 +746,61 @@ void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 valu
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
}
void Translator::AddBranchCondition(const CFG::BasicBlock& source, IR::BlockInfo& info) {
if (source.terminator.goto_value >= 0) {
if (source.terminator.goto_variable == UINT32_MAX) {
EXIT("block %u sets an invalid goto variable", source.id);
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
IR::BlockInfo& info) {
const auto native_condition = [&](CFG::BranchCondition kind) -> IR::U1 {
IR::U1 condition;
switch (kind) {
case CFG::BranchCondition::Always: condition = IR::U1(IR::Value(true)); break;
case CFG::BranchCondition::SccZero: condition = ir.LogicalNot(ir.GetScc()); break;
case CFG::BranchCondition::SccNonZero: condition = ir.GetScc(); break;
case CFG::BranchCondition::VccZero: condition = ir.LogicalNot(ir.GetVcc()); break;
case CFG::BranchCondition::VccNonZero: condition = ir.GetVcc(); break;
case CFG::BranchCondition::ExecZero: condition = ir.LogicalNot(ir.GetExec()); break;
case CFG::BranchCondition::ExecNonZero: condition = ir.GetExec(); break;
case CFG::BranchCondition::ScalarInstruction:
EXIT_IF(instruction_branch_condition.IsEmpty());
condition = instruction_branch_condition;
break;
default: EXIT("block %u has an invalid native branch condition", source.id);
}
ir.SetGotoVariable(source.terminator.goto_variable,
IR::U1(IR::Value(source.terminator.goto_value != 0)));
return IR::U1(ir.Emit(IR::ValueOpcode::ConditionRef, {condition}, kind));
};
const auto expression = [&](auto&& self, uint32_t index) -> IR::U1 {
const auto& value = graph.expressions.at(index);
switch (value.op) {
case CFG::ConditionExpression::Op::Constant: return IR::U1(IR::Value(value.lhs != 0));
case CFG::ConditionExpression::Op::Variable: return ir.GetGotoVariable(value.lhs);
case CFG::ConditionExpression::Op::Native:
return native_condition(static_cast<CFG::BranchCondition>(value.lhs));
case CFG::ConditionExpression::Op::Not: return ir.LogicalNot(self(self, value.lhs));
case CFG::ConditionExpression::Op::Or:
return ir.LogicalOr(self(self, value.lhs), self(self, value.rhs));
}
EXIT("invalid CFG condition expression");
};
for (const auto& assignment: source.assignments) {
ir.SetGotoVariable(assignment.variable, expression(expression, assignment.expression));
}
if (source.terminator.kind == CFG::TerminatorKind::IndirectBranch) {
if (source.terminator.indirect_selector_code != UINT32_MAX) {
info.indirect_target = ReadScalarCode(source.terminator.indirect_selector_code);
} else if (source.terminator.indirect_pc_sgpr != UINT32_MAX) {
info.indirect_target =
ir.GetScalarReg(static_cast<IR::ScalarReg>(source.terminator.indirect_pc_sgpr));
const auto& term = source.terminator;
if (term.kind == CFG::TerminatorKind::IndirectBranch) {
if (term.indirect_selector_code != UINT32_MAX) {
info.indirect_target = ReadScalarCode(term.indirect_selector_code);
} else if (term.indirect_pc_sgpr != UINT32_MAX) {
info.indirect_target = ir.GetScalarReg(static_cast<IR::ScalarReg>(term.indirect_pc_sgpr));
} else {
EXIT("block %u has no indirect branch selector", source.id);
}
ir.Emit(IR::ValueOpcode::ReferenceU32, {info.indirect_target});
return;
}
if (source.terminator.kind != CFG::TerminatorKind::ConditionalBranch) {
return;
if (term.kind == CFG::TerminatorKind::ConditionalBranch) {
const auto condition = term.expression != UINT32_MAX
? expression(expression, term.expression)
: native_condition(term.condition);
info.condition = condition;
ir.Emit(IR::ValueOpcode::Reference, {condition});
}
// EXEC and VCC are invocation-local Boolean masks. Branching on that Boolean lets inactive
// invocations leave the region without reconstructing a host-subgroup mask.
IR::U1 condition;
switch (source.terminator.condition) {
case CFG::BranchCondition::Always: condition = IR::U1(IR::Value(true)); break;
case CFG::BranchCondition::SccZero: condition = ir.LogicalNot(ir.GetScc()); break;
case CFG::BranchCondition::SccNonZero: condition = ir.GetScc(); break;
case CFG::BranchCondition::VccZero: condition = ir.LogicalNot(ir.GetVcc()); break;
case CFG::BranchCondition::VccNonZero: condition = ir.GetVcc(); break;
case CFG::BranchCondition::ExecZero: condition = ir.LogicalNot(ir.GetExec()); break;
case CFG::BranchCondition::ExecNonZero: condition = ir.GetExec(); break;
case CFG::BranchCondition::ScalarInstruction:
EXIT_IF(instruction_branch_condition.IsEmpty());
condition = instruction_branch_condition;
break;
case CFG::BranchCondition::GotoVariable:
if (source.terminator.goto_variable == UINT32_MAX) {
EXIT("block %u reads an invalid goto variable", source.id);
}
condition = ir.GetGotoVariable(source.terminator.goto_variable);
break;
case CFG::BranchCondition::Unknown:
EXIT("block %u has an unknown branch condition", source.id);
}
info.condition = condition;
ir.Emit(IR::ValueOpcode::Reference, {condition});
}
namespace {
@@ -1297,7 +1303,7 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
translator.TranslateInstruction(instruction);
}
translator.AddBranchCondition(cfg_block, result.block_info[typed_index]);
translator.AddBranchCondition(cfg, cfg_block, result.block_info[typed_index]);
}
IR::ValidateProgram(result, false);
return result;
@@ -15,7 +15,7 @@ public:
void TranslateInstruction(const Decoder::Instruction& inst);
void TranslateEmbeddedFetch(const Decoder::Instruction& inst, uint32_t attribute,
uint32_t component_count, const ShaderBufferResource& resource);
void AddBranchCondition(const CFG::BasicBlock& source, IR::BlockInfo& info);
void AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source, IR::BlockInfo& info);
private:
const Decoder::Operand& SourceAt(const Decoder::Instruction& inst, uint32_t index);
@@ -4,6 +4,7 @@ VALUE_OPCODE(Identity, Opaque, Opaque)
VALUE_OPCODE(Void, Void)
VALUE_OPCODE(Reference, Void, U1)
VALUE_OPCODE(ReferenceU32, Void, U32)
VALUE_OPCODE(ConditionRef, U1, U1)
// Translation and register-state pseudo operations.
VALUE_OPCODE(GetUserData, U32, ScalarReg)
@@ -630,6 +630,11 @@ void FoldInstruction(Block& block, Block::iterator instruction,
}
}
return;
case ValueOpcode::ConditionRef: {
const auto value = Arg(inst, 0);
if (IsImmediate(value, Type::U1)) Replace(inst, value);
return;
}
case ValueOpcode::LogicalNot: {
const auto value = Arg(inst, 0);
if (IsImmediate(value, Type::U1)) {
@@ -89,6 +89,7 @@ bool IsRuntimeSelect(ValueOpcode op) {
bool IsRuntimeUniformOp(ValueOpcode op) {
switch (op) {
case ValueOpcode::ConditionRef:
case ValueOpcode::BitCastU32F32:
case ValueOpcode::BitCastF32U32:
case ValueOpcode::ConvertU32F32:
@@ -970,6 +971,7 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return true;
}
return false;
case ValueOpcode::ConditionRef: return Arg(inst, 0, result);
case ValueOpcode::LogicalNot:
if (Arg(inst, 0, a)) {
result = a == 0u;
+69
View File
@@ -23390,6 +23390,73 @@ TestCase SharedReturnKeepsSelectedValues() {
return test;
}
TestCase SiblingSharedExitKeepsCapturedConditions() {
using O = ShaderOpcode;
std::vector<u32> code = {
EncodeVop1(0x01, 5, 4),
EncodeVop2(0x1a, 5, InlineU32(6), 5),
EncodeVop2(0x25, 5, Vgpr(0), 5), // group * 64 + lane
EncodeSop2(0x0e, 6, 4, InlineU32(1)),
0, // choose left or right
EncodeSMovB32(8, InlineU32(20)),
EncodeSop2(0x0e, 6, 4, InlineU32(2)),
0, // left: skip nested exit
EncodeSop2(0x0e, 6, 4, InlineU32(4)),
0, // nested left: shared exit
0, // left -> normal return
EncodeSMovB32(8, InlineU32(30)),
EncodeSop2(0x0e, 6, 4, InlineU32(8)),
0, // first right shared exit
EncodeSMovB32(8, InlineU32(40)),
EncodeSop2(0x0e, 6, 4, InlineU32(16)),
0, // second right shared exit
EncodeSMovB32(8, InlineU32(50)),
EncodeSopc(0x06, InlineU32(0), InlineU32(1)), // overwrite SCC
};
AppendStoreSgprAtLaneDwordOffset(&code, 8, 5, 0);
AppendEnd(&code);
const auto shared_exit = static_cast<u32>(code.size());
code.push_back(EncodeSop2(0x00, 8, 8, InlineU32(1))); // overwrite SCC again
AppendStoreSgprAtLaneDwordOffset(&code, 8, 5, 0);
AppendEnd(&code);
code[4] = EncodeSopp(0x04, 11u - 5u);
code[7] = EncodeSopp(0x04, 10u - 8u);
code[9] = EncodeSopp(0x04, shared_exit - 10u);
code[10] = EncodeSopp(0x02, 17u - 11u);
code[13] = EncodeSopp(0x04, shared_exit - 14u);
code[16] = EncodeSopp(0x04, shared_exit - 17u);
TestCase test;
test.name = "SiblingSharedExitKeepsCapturedConditions";
test.code = std::move(code);
for (u32 group = 0; group < 32; ++group) {
u32 result = 50;
if (group & 1u) {
if ((group & 2u) && !(group & 4u)) result = 21;
} else if (!(group & 8u)) {
result = 31;
} else if (!(group & 16u)) {
result = 41;
}
test.expected.insert(test.expected.end(), 64, result);
}
test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32,
O::S_AND_B32, O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_ADD_U32,
O::S_CBRANCH_SCC0, O::S_BRANCH, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpSelectionMerge"};
test.forbidden_spirv = {"OpSwitch"};
test.compute_info.wave_size = 64;
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.group_id[0] = true;
test.compute_info.thread_ids_num = 1;
test.compute_info.workgroup_register = 4;
test.has_compute_info = true;
test.dispatch_x = 32;
return test;
}
TestCase BranchVccnzUsesWaveMask() {
using O = ShaderOpcode;
@@ -29477,6 +29544,7 @@ std::vector<TestCase> MakeCases() {
AddCase(BranchSelect);
AddCase(SimpleLoop);
AddCase(SharedReturnKeepsSelectedValues);
AddCase(SiblingSharedExitKeepsCapturedConditions);
AddCase(BranchVccnzUsesWaveMask);
AddCase(BranchVccnzUsesCarryProducedWaveMask);
AddCase(ScalarMemRealtimeCapturedPlaceholder);
@@ -34450,6 +34518,7 @@ int main(int argc, char **argv) {
RunCase(&vulkan, ImageSampleAndGather());
RunCase(&vulkan, ImageGatherLodApproximatesLevelZero());
RunCase(&vulkan, SharedReturnKeepsSelectedValues());
RunCase(&vulkan, SiblingSharedExitKeepsCapturedConditions());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--position-w-only") == 0) {
+131 -158
View File
@@ -1818,12 +1818,12 @@ void TestSopkCompareImmediateExtension() {
TranslateProgram(shader, MakeCompileOptions(ShaderType::Vertex));
const auto branch = std::ranges::find_if(
translated.program.block_info, [](const auto &block) {
return block.terminator.condition == CFG::BranchCondition::SccZero;
return block.terminator.kind == CFG::TerminatorKind::ConditionalBranch;
});
Check(branch != translated.program.block_info.end(),
"captured LUT parameter export branch was lost");
const auto condition = branch->condition.Resolve();
Check(condition.IsImmediate() && condition.U1() == (counts == 6u),
Check(condition.IsImmediate() && condition.U1() == (counts != 6u),
"captured LUT unsigned compare suppressed a live parameter export");
}
}
@@ -7882,17 +7882,13 @@ void TestNewShaderRecompilerCfgLoopExitSharedWithSelection() {
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(block_count == 8u && graph.natural_loops.size() == 1u,
"shared loop-exit fixture has the wrong native CFG");
Check(!ShaderRecompiler::CFG::Structurize(graph) &&
graph.unsupported_reason.find("duplicate structured merge block") !=
std::string::npos,
"shared loop exit did not terminate at its structured merge conflict");
Check(graph.blocks.size() == block_count &&
CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared loop-exit fallback changed semantic instruction coverage");
Check(ShaderRecompiler::CFG::Structurize(graph), graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared loop-exit routing changed semantic instruction coverage");
auto result = RecompileForTest(shader, MakeCompileOptions(ShaderType::Compute));
Check(result.program.dispatcher_fallback && SpirvContainsOpcode(result.spirv, 251),
"shared loop exit did not emit its dispatcher fallback");
Check(!result.program.dispatcher_fallback && !SpirvContainsOpcode(result.spirv, 251),
"shared loop exit did not retain structured control flow");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -7922,7 +7918,7 @@ void TestNewShaderRecompilerCfgLoopEarlyBreakNoSelection() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedLoopNonlocalExitDispatcher() {
void TestNewShaderRecompilerCfgNestedLoopNonlocalExitStructured() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // outer loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 9), // outer exit -> end
@@ -7942,10 +7938,10 @@ void TestNewShaderRecompilerCfgNestedLoopNonlocalExitDispatcher() {
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check((result.ir_dump.find("mode=dispatcher") != std::string::npos),
"nested-loop nonlocal exit did not select dispatcher fallback");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) != 0,
"nested-loop nonlocal exit dispatcher SPIR-V lacks OpSwitch");
Check((result.ir_dump.find("mode=structured") != std::string::npos),
"nested-loop nonlocal exit did not retain structured control flow");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"nested-loop nonlocal exit unexpectedly selected dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -8006,27 +8002,23 @@ void TestNewShaderRecompilerCfgNestedLoopExitTailMergeSplit() {
Check(graph.blocks.size() > original_block_count,
"nested loop exit tails did not create a private inner merge");
const auto *outer_header = graph.FindBlockByPc(0);
const auto *inner_header = graph.FindBlockByPc(8);
Check(outer_header != nullptr && inner_header != nullptr &&
outer_header->terminator.loop_header &&
inner_header->terminator.loop_header,
"nested loop exit-tail fixture did not retain both loop headers");
Check(inner_header->terminator.merge_block !=
outer_header->terminator.continue_block,
"inner loop merge still aliases the outer continue target");
const auto *inner_merge =
graph.FindBlock(inner_header->terminator.merge_block);
Check(inner_merge != nullptr &&
inner_merge->inst_begin == inner_merge->inst_end &&
inner_merge->terminator.kind ==
ShaderRecompiler::CFG::TerminatorKind::Branch &&
inner_merge->terminator.true_block ==
outer_header->terminator.continue_block,
"private inner merge does not forward to the outer continue target");
std::vector<const ShaderRecompiler::CFG::BasicBlock *> headers;
for (const auto &block : graph.blocks)
if (block.terminator.loop_header) headers.push_back(&block);
Check(headers.size() == 2u, "nested loop exit-tail fixture lost a loop header");
if (!graph.Dominates(headers[0]->id, headers[1]->id)) std::swap(headers[0], headers[1]);
const auto *outer_header = headers[0];
const auto *inner_header = headers[1];
Check(inner_header->terminator.merge_block != outer_header->terminator.continue_block &&
graph.Dominates(outer_header->id, inner_header->terminator.merge_block),
"inner loop merge escaped the enclosing loop body");
auto result = RecompileForTest(shader, MakeCompileOptions(ShaderType::Compute));
Check(!result.program.dispatcher_fallback,
"nested loop exit tails selected the dispatcher");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgMixedContinueNonmergeExitDispatcher() {
void TestNewShaderRecompilerCfgMixedContinueNonmergeExitStructured() {
const uint32_t shader[] = {
EncodeSopc(0x06, 7, 7), // entry branch bypasses loop -> exit X
EncodeSopp(0x05, 5), // entry -> X
@@ -8046,10 +8038,10 @@ void TestNewShaderRecompilerCfgMixedContinueNonmergeExitDispatcher() {
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check((result.ir_dump.find("mode=dispatcher") != std::string::npos),
"mixed continue/nonmerge exit did not select dispatcher fallback");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) != 0,
"mixed continue/nonmerge exit dispatcher SPIR-V lacks OpSwitch");
Check((result.ir_dump.find("mode=structured") != std::string::npos),
"mixed continue/nonmerge exit did not retain structured control flow");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"mixed continue/nonmerge exit unexpectedly selected dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -8100,7 +8092,7 @@ void TestNewShaderRecompilerCfgDirectConditionalLatchNoSelection() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgLoopEarlyContinuesNoSelection() {
void TestNewShaderRecompilerCfgLoopEarlyContinuesStructured() {
const uint32_t shader[] = {
EncodeSMovB32(0, 128), // s0 = 0
EncodeSopc(0x0a, 0, 130), // loop: s_cmp_lt_u32 s0, 2
@@ -8125,8 +8117,8 @@ void TestNewShaderRecompilerCfgLoopEarlyContinuesNoSelection() {
"loop early continues should stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) != 0,
"loop early continues SPIR-V lacks OpLoopMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"loop early continues SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 250) <= 4u,
"loop early continues introduced duplicate conditional branches");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"loop early continues unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
@@ -8242,29 +8234,28 @@ void TestNewShaderRecompilerCfgMultipleLoopLatches() {
ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded);
ShaderRecompiler::CFG::Graph graph;
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_block_count = graph.blocks.size();
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.back_edges.size() == 2u,
"multiple-latch fixture lacks two native backedges");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
Check(graph.blocks.size() == original_block_count + 2u,
"multiple native latches did not create one synthetic continue and one "
"empty header");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared backedge construction duplicated guest instructions");
Check(graph.back_edges.size() == 1u && graph.natural_loops.size() == 1u,
"multiple native latches were not coalesced to one SPIR-V backedge");
const auto &loop = graph.natural_loops.front();
const auto *continue_block = graph.FindBlock(loop.continue_block);
Check(continue_block != nullptr &&
continue_block->inst_begin == continue_block->inst_end &&
continue_block->predecessors.size() == 2u,
"canonical continue does not join both native latches");
continue_block->terminator.true_block == loop.header &&
graph.Dominates(loop.header, continue_block->id),
"canonical continue does not return to its natural loop header");
auto options = MakeCompileOptions(ShaderType::Compute);
auto result = RecompileForTest(shader, options);
Check(SpirvInstructionOpcodeCount(result.spirv, 246) == 1u,
"multiple-latch SPIR-V has the wrong loop-merge count");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0u,
"multiple-latch SPIR-V unexpectedly used a selection merge");
Check(SpirvInstructionOpcodeCount(result.spirv, 250) <= 2u,
"multiple-latch routing duplicated a native conditional branch");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"multiple-latch SPIR-V unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
@@ -8319,36 +8310,20 @@ void TestNewShaderRecompilerCfgNestedEarlyExitLoopForwarders() {
const auto original_block_count = graph.blocks.size();
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
const auto original_empty_blocks =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.inst_begin == block.inst_end;
});
Check(original_block_count == 7u && graph.natural_loops.size() == 1u,
"nested early-exit fixture has the wrong native CFG");
const bool structured = ShaderRecompiler::CFG::Structurize(graph);
Check(structured, graph.unsupported_reason.c_str());
Check(graph.blocks.size() == original_block_count + 4u &&
CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage &&
std::ranges::count_if(graph.blocks,
[](const auto &block) {
return block.inst_begin == block.inst_end;
}) == original_empty_blocks + 4,
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == original_coverage,
"nested early-exit structurization changed semantic coverage");
const auto *preheader = graph.FindBlockByPc(0x00u);
const auto *outer = graph.FindBlockByPc(0x08u);
const auto *inner = graph.FindBlockByPc(0x10u);
const auto *loop = graph.FindBlockByPc(0x20u);
Check(
preheader != nullptr && outer != nullptr && inner != nullptr &&
loop != nullptr && loop->terminator.loop_header &&
preheader->terminator.merge_block != UINT32_MAX &&
outer->terminator.merge_block != UINT32_MAX &&
inner->terminator.merge_block != UINT32_MAX &&
preheader->terminator.merge_block != outer->terminator.merge_block &&
preheader->terminator.merge_block != inner->terminator.merge_block &&
outer->terminator.merge_block != inner->terminator.merge_block,
"nested early-exit constructs do not have distinct structured merges");
std::unordered_set<uint32_t> merges;
for (const auto &block : graph.blocks) {
if (block.terminator.merge_block != UINT32_MAX)
Check(merges.insert(block.terminator.merge_block).second,
"nested early-exit constructs share a structured merge");
}
Check(graph.natural_loops.size() == 1u,
"nested early-exit routing changed the natural loop count");
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
@@ -8365,7 +8340,7 @@ void TestNewShaderRecompilerCfgNestedEarlyExitLoopForwarders() {
void TestNewShaderRecompilerCfgExecSccSharedArm() {
const uint32_t shader[] = {
EncodeSop2(0x15, 126, 4, 126), // s_andn2_b64 exec, s4, exec
EncodeSop2(0x0f, 126, 4, 126), // s_and_b64 exec, s4, exec (runtime mask)
EncodeSopp(0x08, 2), // execz -> shared arm
EncodeSop2(0x15, 30, 30, 126), // s_andn2_b64 s30, s30, exec
EncodeSopp(0x04, 2), // scc0 -> other arm, else shared arm
@@ -8386,15 +8361,6 @@ void TestNewShaderRecompilerCfgExecSccSharedArm() {
"shared-arm fixture already duplicated a semantic instruction");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
uint32_t route_selects = 0;
uint32_t route_sets = 0;
for (const auto &block : graph.blocks) {
route_selects += block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
route_sets += block.terminator.goto_value >= 0;
}
Check(graph.blocks.size() == 5u && route_selects == 0u && route_sets == 0u,
"shared return arm introduced synthetic routing state");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"EXEC/SCC shared-arm structurization changed semantic coverage");
@@ -8437,7 +8403,7 @@ void TestSharedReturnPreservesDescriptorDominance() {
graph.unsupported_reason.c_str());
const auto *overwrite = graph.FindBlockByPc(0x10u);
const auto *body = graph.FindBlockByPc(0x20u);
Check(graph.blocks.size() == 5u && overwrite != nullptr && body != nullptr &&
Check(overwrite != nullptr && body != nullptr &&
graph.Dominates(overwrite->id, body->id) &&
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.kind ==
@@ -8466,6 +8432,78 @@ void TestSharedReturnPreservesDescriptorDominance() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestCfgSiblingSharedExit() {
// f7030726b9470dd8: a nested exit on one side and two exits on the
// other side share an epilogue, separate from the normal return.
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 128), EncodeSopp(0x04, 5), // right or left
EncodeSopc(0x06, 1, 128), EncodeSopp(0x04, 2), // join or nested exit
EncodeSopc(0x06, 2, 128), EncodeSopp(0x04, 7), // shared exit
EncodeSopp(0x02, 4), // left -> main
EncodeSopc(0x06, 3, 128), EncodeSopp(0x04, 4), // right -> shared exit
EncodeSopc(0x06, 4, 128), EncodeSopp(0x04, 2), // second shared exit
EncodeSMovB32(5, 129), EncodeSopp(0x01), // main and normal return
EncodeSMovB32(5, 130), EncodeSopp(0x01), // shared exit epilogue
};
using namespace ShaderRecompiler;
Decoder::Program decoded;
Decoder::DecodeProgram(std::span{shader}, decoded);
const auto original = CFG::BuildGraph(decoded);
auto structured = original;
Check(CFG::Structurize(structured), structured.unsupported_reason.c_str());
Check(CfgInstructionCoverage(structured, decoded.instructions.size()) ==
CfgInstructionCoverage(original, decoded.instructions.size()),
"shared sibling exit duplicated or dropped guest instructions");
const auto trace = [&](const CFG::Graph &graph, uint32_t decisions) {
std::vector<uint32_t> executed;
std::vector<bool> variables(original.blocks.size() * 2);
const auto expression = [&](auto &&self, uint32_t index) -> bool {
const auto &value = graph.expressions.at(index);
switch (value.op) {
case CFG::ConditionExpression::Op::Constant: return value.lhs != 0;
case CFG::ConditionExpression::Op::Variable: return variables.at(value.lhs);
case CFG::ConditionExpression::Op::Native: std::abort();
case CFG::ConditionExpression::Op::Not: return !self(self, value.lhs);
case CFG::ConditionExpression::Op::Or:
return self(self, value.lhs) || self(self, value.rhs);
}
std::abort();
};
auto current = graph.entry_block;
for (unsigned steps = 0; steps < 100; ++steps) {
const auto &block = graph.blocks.at(current);
for (auto inst = block.inst_begin; inst < block.inst_end; ++inst)
executed.push_back(inst);
for (const auto &assignment : block.assignments) {
variables.at(assignment.variable) =
graph.expressions[assignment.expression].op == CFG::ConditionExpression::Op::Native
? (decisions & (1u << (assignment.variable - original.blocks.size()))) != 0
: expression(expression, assignment.expression);
}
const auto &term = block.terminator;
if (term.kind == CFG::TerminatorKind::Return) return executed;
Check(term.kind == CFG::TerminatorKind::Branch ||
term.kind == CFG::TerminatorKind::ConditionalBranch,
"shared exit trace has an unsupported terminator");
const bool condition = term.expression != UINT32_MAX
? expression(expression, term.expression)
: (decisions & (1u << block.id)) != 0;
current = term.kind == CFG::TerminatorKind::Branch || condition
? term.true_block : term.false_block;
}
Check(false, "shared exit trace did not terminate");
return executed;
};
for (uint32_t decisions = 0; decisions < (1u << original.blocks.size()); ++decisions)
Check(trace(original, decisions) == trace(structured, decisions),
"goto elimination changed the executed shared-exit path");
auto result = RecompileForTest(shader, MakeCompileOptions(ShaderType::Compute));
Check(!result.program.dispatcher_fallback &&
SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"shared sibling exit selected dispatcher lowering");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedTailEarlyExit() {
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 0), // preceding outer condition
@@ -8499,17 +8537,6 @@ void TestNewShaderRecompilerCfgNestedTailEarlyExit() {
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"nested-tail routing changed semantic instruction coverage");
Check(std::ranges::count_if(
graph.blocks,
[](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
}) == 1u &&
std::ranges::count_if(graph.blocks,
[](const auto &block) {
return block.terminator.goto_value >= 0;
}) == 3u,
"nested-tail early exit did not use typed route state");
auto options = MakeCompileOptions(ShaderType::Pixel);
options.dump_ir = true;
@@ -8549,17 +8576,6 @@ void TestNewShaderRecompilerCfgSharedReturnAfterNestedSelections() {
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"shared-exit route ordering changed semantic instruction coverage");
const auto route_selects =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
});
const auto route_sets =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.goto_value >= 0;
});
Check(route_selects == 0u && route_sets == 0u,
"nested selections introduced routing state for a shared return");
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
@@ -8628,7 +8644,6 @@ void TestNewShaderRecompilerCfgLoopSharedRegion() {
ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded);
ShaderRecompiler::CFG::Graph graph;
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_block_count = graph.blocks.size();
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.natural_loops.size() == 1u,
@@ -8641,18 +8656,8 @@ void TestNewShaderRecompilerCfgLoopSharedRegion() {
loop_header->terminator.continue_block != UINT32_MAX,
"loop shared-region routing did not preserve the natural loop");
uint32_t route_selects = 0;
uint32_t route_sets = 0;
for (const auto &block : graph.blocks) {
route_selects += block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
route_sets += block.terminator.goto_value >= 0;
}
Check(route_selects == 1u && route_sets == 3u &&
graph.blocks.size() >= original_block_count + 5u &&
CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"loop shared region was not routed without semantic duplication");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == original_coverage,
"loop shared region changed guest instruction coverage");
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
@@ -8701,11 +8706,6 @@ void TestNewShaderRecompilerCfgSharedRegionBeforeEarlyBreakLoop() {
graph.unsupported_reason.c_str());
Check(graph.natural_loops.size() == 1u && graph.back_edges.size() == 1u,
"selection routing introduced a cycle around a structured loop exit");
Check(std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
}) == 1u,
"shared-region routing rewrote unrelated loop-control branches");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"shared-region/early-break routing changed semantic coverage");
@@ -8770,17 +8770,6 @@ void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() {
"early-exit ladder routing changed semantic instruction coverage");
Check(graph.blocks.size() > original_block_count,
"early-exit ladder routing did not add forwarding blocks");
const auto route_selects =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
});
const auto route_sets =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.goto_value >= 0;
});
Check(route_selects != 0u && route_sets >= 3u,
"early-exit ladder lacks explicit typed route state");
auto options = MakeCompileOptions(ShaderType::Pixel);
options.dump_ir = true;
@@ -8818,17 +8807,6 @@ void TestNewShaderRecompilerCfgNestedEarlyExitSharedTerminal() {
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"nested early-exit structurization changed semantic coverage");
const auto route_selects =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
});
const auto route_sets =
std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.goto_value >= 0;
});
Check(route_selects == 1u && route_sets == 3u,
"nested early exit lacks complete typed route state");
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
@@ -8873,12 +8851,6 @@ void TestNewShaderRecompilerCfgEarlyReturnSharedLoopContinuation() {
original_coverage &&
graph.natural_loops.size() == 2u,
"shared-continuation gateway changed semantic blocks or the loops");
Check(std::ranges::none_of(graph.blocks,
[](const auto &block) {
return block.terminator.goto_variable !=
UINT32_MAX;
}),
"private early return introduced unnecessary routing state");
auto options = MakeCompileOptions(ShaderType::Compute);
auto result = RecompileForTest(shader, options);
@@ -13629,13 +13601,13 @@ int main() {
TestNewShaderRecompilerCfgSharedOuterAndLoopMerge();
TestNewShaderRecompilerCfgLoopExitSharedWithSelection();
TestNewShaderRecompilerCfgLoopEarlyBreakNoSelection();
TestNewShaderRecompilerCfgNestedLoopNonlocalExitDispatcher();
TestNewShaderRecompilerCfgNestedLoopNonlocalExitStructured();
TestNewShaderRecompilerCfgNestedLoopLocalExitNoSelection();
TestNewShaderRecompilerCfgNestedLoopExitTailMergeSplit();
TestNewShaderRecompilerCfgMixedContinueNonmergeExitDispatcher();
TestNewShaderRecompilerCfgMixedContinueNonmergeExitStructured();
TestNewShaderRecompilerCfgConditionalLatchNoSelection();
TestNewShaderRecompilerCfgDirectConditionalLatchNoSelection();
TestNewShaderRecompilerCfgLoopEarlyContinuesNoSelection();
TestNewShaderRecompilerCfgLoopEarlyContinuesStructured();
TestNewShaderRecompilerCfgLoopGatewaySelection();
TestNewShaderRecompilerCfgConditionalLoopHeaderSelection();
TestNewShaderRecompilerCfgMultipleLoopLatches();
@@ -13643,6 +13615,7 @@ int main() {
TestNewShaderRecompilerCfgNestedEarlyExitLoopForwarders();
TestNewShaderRecompilerCfgExecSccSharedArm();
TestSharedReturnPreservesDescriptorDominance();
TestCfgSiblingSharedExit();
TestNewShaderRecompilerCfgNestedTailEarlyExit();
TestNewShaderRecompilerCfgSharedReturnAfterNestedSelections();
TestNewShaderRecompilerCfgAlternatingSharedReturns();