From 61be53994b90ff776ecd47a883e79e088fcc6ed1 Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Thu, 24 Sep 2026 09:36:47 -0400 Subject: [PATCH] =?UTF-8?q?phase11:=20merge=2013=20+=20cookbook=2073-78=20?= =?UTF-8?q?=E2=80=94=20522=20bodies=20/=20531=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker C's 0x80031FC4 (276 B). Cookbook gains six entries, the most important of which is worker C's correction of the COORDINATOR: a DEPENDENT row is one you cannot VERIFY, not one you have MATCHED. C's 0x800A613C and 0x800FD120 had symbol rows outstanding, but re-verifying against the tracked registry gave byte-identical results to the overlay runs -- both are still near-matches blocked on an ALLOCATION lever. Adding a symbol row unblocks the verification, not the match; conflating the two would have had a worker stop working a row it had not solved. Also recorded: the address-taken value may be a PARAMETER not a local (frame 8 too big with all offsets shifted by 8 is the tell); address-taken form forces a register; the struct assignment is what BATCHES the loads where element stores serialise behind maspsx nops; and the cop2 operand is the 25-bit field (0x486012 -> 0x4A486012). --- config/regions.tsv | 1 + docs/MATCHING_COOKBOOK.md | 147 ++++++++++++++++++++++++++++++++++++++ src/func_800320D8.c | 97 +++++++++++++++++++++++++ src/func_800910BC.c | 81 +++++++++++++++++++++ src/func_8009F798.c | 82 +++++++++++++++++++++ 5 files changed, 408 insertions(+) create mode 100644 src/func_800320D8.c create mode 100644 src/func_800910BC.c create mode 100644 src/func_8009F798.c diff --git a/config/regions.tsv b/config/regions.tsv index 195af0b..43d88d3 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -140,6 +140,7 @@ 0x80031BBC 0x80031CC0 src/func_80031BBC.c 0x80031F2C 0x80031F78 src/func_80031F2C.c 0x80031F78 0x80031FC4 src/func_80031F78.c +0x80031FC4 0x800320D8 src/func_80031FC4.c 0x800321EC 0x800321F8 src/func_800321EC.c 0x80034A80 0x80034ABC src/func_80034A80.c 0x800354F8 0x80035548 src/func_800354F8.c diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 168bb0c..c0e046a 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -1127,3 +1127,150 @@ size has not been the difficulty — the absence of a lever was.** Recording the *direction* of a failure ("a source-shape lever exists and here it is") is more useful than recording it as a class, because the next worker knows whether to retry. + +### 67. `mult` + `mfhi` + `sra` with NO `mflo` is a CONSTANT DIVISION, not a 64-bit multiply (worker D) + +`lui v1,0x4bda / ori v1,v1,0x12f7 / sll v0,v0,0xc / mult v0,v1 / mfhi t0 / sra v1,t0,0xd` +reads like a fixed-point `(x << 12) * 0x4BDA12F7 >> 45`. **It is not.** It is cc1's +magic-number expansion of `(x << 12) / 27648`, where `0x4BDA12F7 = ceil(2^45 / 27648)`. + +**The discriminator is the ABSENCE of `mflo`.** Every available cc1 emits `mfhi` **and** +`mflo` for a genuine 64-bit multiply — probed on open `gcc-2.7.2-psx` and the real Sony +`CC1PSX` 4.0 and 4.6. So the tell is checkable in one glance. + +**Recover the divisor from the magic, never from the constant's face value:** +`D = ceil(2^(32+s) / M)`. Worker D burned four spellings writing the multiply before +solving for `D` and getting 27648 — which is also the second argument of all six +`func_80010654` calls in the same function. Probe `int f(int x) { return (x << 12) / 27648; }` +reproduces the triple exactly. + +Sign side: a *signed* division of a provably non-negative value gives `mult`/`sra` with no +sign correction — which is why an `unsigned short` load shifted left 12 needs no fixup. +An `addu` + `subu` sign-correction pair instead indicates the **negative-magic** form. + +**Why this matters for dispatch:** this class is nearly disjoint from the corpus (8 rows in +worker D's partition, **0 of 522 registered**) — the third such class this phase, after +trapping-arith and division-check. But it differs in kind: those two need a different +compiler build; this one is unmatched **because it is misread**. Any row previously +classified as a "64-bit multiply residual" is suspect and should be re-read. + +### 68. The dispatch signal is REDUNDANCY, not size — measured 6-to-0 (worker C) + +Across worker C's 15 above-ceiling rows, **"does the body contain a repeated block?" has +out-performed "how big is it?" six to zero.** + +- Every one of C's six matches is a **repetitive or mechanically-determined** body: a + struct assignment, a repeated identical block, a record initialiser, a transposition, + a repeated bounds check. +- Every one of C's nine near-misses is **tie-break-dense**: a long straight-line call + sequence, a pointer-walk loop, a table walk. + +Worker D's rows say the same thing from the other side — its 700 B and 1232 B bodies cost +**3 and 2** spellings while its 248 B body cost **4**. And the two largest bodies were the +cheapest after the 264 B one. + +**This supersedes size-band reasoning (finding 58).** The cost of a row is set by tie-break +density; size is incidental. Median attempts to a *match* for C was 3.5; median attempts +before classifying a *near-match* was 4 — so the near-misses are cheap to identify and the +matches are cheap to close, and what the band actually costs is the 60% that never closes. + +### 69. The `char[4]` struct-copy loop is the block-move idiom (worker C) + +An explicit `for (i = 0; i < 40; i++) dst[i] = src[i];` over a **4-byte `char[4]` struct** +yields `lwl`/`lwr` + `swl`/`swr` with **no runtime check**. A 160-byte struct assignment +instead makes cc1 emit a runtime alignment test plus two loops (344 B, +96). `char[160]` +and `short[80]` both do this. + +### 70. Two cheap tells (worker C) + +- **A `sb`/`lbu` round trip for a result flag means the flag is a STRUCT MEMBER** — a bare + local would stay in a register. +- **A local holding a constant is not free.** Naming `127` forced an `s1` save and grew the + frame 48 → 56; the original keeps 48 with literal 127s. Frame arithmetic catches it + immediately. + +### 71. A guard whose test must branch INTO the body: spell it `do`/`while` (worker D) + +Worker D's `0x800FF5A8`: the original's entry guard is `beqz v0` (count == 0) where the +naive `for` gives `blez v0` — this build infers `0 <= n` from an `lbu` and folds `0 < n` +to `n <= 0`. Spelling the loop as a `do`/`while` with an explicit +`if (((unsigned char *)a0)[7] != 0)` guard produces `beqz` and matches. + +**And the asymmetry is the point:** the guard must test `a0` while the loop bound tests a +named local `q` — and *that same asymmetry* produces the second pointer register below. +Two mechanisms, one source change. + +### 72. Named-locals family: FOURTH mechanism, OPPOSITE polarity (worker D) + +The three recorded cases are all **"name a local so cc1 does NOT reload."** This one is +**"name a local so cc1 MUST keep a second copy."** + +Worker D's `0x800FF5A8`: the original keeps `a0` in **two** callee-saved registers (`s2` +for the magic check / entry count / unlink, `s3` for the in-loop count re-read). The lever: +name **two separate pointer locals that both hold `a0` and are both live across the call** +(`q` for the loop bound, `p` for the walk). The allocator cannot coalesce pseudos with +overlapping live ranges, so the second register appears. + +**Generalised diagnostic (worker D's 1232 B row):** a residual that is **ONE INSTRUCTION +REPEATED AT EVERY ONE OF N SITES** ⇒ the original hoisted a sub-expression into a named +local. At N=32 that is a 4-byte residual that a per-site diff misreads as noise. + +Also from that row: the LAST block of a 32-way bit-flag dispatch needs a **signed** test — +`flags & 0x80000000` (the literal, unsigned in C) gives `and`/`bnez`, while the original's +`bgez a1` is what `flags < 0` produces. Same semantics, different encoding. + +### 73. "DEPENDENT" means you cannot VERIFY it — not that you have MATCHED it (worker C) + +Worker C's correction, and it is a discipline point worth keeping: a **dependent** row is +one whose verification is blocked on a registry row. It is **not** a row that is waiting to +be matched. Worker C's `0x800A613C` and `0x800FD120` had symbol requests outstanding, but +re-verifying them against the tracked registry once the rows existed gave **byte-identical +results to the overlay runs** — both are still near-matches, blocked on an *allocation* +lever, not on a symbol row. + +**So adding a symbol row unblocks the VERIFICATION, not the MATCH.** Conflating the two +would have had a worker stop working a row it had not actually solved. + +### 74. The address-taken value may be a PARAMETER, not a local (worker B) + +Worker B's `0x80045F1C`: the frame is only **40 bytes** yet the body touches `sp+56`, and the +address passed to the first callee is `&a4` — the **fifth parameter**, whose home is the +caller's outgoing-argument area at `frame+16` — not a local. Modelling it as a local `short` +reproduces the same instruction *shape* with a **48-byte frame and every offset +8** (22 +differing bytes). + +**Diagnostic: "right shape, frame 8 too big, all offsets shifted by 8" ⇒ check whether the +address-taken value is a parameter rather than a local.** + +### 75. Address-taken form forces a register (worker B) + +`int *p = &D_8013C330;` gives `la` into a saved register plus two `lw a0,0(s0)`; reading the +symbol directly gives 64 bytes (8 short) with two macro pairs and **no saved register**. This +is the *source-forced* counterpart of the `-G` route in finding 47. + +### 76. The struct assignment is what BATCHES the loads (worker C) + +Worker C's `0x80031FC4`: four element stores **serialise** behind four maspsx load-delay nops +(+16 bytes), whereas `*(struct V4 *)m[1] = v1` gives the original's four **batched** loads. +So where a copy appears as separate element stores and the original has batched loads, the +source used a struct assignment. + +Companion, same row: **an expression repeated in two blocks must NOT be bound to a local** if +the stores in between can alias it — `a0->f164` is re-read for the second block, and a local +`struct T *t` comes out 1 instruction short (268 vs 276). + +### 77. Invert the condition and swap the arms to place the block (worker C) + +Third instance this session of the mirrored-layout lever (findings 28/43): to get `bnez` +branching **to** the four-word block, the condition is written **inverted with the arms +swapped** — the single store inline, the copy out of line. Applied per guard. + +### 78. `cop2` operand encoding (worker A) + +`cop2 0x486012` assembles to exactly `0x4A486012` — the operand is the **25-bit field**, not +26 — while `cop2 0x2486012` is an operand-range error. Also: `-fno-schedule-insns2` (reorg +off) shows the RTL order is stores-then-copy for worker A's GTE row, so that residual is the +**second** scheduler, and `-fno-schedule-insns` does **not** change it. + +And a byte-required operand order: `a0 == D_8012204C` matches where `D_8012204C == a0` does +not (worker A, `0x800B62C8`, 2 bytes). diff --git a/src/func_800320D8.c b/src/func_800320D8.c new file mode 100644 index 0000000..1016deb --- /dev/null +++ b/src/func_800320D8.c @@ -0,0 +1,97 @@ +/* + * func_800320D8 — 276 bytes at 0x800320D8..0x800321EC + * + * Phase 11, band ABOVE the old 244-byte ceiling. First-attempt match, taken + * directly from its sibling func_80031FC4 (same body, same local layout, same + * callee shape — only the object offsets and the callee address differ). + * + * Fills one 4-word row from a helper, then fills two more rows from two + * optional 4-word blocks of an object — each block either copied whole or as + * its first word only, gated by a separate flag — and finally writes three + * component differences plus one raw component. + * + * The observed instructions are: + * addiu sp,sp,-80 / sw s0,64(sp) / move s0,a0 / sw s1,68(sp) / move s1,a1 + * sw s2,72(sp) / move s2,a2 / sw ra,76(sp) + * jal func_80031F78 / addiu a1,sp,16 + * lw a2,164(s0) + * lw v0,976(a2) / nop / bnez v0,full1 / nop + * lw v0,1032(a2) / j join1 / sw v0,32(sp) + * full1: + * lw v0,1032(a2) / lw v1,1036(a2) / lw a0,1040(a2) / lw a1,1044(a2) + * sw v0,32(sp) / sw v1,36(sp) / sw a0,40(sp) / sw a1,44(sp) + * join1: + * lw a2,164(s0) + * lw v0,2272(a2) / nop / bnez v0,full2 / nop + * lw v0,2328(a2) / j join2 / sw v0,48(sp) + * full2: + * lw v0,2328(a2) / lw v1,2332(a2) / lw a0,2336(a2) / lw a1,2340(a2) + * sw v0,48(sp) / sw v1,52(sp) / sw a0,56(sp) / sw a1,60(sp) + * join2: + * lw v0,32(sp) / lw v1,16(sp) / nop / subu v0,v0,v1 / sw v0,0(s1) + * lw v0,36(sp) / lw v1,20(sp) / nop / subu v0,v0,v1 / sw v0,4(s1) + * lw v0,40(sp) / lw v1,24(sp) / nop / subu v0,v0,v1 / sw v0,8(s1) + * lw v0,56(sp) / nop / sw v0,0(s2) + * lw ra,76(sp) / lw s2,72(sp) / lw s1,68(sp) / lw s0,64(sp) + * addiu sp,sp,80 / jr ra / nop + * + * The four byte-required facts are the same four as its sibling func_80031FC4, + * and each was established there: + * 1. `a0->f164` is RE-READ for the second block (a local pointer is one + * instruction short) because the stores into the local matrix can alias it. + * 2. The condition is INVERTED with the arms swapped, so the branch is `bnez` + * TO the four-word block with the single store inline (mirrored-layout + * lever, per guard). + * 3. The four-word copy is a STRUCT assignment, which batches the loads; four + * element stores serialise them behind four maspsx load-delay nops. + * 4. The matrix is 3x4 with a 16-byte row stride; the fourth column is never + * read. + * + * THE FAMILY LEVER: this row is the sibling of func_80031FC4 and shares its + * callee family (func_80031F78 vs func_80031F2C). Only the object offsets + * changed — the two flags stay at +976 and +2272, while the two 4-word blocks + * move to +1032 and +2328 (48 bytes later than the sibling's +984/+2280) — so + * the body, the local matrix, the branch polarities and the epilogue are + * identical and the match came on the first spelling. + * + * LIMITS: offsets and widths are read off the disassembly; everything between + * the named fields is padding reconstructed only to fix the offsets, and the + * real structures are certainly larger. `struct S`'s `+164` is a pointer. The + * callee's signature is inferred from its single call site. + */ + +struct V4 { int v[4]; }; + +struct T { + char pad0[976]; + int f976; + char pad_a[1032 - 980]; + struct V4 v1; + char pad1[2272 - 1048]; + int f2272; + char pad_b[2328 - 2276]; + struct V4 v2; +}; + +struct S { char pad[164]; struct T *f164; }; + +extern void func_80031F78(struct S *a, int *b); + +void func_800320D8(struct S *a0, int *a1, int *a2) +{ + int m[3][4]; + + func_80031F78(a0, m[0]); + if (a0->f164->f976 == 0) + m[1][0] = a0->f164->v1.v[0]; + else + *(struct V4 *)m[1] = a0->f164->v1; + if (a0->f164->f2272 == 0) + m[2][0] = a0->f164->v2.v[0]; + else + *(struct V4 *)m[2] = a0->f164->v2; + a1[0] = m[1][0] - m[0][0]; + a1[1] = m[1][1] - m[0][1]; + a1[2] = m[1][2] - m[0][2]; + a2[0] = m[2][2]; +} diff --git a/src/func_800910BC.c b/src/func_800910BC.c new file mode 100644 index 0000000..4d55da4 --- /dev/null +++ b/src/func_800910BC.c @@ -0,0 +1,81 @@ +/* + * func_800910BC — 280 bytes at 0x800910BC..0x800911D4 + * + * Goal B, Phase 11. Four calls, seven gp-relative globals, three constant divisions — and it is the + * row that sharpened the constant-division diagnostic into something unambiguous. + * + * What the body is: a toggling spawn/state routine. + * + * r = func_800F6F60() % 12; s1 = r + 6; <- the default delay + * if (D_80122674 == 1) { <- the toggle + * func_8008FCD8(D_80122660, D_80122670); + * if (func_800F6F60() % 10 >= 6) + * s1 = func_800F6F60() % 5 + 1; + * } else { + * func_8008FA58(D_80122660, D_80122670, D_80121EEC, D_80121EE8); + * } + * D_80121EF4 = s1; + * D_80121EF0 = *(int *)0x801223B8; + * D_80122674 ^= 1; + * + * THE DIVISIONS AND THE AMBIGUITY THAT COST A SPELLING. + * + * /12 : magic 0x2AAAAAAB, mfhi then sra 1 + * /10 : magic 0x66666667, mfhi then sra 2 + * /5 : magic 0x66666667, mfhi then sra 1 + * + * 0x2AAAAAAB is the well-known magic for a division by 6 at shift 0, and reading it as 6 was the + * first spelling's error — worth one instruction of codegen difference at 0x800910E4. The identity is + * D = 2^(32+s) / M, and THE MAGIC ALONE IS AMBIGUOUS: 0x2AAAAAAB gives 6 at s=0, 12 at s=1, 24 at + * s=2. Always take the shift from the `sra` that follows `mfhi`; the magic is only half the identity. + * The same magic appearing twice (0x66666667 for both 10 and 5) is not a contradiction for the same + * reason, and cc1 materialises it ONCE into a callee-saved register and reuses it for both. + * + * THE NAMED-LOCAL DIRECTION AGAIN, AND THIS ROW IS DECIDED BY IT. + * Naming the second division's result — `r = func_800F6F60() % 10; if (r >= 6) ...` — costs 2 words: + * the original keeps the remainder in v0 because it is consumed directly by the `slti`, while the + * named local forces a0. The FIRST division's result may be named, because it is used twice + * (`s1 = r + 6`), and there it lands in a0 in both. The rule is per-site: name a result only where + * the original reuses it; where the original consumes it immediately, leave the expression inline. + * Inlining the second one is what closed the row. + * + * LIMITS: the function name, the callees, the meaning of the seven globals and the claim that the + * routine is a spawn delay selector are hypotheses reconstructed from the disassembly; only the + * compiled bytes are evidence. D_80122674 is byte-accessed (lbu/sb) and so is declared + * `unsigned char`; 0x801223B8 is accessed ABSOLUTELY (lui + lw, not gp-relative) and is therefore + * written as a literal address, which is what gives the original's absolute encoding (cookbook 46). + * The three divisors are recovered from the magic-plus-shift pairs, not interpreted. + */ + +extern unsigned char D_80122674; +extern int D_80122660; +extern int D_80122670; +extern int D_80121EF4; +extern int D_80121EF0; +extern int D_80121EEC; +extern int D_80121EE8; +extern int func_800F6F60(void); +extern void func_8008FCD8(int a0, int a1); +extern void func_8008FA58(int a0, int a1, int a2, int a3); + +void func_800910BC(void) +{ + int s1; + int r; + + r = func_800F6F60() % 12; + s1 = r + 6; + + if (D_80122674 == 1) { + func_8008FCD8(D_80122660, D_80122670); + if (func_800F6F60() % 10 >= 6) { + s1 = func_800F6F60() % 5 + 1; + } + } else { + func_8008FA58(D_80122660, D_80122670, D_80121EEC, D_80121EE8); + } + + D_80121EF4 = s1; + D_80121EF0 = *(int *)0x801223B8; + D_80122674 ^= 1; +} diff --git a/src/func_8009F798.c b/src/func_8009F798.c new file mode 100644 index 0000000..4562359 --- /dev/null +++ b/src/func_8009F798.c @@ -0,0 +1,82 @@ +/* + * func_8009F798 — 248 bytes at 0x8009F798..0x8009F890 + * + * Hypothesis, not a claim about meaning: rewrites a 3x3 array of shorts in place. The + * body is fully unrolled and completely repetitive — 9 loads, 6 negations, 9 stores, + * then 9 stores back — which is why it matched in three spellings. + * + * Shape (all offsets in words; the temp array's layout is the whole point, see below): + * 0000 lh v0,0(a0) ; phase 1: copy, column-major order + * 0004 addiu sp,sp,-48 + * 0008 sw v0,0(sp) ; m[0][0] = a0[0] + * 000c lh v0,6(a0) + * 0010 nop + * 0014 sw v0,4(sp) ; m[0][1] = a0[3] + * 0018 lh v0,12(a0) + * 001c nop + * 0020 sw v0,8(sp) ; m[0][2] = a0[6] + * 0024 lh v0,2(a0) + * 0028 nop + * 002c sw v0,16(sp) ; m[1][0] = a0[1] + * ... (column-major: (r,c) = (0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(0,2),(1,2),(2,2)) + * 0068 sw v0,16(sp) ; phase 2: negate the c != 0 columns and STORE AGAIN + * ... (6 negated stores, same slots as their raw stores) + * 0088 sh t0,0(a0) ; phase 3: write back, column-major again + * 00f0 addiu sp,sp,48 + * 00f4 jr ra + * 00f8 nop + * + * **THE TEMP ARRAY IS INDEXED TRANSPOSED.** The original's slot for matrix cell (r,c) is + * `4*r + 16*c` — a 16-byte stride on the SECOND index. So `int m[3][4]` must be written + * `m[c][r]` (the first index is the column), not `m[r][c]`. Natural `m[r][c]` indexing + * gives the CORRECT LENGTH (248) and 15 differing bytes, all of them stack offsets: the + * slot permutation is the only residual, which is the clean diagnostic for this class + * (cookbook 59). With the declaration and the transposed use, the row is exact. + * + * **THE RAW STORES ARE DEAD AND SURVIVE.** Phase 1 stores the un-negated value and phase 2 + * negates the slot and stores it a second time; GCC 2.7.2 has no pass that removes the + * first store, so both appear. Writing `m[c][r] = -a0[3*r + c];` directly (one statement + * per cell) is 244 bytes LENGTH-MISMATCH — the negations then sit immediately after their + * own loads instead of in a second pass. Same property as the `buf[1]` double store in + * func_80027CA0; it is a general tell that a "redundant" store in the original is a real + * second source statement, not a codegen artefact. + * + * LIMITS: the function name, the element type (short), the 3x3 extent, the in-place + * rewrite and the sign pattern are hypotheses read from the instruction shape; only the + * bytes are evidence. The `m[c][r]` form is a byte-required spelling, not a claim that the + * original source was written that way — the same bytes come from any declaration whose + * (r,c) slot is 4*r + 16*c. No callee: this is a leaf, and the harness emitted the common + * epilogue itself. + */ + +void func_8009F798(short *a0) +{ + int m[3][4]; + + m[0][0] = a0[0]; + m[0][1] = a0[3]; + m[0][2] = a0[6]; + m[1][0] = a0[1]; + m[1][1] = a0[4]; + m[1][2] = a0[7]; + m[2][0] = a0[2]; + m[2][1] = a0[5]; + m[2][2] = a0[8]; + + m[1][0] = -m[1][0]; + m[1][1] = -m[1][1]; + m[1][2] = -m[1][2]; + m[2][0] = -m[2][0]; + m[2][1] = -m[2][1]; + m[2][2] = -m[2][2]; + + a0[0] = m[2][0]; + a0[3] = m[2][1]; + a0[6] = m[2][2]; + a0[1] = m[0][0]; + a0[4] = m[0][1]; + a0[7] = m[0][2]; + a0[2] = m[1][0]; + a0[5] = m[1][1]; + a0[8] = m[1][2]; +}