From 8b4236038ad6e36d26ed36f49b565d2c318bfc08 Mon Sep 17 00:00:00 2001 From: Sinan KARAKAYA Date: Mon, 17 Aug 2026 18:29:04 +0200 Subject: [PATCH] Don't promote a whole function because of an indirect call When a computed jump cannot be resolved to a jump table, every instruction in the function becomes an entry point, because the jump could land on any of them. That fallback was also applied to JALR, which is not a jump but a call: it transfers control to another function and returns to the instruction after the delay slot. That return address is already queued as a resume target a few lines above, so nothing else in the function needs to be reachable from outside. Indirect calls are ordinary code -- function pointers, virtual dispatch, callbacks -- so the fallback fired constantly. On a 3 MB PS2 executable, 2,210 of the 2,422 unresolved sites were JALR, and 1,078 of the 1,282 affected functions contained no unresolved jump at all. Restrict the fallback to JR. Promoted entries drop from 189,876 to 1,688, registered table entries from 156,783 to 75,386, the generated registration file from 13 MB to 6 MB, and total output from 180 MB to 163 MB. Every indirect call site in real code keeps its return-address resume entry (the only sites that lose one are bogus functions carved out of rodata, where the address is outside the function anyway). --- ps2xRecomp/src/lib/control_flow_analyzer.cpp | 9 ++++++++- ps2xTest/src/code_generator_tests.cpp | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/ps2xRecomp/src/lib/control_flow_analyzer.cpp b/ps2xRecomp/src/lib/control_flow_analyzer.cpp index a099f5cf9..2b27ffc9f 100644 --- a/ps2xRecomp/src/lib/control_flow_analyzer.cpp +++ b/ps2xRecomp/src/lib/control_flow_analyzer.cpp @@ -385,7 +385,14 @@ namespace ps2recomp } } } - if (!foundTable) + // Only an unresolved computed *jump* can land on an arbitrary + // instruction of this function and therefore force every address to + // become an entry point. JALR is a call: it transfers control to + // another function and comes back to the instruction after the delay + // slot, which is already queued as a resume target above. Treating a + // call like a jump here promotes the whole function for what is + // usually just a function pointer or virtual dispatch. + if (!foundTable && jrInst->function != SPECIAL_JALR) { needsIndirectFallback = true; } diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 2dd9c531f..d9128c644 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -529,7 +529,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 resumes after the call without promoting the function", [](TestCase &t) { Function func; func.name = "unresolved_jalr_fallback"; func.start = 0x3200; @@ -548,10 +548,21 @@ 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"); + // JALR is a call: it returns past the delay slot, so 0x320C is the only + // address in this function that has to be reachable from outside. + t.IsTrue(analysis.resumeEntryPoints.contains(0x320Cu), + "unresolved JALR should mark its return pc as resumable"); t.IsTrue(analysis.entryPoints.contains(0x320Cu), - "unresolved JALR fallback targets should still emit labels in the owner"); + "unresolved JALR resume pc should still emit a label in the owner"); + + // Both sets feed the same owner resume-target list, so the return pc is + // registered either way; what must not happen is the whole-function + // promotion reserved for jumps that could land anywhere. + t.IsFalse(analysis.indirectFallbackEntryPoints.contains(0x3210u), + "an indirect call must not promote unrelated instructions to entry points"); + t.IsFalse(analysis.indirectFallbackEntryPoints.contains(0x3200u), + "an indirect call must not promote the function start to a fallback entry"); + t.IsFalse(analysis.jumpTableTargets.contains(0x3204u), "unresolved JALR should not pretend it has a resolved local jump table"); });