The bounded-jump-table resolver requires the guest's own bounds check
(`sltiu/beq`) and index scaling (`sll`), because those are what prove the
table's extent. A decoder that keeps its state as a pre-scaled byte offset
and dispatches through `lui/ori base; addu base,offset; lw; jr` has neither,
so the jr fell back to a bare CPS tail-transfer and every entry it reached
that was not already a dispatch key ran on the dirty-RAM interpreter. On
Breath of Fire III that is the file decoder's state dispatch at
`0x80164AB0` (five entries; the first entry is the word right after the
table), the last boot-EXE row a play session still interpreted after the
computed-stride resolver.
resolve_self_limited_jump_table recovers the extent from the table's own
layout instead: words are taken from the base while each is a 4-aligned
in-function code address outside every delay slot, and the table ends
where its lowest target begins, since a pointer table cannot overlap the
code it points at. As with the stride resolver the emitter switches on
the actual runtime target with the CPS tail-transfer as default, so the
recovered entries only add native cases; an index past the recovered
extent still takes the fallback. Wired as the third fallback at both
code-generator sites and in the exact-entry discovery walker.
On the title's boot EXE it matches exactly one site (five entries); every
other shard is byte-identical. The synthetic test gains a second function
with this shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A `jr R` with `R = base + index * stride` and no table in memory is how
unrolled copy loops enter their body in the middle (Duff's device). The
bounded-jump-table resolver only knows the `sltiu/beq; sll; addu; lw; jr`
chain, so the emitter fell back to a bare CPS tail-transfer; the trampoline
then found no dispatch key at the interior PC (straight-line code, no block
leader) and the dirty-RAM interpreter ran the rest of the copy on every
call. On Breath of Fire III that is the file decompressor's literal-copy
loop (`0x80164D1C`, 32 groups of `lbu/nop/sb`), which fires on every load
and was the whole non-kernel interpreted residual of a play session once
the kernel patch ranges (#346) and the call vectors (#381) ran native.
resolve_computed_stride_jump walks the chain back from the jr through
nearest definitions in straight-line code: `addu R, B, S` with S from
`sll S, I, k` or the two-sll sum `sll P,I,a; sll Q,I,b; addu S,P,Q`, and
then requires the code after the delay slot to be N >= 2 shape-identical
groups of `stride` bytes (same opcodes and register fields, immediates
free, no control flow inside a group). Targets are run_start + k * stride
for k = 0..N. The base register's value is never assumed: the emitter
switches on the actual runtime target with the CPS tail-transfer as the
default, so the recovered cases can only add native entries, never
redirect a jump that lands elsewhere. Wired into both code-generator
sites (interior labels + the switch) and the exact-entry discovery walker.
On the title's boot EXE it matches exactly one site, the decompressor loop
(33 entries); every other jr is untouched.
Adds recompiler/tests/test_computed_stride_jump_codegen.py (synthetic
PS-EXE with the idiom; asserts the switch, every case and the interior
labels), registered with CTest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>