From ea51ac96295b4cbf91b23538da35fb35291a2e2b Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Thu, 24 Sep 2026 10:50:09 -0400 Subject: [PATCH] =?UTF-8?q?phase11:=20merge=2040=20+=20the=20epilogue=20tr?= =?UTF-8?q?ansform=20now=20handles=20BOTH=20shapes=20=E2=80=94=20571=20bod?= =?UTF-8?q?ies=20/=20580=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker A's two epilogue-class rows (0x800F42AC 96 B, 0x80100038 104 B), both carrying the maspsx=epilogue token -- the first rows closed through the new mode. AND THE TRANSFORM IS NOW CORRECT FOR BOTH SHAPES, which worker A's report 24 showed was necessary. The 120 rows split: A) lw $31 IMMEDIATELY before the release -> the release moves into the slot AND a nop must be inserted after lw $31, or j $31 lands in its load-delay slot. 0x800FFBEC. B) other loads between lw $31 and the release -> the release moves into the slot and the trailing nop is DROPPED; no load-delay nop is needed. Worker A's 0x800F44D0. My first implementation did A only and left every B row 4 bytes long. Both are handled now, and the discriminator is whether the jump's own register was loaded immediately before the release. A BUG WORTH RECORDING: reading out[-1] to find that preceding instruction saw maspsx's own '#nop # DEBUG: ...' comment instead of the lw, silently producing the shape-B answer for a shape-A row and turning a MATCH back into a LENGTH-MISMATCH. The scan now skips comments. --- config/regions.tsv | 3 ++ src/func_80028C34.c | 53 +++++++++++++++++++++++++++++ src/func_8009D798.c | 55 ++++++++++++++++++++++++++++++ src/func_800F4098.c | 68 +++++++++++++++++++++++++++++++++++++ src/func_800F42AC.c | 70 ++++++++++++++++++++++++++++++++++++++ src/func_800FAF84.c | 50 +++++++++++++++++++++++++++ src/func_80100038.c | 72 +++++++++++++++++++++++++++++++++++++++ src/func_801059E8.c | 82 +++++++++++++++++++++++++++++++++++++++++++++ tools/sf3_match | 10 ++++-- 9 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 src/func_80028C34.c create mode 100644 src/func_8009D798.c create mode 100644 src/func_800F4098.c create mode 100644 src/func_800F42AC.c create mode 100644 src/func_800FAF84.c create mode 100644 src/func_80100038.c create mode 100644 src/func_801059E8.c diff --git a/config/regions.tsv b/config/regions.tsv index 555c9cb..3b2d3c6 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -477,6 +477,7 @@ 0x800F3160 0x800F316C src/func_800F3160.c maspsx=off 0x800F3A00 0x800F3A24 src/func_800F3A00.c 0x800F3E70 0x800F3E88 src/func_800F3E70.c +0x800F42AC 0x800F430C src/func_800F42AC.c maspsx=epilogue 0x800F5200 0x800F521C src/func_800F5200.c 0x800F5AF8 0x800F5B40 src/func_800F5AF8.c 0x800F5B40 0x800F5B70 src/func_800F5B40.c @@ -527,6 +528,7 @@ 0x800FEFFC 0x800FF008 src/func_800FEFFC.c 0x800FF6A4 0x800FF6DC src/func_800FF6A4.c 0x800FFBEC 0x800FFC3C src/func_800FFBEC.c maspsx=epilogue +0x80100038 0x801000A0 src/func_80100038.c maspsx=epilogue 0x80100318 0x80100334 src/func_80100318.c 0x80100740 0x801007E0 src/func_80100740.c 0x80100964 0x8010097C src/func_80100964.c @@ -556,6 +558,7 @@ 0x8010513C 0x80105148 src/func_8010513C.c 0x80105148 0x801051CC src/func_80105148.c 0x8010543C 0x80105474 src/func_8010543C.c +0x801059E8 0x80105A50 src/func_801059E8.c maspsx=epilogue 0x80105B34 0x80105B54 src/func_80105B34.c 0x80105B54 0x80105B68 src/func_80105B54.c 0x80105B68 0x80105B88 src/func_80105B68.c diff --git a/src/func_80028C34.c b/src/func_80028C34.c new file mode 100644 index 0000000..1649c0f --- /dev/null +++ b/src/func_80028C34.c @@ -0,0 +1,53 @@ +/* + * func_80028C34 — 172 bytes at 0x80028C34..0x80028CE0 + * + * The base case of the basis builder whose axis-permuted variant is func_80028CE0. + * Same body, minus that variant's z-vector and its three in-place element shifts, + * and with the incoming a0 forwarded to the callee unchanged: + * + * addiu sp,sp,-0x50 / sw ra,0x48(sp) frame 80 = 16 (outgoing arg area) + + * 48 (three 16-byte vectors at + * sp+24/40/56) + 8 (ra). The 16-byte + * stride is cookbook 96's vector tell. + * lw v0,20(a1) .. lw t0,32(a1) four loads ... + * sw v0,24(sp) .. sw t0,36(sp) ... then four stores: one 16-byte + * STRUCT ASSIGNMENT (cookbook 28/102). + * lw v0,24(sp) / lw t0,24(sp) / lw a3,28(sp) + * the copy is re-read from memory, so the + * source reads elements, not a local. + * subu / sw -> 40,44,48 differences against a1[9..11] + * subu / sw -> 56,60,64 differences against a1[1..3] + * sw a2,16(sp) / jal 0x800289B4 call(a0, &t, &u, &w, a2) -- five + * arguments, the 5th in the outgoing area + * at sp+16 (cookbook 33). + * andi v0,v0,0xff the 0xff mask is the return type. + * + * Verified matched on the first spelling, which is expected: the body is the sibling + * of an already-matched row (cookbook 99/129 adjacency) and reuses its derivation. + * + * LIMITS: as func_80028CE0 -- `struct V` is inferred from the 16-byte stride and the + * batched 4-word copy; the fourth element is copied and never read (it survives at + * sp+36); a1 is indexed as an `int *` at 1/2/3, 5/6/7/8 and 9/10/11; a2's type is + * unknown; func_800289B4 is a cross-reference by address only. + */ + +struct V { int v[4]; }; + +extern int func_800289B4(struct V *a0, struct V *a1, struct V *a2, struct V *a3, int a4); + +int func_80028C34(struct V *a0, int *a1, int a2) +{ + struct V t, u, w; + + t = *(struct V *)(a1 + 5); + + u.v[0] = a1[9] - t.v[0]; + u.v[1] = a1[10] - t.v[1]; + u.v[2] = a1[11] - t.v[2]; + + w.v[0] = a1[1] - t.v[0]; + w.v[1] = a1[2] - t.v[1]; + w.v[2] = a1[3] - t.v[2]; + + return func_800289B4(a0, &t, &u, &w, a2) & 0xff; +} diff --git a/src/func_8009D798.c b/src/func_8009D798.c new file mode 100644 index 0000000..e6fab1b --- /dev/null +++ b/src/func_8009D798.c @@ -0,0 +1,55 @@ +/* + * func_8009D798 — 264 bytes at 0x8009D798..0x8009D8A0 + * + * Goal B, Phase 11. Sibling of the matched func_80018458 (`sf3_family` score 1.000, ratio 1.00), + * which is why it matched on the first spelling: the two bodies are instruction-identical except + * for the TWO WORDS that set the first call's argument registers. + * + * Two 3-element signed-short differences, fed to the GTE transform 0x800F3E18, then rounded through + * 0x8009C56C and stored as three shorts: + * + * short *s1 = *(short **)(a0 + 8); + * int a[3], b[3], r[3]; + * a[i] = *(short *)(*(int *)(a0 + 32) + 2i) - *(short *)(*(int *)(a0 + 28) + 2i); i = 0,1,2 + * b[i] = *(short *)(*(int *)(a0 + 24) + 2i) - *(short *)(*(int *)(a0 + 28) + 2i); i = 0,1,2 + * func_800F3E18(a, b, r); <- the GTE rotation/translation helper + * func_8009C56C(r, r); + * s1[0] = r[0]; s1[1] = r[1]; s1[2] = r[2]; + * + * Both difference blocks are **fully unrolled** (three statements each) and both re-dereference the + * record's pointers at every site — `*(int *)(a0 + 32)` / `+28` / `+24` are re-loaded per element, + * which is cookbook 105's per-statement re-read setting again. The frame is 80 bytes with the two + * scratch vectors at sp+0x10 and sp+0x20 and the result at sp+0x30; the stores to `s1` are `sh` fed + * by 32-bit loads. + * + * LIMITS: the function name, the two callees, the meaning of the record at a0 and of the offsets + * 8/24/28/32 are hypotheses reconstructed from the disassembly; only the compiled bytes are evidence. + * The three int vectors are declared `int[3]` because the slots are word-sized and the callees take + * `int *`; the sources are signed halfword loads. `s1` is the only callee-saved value, so only s0/s1 + * are saved. + */ + +extern void func_800F3E18(int *a0, int *a1, int *a2); +extern void func_8009C56C(int *a0, int *a1); + +void func_8009D798(int a0) +{ + short *s1 = *(short **)(a0 + 8); + int a[3]; + int b[3]; + int r[3]; + + a[0] = *(short *)(*(int *)(a0 + 32) + 0) - *(short *)(*(int *)(a0 + 28) + 0); + a[1] = *(short *)(*(int *)(a0 + 32) + 2) - *(short *)(*(int *)(a0 + 28) + 2); + a[2] = *(short *)(*(int *)(a0 + 32) + 4) - *(short *)(*(int *)(a0 + 28) + 4); + b[0] = *(short *)(*(int *)(a0 + 24) + 0) - *(short *)(*(int *)(a0 + 28) + 0); + b[1] = *(short *)(*(int *)(a0 + 24) + 2) - *(short *)(*(int *)(a0 + 28) + 2); + b[2] = *(short *)(*(int *)(a0 + 24) + 4) - *(short *)(*(int *)(a0 + 28) + 4); + + func_800F3E18(a, b, r); + func_8009C56C(r, r); + + s1[0] = r[0]; + s1[1] = r[1]; + s1[2] = r[2]; +} diff --git a/src/func_800F4098.c b/src/func_800F4098.c new file mode 100644 index 0000000..989704b --- /dev/null +++ b/src/func_800F4098.c @@ -0,0 +1,68 @@ +/* + * func_800F4098 — 104 bytes at 0x800F4098..0x800F4100 + * + * Hypothesis, not a claim about meaning: conditionally calls a global handler with a fixed + * descriptor, then unconditionally calls a virtual method on a global object. Matched on the + * THIRD spelling; it needs the harness's `maspsx=epilogue` mode. + * + * Original words: + * 3C028012 lui v0,0x8012 + * 9082A8DE lbu v0,-22306(v0) ; v0 = D_8011A8DE + * 27BDFFE8 addiu sp,sp,-24 + * AFB00010 sw s0,16(sp) + * 00808021 move s0,a0 + * 2C420002 sltiu v0,v0,2 ; UNSIGNED compare + * 1440000E bnez v0,0x800F40D4 ; if (v0 < 2) SKIP THE FIRST CALL ONLY + * AFBF0014 sw ra,20(sp) ; (delay) + * 3C048011 lui a0,0x8011 ; a0 = 0x8010E5F4 + * 2484E5F4 addiu a0,a0,-6668 + * 3C028012 lui v0,0x8012 + * 8C42A8D8 lw v0,-22312(v0) ; v0 = D_8011A8D8 + * 00000000 nop + * 0040F809 jalr v0 ; ((void (*)(char *, int))D_8011A8D8)(&D_8010E5F4, s0) + * 02002821 move a1,s0 ; (delay) + * 3C028012 lui v0,0x8012 ; 0x800F40D4: the UNCONDITIONAL second call + * 8C42A8D4 lw v0,-22316(v0) ; v0 = D_8011A8D4 + * 00000000 nop + * 8C42003C lw v0,60(v0) ; v0 = *(int *)(v0 + 60) + * 00000000 nop + * 0040F809 jalr v0 ; (*(int *)(D_8011A8D4 + 60))(s0) + * 02002021 move a0,s0 ; (delay) + * 8FBF0014 lw ra,20(sp) ; (END) + * 8FB00010 lw s0,16(sp) + * 03E00008 jr ra + * 27BD0018 addiu sp,sp,24 ; THE FRAME RELEASE IS IN THE jr DELAY SLOT + * + * **THE GUARD SKIPS THE FIRST CALL ONLY — IT IS NOT AN EARLY RETURN.** The branch target is + * `0x800F40D4`, which is the *second* call's setup, not the epilogue. Writing + * `if (D_8011A8DE < 2) return;` gives the CORRECT LENGTH but the branch goes to the end + * (2 differing bytes); the source is `if (D_8011A8DE >= 2) ; ;` + * with the second call unconditional. **When a branch target lands in the middle of a + * function rather than at the epilogue, the guard is scoped to one statement, not the whole + * body** — the same "read the control flow literally" method as my boolean-structure finding. + * + * Also byte-required: `maspsx=epilogue` (the rare tail, and again with no load-delay `nop`), + * and the `lbu`/`sltiu` pair means the global is an **unsigned char** compared against 2. + * + * **AND THE `lui`-PAGE ERROR AGAIN — FOURTH TIME, SAME CLASS.** `lui a0,0x8011; addiu + * a0,a0,-6668` is 0x80110000 − 6668 = **0x8010E5F4**, not 0x8011E5F4. I derived it wrong, + * the row was 1 byte off, and the only difference was the `lui` immediate. **Cookbook line: + * derive every absolute address from the (page, signed immediate) PAIR; and a one-byte + * residual whose only difference is a `lui` immediate is the signature of this mistake.** + * + * LIMITS: the function name, all three globals, the descriptor and the whole "conditional + * then unconditional dispatch" reading are hypotheses taken from the instruction shape; only + * the bytes are evidence. Every symbol is referenced by its address-named spelling. + */ + +extern unsigned char D_8011A8DE; +extern int D_8011A8D8; +extern int *D_8011A8D4; +extern char D_8010E5F4; + +void func_800F4098(int s0) +{ + if (D_8011A8DE >= 2) + ((void (*)(char *, int))D_8011A8D8)(&D_8010E5F4, s0); + ((void (*)(int))*(int *)((char *)D_8011A8D4 + 60))(s0); +} diff --git a/src/func_800F42AC.c b/src/func_800F42AC.c new file mode 100644 index 0000000..1e70e75 --- /dev/null +++ b/src/func_800F42AC.c @@ -0,0 +1,70 @@ +/* + * func_800F42AC — 96 bytes at 0x800F42AC..0x800F430C + * + * Hypothesis, not a claim about meaning: forwards to a fixed helper and then to a virtual + * method on a global object, passing the object's own field as the first argument. Matched + * on the SECOND spelling, and it needs the harness's `maspsx=epilogue` mode (see below). + * + * Original words: + * 27BDFFE0 addiu sp,sp,-32 + * AFB00010 sw s0,16(sp) + * 00808021 move s0,a0 + * AFB10014 sw s1,20(sp) + * 00A08821 move s1,a1 + * 3C048011 lui a0,0x8011 ; func_800F4100(0x8010E644, s0) + * 2484E644 addiu a0,a0,-6588 + * AFBF0018 sw ra,24(sp) + * 0C03D040 jal 0x800F4100 + * 02002821 move a1,s0 ; (delay) + * 02002821 move a1,s0 ; a SECOND copy of the argument setup + * 3C028012 lui v0,0x8012 + * 8C42A8D4 lw v0,-22316(v0) ; v0 = D_8011A8D4 + * 24060008 li a2,8 + * 8C440020 lw a0,32(v0) ; a0 = *(int *)(v0 + 32) + * 8C420008 lw v0,8(v0) ; v0 = *(int *)(v0 + 8) + * 00000000 nop + * 0040F809 jalr v0 ; (*(int *)(v0+8))(*(int *)(v0+32), s0, 8, s1) + * 02203821 move a3,s1 ; (delay) + * 8FBF0018 lw ra,24(sp) ; (END) + * 8FB10014 lw s1,20(sp) + * 8FB00010 lw s0,16(sp) + * 03E00008 jr ra + * 27BD0020 addiu sp,sp,32 ; THE FRAME RELEASE IS IN THE jr DELAY SLOT + * + * **THIS ROW REQUIRES `maspsx=epilogue`** (the claim row carries the token). The original's + * epilogue is the RARE form — `jr ra` with `addiu sp,sp,32` in the jump slot — which the + * harness's default maspsx does not produce. Without the mode the row is 92 bytes and 6 + * bytes wrong; with `--fill-epilogue` it is exact. **The tail here has NO load-delay `nop`, + * which is what makes it match once the swap is done** (contrast 0x800F44D0, whose + * three-load epilogue does get an inserted nop and therefore does not close). + * + * TWO BYTE-REQUIRED DETAILS: + * + * 1. **`D_8011A8D4` is a POINTER, not a pointer-to-pointer.** The original emits + * `lui v0,0x8012; lw v0,-22316(v0)` and then dereferences `v0` — declaring it + * `extern int *D_8011A8D4;` and using it directly gives that. Writing + * `*(int **)D_8011A8D4` adds a whole extra `lw v0,0(v0)` and the row is 8 bytes long. + * 2. **THE ADDRESS IS DERIVED FROM THE (lui PAGE, SIGNED IMMEDIATE) PAIR, NOT THE IMMEDIATE + * ALONE.** `lui a0,0x8011; addiu a0,a0,-6588` is 0x80110000 − 6588 = **0x8010E644**. I + * read it as 0x8011E644 and the row was one byte wrong (the `lui` differs, the `addiu` + * does not). This is the third time I have made this class of error — once with a + * gp-relative offset and twice with `lui` — and it deserves a cookbook line: + * *derive every absolute address from the page plus the signed immediate.* + * + * LIMITS: the function name, both callees, the global object and the whole "dispatch a + * virtual method" reading are hypotheses taken from the instruction shape; only the bytes + * are evidence. Both callees and the global are referenced by their address-named spellings. + */ + +extern int *D_8011A8D4; +extern char D_8010E644; +int func_800F4100(); + +void func_800F42AC(int s0, int s1) +{ + int *p; + + func_800F4100(&D_8010E644, s0); + p = D_8011A8D4; + ((void (*)(int, int, int, int))*(int *)((char *)p + 8))(*(int *)((char *)p + 32), s0, 8, s1); +} diff --git a/src/func_800FAF84.c b/src/func_800FAF84.c new file mode 100644 index 0000000..30e0c73 --- /dev/null +++ b/src/func_800FAF84.c @@ -0,0 +1,50 @@ +/* + * func_800FAF84 — 104 bytes at 0x800FAF84..0x800FAFEC + * + * Goal B, Phase 11. Four spellings. **Requires the harness mode `maspsx=epilogue`** (region token in + * column 4 of the claim row). + * + * int t[2]; + * func_800FB038(a1, t); + * func_800F8B80(2, t, 0); + * func_800FA338(a0, a2, 128); + * return func_800FA4D4(0, 0) == 0; + * + * TWO LEVERS, and the first is the one that made the frame match: + * + * 1. **The local is 8 bytes, not 12.** The saved `s0` sits at sp+24 and the callee writes through + * `t` at sp+16 — so a 12-byte local would overlap the saved register. `int t[3]` and a 12-byte + * struct both give **vars=16 and a 48-byte frame**; `int t[2]` gives **vars=8 and the original's + * 40-byte frame**. The tell is arithmetic: 16 (args) + 12 (three saved words) + vars = 40 ⇒ + * vars = 12, and the register slots at 24/28/32 leave only sp+16..sp+23 free. cc1 rounds a + * 12-byte local up to 16, so the source's object must be 8 bytes. **Read the frame arithmetic and + * the saved-register offsets together before choosing a local's size.** + * + * 2. **`maspsx=epilogue` — the only residual after the size fix was two swapped words.** The original + * ends `jr ra` / `addiu sp,sp,40` (the frame release in the jump's delay slot); the candidate ends + * `addiu sp,sp,40` / `jr ra`. That is exactly the transform the new harness mode performs, and + * with it the row reports 104 bytes / 0 differing / MATCH. GNU `as` will not do the fill itself + * (it would put `jr $31` in the `lw $31` load-delay slot), which is why the harness has to. + * + * LIMITS: the function name, the four callees, the meaning of the record and of the 2/0/128 + * arguments are hypotheses reconstructed from the disassembly; only the compiled bytes are evidence. + * The local is declared `int t[2]`; the callee writes past it, which is a real property of the + * original — do NOT "fix" it by enlarging the array, because that changes the frame and the row stops + * matching. `func_800FA4D4(0, 0) == 0` is written as an equality, not `< 1`, because the original + * emits `sltiu`. + */ + +extern void func_800FB038(int a0, int *a1); +extern void func_800F8B80(int a0, int *a1, int a2); +extern void func_800FA338(int a0, int a1, int a2); +extern int func_800FA4D4(int a0, int a1); + +int func_800FAF84(int a0, int a1, int a2) +{ + int t[2]; + + func_800FB038(a1, t); + func_800F8B80(2, t, 0); + func_800FA338(a0, a2, 128); + return func_800FA4D4(0, 0) == 0; +} diff --git a/src/func_80100038.c b/src/func_80100038.c new file mode 100644 index 0000000..448c3b0 --- /dev/null +++ b/src/func_80100038.c @@ -0,0 +1,72 @@ +/* + * func_80100038 — 104 bytes at 0x80100038..0x801000A0 + * + * Hypothesis, not a claim about meaning: clears a per-index slot, then calls a per-index + * handler with the index, a stored argument and a fixed fourth argument. Matched on the + * SECOND spelling, and it needs the harness's `maspsx=epilogue` mode. + * + * Original words: + * 27BDFFE8 addiu sp,sp,-24 + * AFB00010 sw s0,16(sp) + * 00808021 move s0,a0 + * 00002021 move a0,zero + * 24050001 li a1,1 + * AFBF0014 sw ra,20(sp) + * 0C0421F4 jal 0x801087D0 ; func_801087D0(0, 1 << s0) + * 00A08804 sllv a1,a1,s0 ; (delay) + * 3C018014 lui at,0x8014 ; base = 0x80145AC0 + * 24215AC0 addiu at,at,23232 + * 001080C0 sll v0,s0,0x3 ; v0 = base + s0 * 72 + * 00501021 addu v0,v0,s0 + * 000210C0 sll v0,v0,0x3 + * 00411021 addu v0,v0,at + * 8C430044 lw v1,68(v0) ; v1 = *(int *)(p + 68) + * 00000000 nop + * 10600004 beqz v1,0x80100090 ; if (v1 != 0) + * AC400000 sw zero,0(v0) ; (delay) *(int *)p = 0 -- runs on BOTH paths + * 02002021 move a0,s0 ; v1(s0, *(int *)(p + 64), 4) + * 8C450040 lw a1,64(v0) + * 0040F809 jalr v1 + * 24060004 li a2,4 ; (delay) + * 8FBF0014 lw ra,20(sp) ; 0x80100090: (END) + * 8FB00010 lw s0,16(sp) + * 03E00008 jr ra + * 27BD0018 addiu sp,sp,24 ; THE FRAME RELEASE IS IN THE jr DELAY SLOT + * + * **TWO BYTE-REQUIRED SHAPES:** + * + * 1. **`maspsx=epilogue`** (the claim row carries the token). The original's tail is the + * rare form — `jr ra` with `addiu sp,sp,24` in the jump slot — which the default maspsx + * does not emit. With `--fill-epilogue` the row is exact. Like `0x800F42AC`, this tail + * has NO load-delay `nop`, which is why the swap alone closes it. + * 2. **THE BASE POINTER MUST BE A SEPARATE VARIABLE DECLARED BEFORE THE CALL.** Writing + * `p = &D_80145AC0 + s0 * 72;` lets cc1 schedule the index scaling FIRST and the base + * materialisation second; the original materialises the base (`lui at` / `addiu at`) + * BEFORE the scale. Hoisting the base into its own local declared above the call — + * `char *base = &D_80145AC0;` then `p = base + s0 * 72;` — reproduces the original's + * order and is exact. The residual in the first spelling was 16 bytes, all of it the + * two groups swapped. + * + * Also note `*(int *)p = 0` is UNCONDITIONAL in the source but lands in the branch's delay + * slot, so it runs on both paths — the same shape as `0x800F6D60`. + * + * LIMITS: the function name, the callee, the table base, the 72-byte element stride and the + * whole "clear and dispatch" reading are hypotheses taken from the instruction shape; only + * the bytes are evidence. Both the table and the callee are referenced by their address-named + * spellings. + */ + +extern char D_80145AC0; +void func_801087D0(); + +void func_80100038(int s0) +{ + char *base = &D_80145AC0; + char *p; + + func_801087D0(0, 1 << s0); + p = base + s0 * 72; + *(int *)p = 0; + if (*(int *)(p + 68) != 0) + ((void (*)(int, int, int))*(int *)(p + 68))(s0, *(int *)(p + 64), 4); +} diff --git a/src/func_801059E8.c b/src/func_801059E8.c new file mode 100644 index 0000000..be802e5 --- /dev/null +++ b/src/func_801059E8.c @@ -0,0 +1,82 @@ +/* + * func_801059E8 — 104 bytes at 0x801059E8..0x80105A50 + * + * Hypothesis, not a claim about meaning: asks a global predicate about an argument; if it + * refuses, returns 0; otherwise initialises the object's flag, two handler pointers and a + * stored argument, and returns 1. Matched on the SECOND spelling and it needs the harness's + * `maspsx=epilogue` mode. + * + * Original words: + * 27BDFFE0 addiu sp,sp,-32 + * AFB00010 sw s0,16(sp) + * 00808021 move s0,a0 + * AFB10014 sw s1,20(sp) + * 3C028012 lui v0,0x8012 + * 8C42FE2C lw v0,-468(v0) ; v0 = D_8011FE2C (a function pointer) + * AFBF0018 sw ra,24(sp) + * 0040F809 jalr v0 ; if (f(a0, s1) != 0) return 0 + * 00A08821 move s1,a1 ; (delay) -- the SECOND argument only; a0 is the + * ; caller's own a0, still live, so the source + * ; passes its parameter directly + * 14400007 bnez v0,0x80105A3C + * 00001021 move v0,zero ; (delay) + * 24020001 li v0,1 + * 24030001 li v1,1 + * A2030046 sb v1,70(s0) ; *(char *)(s0 + 70) = 1 + * 3C018010 lui at,0x8010 + * 24215A50 addiu at,at,23120 ; *(int *)(s0 + 20) = (int)func_80105A50 + * AC010014 sw at,20(s0) + * 3C018010 lui at,0x8010 + * 24215A6C addiu at,at,23148 ; *(int *)(s0 + 24) = (int)func_80105A6C + * AE110020 sw s1,32(s0) ; *(int *)(s0 + 32) = s1 + * AC010018 sw at,24(s0) + * 8FBF0018 lw ra,24(sp) ; 0x80105A3C: (END) + * 8FB10014 lw s1,20(sp) + * 8FB00010 lw s0,16(sp) + * 03E00008 jr ra + * 27BD0020 addiu sp,sp,32 ; THE FRAME RELEASE IS IN THE jr DELAY SLOT + * + * **THREE BYTE-REQUIRED SHAPES:** + * + * 1. **`maspsx=epilogue`** (the claim row carries the token). The tail is the rare form and, + * like `0x800F42AC` and `0x80100038`, it has NO load-delay `nop`, so the swap alone is + * sufficient. Three of my four epilogue-class rows have this short tail. + * 2. **THE HANDLER ADDRESSES MUST BE FUNCTION SYMBOLS, NOT LITERALS.** Writing + * `*(int *)(s0 + 20) = 0x80105A50;` makes cc1 treat both addresses as constants, hoist + * them to the top of the function and reorder every store (31 bytes of residual); + * `(int)func_80105A50` emits each `lui/addiu` immediately before its store, in the + * original's order. This is the same lever as `0x80100740`. + * 3. **The predicate call passes only the SECOND argument explicitly.** The emitted sets + * `a1 = s1` in the delay slot and never touches `a0`, so the source must pass its own + * first parameter (still live in `a0`) as the call's first argument — `f(a0, s1)`. + * Binding the parameter to a local first (`s0`) and passing that would emit a `move`. + * + * Also note the returns are `0` on the refusal path and `1` at the end, with `li v0,1` and + * `li v1,1` materialised separately. + * + * LIMITS: the function name, the predicate, both handlers, the object offsets (70, 20, 24, + * 32) and the whole "predicate then initialise" reading are hypotheses taken from the + * instruction shape; only the bytes are evidence. The predicate global and both handlers are + * referenced by their address-named spellings. + */ + +extern int D_8011FE2C; +void func_80105A50(void); +void func_80105A6C(void); + +int func_801059E8(int a0, int a1) +{ + int s0 = a0; + int s1 = a1; + int (*f)(); + + f = (int (*)())D_8011FE2C; + if (f(a0, s1) != 0) + return 0; + + *(char *)(s0 + 70) = 1; + *(int *)(s0 + 20) = (int)func_80105A50; + *(int *)(s0 + 32) = s1; + *(int *)(s0 + 24) = (int)func_80105A6C; + return 1; +} diff --git a/tools/sf3_match b/tools/sf3_match index 509426e..2e2c20c 100755 --- a/tools/sf3_match +++ b/tools/sf3_match @@ -217,7 +217,8 @@ _REGION_OPTION_KEYS = ("cc1", "as", "gp", "maspsx") # left empty while leaving cc1's own `.set noreorder` windows alone. _MASPSX_MODES = {"off": "--off", "noreordernop": "--no-jump-slot-nop", "regread": "--nop-on-reg-read", "moves": "moves", - "nopmarker": "--honour-nop-marker"} + "nopmarker": "--honour-nop-marker", + "epilogue": "--fill-epilogue"} def parse_region_options(text: str, line_number: int) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...], bool, tuple[str, ...], bool]: @@ -853,6 +854,10 @@ def add_toolchain_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument("--no-jump-slot-nop", action="store_true", help="maspsx mode: suppress the unconditional reorder nop after a " "branch/jump so GNU as can fill the slot (region: maspsx=noreordernop)") + parser.add_argument("--fill-epilogue", action="store_true", + help="maspsx mode: move the frame release INTO the jump's delay slot " + "(region token: maspsx=epilogue). 120 unclaimed rows have this " + "shape in the original; GNU as will not do the fill itself.") parser.add_argument("--honour-nop-marker", action="store_true", help="maspsx mode: honour cc1's explicit `#nop` marker even when the " "following macro expansion would fill the delay slot " @@ -969,7 +974,8 @@ def resolve_toolchain(args: argparse.Namespace) -> Toolchain: flag for flag, enabled in ( ("--no-jump-slot-nop", getattr(args, "no_jump_slot_nop", False)), ("--nop-on-reg-read", getattr(args, "nop_on_reg_read", False)), - ("--honour-nop-marker", getattr(args, "honour_nop_marker", False))) if enabled), + ("--honour-nop-marker", getattr(args, "honour_nop_marker", False)), + ("--fill-epilogue", getattr(args, "fill_epilogue", False))) if enabled), aspsx_version=args.aspsx_version, )