phase11: merge 54 + cookbook 166 — 590 bodies / 599 regions

0x800F3DC0 (88 B) — a ONE-WORD sibling of the matched 0x800F3E18, found by worker E via
sf3_family at ratio 1.000 and confirmed by raw-word diff: identical in all 22 words except the
COP2 command field (0x4B70000C vs 0x4B78000C). The route was one copy, two renames and one field
change; every __asm__ and register binding carried over untouched.

166 records it, and notes it is the MIRROR of finding 161: on 0x800F3E18 the field 0x178000c was
the WRONG answer (one byte off, 0x170000c correct); on 0x800F3DC0 0x178000c IS correct. A count
tells you a field is COMMON, not that it is right -- and a ratio-1.000 sibling is the cheapest
place to learn which one a row wants. When the family tool reports one, diff the raw words FIRST.
This commit is contained in:
Christopher Williams
2026-09-24 11:18:58 -04:00
parent 430f141a4f
commit db6022c9f7
6 changed files with 1191 additions and 1015 deletions
+1011 -1014
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -482,6 +482,7 @@
0x800F3140 0x800F3160 src/func_800F3140.c
0x800F3160 0x800F316C src/func_800F3160.c maspsx=off
0x800F3A00 0x800F3A24 src/func_800F3A00.c
0x800F3DC0 0x800F3E18 src/func_800F3DC0.c
0x800F3E18 0x800F3E70 src/func_800F3E18.c
0x800F3E70 0x800F3E88 src/func_800F3E70.c
0x800F4098 0x800F4100 src/func_800F4098.c maspsx=epilogue
@@ -595,6 +596,7 @@
0x801085B0 0x801085D0 src/func_801085B0.c
0x80108690 0x801086AC src/func_80108690.c
0x80108710 0x80108734 src/func_80108710.c
0x80108740 0x801087C8 src/func_80108740.c maspsx=epilogue
0x80108990 0x801089B4 src/func_80108990.c
0x80109300 0x80109314 src/func_80109300.c
0x80109314 0x80109338 src/func_800F8F9C.c
1 # Code-region registry: one C region per matched function.
482 0x800F3140
483 0x800F3160
484 0x800F3A00
485 0x800F3DC0
486 0x800F3E18
487 0x800F3E70
488 0x800F4098
596 0x801085B0
597 0x80108690
598 0x80108710
599 0x80108740
600 0x80108990
601 0x80109300
602 0x80109314
+20
View File
@@ -2696,3 +2696,23 @@ toolchain matches at 132 B and `--fill-epilogue` is a no-op (132/0 both ways).**
**Second independent confirmation of finding 147:** the epilogue list was selected on the **original's**
tail, and the token must be decided per row from the **candidate's** tail.
### 166. A one-word sibling is a NEAR-FREE body — and the family tool finds them (worker E)
Worker E found `0x800F3DC0` (88 B) via `tools/sf3_family` at **ratio 1.000** against the already-matched
`0x800F3E18`. A raw-word diff confirms it: **the two bodies are identical in all 22 words except ONE:**
func_800F3E18 word 13 = 0x4B70000C (gte_cmd(0x170000c))
func_800F3DC0 word 13 = 0x4B78000C (gte_cmd(0x178000c))
**The route took one copy, two renames and one field change** — every `__asm__` and every
`register int ... __asm__("$n")` binding carried over untouched. `0x800F3DC0` now reports
**88 bytes / 0 differing / MATCH.**
**And it is the mirror image of finding 161:** on `0x800F3E18` the field `0x178000c` was the WRONG
answer (the row came out one byte off, and `0x170000c` was right); on `0x800F3DC0` **`0x178000c` IS
correct.** *A count tells you a field is COMMON, not that it is right* — and the sibling is the
cheapest possible place to learn which one a row wants.
> **When `tools/sf3_family` reports a ratio-1.000 sibling, diff the raw words FIRST.** A one-word diff
> means a copy and a field change, not a derivation.
+1 -1
View File
@@ -8,7 +8,7 @@ int func_80026274(int a0) {
if (x < 0) {
neg = 0;
x = -x;
x = -a0;
}
d = D_80121B18 - x;
if (x == 0) {
+83
View File
@@ -0,0 +1,83 @@
/*
* func_800F3DC0 — 88 bytes at 0x800F3DC0..0x800F3E18
*
* Goal B, Phase 11. **A one-word sibling of func_800F3E18** — found by worker E via
* `tools/sf3_family` at ratio 1.000 and confirmed by raw-word diff: the two bodies are identical
* in all 22 words EXCEPT the COP2 command field.
*
* func_800F3E18 word 13 = 0x4B70000C (gte_cmd(0x170000c))
* func_800F3DC0 word 13 = 0x4B78000C (gte_cmd(0x178000c))
*
* That single word is exactly the trap recorded below in item 3: two command fields differing
* only in the low selector bits are different instructions. This file is a copy of
* func_800F3E18.c with that one field, the name and the extent changed — every `__asm__` and
* every `register int ... __asm__("$n")` binding carries over untouched.
*
* save the three control words (cfc2 $0/$2/$4 into $13/$14/$15)
* load the 3x3 matrix from a0 into $0/$2/$4 (ctc2, i.e. RT1RT2 / RT22RT23 / RT33)
* lwc2 $9/$10/$11 from a1 (the vector, as three WORDS from memory)
* cop2 0x170000c (one command)
* swc2 $25/$26/$27 to a2 (the MAC result, as three words)
* restore the three control words
*
* WHAT THIS ROW ADDS TO THE TOOLKIT — three things, all of them needed to reach the bytes:
*
* 1. **`lwc2`/`swc2` are not `mtc2`/`mfc2`.** `include/gtemac.h`'s `gte_ldIR1/2/3` and
* `gte_stMAC1/2/3` expand to `mtc2`/`mfc2` (register <-> COP2); this body moves the vector and the
* result **straight between memory and COP2** with `lwc2`/`swc2`. Those had to be written as raw
* `__asm__ volatile` with the address in a register operand. **A row can use the IR/MAC registers
* without using the IR/MAC macros.**
* 2. **Register variables pin the COP2 operand registers.** `register int r13 __asm__("$13");` plus
* `"=r"(r13)` makes cc1 emit `cfc2 $13,...` rather than choosing its own destination; the same for
* `$8/$9/$10` on the matrix loads. Without that the candidate loads a0[0..2] in a different order
* into `v0/v1/a0` and is one instruction long. **An inline-asm row's residual is usually the
* operand registers, and register variables are the lever.**
* 3. **Read the command field off the ORIGINAL WORD, not off a table.** On func_800F3E18 worker D
* wrote `0x178000c` (the value counted elsewhere in the binary) and the row came out **exactly
* one byte wrong** — the correct field there is `0x170000c`. **This row is the mirror image:
* here `0x178000c` IS correct.** A count tells you a field is common, not that it is right.
*
* The save/restore of $0/$2/$4 around the command is real: the caller's matrix must survive.
*
* LIMITS: the function name and the meaning of the three pointers are hypotheses reconstructed from
* the disassembly; only the compiled bytes are evidence. The raw `__asm__` statements are inside the
* project's documented GTE exception (cookbook 24, Option A: the integer logic stays in C and the
* COP2 operations are inline asm, documented per file). The command field `0x178000c` is named by its
* VALUE and not semantically, per the gtemac.h convention.
*/
#include "../include/gtemac.h"
void func_800F3DC0(int *a0, int *a1, int *a2)
{
register int r13 __asm__("$13");
register int r14 __asm__("$14");
register int r15 __asm__("$15");
register int t0 __asm__("$8");
register int t1 __asm__("$9");
register int t2 __asm__("$10");
__asm__ volatile ("cfc2 %0,$0" : "=r"(r13));
__asm__ volatile ("cfc2 %0,$2" : "=r"(r14));
__asm__ volatile ("cfc2 %0,$4" : "=r"(r15));
t0 = a0[0];
t1 = a0[1];
t2 = a0[2];
gte_ldRT1RT2(t0);
gte_ldRT22RT23(t1);
gte_ldRT33(t2);
__asm__ volatile ("lwc2 $11, 8(%0)" : : "r"(a1));
__asm__ volatile ("lwc2 $9, 0(%0)" : : "r"(a1));
__asm__ volatile ("lwc2 $10, 4(%0)" : : "r"(a1));
__asm__ volatile ("nop");
gte_cmd(0x178000c);
__asm__ volatile ("swc2 $25, 0(%0)" : : "r"(a2));
__asm__ volatile ("swc2 $26, 4(%0)" : : "r"(a2));
__asm__ volatile ("swc2 $27, 8(%0)" : : "r"(a2));
__asm__ volatile ("ctc2 %0,$0" : : "r"(r13));
__asm__ volatile ("ctc2 %0,$2" : : "r"(r14));
__asm__ volatile ("ctc2 %0,$4" : : "r"(r15));
}
+74
View File
@@ -0,0 +1,74 @@
/*
* func_80108740 — 136 bytes at 0x80108740..0x801087C8
*
* Clamp an unsigned argument, arm a delay, run two helpers against it, store the helper's result
* into the word at 0x80121060, and return the clamped argument. First spelling.
*
* addiu sp,sp,-0x20 / sw s1,20 / move s1,a1
* frame 32: s0 at 0x10, s1 at 0x14, ra at 0x18. a1 is kept
* in s1 for the whole body (it is both the clamp input and
* the return value), so s1 is the only value live across
* the calls.
* lui v0,0x7 / ori v0,v0,0xeff0 0x7EFF0 as a LITERAL -- `ori`, not `addiu`, is the tell
* (cookbook 89)
* sltu v0,v0,s1 UNSIGNED compare: `sltu`, not `slt`, so the clamped
* parameter is `unsigned int` (cookbook 48)
* beqz v0,SKIP / sw s0,16(sp) if (a1 > 0x7EFF0) a1 = 0x7EFF0; -- the s0 save lands in
* the branch's delay slot
* lui s1,0x7 / ori s1,s1,0xeff0
* SKIP:
* lhu s0,4192(s0) / lw v0,4208(v0)
* v = D_80121060 << D_80121070 -- note `lhu`: the source
* global is an `unsigned short` (cookbook 7), and the shift
* is the `sllv` scheduled into the first call's delay slot
* move a1,s1 / jal 0x8010A6C4 func_8010A6C4(a0, a1) -- a0 is the incoming first parameter,
* passed through untouched
* li a0,-1 / jal 0x8010A78C func_8010A78C(-1, v + a1), the sum in the delay slot
* sh v0,4192(at) D_80121060 = <that result>, narrowed by the store width
* lw v1,4224(v1)
* bnez v1,END / move v0,s1 if (D_80121080 == 0) D_8012107C = 0;
* sw zero,4220(at)
* END: epilogue frame release in the jump slot -> maspsx=epilogue
*
* The whole-thing-match came on the first spelling, but it needs the harness token: with the
* default toolchain the same source is 140 bytes (one instruction long) because the tail is
* `lw ra / lw s1 / lw s0 / jr ra` with `addiu sp,sp,32` in the jump slot.
*
* The address forms are byte-load-bearing here and they are NOT the same for loads and stores:
* all four global accesses are absolute. As LOADS they happen to fold into the displacement, but
* as STORES the original emits `lui $at,%hi` + `sh`/`sw` -- the macro store form that goes through
* `$at` (cookbook 3) -- so the two stores must be written as SYMBOL stores, not through a literal
* address. The `$at` in `sh v0,4192(at)` / `sw zero,4220(at)` is the evidence.
*
* LIMITS: the two callees are cross-references by address only; the summary line is read off this
* body's shape. The four globals are address symbols (name == address) so they need no registry
* row, but their real types are only as strong as the access widths: `lhu`/`sh` make 0x80121060 a
* 16-bit object, and the other three are read/written as words. Whether the source named them or
* used literals is not recoverable for the two loads -- both emit the same bytes.
*/
extern unsigned short D_80121060;
extern int D_80121070;
extern int D_80121080;
extern int D_8012107C;
extern void func_8010A6C4(int a0, int a1);
extern int func_8010A78C(int a0, int a1);
int func_80108740(int a0, unsigned int a1)
{
int v;
if (a1 > 0x7EFF0)
a1 = 0x7EFF0;
v = D_80121060 << D_80121070;
func_8010A6C4(a0, a1);
D_80121060 = func_8010A78C(-1, v + a1);
if (D_80121080 == 0)
D_8012107C = 0;
return a1;
}