XenonAnalyse: add optional AnalyzerSwitchTableMap parameter to Function::Analyze

Defines AnalyzerSwitchTable { defaultLabel, labels[] } and
AnalyzerSwitchTableMap = unordered_map<uint32_t /* bctr VA */,
AnalyzerSwitchTable>. Function::Analyze gains an optional
`const AnalyzerSwitchTableMap* switchMap = nullptr` parameter.

Default nullptr preserves existing behavior exactly: no call site
needs to change, and the walker currently ignores the parameter.

The next commit wires the parameter into the walker's unconditional-
bctr handling: when switchMap is populated and the walker reaches a
bctr whose guest VA has a map entry, it will push every switch label
(and the default) as a successor block rather than terminating.

Rationale for keying by BCTR VA rather than by XenonAnalyse's own
`SwitchTable::base` (first-insn-of-pattern VA): the producer of the
TOML-emitted switch tables and the consumer of Function::Analyze are
often the same caller, but the walker encounters instructions at the
BCTR's address directly. Keying the map by BCTR VA removes the need
for the walker to track pattern offsets (6 insns for absolute-form
switches, 8 for computed / short-offset, 7 for byte-offset) or to
handle NOP padding variations — the caller does the offset math once
at map-construction time.

Fixes the well-known class of issues where switch-dispatched functions
have their successor blocks swept away by the discontinuity pass, and
downstream consumers (e.g., recompilers) emit "jump outside function"
errors for labels that are actually reachable. Observed on an Xbox 360
title whose compiler emitted forward-jumping switch tables in this
shape, and referenced in UnleashedRecomp-lineage work; other Xbox 360
recompile targets likely hit it too.

See also:
- A follow-up test commit adds synthetic hand-crafted PPC fixtures
  covering the happy path + null-map safety + wrong-map miss.
This commit is contained in:
Uproared 2026-04-24 05:16:28 -04:00
parent ddd128bcca
commit e6c9124119
2 changed files with 38 additions and 6 deletions

View file

@ -38,8 +38,14 @@ size_t Function::SearchBlock(size_t address) const
return -1;
}
Function Function::Analyze(const void* code, size_t size, size_t base)
Function Function::Analyze(const void* code, size_t size, size_t base,
const AnalyzerSwitchTableMap* switchMap)
{
(void)switchMap; // consumed in a follow-up commit; receiving now so
// that callers can adopt the signature and pass a
// map immediately without waiting for the walker
// change to land.
Function fn{ base, 0 };
if (*((uint32_t*)code + 1) == 0x04000048) // shifted ptr tail call

View file

@ -1,6 +1,8 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
#ifdef _DEBUG
@ -9,6 +11,29 @@
#define DEBUG(X)
#endif
// Optional switch-table map consumed by the block walker in
// Function::Analyze when it reaches an unconditional bctr at a known
// switch-dispatch site. When the caller hands in a populated map, the
// walker pushes every label (and the default) as a successor block
// instead of terminating, which grows `fn.size` to include switch-
// dispatched code that the legacy walker would discard via the end-
// of-Analyze discontinuity pass.
//
// The map is keyed by the BCTR guest VA — NOT by the first-instruction-
// of-pattern VA that XenonAnalyse's own switch-table TOML emits. The
// caller (typically XenonRecomp) performs the switch-base → bctr-VA
// offset math at map-construction time and hands us a pre-keyed map.
//
// Passing nullptr (the default) preserves the legacy walker behavior
// byte-for-byte. No existing callers need to change.
struct AnalyzerSwitchTable
{
uint32_t defaultLabel{};
std::vector<uint32_t> labels{};
};
using AnalyzerSwitchTableMap = std::unordered_map<uint32_t, AnalyzerSwitchTable>;
struct Function
{
struct Block
@ -18,16 +43,16 @@ struct Function
size_t projectedSize{ static_cast<size_t>(-1) }; // scratch
DEBUG(size_t parent{});
Block()
Block()
{
}
Block(size_t base, size_t size)
: base(base), size(size)
: base(base), size(size)
{
}
Block(size_t base, size_t size, size_t projectedSize)
Block(size_t base, size_t size, size_t projectedSize)
: base(base), size(size), projectedSize(projectedSize)
{
}
@ -45,7 +70,8 @@ struct Function
: base(base), size(size)
{
}
size_t SearchBlock(size_t address) const;
static Function Analyze(const void* code, size_t size, size_t base);
static Function Analyze(const void* code, size_t size, size_t base,
const AnalyzerSwitchTableMap* switchMap = nullptr);
};