From f07c61be385400d7d8a40c0fa763bed34f9229c5 Mon Sep 17 00:00:00 2001 From: Tzeentchnet <58480142+Tzeentchnet@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:57:54 -0800 Subject: [PATCH 1/2] fix: limit unresolved JALR resume entries --- ps2xRecomp/src/lib/control_flow_analyzer.cpp | 3 +++ ps2xTest/src/code_generator_tests.cpp | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ps2xRecomp/src/lib/control_flow_analyzer.cpp b/ps2xRecomp/src/lib/control_flow_analyzer.cpp index ed7578dd19..ad9b811a1b 100644 --- a/ps2xRecomp/src/lib/control_flow_analyzer.cpp +++ b/ps2xRecomp/src/lib/control_flow_analyzer.cpp @@ -194,7 +194,10 @@ namespace ps2recomp { if (jrInst->function == SPECIAL_JALR) { + // JALR is emitted as an indirect call through the runtime dispatcher. Only its return PC can + // resume in the caller; the call target is not an unresolved local jump target. 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..656b174284 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,10 +647,16 @@ 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"); }); From 70354045ef118b11452d16f835575efa359c816f Mon Sep 17 00:00:00 2001 From: Tzeentchnet <58480142+Tzeentchnet@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:11:44 -0800 Subject: [PATCH 2/2] fix: preserve fallback for JALR tail jumps --- ps2xRecomp/src/lib/control_flow_analyzer.cpp | 6 ++-- ps2xTest/src/code_generator_tests.cpp | 29 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/ps2xRecomp/src/lib/control_flow_analyzer.cpp b/ps2xRecomp/src/lib/control_flow_analyzer.cpp index ad9b811a1b..13d5639437 100644 --- a/ps2xRecomp/src/lib/control_flow_analyzer.cpp +++ b/ps2xRecomp/src/lib/control_flow_analyzer.cpp @@ -192,10 +192,10 @@ namespace ps2recomp bool needsIndirectFallback = false; for (const Instruction *jrInst : indirectJumps) { - if (jrInst->function == SPECIAL_JALR) + if (jrInst->function == SPECIAL_JALR && jrInst->rd != 0u) { - // JALR is emitted as an indirect call through the runtime dispatcher. Only its return PC can - // resume in the caller; the call target is not an unresolved local jump target. + // 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; } diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 656b174284..824d54fc3f 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -661,6 +661,35 @@ void register_code_generator_tests() "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 ;