Scope Kamek bl-patch LR-continuation detection to genuine skip-return targets

Fix crash from Kamek skip-return hooks (Item Rain crash) (#182) added every
Kamek BranchLink patch target to lrContinuationCallTargets unconditionally,
with no filter analogous to the RetroWfcHookSetsLinkRegister check already
used for RetroWFC hooks. Since bl is the ordinary PowerPC call instruction,
this made the codegen treat effectively every patched call in the mod as a
potential skip-return hook, forcing conservative handling (full register
reload, disabled resident-call fast paths, local LR-continuation dispatch
tables) onto thousands of calls that just return normally.

For Retro Rewind this inflated total translated mod size by +42%
(1,414,327 -> 2,005,284 lines), concentrated in ~10 unrelated overlay
functions that happened to call a patched target, and was enough to make
one aggregate build shard pathologically slow to compile (hangs Linux CI).

Instead, only mark a bl target as LR-continuation-aware if a lightweight
discovery-only decode of its own body actually finds evidence of
skip-return behavior via DiscoverLrRelativeIndirectJumpOffsets. Falls back
to the conservative (old) behavior if a target can't be statically
analyzed, so no skip-return case is silently missed.

Verified against the real Retro Rewind mod: total mod size returns to
1,416,350 lines (+0.14% vs. pre-fix, down from +42%), all 6 genuinely new
continuation functions from the original fix are preserved, zero
functions lost, and all 609 existing translator tests still pass.
This commit is contained in:
theofficialgman
2026-09-12 14:47:46 -04:00
parent 53d8f71c68
commit ad2d4e7aa5
+43
View File
@@ -2282,9 +2282,52 @@ int EmitModCpp(
.Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h))
.Select(h => (h.TargetAddress!.Value, h.ContinuationAddress)));
}
// An ordinary bl patch returns normally like any call and needs none of the
// conservative LR-continuation codegen below (forced register reload, disabled
// resident-call fast paths, local dispatch tables) - only a target that actually
// manipulates LR to resume somewhere other than the call's own return address
// does. Without this check every bl patch target would qualify, which is far
// broader than the skip-return hooks this plumbing exists for and bloats
// unrelated callers that merely call into a patched function (see the Retro
// Rewind mod-size regression this was found to cause).
var lrSkipReturnTargetCache = new Dictionary<uint, bool>();
bool TargetExhibitsLrSkipReturn(uint target)
{
if (lrSkipReturnTargetCache.TryGetValue(target, out var cached))
{
return cached;
}
bool exhibitsSkipReturn;
try
{
var discovery = modTranslator.Discover(
target,
new TranslationOptions(KnownFunctionEntryPoints: knownFunctionEntryPoints));
exhibitsSkipReturn = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(discovery.Instructions).Any();
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException
or IndexOutOfRangeException or NotSupportedException
or OverflowException)
{
// Could not statically analyze this target - fail safe and keep the
// conservative handling rather than risk silently reintroducing a
// skip-return crash for a target this check could not examine.
exhibitsSkipReturn = true;
}
lrSkipReturnTargetCache[target] = exhibitsSkipReturn;
return exhibitsSkipReturn;
}
foreach (var patch in patchPlan.ExecutablePatches.Where(p => p.CommandId == KamekCommandId.BranchLink && p.Arguments.Count > 0))
{
var target = KamekAddress.Resolve(patch.Arguments[0], patchPlan.ModuleGuestBase);
if (!TargetExhibitsLrSkipReturn(target))
{
continue;
}
hookLrBases.Add((target, checked(patch.CommandAddress + 4u)));
}