diff --git a/ps2xRecomp/src/lib/control_flow_analyzer.cpp b/ps2xRecomp/src/lib/control_flow_analyzer.cpp index ed7578dd19..13d5639437 100644 --- a/ps2xRecomp/src/lib/control_flow_analyzer.cpp +++ b/ps2xRecomp/src/lib/control_flow_analyzer.cpp @@ -192,9 +192,12 @@ namespace ps2recomp bool needsIndirectFallback = false; for (const Instruction *jrInst : indirectJumps) { - if (jrInst->function == SPECIAL_JALR) + if (jrInst->function == SPECIAL_JALR && jrInst->rd != 0u) { + // A link-producing JALR is emitted as an indirect call through the runtime dispatcher. Only its + // return PC can resume in the caller; JALR $zero remains an unresolved indirect jump below. queueResumeEntryTarget(jrInst->address + 8u); + continue; } bool foundTable = false; diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 463b6cff1a..824d54fc3f 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -628,7 +628,7 @@ void register_code_generator_tests() "unresolved JR should not pretend it has a resolved local jump table"); }); - tc.Run("unresolved JALR marks internal labels as indirect fallback resume entries", [](TestCase &t) { + tc.Run("unresolved JALR only marks its return pc as resumable", [](TestCase &t) { Function func; func.name = "unresolved_jalr_fallback"; func.start = 0x3200; @@ -647,14 +647,49 @@ void register_code_generator_tests() CodeGenerator gen({}, {}); CodeGenerator::AnalysisResult analysis = gen.collectInternalBranchTargets(func, instructions); - t.IsTrue(analysis.indirectFallbackEntryPoints.contains(0x320Cu), - "unresolved JALR should register internal labels as resumable entries for the owning function"); + t.IsTrue(analysis.resumeEntryPoints.contains(0x320Cu), + "unresolved JALR should register its return pc as resumable"); t.IsTrue(analysis.entryPoints.contains(0x320Cu), - "unresolved JALR fallback targets should still emit labels in the owner"); + "unresolved JALR return pc should emit a label in the caller"); + t.IsFalse(analysis.indirectFallbackEntryPoints.contains(0x3200u), + "unresolved JALR should not promote the caller start as an indirect jump target"); + t.IsFalse(analysis.indirectFallbackEntryPoints.contains(0x3210u), + "unresolved JALR should not promote unrelated caller instructions"); + t.IsFalse(analysis.entryPoints.contains(0x3210u), + "unresolved JALR should not emit labels for unrelated caller instructions"); t.IsFalse(analysis.jumpTableTargets.contains(0x3204u), "unresolved JALR should not pretend it has a resolved local jump table"); }); + tc.Run("unresolved JALR without a link register retains indirect fallback", [](TestCase &t) { + Function func; + func.name = "unresolved_jalr_tail_jump"; + func.start = 0x3300; + func.end = 0x3320; + func.isRecompiled = true; + func.isStub = false; + + std::vector instructions{ + makeNop(0x3300), + makeJalr(0x3304, 25, 0), + makeNop(0x3308), + makeNop(0x330C), + makeNop(0x3310), + }; + + CodeGenerator gen({}, {}); + CodeGenerator::AnalysisResult analysis = gen.collectInternalBranchTargets(func, instructions); + + t.IsFalse(analysis.resumeEntryPoints.contains(0x330Cu), + "JALR without a link register should not add a return resume entry"); + t.IsTrue(analysis.indirectFallbackEntryPoints.contains(0x330Cu), + "JALR without a link register should retain unresolved indirect-jump fallback"); + t.IsTrue(analysis.entryPoints.contains(0x3310u), + "JALR without a link register should retain fallback labels in the owner"); + t.IsFalse(analysis.jumpTableTargets.contains(0x3304u), + "unresolved JALR without a link register should not fabricate a jump table"); + }); + tc.Run("syscall marks the following instruction as a resume entry", [](TestCase &t) { // Shape of a real SDK syscall wrapper: // addiu $v1, $zero, ; syscall ; jr $ra ;