diff --git a/config/regions.tsv b/config/regions.tsv index fb56ccf..69c010d 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -522,6 +522,7 @@ 0x800FEEB8 0x800FEED0 src/func_800FEEB8.c 0x800FEFFC 0x800FF008 src/func_800FEFFC.c 0x800FF6A4 0x800FF6DC src/func_800FF6A4.c +0x800FFBEC 0x800FFC3C src/func_800FFBEC.c maspsx=epilogue 0x80100318 0x80100334 src/func_80100318.c 0x80100740 0x801007E0 src/func_80100740.c 0x80100964 0x8010097C src/func_80100964.c diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index aa8c78e..42038f6 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -2279,3 +2279,30 @@ is a conversion. Worker D's rule, stated as it would apply it: *"if the frame is 8 bytes short and all 11 differing bytes are frame-and-displacement, add `int pad[2];`"* — with three instances behind it (finding 131). **The diagnostic is the shape of the residual, not the size of the body.** + +### 140. THE EPILOGUE POST-PASS IS SHIPPED — `maspsx=epilogue` — and the transform is a SWAP (coordinator) + +Finding 84 named the transform; it is now implemented, and **it cost one wrong implementation to +learn that it is a SWAP, not a move.** Verified instruction-by-instruction against the original at +`0x800FFBEC`: + + candidate: lw $31,16($sp) / addiu $sp,$sp,24 / jr $31 / nop + original: lw $31,16($sp) / nop / jr $31 / addiu $sp,$sp,24 + +**Same instruction COUNT — two words swapped.** The first implementation moved the release *after* +the jump and dropped the nop, producing 3 instructions instead of 4 and turning an 80-byte row into a +**76-byte LENGTH-MISMATCH**. That is a good reminder that a "small mechanical transform" still has to +be checked against the bytes. + +`0x800FFBEC` now reports **80 bytes / 0 differing / MATCH** with the mode, and **6 differing bytes / +DIFF** without it. Shipped as the region token `maspsx=epilogue`; the tracked patch reproduces both +modified maspsx files from the pristine checkout. + +**WHY IT IS NEEDED AT ALL:** GNU `as` will not perform this fill itself, because doing so would put +`jr $31` in the `lw $31` **load-delay slot** — which is exactly why worker B's oracle work mattered: +the fill is not something the assembler will do for you, so the harness has to. + +**SCALE: 120 unclaimed rows have the filled epilogue in the ORIGINAL** (measured by scanning every +worklist row's tail for `jr $31` followed by a positive `addiu sp,sp,N`). They are mostly **small** — +76, 76, 80, 92, 92, 96, 104 B — so this is a large class of cheap rows that were blocked on a +harness gap rather than on source shape. **770 other rows have the unfilled shape and need nothing.** diff --git a/src/func_80038D48.c b/src/func_80038D48.c new file mode 100644 index 0000000..88ddb21 --- /dev/null +++ b/src/func_80038D48.c @@ -0,0 +1,81 @@ +/* + * func_80038D48 — 144 bytes at 0x80038D48..0x80038DD8 + * + * Hypothesis, not a claim about meaning: computes the midpoint of two 3-element vectors of + * halfwords and writes it to a third. Pure data movement plus one division by two — it + * matched on the SECOND spelling, and the only difference between the two was the OPERAND + * ORDER of the additions. + * + * Original words: + * 84A30000 lh a3,0(a1) ; t[0] = a1[0] + a0[0] + * 84820000 lh v0,0(a0) + * 27BDFFF0 addiu sp,sp,-16 + * 00E31821 addu a3,a3,v0 + * AFA30000 sw a3,0(sp) + * 84A20002 lh v0,2(a1) ; t[1] = a1[1] + a0[1] + * 84830002 lh v1,2(a0) + * 00000000 nop + * 00431021 addu v0,v0,v1 + * 00031FC2 srl v1,a3,0x1f ; t[0] /= 2 (the sign-bias division form) + * 00E31821 addu a3,a3,v1 + * 00031843 sra a3,a3,0x1 + * AFA20004 sw v0,4(sp) + * 84A10004 lh v1,4(a1) ; t[2] = a1[2] + a0[2] + * 84810004 lh a1,4(a0) + * 000227C2 srl a0,v0,0x1f ; t[1] /= 2 + * 00441021 addu v0,v0,a0 + * 00021043 sra v0,v0,0x1 + * AFA30000 sw a3,0(sp) + * AFA20004 sw v0,4(sp) + * 00650821 addu v1,v1,a1 + * 000107C2 srl v0,v1,0x1f ; t[2] /= 2 + * AFA10008 sw v1,8(sp) + * 00621821 addu v1,v1,v0 + * 00010843 sra v1,v1,0x1 + * AFA10008 sw v1,8(sp) + * A4A30000 sh a3,0(a2) ; a2[0] = t[0] + * 8FA20004 lw v0,4(sp) + * 00000000 nop + * A4A20002 sh v0,2(a2) ; a2[1] = t[1] + * 8FA20008 lw v0,8(sp) + * 00000000 nop + * A4A20004 sh v0,4(a2) ; a2[2] = t[2] + * 27BD0010 addiu sp,sp,16 + * 03E00008 jr ra + * 00000000 nop + * + * **THE OPERAND ORDER OF THE ADDITION IS BYTE-REQUIRED: `a1[i] + a0[i]`, not `a0[i] + a1[i]`.** + * The two spellings give the SAME LENGTH (144) and differ by 16 bytes — the loads and the + * `addu` register assignments. cc1 evaluates the right-hand operand first, so the source's + * `a1[i] + a0[i]` is what produces the original's `lh a3,0(a1)` before `lh v0,0(a0)`. This is + * the same class as my `0x800B62C8` finding (a comparison's operand order being + * byte-load-bearing) and it is worth checking FIRST on any row that is the right length with + * a small residual spread over the operand setup. + * + * The other two shapes are inherited from the row's structure: + * + * 1. **The scratch is `int t[3]`** at sp+0/4/8 — the halfwords are loaded with `lh` but the + * sums and quotients are stored as WORDS, so the array must be `int`, not `short`. + * 2. **Each sum is divided by two as a SEPARATE statement** (`t[i] = t[i] / 2;`), which + * produces cc1's sign-bias form `srl; addu; sra` and the double store (`sw` before and + * after the division). + * + * LIMITS: the function name, the element count, the "midpoint" reading and the destination + * type are hypotheses taken from the instruction shape; only the bytes are evidence. No + * callee: this is a leaf and the harness emitted the common epilogue itself. + */ + +void func_80038D48(short *a0, short *a1, short *a2) +{ + int t[3]; + + t[0] = a1[0] + a0[0]; + t[1] = a1[1] + a0[1]; + t[2] = a1[2] + a0[2]; + t[0] = t[0] / 2; + t[1] = t[1] / 2; + t[2] = t[2] / 2; + a2[0] = t[0]; + a2[1] = t[1]; + a2[2] = t[2]; +} diff --git a/src/func_8009B56C.c b/src/func_8009B56C.c new file mode 100644 index 0000000..8e095b9 --- /dev/null +++ b/src/func_8009B56C.c @@ -0,0 +1,62 @@ +/* + * func_8009B56C — 204 bytes at 0x8009B56C..0x8009B638 + * + * Goal B, Phase 11. Three spellings. Found by the size-capped redundancy rank (0.65). + * + * int *p = *(int **)a0; + * v = (*(int *)p < 0) ? 4 : 3; + * a1[5] = v; + * if (v == 3) { + * a1[6] = (int)(BASE + B(7)); a1[7] = (int)(BASE + B(10)); a1[8] = (int)(BASE + B(11)); + * } else if (v == 4) { + * a1[6] = (int)(BASE + B(7)); a1[7] = (int)(BASE + B(10)); + * a1[8] = (int)(BASE + B(8)); a1[9] = (int)(BASE + B(11)); + * } + * + * where BASE = *(int **)(a0 + 4) re-read at every site and B(i) = ((unsigned char *)p)[i]. The two + * arms compute byte-indexed offsets into the same base (`base + p[i]`, scaled by 4 because base is + * `int *`) and differ only in the third element: the `v == 3` arm uses p[11], the `v == 4` arm uses + * p[8] and shifts p[11] to the fourth slot. + * + * THE LEVER — COOKBOOK 43 TRIGGER 1 (POLARITY), and this is the first row of this worker's where a + * BRANCH diagnostic was what closed it. The natural spelling `(*(int *)p >= 0) ? 3 : 4` compiles to + * `bltz v0,L` with **4** in the branch delay slot and 3 as the fall-through; the original has + * `bgez v0,L` with **3** in the slot and 4 falling through — semantically mirrored, 3 differing + * bytes, and the only difference in the whole row. Inverting the condition and swapping the arms — + * `(*(int *)p < 0) ? 4 : 3` — flips the emitted polarity and matches exactly. + * + * So my earlier claim that the branch-shaped diagnostics "do not bite on my rows" was too strong: + * this is a redundant row (two near-identical arms) that failed on a **branch polarity**, not on + * frame, combiner, allocation or batch shape. The corrected statement is that they bite *less often* + * here, not that they do not bite. + * + * LIMITS: the function name, the meaning of the record at a0 and of the output array are hypotheses + * reconstructed from the disassembly; only the compiled bytes are evidence. `v` is 3 or 4 and is + * stored to a1[5] on both paths (it lands in the `bne` delay slot). Whether the record's fields are + * a tagged union or three parallel arrays is not recoverable. + */ + +void func_8009B56C(int a0, int *a1) +{ + int *p = *(int **)a0; + int v; + +#define BASE (*(int **)(a0 + 4)) +#define B(i) ((unsigned char *)p)[i] + + v = (*(int *)p < 0) ? 4 : 3; + a1[5] = v; + + if (v == 3) { + a1[6] = (int)(BASE + B(7)); + a1[7] = (int)(BASE + B(10)); + a1[8] = (int)(BASE + B(11)); + } else if (v == 4) { + a1[6] = (int)(BASE + B(7)); + a1[7] = (int)(BASE + B(10)); + a1[8] = (int)(BASE + B(8)); + a1[9] = (int)(BASE + B(11)); + } +#undef B +#undef BASE +} diff --git a/src/func_800F6D60.c b/src/func_800F6D60.c new file mode 100644 index 0000000..b8c013a --- /dev/null +++ b/src/func_800F6D60.c @@ -0,0 +1,65 @@ +/* + * func_800F6D60 — 112 bytes at 0x800F6D60..0x800F6DD0 + * + * Hypothesis, not a claim about meaning: initialises a node — copying an 8-int template + * into it, storing a key, clearing its first word, and linking it into a parent when the key + * is large enough. Pure data movement plus one comparison; matched on the FIRST spelling. + * + * Original words: + * 00A03821 move a3,a1 ; p = a1 + * 3C028014 lui a2,0x8014 ; src = D_80141410 + * 24421410 addiu a2,a2,5136 + * 8C420000 lw v0,0(a2) ; *(S *)(p + 1) = *src -- 8-int block move, + * 8C430004 lw v1,4(a2) ; emitted as 3 loads + 3 stores, 3 + 3, then 2 + 2 + * 8C450008 lw a1,8(a2) + * ACE20004 sw v0,4(a3) + * ACE30008 sw v1,8(a3) + * ACE5000C sw a1,12(a3) + * 8C42000C lw v0,12(a2) + * ... ACE50018 sw a1,24(a3) + * 8C420018 lw v0,24(a2) + * 8C43001C lw v1,28(a2) + * ACE2001C sw v0,28(a3) + * ACE30020 sw v1,32(a3) + * ACE40048 sw a0,72(a3) ; p[18] = a0 + * 2C840002 sltiu a0,a0,2 ; UNSIGNED compare + * 14800004 bnez a0,0x800F6DC8 ; if (a0 < 2) done + * ACE00000 sw zero,0(a3) ; (delay) p[0] = 0 -- runs on BOTH paths + * 8CE20048 lw v0,72(a3) ; v0 = p[18] + * 00000000 nop + * ACE2004C sw a3,76(v0) ; *(int *)(v0 + 76) = (int)p + * 03E00008 jr ra ; 0x800F6DC8 + * 00000000 nop + * + * BYTE-REQUIRED SHAPES: + * + * 1. **The template copy is an 8-int STRUCT ASSIGNMENT** (`*(struct S *)(p + 1) = D_80141410;`) + * — cc1 splits it 3+3+2, which is the block-move shape. Eight element stores interleave + * load/store and change the bytes. + * 2. **The key comparison is UNSIGNED** (`sltiu`). Declaring the parameter `unsigned int` + * (or writing `(unsigned)a0 < 2`) is what produces it — the same `unsigned` dial that + * finding 133 records for a family's induction variable. + * 3. **`p[0] = 0` is UNCONDITIONAL in the source but sits in the branch's delay slot**, so + * it runs on both paths. Writing it inside the `if` changes the bytes. + * + * LIMITS: the function name, the template symbol, the node layout (the key at int 18, the + * link at int 19, the parent back-pointer at +76) and the "initialise and link" reading are + * hypotheses taken from the instruction shape; only the bytes are evidence. The `struct S` + * exists purely to obtain the 8-int block move. No callee: this is a leaf and the harness + * emitted the common epilogue itself. + */ + +struct S { int v[8]; }; + +extern struct S D_80141410; + +void func_800F6D60(unsigned int a0, int a1) +{ + int *p = (int *)a1; + + *(struct S *)(p + 1) = D_80141410; + p[18] = a0; + p[0] = 0; + if (a0 >= 2) + *(int *)(p[18] + 76) = (int)p; +} diff --git a/src/func_800FFBEC.c b/src/func_800FFBEC.c new file mode 100644 index 0000000..85dabed --- /dev/null +++ b/src/func_800FFBEC.c @@ -0,0 +1,31 @@ +/* 0x800FFBEC (80 B) — flush two deferred callbacks. + * + * Validation row for the Phase 11 epilogue post-pass (`maspsx=epilogue`). + * The original's epilogue is `lw $31,16($sp) / nop / jr $31 / addu $sp,$sp,24` — + * the frame release is IN the jump's delay slot. cc1 + maspsx emit + * `lw $31 / nop / addu $sp,$sp,24 / jr $31 / nop` and GNU `as` will not fill it, + * because doing so would put `jr $31` in the `lw $31` load-delay slot. + */ + +extern int D_80122174; +extern int D_80122170; +extern int D_80122168; + +extern void func_801078A4(void); + +void func_800FFBEC(void) +{ + void (*callback)(void) = (void (*)(void))D_80122174; + + D_80122170 = 0; + + if (callback != 0) { + D_80122174 = 0; + callback(); + } + + if (D_80122168 != 0) { + func_801078A4(); + D_80122168 = 0; + } +} diff --git a/tools/patches/maspsx-phase10-r1r2.patch b/tools/patches/maspsx-phase10-r1r2.patch index cbbcc08..3a8ae77 100644 --- a/tools/patches/maspsx-phase10-r1r2.patch +++ b/tools/patches/maspsx-phase10-r1r2.patch @@ -113,25 +113,87 @@ elif op == "move": --- a/maspsx.py +++ b/maspsx.py -@@ -62,8 +62,11 @@ - parser.add_argument("--passthrough", action="store_true") - parser.add_argument("--use-comm-section", action="store_true") - parser.add_argument("--use-comm-for-lcomm", action="store_true") -+ # Phase 10 local additions (developer-authorised, opt-in; see -+ # tools/patches/maspsx-phase10-r1r2.patch and docs/SETUP.md). +@@ -1,4 +1,5 @@ + import argparse ++import re + import shutil + import subprocess + import sys +@@ -49,6 +50,50 @@ + return config + + ++_FRAME_RELEASE = re.compile( ++ r"^(?P\s*)(?P(?:addu|addiu)\s+\$sp\s*,\s*\$sp\s*,\s*[-\w]+)\s*$") ++_JUMP_RA = re.compile(r"^\s*(?:j|jr)\s+\$31\s*$") ++_NOP = re.compile(r"^\s*nop\b.*$") ++ ++ ++def fill_epilogue(text: str) -> str: ++ """Swap the frame release with the jump's delay-slot nop. ++ ++ Phase 11 finding 84 (worker B), and the transform is a SWAP, not a move -- getting this ++ wrong cost one implementation. Verified instruction-by-instruction against the original ++ at 0x800FFBEC: ++ ++ candidate: lw $31,16($sp) / addiu $sp,$sp,24 / jr $31 / nop ++ original: lw $31,16($sp) / nop / jr $31 / addiu $sp,$sp,24 ++ ++ Same instruction COUNT, two words swapped. The first version of this function moved the ++ release after the jump and dropped the nop, producing 3 instructions instead of 4 and ++ turning an 80-byte row into a 76-byte LENGTH-MISMATCH. ++ ++ GNU `as` will not perform this fill itself, because doing so would put `jr $31` in the ++ `lw $31` load-delay slot -- which is why it must be done by hand here. ++ ++ Opt-in per region (`maspsx=epilogue`): 120 unclaimed rows have this shape in the ORIGINAL. ++ """ ++ lines = text.splitlines() ++ out: list[str] = [] ++ index = 0 ++ while index < len(lines): ++ line = lines[index] ++ release = _FRAME_RELEASE.match(line) ++ # release / jump / nop -> nop / jump / release (same instruction count) ++ if (release and index + 2 < len(lines) ++ and _JUMP_RA.match(lines[index + 1]) and _NOP.match(lines[index + 2])): ++ out.append(lines[index + 2]) ++ out.append(lines[index + 1]) ++ out.append(release.group("indent") + release.group("release")) ++ index += 3 ++ continue ++ out.append(line) ++ index += 1 ++ return "\n".join(out) + ("\n" if text.endswith("\n") else "") ++ ++ + def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--aspsx-version", type=str) +@@ -66,7 +111,8 @@ + # tools/patches/maspsx-phase10-r1r2.patch and docs/SETUP.md). parser.add_argument("--no-jump-slot-nop", action="store_true") parser.add_argument("--nop-on-reg-read", action="store_true") +- parser.add_argument("--nop-on-reg-read", action="store_true") + parser.add_argument("--honour-nop-marker", action="store_true") ++ parser.add_argument("--fill-epilogue", action="store_true") # decomp.me debugging parser.add_argument("--print-output", action="store_true") parser.add_argument("--print-input", action="store_true") -@@ -154,6 +157,9 @@ - gp_allow_la=version_config.gp_allow_la, - use_comm_section=args.use_comm_section, +@@ -159,6 +205,7 @@ use_comm_for_lcomm=args.use_comm_for_lcomm, -+ no_jump_slot_nop=args.no_jump_slot_nop, -+ nop_on_reg_read=args.nop_on_reg_read, + no_jump_slot_nop=args.no_jump_slot_nop, + nop_on_reg_read=args.nop_on_reg_read, + honour_nop_marker=args.honour_nop_marker, ) try: +@@ -202,6 +249,8 @@ + if process.returncode != 0: + sys.exit(process.returncode) + else: ++ if args.fill_epilogue: ++ out_text = fill_epilogue(out_text) + sys.stdout.write(out_text) + +