Discover scheduled overlay prologues through their first basic block

This commit is contained in:
Matthew Stanley
2026-09-30 18:58:25 -07:00
parent 52dd4dffbc
commit 6901aac851
3 changed files with 81 additions and 20 deletions
+10
View File
@@ -213,6 +213,16 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- **2026-09-30 — Original-disc overlay discovery (beads-eio.3.215).**
Ape Escape Primordial Ooze exposed return-adjacent functions omitted by the
archive scanner: a scheduled LUI/load before stack setup and an RA save beyond
its fixed twelve-instruction prefix. Reuse the proven prelude recognizer and
validate the first basic block through its control transfer, rejecting invalid
instructions, SP changes and out-of-frame saves. Synthetic positive/negative
regressions pass. This changes offline discovery; guest instructions and
timing semantics are unchanged. Release regeneration and gameplay validation
are tracked in beads-eio.6.9.
- **2026-08-31 (GPU DMA2 review correction — source gate passed):**
The first fork review found two valid timing defects in the DMA2 candidate. The
linked-list engine now reads and emits one live payload word at each
@@ -3714,6 +3714,43 @@ def check_packaged_game_toml_resolves_bios_profile(recompiler):
assert MOD.recompiler_project_root_args(args) == []
def check_return_adjacent_scheduled_frames():
# A compiler may schedule address setup before allocating the stack, or
# delay saving RA until after other callee-saved registers. Neither changes
# the function boundary proved by the preceding return and its delay slot.
def image(prefix):
words = [0x03E00008, 0x27BD0020] + prefix
return bytearray(struct.pack('<' + 'I' * len(words), *words))
delayed = ([0x27BDFFC8] + [0x26100001] * 12 +
[0xAFBF0034, 0xAFB10030, 0x03E00008, 0x27BD0038])
scheduled = [0x3C038001, 0x90620080, 0x27BDFFD8,
0xAFB10014, 0xAFBF0020, 0x03E00008, 0x27BD0028]
for prefix in (delayed, scheduled):
data = image(prefix)
assert EXTRACT.return_adjacent_framed_entries(data, LOAD) == [LOAD + 8]
data = image(scheduled)
assert EXTRACT.prologues(data, LOAD) == [LOAD + 8]
assert LOAD + 16 not in EXTRACT.return_adjacent_framed_entries(data, LOAD)
# The extra reach must not promote data, an unrelated load, a second stack
# adjustment, or a save reached only after a control transfer.
bad_prefixes = [
[0x3C038001, 0x90820080] + scheduled[2:], # load uses a different base
[0x27BDFFC8, jal(LOAD + 0x100), 0, 0xAFBF0034, 0x03E00008, 0],
[0x27BDFFC8, 0x27BDFFF0, 0xAFBF0034, 0x03E00008, 0],
[0x27BDFFC8, 0xAFBF0034, 0xAFB00038, 0x03E00008, 0],
[0x27BDFFC8, 0xAFBF0034, 0xFFFFFFFF, 0x03E00008, 0],
[0x27BDFFC8, 0xAFBF0034], # no terminating control-flow evidence
]
for prefix in bad_prefixes:
assert EXTRACT.return_adjacent_framed_entries(image(prefix), LOAD) == []
no_boundary = image(delayed)
put(no_boundary, 0, 0)
assert EXTRACT.return_adjacent_framed_entries(no_boundary, LOAD) == []
def main():
default_recompiler = ROOT / "recompiler" / "build" / "psxrecomp-game.exe"
parser = argparse.ArgumentParser()
@@ -3722,6 +3759,7 @@ def main():
if not os.path.isfile(args.recompiler):
raise SystemExit(f"recompiler not found: {args.recompiler}")
check_return_adjacent_scheduled_frames()
check_composite_call_boundaries()
check_bounded_jump_table_discovery()
check_scheduled_jump_table_discovery()
+33 -20
View File
@@ -66,19 +66,21 @@ def prologues(data, base):
the return boundary, LUI/load opcode pair, and matching address register are
all required data-as-code gates.
"""
entries=[]
for off in prologue_offsets(data):
entry=off
if off >= 16 and _word(data, off-16) == 0x03E00008:
lui=_word(data, off-8)
load=_word(data, off-4)
lui_rt=(lui >> 16) & 0x1F
if (lui >> 26) == 0x0F and lui_rt != 0 and \
0x20 <= (load >> 26) <= 0x26 and \
((load >> 21) & 0x1F) == lui_rt:
entry=off-8
entries.append(base+entry)
return entries
return [base + framed_entry_offset(data, off)
for off in prologue_offsets(data)]
def framed_entry_offset(data, off):
"""Recover the same return-proven scheduled prelude for every frame scan."""
if off >= 16 and _word(data, off-16) == 0x03E00008:
lui=_word(data, off-8)
load=_word(data, off-4)
lui_rt=(lui >> 16) & 0x1F
if (lui >> 26) == 0x0F and lui_rt != 0 and \
0x20 <= (load >> 26) <= 0x26 and \
((load >> 21) & 0x1F) == lui_rt:
return off-8
return off
def prologue_offsets(data):
"""File offsets whose word is an `addiu $sp,$sp,-N` stack-frame prologue."""
@@ -397,10 +399,16 @@ def return_adjacent_framed_entries(data, base):
instruction alone is insufficient. Require a preceding ``jr ra`` and its
arbitrary architectural delay slot, at most six alignment NOPs, plus an
in-frame save of ``ra`` before the new function's first control transfer.
Every stack-relative word save observed in that prefix must fit the frame.
Recover a proven LUI/load prelude before the frame allocation. Scan the
first basic block, rather than a fixed number of instructions: register
scheduling can put the RA save after many other saves/address calculations.
Every instruction must decode, SP must stay unchanged, and every observed
stack-relative word save must fit the frame. A control transfer must end
the prefix; truncated bytes are not a complete proof.
"""
entries=set()
for off in prologue_offsets(data):
entry=framed_entry_offset(data,off)
word=_word(data,off)
frame_imm=word&0xFFFF
frame_size=0x10000-frame_imm
@@ -408,22 +416,27 @@ def return_adjacent_framed_entries(data, base):
continue
boundary=False
for padding in range(7):
jr_off=off-8-padding*4
jr_off=entry-8-padding*4
if _word(data,jr_off)!=0x03E00008:
continue
if all(_word(data,p)==0 for p in range(jr_off+8,off,4)):
if all(_word(data,p)==0 for p in range(jr_off+8,entry,4)):
boundary=True
break
if not boundary:
continue
saved_ra=False
consistent=True
for pc in range(off+4,min(len(data),off+4+12*4),4):
ended=False
for pc in range(off+4,len(data)-3,4):
candidate=_word(data,pc)
if candidate is None:
if not co._is_valid_mips_word(candidate):
consistent=False
break
if _is_control_flow_word(candidate):
ended=True
break
if co._instruction_writes_gpr(candidate,29):
consistent=False
break
op=(candidate>>26)&0x3F
rs=(candidate>>21)&0x1F
@@ -437,8 +450,8 @@ def return_adjacent_framed_entries(data, base):
break
if rt==31:
saved_ra=True
if consistent and saved_ra:
entries.add(base+off)
if consistent and ended and saved_ra:
entries.add(base+entry)
return sorted(entries)
def _is_control_flow_word(word):