mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Discover scheduled overlay prologues through their first basic block
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user