From d80fbea51d3e8a28daebcf8cb8b944cc2c5c6d32 Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 17 Aug 2026 06:09:54 +0200 Subject: [PATCH] shader: resolve subtractive dword SETPC tables --- .../recompiler/frontend/cfg/ShaderCFG.cpp | 170 ++++++++++++++++-- tests/shaderCfgTests.cpp | 65 +++++++ 2 files changed, 219 insertions(+), 16 deletions(-) diff --git a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp index ea110e0..eed9df3 100644 --- a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp +++ b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp @@ -153,6 +153,9 @@ bool InstructionWritesScalarCode(const Instruction& inst, uint32_t code) { return code >= dst_code && code < dst_code + count; } +bool ScalarCodeWrittenInRange(const Decoder::Program& program, uint32_t begin_index, + uint32_t end_index, uint32_t code); + bool FindPreviousGetpc(const Decoder::Program& program, uint32_t before_index, uint32_t dst_code, uint32_t* index) { for (uint32_t i = before_index; i > 0; i--) { @@ -193,11 +196,46 @@ bool ResolvePcRelativeBase(const Decoder::Program& program, uint32_t before_inde const Decoder::Operand* offset = nullptr; int32_t imm = 0; uint32_t getpc_index = 0; - if (!OtherScalarSource(candidate, base_code, &offset) || !IsImmediateSigned(*offset, imm) || + if (adds) { + if (!OtherScalarSource(candidate, base_code, &offset)) { + return false; + } + } else { + if (!IsScalarCode(candidate.src0, base_code)) { + return false; + } + offset = &candidate.src1; + } + if (!IsImmediateSigned(*offset, imm) || !FindPreviousGetpc(program, candidate_index, base_code, &getpc_index)) { return false; } + if (candidate_index + 1u >= before_index) { + return false; + } + const auto& high = program.instructions[candidate_index + 1u]; + const Decoder::Operand* high_other = nullptr; + uint32_t zero = 0; + if (high.opcode != (adds ? Opcode::SAddcU32 : Opcode::SSubbU32) || + !IsScalarCode(high.dst, base_code + 1u)) { + return false; + } + if (adds) { + if (!OtherScalarSource(high, base_code + 1u, &high_other)) { + return false; + } + } else { + if (!IsScalarCode(high.src0, base_code + 1u)) { + return false; + } + high_other = &high.src1; + } + if (!IsImmediate(*high_other, zero) || zero != 0u || + ScalarCodeWrittenInRange(program, candidate_index + 2u, before_index, base_code + 1u)) { + return false; + } + const auto base = InstructionEndPc(program.instructions[getpc_index]); *pc = adds ? base + static_cast(imm) : base - static_cast(imm); *pc &= ~3u; @@ -206,13 +244,22 @@ bool ResolvePcRelativeBase(const Decoder::Program& program, uint32_t before_inde return false; } -bool FindPreviousScalarLoadPair(const Decoder::Program& program, uint32_t before_index, - uint32_t dst_code, uint32_t* index) { +bool AddSignedByteOffset(uint32_t base, uint32_t encoded_offset, uint32_t* address) { + const auto value = static_cast(base) + static_cast(encoded_offset); + if (address == nullptr || value < 0 || value > UINT32_MAX) { + return false; + } + *address = static_cast(value); + return true; +} + +bool FindPreviousScalarLoad(const Decoder::Program& program, uint32_t before_index, + uint32_t dst_code, Opcode opcode, uint32_t* index) { for (uint32_t i = before_index; i > 0; i--) { const uint32_t candidate_index = i - 1u; const auto& candidate = program.instructions[candidate_index]; if (InstructionWritesScalarCode(candidate, dst_code)) { - if (candidate.opcode == Opcode::SLoadDwordx2 && IsScalarCode(candidate.dst, dst_code)) { + if (candidate.opcode == opcode && IsScalarCode(candidate.dst, dst_code)) { if (index != nullptr) { *index = candidate_index; } @@ -225,8 +272,8 @@ bool FindPreviousScalarLoadPair(const Decoder::Program& program, uint32_t before } bool ResolveJumpTableEntryCount(const Decoder::Program& program, uint32_t before_index, - const Decoder::Operand& byte_offset_operand, - uint32_t* entry_count) { + const Decoder::Operand& byte_offset_operand, uint32_t stride_shift, + uint32_t* entry_count) { if (entry_count == nullptr) { return false; } @@ -248,7 +295,7 @@ bool ResolveJumpTableEntryCount(const Decoder::Program& program, uint32_t before const Decoder::Operand* shift_amount_operand = nullptr; uint32_t shift_amount = 0; if (!OtherScalarSource(shift, byte_offset_code, &shift_amount_operand) || - !IsImmediate(*shift_amount_operand, shift_amount) || shift_amount != 3u) { + !IsImmediate(*shift_amount_operand, shift_amount) || shift_amount != stride_shift) { return false; } @@ -264,10 +311,8 @@ bool ResolveJumpTableEntryCount(const Decoder::Program& program, uint32_t before return false; } - const Decoder::Operand* clamp_other = nullptr; - uint32_t max_index = 0; - if (!OtherScalarSource(clamp, index_code, &clamp_other) || - !IsImmediate(*clamp_other, max_index)) { + uint32_t max_index = 0; + if (!IsImmediate(clamp.src0, max_index) && !IsImmediate(clamp.src1, max_index)) { return false; } *entry_count = max_index + 1u; @@ -332,7 +377,8 @@ bool ResolveSetpcJumpTable(const Decoder::Program& program, uint32_t setpc_index } uint32_t load_index = 0; - if (!FindPreviousScalarLoadPair(program, setpc_index - 3u, offset_low_code, &load_index)) { + if (!FindPreviousScalarLoad(program, setpc_index - 3u, offset_low_code, Opcode::SLoadDwordx2, + &load_index)) { return false; } const auto& load = program.instructions[load_index]; @@ -342,12 +388,13 @@ bool ResolveSetpcJumpTable(const Decoder::Program& program, uint32_t setpc_index } uint32_t table_pc = 0; - if (!ResolvePcRelativeBase(program, load_index, table_base_code, &table_pc)) { + if (!ResolvePcRelativeBase(program, load_index, table_base_code, &table_pc) || + !AddSignedByteOffset(table_pc, load.offset, &table_pc)) { return false; } uint32_t entry_count = 0; - if (!ResolveJumpTableEntryCount(program, load_index, load.src1, &entry_count)) { + if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 3u, &entry_count)) { return false; } uint32_t selector_code = UINT32_MAX; @@ -357,8 +404,10 @@ bool ResolveSetpcJumpTable(const Decoder::Program& program, uint32_t setpc_index } const uint32_t target_base = InstructionEndPc(getpc); - const uint32_t table_word = table_pc / 4u; - if ((table_pc & 3u) != 0 || table_word + entry_count * 2u > program.code.size()) { + const size_t table_word = table_pc / 4u; + const size_t table_words = static_cast(entry_count) * 2u; + if ((table_pc & 3u) != 0 || table_word > program.code.size() || + table_words > program.code.size() - table_word) { return false; } @@ -390,6 +439,92 @@ bool ResolveSetpcJumpTable(const Decoder::Program& program, uint32_t setpc_index return true; } +bool ResolveSetpcDwordJumpTable(const Decoder::Program& program, uint32_t setpc_index, + SetpcTargetInfo& info) { + if (setpc_index < 3u || setpc_index >= program.instructions.size()) { + return false; + } + + const auto& setpc = program.instructions[setpc_index]; + uint32_t pc_reg = 0; + if (setpc.opcode != Opcode::SSetpcB64 || setpc.src0.kind != Decoder::OperandKind::Sgpr || + !ScalarOperandCode(setpc.src0, pc_reg)) { + return false; + } + const auto& low_sub = program.instructions[setpc_index - 2u]; + const auto& high_sub = program.instructions[setpc_index - 1u]; + uint32_t offset_code = 0; + uint32_t zero = 0; + if (low_sub.opcode != Opcode::SSubU32 || !IsScalarCode(low_sub.dst, pc_reg) || + !IsScalarCode(low_sub.src0, pc_reg) || !ScalarOperandCode(low_sub.src1, offset_code) || + high_sub.opcode != Opcode::SSubbU32 || !IsScalarCode(high_sub.dst, pc_reg + 1u) || + !IsScalarCode(high_sub.src0, pc_reg + 1u) || !IsImmediate(high_sub.src1, zero) || + zero != 0u || offset_code == pc_reg || offset_code == pc_reg + 1u) { + return false; + } + + uint32_t load_index = 0; + if (!FindPreviousScalarLoad(program, setpc_index - 2u, offset_code, Opcode::SLoadDword, + &load_index)) { + return false; + } + const auto& load = program.instructions[load_index]; + if (!IsScalarCode(load.src0, pc_reg)) { + return false; + } + + uint32_t target_base = 0; + if (!ResolvePcRelativeBase(program, load_index, pc_reg, &target_base)) { + return false; + } + uint32_t table_pc = 0; + if (!AddSignedByteOffset(target_base, load.offset, &table_pc)) { + return false; + } + + uint32_t entry_count = 0; + if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 2u, &entry_count)) { + return false; + } + uint32_t selector_code = UINT32_MAX; + if (!ScalarOperandCode(load.src1, selector_code) || selector_code == offset_code || + ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) { + return false; + } + + const size_t table_word = table_pc / 4u; + if ((table_pc & 3u) != 0 || table_word > program.code.size() || + entry_count > program.code.size() - table_word) { + return false; + } + + std::vector targets; + std::vector selector_values; + std::vector selector_target_pcs; + for (uint32_t i = 0; i < entry_count; i++) { + const uint32_t offset = program.code[table_word + i]; + if (offset > target_base) { + return false; + } + const auto target_pc = (target_base - offset) & ~3u; + AddUniqueTargetPc(targets, target_pc); + selector_values.push_back(i * 4u); + selector_target_pcs.push_back(target_pc); + } + if (targets.empty()) { + return false; + } + + info.indirect = true; + info.pc_sgpr = pc_reg; + info.selector_code = selector_code; + info.table_load_pc = load.pc; + info.target_pcs = std::move(targets); + info.selector_values = std::move(selector_values); + info.selector_target_pcs = std::move(selector_target_pcs); + return true; +} + bool ResolveSetpcTarget(const Decoder::Program& program, uint32_t setpc_index, uint32_t& target) { if (setpc_index >= program.instructions.size()) { return false; @@ -436,6 +571,9 @@ bool ResolveSetpcTarget(const Decoder::Program& program, uint32_t setpc_index, u bool ResolveSetpcTargets(const Decoder::Program& program, uint32_t setpc_index, SetpcTargetInfo& info) { info = {}; + if (ResolveSetpcDwordJumpTable(program, setpc_index, info)) { + return true; + } if (ResolveSetpcJumpTable(program, setpc_index, info)) { return true; } diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 96adf6c..5d9c28a 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -7507,6 +7507,70 @@ void TestNewShaderRecompilerSetpcJumpTable() { CheckSpirvBinaryValidates(result.spirv); } +void TestNewShaderRecompilerSetpcDwordJumpTable() { + const uint32_t shader[] = { + EncodeSop2(0x07, 106, 0, 130), // s_min_u32 vcc_lo, s0, 2 + EncodeSop2(0x1e, 106, 106, 130), // s_lshl_b32 vcc_lo, vcc_lo, 2 + EncodeSop1(0x1f, 4, 0), // s_getpc_b64 s[4:5] + EncodeSop2(0x00, 4, 4, 255), // s_add_u32 s4, s4, literal + 0x00000034u, + EncodeSop2(0x04, 5, 5, 128), // s_addc_u32 s5, s5, 0 + EncodeSmem0(0x00, 6, 2), + (106u << 25u) | 4u, // s_load_dword s6, s[4:5], vcc_lo offset:4 + EncodeSopp(0x0c, 0), // s_waitcnt 0 + EncodeSop2(0x01, 4, 4, 6), // s_sub_u32 s4, s4, s6 + EncodeSop2(0x05, 5, 5, 128), // s_subb_u32 s5, s5, 0 + EncodeSop1(0x20, 0, 4), // s_setpc_b64 s[4:5] + EncodeSMovB32(1, 129), // case 0 + EncodeSopp(0x02, 1), + EncodeSMovB32(2, 129), // case 1 + EncodeSopp(0x01, 0), + 0u, + 0x00000010u, + 0x00000008u, + 0x00000010u, // table at pc 0x44, targets relative backward from pc 0x40 + }; + + auto options = MakeCompileOptions(ShaderType::Compute); + options.dump_ir = true; + + ShaderRecompiler::CompileResult result; + std::string error; + Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), + error.c_str()); + Check(Common::ContainsStr(result.ir_dump, "mode=dispatcher"), + "subtractive S_SETPC_B64 table did not select dispatcher fallback"); + const auto jump = std::find_if( + result.program.values->block_info.begin(), + result.program.values->block_info.end(), [](const auto &block) { + return !block.terminator.indirect_targets.empty(); + }); + const auto block_pc = [&](uint32_t id) { + const auto block = + std::find_if(result.program.values->block_info.begin(), + result.program.values->block_info.end(), + [=](const auto &info) { return info.id == id; }); + return block != result.program.values->block_info.end() ? block->start_pc + : UINT32_MAX; + }; + Check(jump != result.program.values->block_info.end() && + jump->terminator.indirect_targets.size() == 2 && + jump->terminator.indirect_target_pcs == + std::vector({0x30u, 0x38u}) && + jump->terminator.indirect_selector_values == + std::vector({0u, 4u, 8u}) && + jump->terminator.indirect_selector_targets.size() == 3 && + block_pc(jump->terminator.indirect_selector_targets[0]) == 0x30u && + block_pc(jump->terminator.indirect_selector_targets[1]) == 0x38u && + block_pc(jump->terminator.indirect_selector_targets[2]) == 0x30u, + "subtractive S_SETPC_B64 table targets or selector mapping changed"); + Check(!Common::ContainsStr(result.ir_dump, "SLoadDword"), + "subtractive S_SETPC_B64 table load reached normal IR"); + Check(SpirvContainsOpcode(result.spirv, 251), + "subtractive S_SETPC_B64 dispatcher lacks OpSwitch"); + CheckSpirvBinaryValidates(result.spirv); +} + void TestNewShaderRecompilerExpVertexOutputs() { const uint32_t shader[] = { EncodeExp0(0x0c, 0xf), EncodeExp1(0, 1, 2, 3), // POS0 @@ -9280,6 +9344,7 @@ int main() { TestNewShaderRecompilerBranchConditionForms(); TestNewShaderRecompilerSetpcBranch(); TestNewShaderRecompilerSetpcJumpTable(); + TestNewShaderRecompilerSetpcDwordJumpTable(); TestNewShaderRecompilerZeroInitialRegisterState(); TestNewShaderRecompilerVertexExportUsesLaneExecMask(); TestNewShaderRecompilerPerInvocationU64Complement();