diff --git a/ps2xRecomp/src/lib/code_generator.cpp b/ps2xRecomp/src/lib/code_generator.cpp index 40c65c9..e3ad867 100644 --- a/ps2xRecomp/src/lib/code_generator.cpp +++ b/ps2xRecomp/src/lib/code_generator.cpp @@ -186,8 +186,7 @@ namespace ps2recomp std::vector sortedInternalTargets; if (branchInst.opcode == OPCODE_SPECIAL && - branchInst.function == SPECIAL_JR && - rs_reg == 31 && + (branchInst.function == SPECIAL_JR || branchInst.function == SPECIAL_JALR) && !internalTargets.empty()) { sortedInternalTargets.reserve(internalTargets.size()); @@ -260,7 +259,11 @@ namespace ps2recomp } else { - ss << " " << funcName << "(rdram, ctx, runtime);\n"; + ss << " {\n"; + ss << " const uint32_t __entryPc = ctx->pc;\n"; + ss << " " << funcName << "(rdram, ctx, runtime);\n"; + ss << fmt::format(" if (ctx->pc == __entryPc) {{ ctx->pc = 0x{:X}u; }}\n", fallthroughPc); + ss << " }\n"; ss << fmt::format(" if (ctx->pc != 0x{:X}u) {{ return; }}\n", fallthroughPc); } } @@ -303,6 +306,7 @@ namespace ps2recomp { ss << " {\n"; ss << fmt::format(" auto targetFn = runtime->lookupFunction(0x{:X}u);\n", target); + ss << " const uint32_t __entryPc = ctx->pc;\n"; ss << " targetFn(rdram, ctx, runtime);\n"; if (branchInst.opcode == OPCODE_J) { @@ -310,6 +314,7 @@ namespace ps2recomp } else { + ss << fmt::format(" if (ctx->pc == __entryPc) {{ ctx->pc = 0x{:X}u; }}\n", fallthroughPc); ss << fmt::format(" if (ctx->pc != 0x{:X}u) {{ return; }}\n", fallthroughPc); } ss << " }\n"; @@ -339,7 +344,7 @@ namespace ps2recomp ss << " ctx->pc = jumpTarget;\n"; - if (branchInst.function == SPECIAL_JR && rs_reg == 31 && !sortedInternalTargets.empty()) + if (!sortedInternalTargets.empty()) { ss << " switch (jumpTarget) {\n"; for (uint32_t t : sortedInternalTargets) @@ -358,7 +363,9 @@ namespace ps2recomp { ss << " {\n"; ss << " auto targetFn = runtime->lookupFunction(jumpTarget);\n"; + ss << " const uint32_t __entryPc = ctx->pc;\n"; ss << " targetFn(rdram, ctx, runtime);\n"; + ss << fmt::format(" if (ctx->pc == __entryPc) {{ ctx->pc = 0x{:X}u; }}\n", fallthroughPc); ss << fmt::format(" if (ctx->pc != 0x{:X}u) {{ return; }}\n", fallthroughPc); ss << " }\n"; } @@ -558,10 +565,17 @@ namespace ps2recomp std::unordered_set targets; std::unordered_set instructionAddresses; instructionAddresses.reserve(instructions.size()); + bool hasIndirectRegisterJump = false; for (const auto &inst : instructions) { instructionAddresses.insert(inst.address); + if (inst.opcode == OPCODE_SPECIAL && + ((inst.function == SPECIAL_JR && inst.rs != 31) || + inst.function == SPECIAL_JALR)) + { + hasIndirectRegisterJump = true; + } } for (const auto &inst : instructions) @@ -600,6 +614,17 @@ namespace ps2recomp } } + if (hasIndirectRegisterJump) + { + for (uint32_t addr : instructionAddresses) + { + if (addr >= function.start && addr < function.end) + { + targets.insert(addr); + } + } + } + return targets; } @@ -621,7 +646,6 @@ namespace ps2recomp } std::unordered_set internalTargets = collectInternalBranchTargets(function, instructions); - ss << "// Function: " << function.name << "\n"; ss << "// Address: 0x" << std::hex << function.start << " - 0x" << function.end << std::dec << "\n"; @@ -1633,7 +1657,7 @@ namespace ps2recomp switch (subfunc) { case MMI3_PMADDUW: - return fmt::format("Unhandled PMADDUW instruction: function 0x{:X}", subfunc); + return fmt::format("// Unhandled PMADDUW instruction: function 0x{:X}", subfunc); case MMI3_PSRAVW: return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PSRAVW(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));", rd, rs, rt); case MMI3_PMTHI: diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 21f68c4..40f81b6 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -472,6 +472,10 @@ void register_code_generator_tests() t.IsTrue(generated.find("SET_GPR_U32(ctx, 31, 0xA008u);") != std::string::npos, "JAL should set RA"); t.IsTrue(generated.find("some_func(rdram, ctx, runtime);") != std::string::npos, "JAL should call function"); + t.IsTrue(generated.find("const uint32_t __entryPc = ctx->pc;") != std::string::npos, + "JAL should capture entry PC before call"); + t.IsTrue(generated.find("if (ctx->pc == __entryPc) { ctx->pc = 0xA008u; }") != std::string::npos, + "JAL should recover fallthrough when callee leaves ctx->pc unchanged"); t.IsTrue(generated.find("if (ctx->pc != 0xA008u) { return; }") != std::string::npos, "JAL should check return PC"); }); @@ -520,7 +524,11 @@ void register_code_generator_tests() t.IsTrue(generated.find("SET_GPR_U32(ctx, 31, 0xD008u);") != std::string::npos, "JALR should set link register"); t.IsTrue(generated.find("auto targetFn = runtime->lookupFunction(jumpTarget);") != std::string::npos, "JALR should lookup function"); t.IsTrue(generated.find("targetFn(rdram, ctx, runtime);") != std::string::npos, "JALR should call function"); - t.IsTrue(generated.find("if (ctx->pc != 0xD008u) { return; }") != std::string::npos, "JALR should check return PC"); + t.IsTrue(generated.find("const uint32_t __entryPc = ctx->pc;") != std::string::npos, + "JALR should capture entry PC before indirect call"); + t.IsTrue(generated.find("if (ctx->pc == __entryPc) { ctx->pc = 0xD008u; }") != std::string::npos, + "JALR should recover fallthrough when callee leaves ctx->pc unchanged"); + t.IsTrue(generated.find("if (ctx->pc != 0xD008u) { return; }") != std::string::npos, "JALR should check return PC"); }); tc.Run("backward BEQ emits label and goto (sign-extended offset)", [](TestCase &t) { @@ -614,6 +622,65 @@ void register_code_generator_tests() t.IsTrue(generated.find("case 0x1308u: goto label_1308;") != std::string::npos, "switch should include return address from internal JAL"); }); + tc.Run("JR non-RA emits switch for in-function jump targets", [](TestCase &t) { + Function func; + func.name = "jr_non_ra_switch"; + func.start = 0x1400; + func.end = 0x1420; + func.isRecompiled = true; + func.isStub = false; + + // 0x1400: nop + // 0x1404: jr $16 (register jump) + // 0x1408: nop (delay slot) + // 0x140c: nop + Instruction i0 = makeNop(0x1400); + Instruction jr = makeJr(0x1404, 16); + Instruction delay = makeNop(0x1408); + Instruction i3 = makeNop(0x140c); + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, {i0, jr, delay, i3}, false); + printGeneratedCode("JR non-RA emits switch for in-function jump targets", generated); + + t.IsTrue(generated.find("switch (jumpTarget)") != std::string::npos, + "JR via non-RA register should emit switch for internal targets"); + t.IsTrue(generated.find("case 0x1400u: goto label_1400;") != std::string::npos, + "switch should include in-function entry label"); + t.IsTrue(generated.find("case 0x140Cu: goto label_140c;") != std::string::npos, + "switch should include other in-function labels"); + }); + + tc.Run("JALR includes switch and fallback/guard pair", [](TestCase &t) { + Function func; + func.name = "jalr_switch_and_fallback"; + func.start = 0x1500; + func.end = 0x1530; + func.isRecompiled = true; + func.isStub = false; + + // A call-like setup so there are multiple in-function labels to dispatch to. + Instruction jal = makeJal(0x1500, 0x1510); + Instruction jalDelay = makeNop(0x1504); + Instruction atReturn = makeNop(0x1508); + Instruction atTarget = makeNop(0x1510); + Instruction jalr = makeJalr(0x1514, 4, 31); + Instruction jalrDelay = makeNop(0x1518); + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, {jal, jalDelay, atReturn, atTarget, jalr, jalrDelay}, false); + printGeneratedCode("JALR includes switch and fallback/guard pair", generated); + + t.IsTrue(generated.find("switch (jumpTarget)") != std::string::npos, + "JALR should emit switch when in-function register-jump targets exist"); + t.IsTrue(generated.find("case 0x1508u: goto label_1508;") != std::string::npos, + "switch should include internal return label from JAL in same function"); + t.IsTrue(generated.find("if (ctx->pc == __entryPc) { ctx->pc = 0x151Cu; }") != std::string::npos, + "JALR should contain unchanged-PC fallback to fallthrough"); + t.IsTrue(generated.find("if (ctx->pc != 0x151Cu) { return; }") != std::string::npos, + "JALR should retain non-fallthrough guard"); + }); + tc.Run("resolveStubTarget allows leading underscore alias", [](TestCase &t) { t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub, "_rand should resolve via rand stub alias");