phase8: merge cycle 6 and record the conventions the phase produced

5 new matches from worker A (func_800F75D0, func_80026180, func_8003768C,
func_80089D14, func_800681A4) -> 158 regions / 149 distinct bodies, candidate gate
0 differing bytes, make check green.

MATCHING_CONVENTIONS.md now records the four per-region override keys with the
byte-level measurement behind each (cc1=, as=, gp=-NAME, maspsx=off), the
inline-assembly convention as the developer decided it (accepted for coprocessor
and kernel instructions, documented per file, integer logic in C, no .word lumps,
and a register-name binding is not inline assembly), the rule that an unmatched
draft does not stay in src/, and the worklist classes that are excluded outright
with counted reasons.
This commit is contained in:
Christopher Williams
2026-09-23 23:26:49 -04:00
parent db8afed4bf
commit 6ef29e45e9
7 changed files with 276 additions and 1 deletions
+5
View File
@@ -37,6 +37,7 @@
0x80022A60 0x80022A98 src/func_80022A60.c
0x80022FB8 0x80022FCC src/func_80022FB8.c
0x80024C14 0x80024C34 src/func_80024C14.c
0x80026180 0x800261C0 src/func_80026180.c
0x80026258 0x80026264 src/func_80026258.c
0x80026264 0x80026274 src/func_80026264.c
0x800262E0 0x800262EC src/func_800262E0.c
@@ -57,6 +58,7 @@
0x80036308 0x80036328 src/func_80036308.c
0x8003636C 0x80036378 src/func_8003636C.c
0x80036378 0x80036380 src/func_80036378.c
0x8003768C 0x800376CC src/func_8003768C.c
0x80038788 0x80038790 src/func_80038788.c
0x80038790 0x8003879C src/func_80038790.c
0x8003B2F0 0x8003B320 src/func_8003B2F0.c
@@ -67,6 +69,7 @@
0x80057DFC 0x80057E04 src/func_80057DFC.c
0x80058288 0x800582AC src/func_80058288.c
0x80065B6C 0x80065B8C src/func_80065B6C.c
0x800681A4 0x800681E0 src/func_800681A4.c
0x80068470 0x8006848C src/func_80068470.c
0x80068F40 0x80068F6C src/func_80068F40.c
0x80068F98 0x80068FA8 src/func_80068F98.c
@@ -87,6 +90,7 @@
0x80089C4C 0x80089C54 src/func_80042088.c
0x80089C54 0x80089C64 src/func_80089C54.c
0x80089C64 0x80089C74 src/func_80089C64.c
0x80089D14 0x80089D54 src/func_80089D14.c
0x80089E74 0x80089E7C src/func_80042088.c
0x8008B8D4 0x8008B8E4 src/func_8008B8D4.c
0x8008B8E4 0x8008B8F4 src/func_8008B8E4.c
@@ -114,6 +118,7 @@
0x800F3160 0x800F316C src/func_800F3160.c maspsx=off
0x800F3E70 0x800F3E88 src/func_800F3E70.c
0x800F5B40 0x800F5B70 src/func_800F5B40.c
0x800F75D0 0x800F760C src/func_800F75D0.c
0x800F7A84 0x800F7A94 src/func_800F7A84.c
0x800F7FB4 0x800F7FD8 src/func_800F7FB4.c
0x800F8294 0x800F82A4 src/func_800F8294.c
1 # Code-region registry: one C region per matched function.
37 0x80022A60
38 0x80022FB8
39 0x80024C14
40 0x80026180
41 0x80026258
42 0x80026264
43 0x800262E0
58 0x80036308
59 0x8003636C
60 0x80036378
61 0x8003768C
62 0x80038788
63 0x80038790
64 0x8003B2F0
69 0x80057DFC
70 0x80058288
71 0x80065B6C
72 0x800681A4
73 0x80068470
74 0x80068F40
75 0x80068F98
90 0x80089C4C
91 0x80089C54
92 0x80089C64
93 0x80089D14
94 0x80089E74
95 0x8008B8D4
96 0x8008B8E4
118 0x800F3160
119 0x800F3E70
120 0x800F5B40
121 0x800F75D0
122 0x800F7A84
123 0x800F7FB4
124 0x800F8294
+46 -1
View File
@@ -68,7 +68,28 @@ Rules the harness enforces:
Unmatched code remains raw data taken from the original. It does not appear as `NON_MATCHING` C, and
it never enters the default build. A failed candidate is recorded as a bounded negative, not hidden
behind a guard.
behind a guard — **and it does not stay in `src/`**: a source that does not match is moved to an ignored
staging path (or deleted) once its evidence is written down. A tracked `src/` file is a claim.
Some classes are excluded from the worklist outright, each with a counted reason, so that no session
spends budget on them: all-zero bodies (a zero-filled region the walk ran through), and
**trapping-arithmetic bodies** — 50 of the 1,937 `exact` extents contain an `add`/`sub` that no available
compiler emits (cookbook finding 26). Note the limit recorded with it: that check is *sufficient but not
complete*, so `excluded_trapping_arith=0` does not mean a row is matchable.
## Inline assembly (coprocessor and kernel instructions)
**Accepted, with per-file documentation** (Phase 8, developer decision). A region may contain a
`__asm__ volatile` statement when C cannot name the instruction — a COP2/GTE operation or a BIOS
`syscall`. The boundary is narrow and enforced by convention:
- the **integer logic stays in C**, and the epilogue and all scheduling come from `cc1`/`maspsx`;
- no `.word` lumps and no whole-function assembly;
- the statement is written in the re-derived macro form, and the file's header says what it is and why;
- `include/gtemac.h` holds the re-derived COP2 macros and the register map as the provenance record.
A **register-name binding is not inline assembly** and needs no exemption: `register int sp __asm__("$29")`
is a documented GNU C extension and reproduces the stack accessor exactly.
## Duplicate bodies
@@ -154,6 +175,21 @@ emulates ASPSX 2.56 (the version on the PsyQ 4.0 SDK banner): it emits `.set nor
`nop`s so delay-slot scheduling matches the original. `--no-maspsx` disables it for comparison;
`--aspsx-version` overrides the pinned version.
### Per-region overrides
The registry's optional fourth field carries space-separated `key=value` overrides. Four keys exist, and
each one exists because a byte-level measurement demanded it:
| Override | Meaning | Why it exists |
|---|---|---|
| `cc1=-O0` | append compiler flags for this region | some translation units were compiled with other `-O`/`-G` values |
| `as=-G8` | append assembler flags for this region | as above |
| `gp=-NAME[,NAME]` | do **not** treat `NAME` as gp-relative here, despite its registry marker | the access form is per **site**: `0x80121F84` is read gp-relative at `0x800A80BC` and written absolutely at `0x8002D288` (cookbook finding 16) |
| `maspsx=off` | run this region without the ASPSX emulation stage | maspsx's unconditional `nop` for a jump destroys the delay-slot fill GNU `as` reorder mode performs on an expanded symbol store — six regions need this, including the store-in-delay-slot shape that stood unresolved for three phases (cookbook finding 17) |
`range` accepts the equivalents `--cc1-flag`, `--as-flag`, `--no-gp NAME` and `--no-maspsx` so a worker can
reproduce a region's exact build while iterating.
## Drafting a function
For anything larger than a few instructions, draft from Ghidra rather than raw disassembly:
@@ -173,6 +209,15 @@ make test # synthetic-only suite; needs no game input
make check # make test + make extents-verify + the full-binary byte gate (needs the executable)
```
### Working in parallel with other sessions
When more than one session works in this repository at once, `docs/PHASE8_PROTOCOL.md` governs: who may
write which file, the staging formats for claims and symbol requests, and the merge workflow. The load-bearing
rule is that the tracked registries, `build/` and all writing git commands belong to a single coordinating
session, and that a claim is only ever merged to a **candidate** registry which is gated before it is promoted.
`tools/sf3_merge` enforces the structural half of that (extent exactness, overlap, payload bounds, source
existence) and `make gate` the byte half.
`make gate` prints `c_regions=<n>` and, when the registry is empty, an explicit note that the build
contains no C. A `MATCH` with `c_regions=0` means the data baseline was reproduced, not that anything
was matched.
+46
View File
@@ -0,0 +1,46 @@
/*
* func_80026180 — 64 bytes at 0x80026180..0x800261C0
*
* Byte-identical reconstruction of a guarded two-halfword sum written through an
* out-parameter, returning a status. `lhu` fixes zero-extended 16-bit reads
* (cookbook finding 7), each of which is followed by a load-delay `nop`. The two
* addends 0x80142EA8/0x80142EAA are adjacent halfwords loaded with the macro
* form that expands into the destination register (cookbook finding 2). The
* `clear v0` before the second store is the `return 0` value hoisted, and the
* `li v0,0x16` in the guard's delay slot is the `return 0x16` value.
*
* The observed instructions are:
* beq a0,zero,exit if (source == 0) return 0x16
* li v0,0x16 (delay slot) v0 = 22
* lhu v0,0x4(a0) v0 = source[2]
* lui v1,0x8014
* lhu v1,0x2ea8(v1) v1 = D_80142EA8
* nop (load delay)
* addu v0,v0,v1 v0 += v1
* sh v0,0x0(a1) out[0] = v0
* lhu v1,0x6(a0) v1 = source[3]
* lui a0,0x8014
* lhu a0,0x2eaa(a0) a0 = D_80142EAA
* clear v0 v0 = 0 (the return value)
* addu v1,v1,a0 v1 += a0
* sh v1,0x2(a1) out[1] = v1
* exit:
* jr ra
* nop
*
* LIMITS: the offsets, the widths and the constant 0x16 are evidence; the
* globals' names, types and meanings and the parameters' meanings are
* hypotheses, as is whether 0x16 is a status code or a count.
*/
extern unsigned short D_80142EA8;
extern unsigned short D_80142EAA;
int func_80026180(unsigned short *source, unsigned short *out) {
if (source == 0) {
return 0x16;
}
out[0] = source[2] + D_80142EA8;
out[1] = source[3] + D_80142EAA;
return 0;
}
+46
View File
@@ -0,0 +1,46 @@
/*
* func_8003768C — 64 bytes at 0x8003768C..0x800376CC
*
* Byte-identical reconstruction of a two-level linked-list search. The head
* pointer is a field of the argument object, each node points at an entry at
* offset 0 and at the next node at offset 8, and the search key is the entry's
* first word. The not-found value 0 is materialised with `clear v0` in the
* guard's delay slot and again in the loop branch's delay slot, while the found
* value moves `a0` into `v0` in the `beq`'s delay slot — two different return
* values, so the epilogue is shared and each path sets `v0` itself.
*
* The observed instructions are:
* lw v1,0x14ec(a0) v1 = *(char **)(object + 0x14ec)
* nop (load delay)
* beq v1,zero,exit if (node == 0) return 0
* clear v0 (delay slot) v0 = 0
* loop:
* lw a0,0x0(v1) a0 = *(char **)node (the entry)
* nop (load delay)
* lw v0,0x0(a0) v0 = *(int *)entry
* nop (load delay)
* beq v0,a1,exit if (v0 == key) return the entry
* move v0,a0 (delay slot) v0 = entry
* lw v1,0x8(v1) v1 = *(char **)(node + 8)
* nop (load delay)
* bne v1,zero,loop while (node != 0)
* clear v0 (delay slot) v0 = 0
* exit:
* jr ra
* nop
*
* LIMITS: the offsets, the widths and the two return values are evidence; the
* list interpretation, the node layout and the key's meaning are hypotheses.
*/
char *func_8003768C(char *object, int key) {
char *node = *(char **)(object + 0x14ec);
while (node != 0) {
char *entry = *(char **)node;
if (*(int *)entry == key) {
return entry;
}
node = *(char **)(node + 8);
}
return 0;
}
+53
View File
@@ -0,0 +1,53 @@
/*
* func_800681A4 — 60 bytes at 0x800681A4..0x800681E0
*
* Byte-identical reconstruction of a table lookup whose index is scaled by 36
* with index 2 special-cased to the same offset (0x24 = 36, so the special case
* is "index 2 maps to element 1"). The multiply is strength-reduced (`sll 3` /
* `addu` / `sll 2` = `(i*8 + i)*4` = `i * 36`), so 36 was a literal (cookbook
* finding 12). The base is a symbol combined with a runtime offset, so it is
* materialised `lui` + `addu` with the low half as the load displacement
* (cookbook finding 5). `srl` fixes an UNSIGNED shift and `andi …,0x1f` the
* 5-bit field, both at the end of the body (cookbook finding 8).
*
* The observed instructions are:
* li v0,0x2 v0 = 2
* beq a0,v0,special if (index == 2) goto the special case
* nop
* sll v0,a0,0x3
* addu v0,v0,a0
* j join
* sll v0,v0,0x2 (delay slot) v0 = index * 36
* special:
* li v0,0x24 v0 = 36
* join:
* lui at,0x8013
* addu at,at,v0 at = D_80132B88 + offset
* lw v0,0x2b88(at) v0 = *(unsigned int *)at
* nop (load delay)
* srl v0,v0,0x1a v0 >>= 26
* jr ra
* andi v0,v0,0x1f (delay slot) return v0 & 0x1f
*
* The special case must be written as the ELSE arm (`if (index != 2) ... else
* ...`). Written the other way round the compiler lays the blocks out mirrored
* -- it makes the special case the fall-through and puts the multiply in the
* `j`'s delay slot, giving `bne` instead of the original's `beq` (19 differing
* bytes). With the inverted condition the region matches with maspsx ON.
*
* LIMITS: the stride 36, the special index 2, the shift 26 and the mask 0x1f are
* evidence; the table's name, type and meaning and the field's meaning are
* hypotheses.
*/
extern char D_80132B88[];
int func_800681A4(int index) {
int offset;
if (index != 2) {
offset = index * 36;
} else {
offset = 0x24;
}
return (*(unsigned int *)(D_80132B88 + offset) >> 26) & 0x1f;
}
+46
View File
@@ -0,0 +1,46 @@
/*
* func_80089D14 — 64 bytes at 0x80089D14..0x80089D54
*
* Byte-identical reconstruction of a hardware-register bit update. The register
* is reached through the pointer field at +0x1c. The OR 0x08800000 is a single
* `lui a0,0x880` and the clear mask 0xcffcffff is `lui 0xcffc` + `ori 0xffff`.
* The store `*field = masked` is scheduled into the `beq`'s delay slot, which
* makes it UNCONDITIONAL — the source therefore assigns it before the test, not
* inside the `if` (that is what the bytes prove). The final byte store uses the
* `$at`-free register form and lands in the `jr` delay slot (cookbook
* finding 8).
*
* The observed instructions are:
* lui v0,0xcffc
* lw a2,0x1c(a0) a2 = *(int **)(object + 0x1c)
* ori v0,v0,0xffff v0 = 0xcffcffff
* lw v1,0x0(a2) v1 = *field
* lui a0,0x880 a0 = 0x08800000
* or v1,v1,a0 v1 |= 0x08800000
* and v0,v1,v0 v0 = v1 & 0xcffcffff
* andi v1,v1,0x100 v1 &= 0x100
* beq v1,zero,exit if (!(v1 & 0x100)) skip the second update
* sw v0,0x0(a2) (delay slot) *field = v0
* lw v0,0x4(a2) v0 = *(int *)(field + 1)
* nop (load delay)
* ori v0,v0,0x2000 v0 |= 0x2000
* sw v0,0x4(a2) *(int *)(field + 1) = v0
* exit:
* jr ra
* sb a1,0x15(a2) (delay slot) *(char *)(field + 0x15) = value
*
* LIMITS: the masks, the offsets and the widths are evidence; the object's and
* the register's meanings are hypotheses. Neither access is marked `volatile`
* because that is not needed to reproduce these bytes.
*/
void func_80089D14(char *object, char value) {
int *field = *(int **)(object + 0x1c);
int bits = *field | 0x08800000;
int masked = bits & 0xcffcffff;
*field = masked;
if ((bits & 0x100) != 0) {
*(int *)(field + 1) |= 0x2000;
}
*(char *)((char *)field + 0x15) = value;
}
+34
View File
@@ -0,0 +1,34 @@
/*
* func_800F75D0 — 60 bytes at 0x800F75D0..0x800F760C
*
* Byte-identical reconstruction of a five-field bit pack. Every source is
* masked and shifted into place and the five pieces are OR'd in the order the
* bytes prove. `sra` (not `srl`) fixes ARITHMETIC right shifts, so the shifted
* operands are signed — that is why the parameters are declared `int`; the masks
* make the values non-negative at run time, but the compiler only sees the type.
*
* The observed instructions are:
* andi v0,a0,0x3 first & 3
* sll v0,v0,0x7 << 7
* andi a1,a1,0x3 second & 3
* sll a1,a1,0x5 << 5
* or v0,v0,a1
* andi v1,a3,0x100 fourth & 0x100
* sra v1,v1,0x4 >> 4
* or v0,v0,v1
* andi a2,a2,0x3ff third & 0x3ff
* sra a2,a2,0x6 >> 6
* or v0,v0,a2
* andi a3,a3,0x200 fourth & 0x200
* sll a3,a3,0x2 << 2
* jr ra
* or v0,v0,a3 (delay slot) v0 |= a3
*
* LIMITS: the masks, the shift amounts and the OR order are evidence; the
* packed word's meaning and the parameters' meanings are hypotheses.
*/
int func_800F75D0(int first, int second, int third, int fourth) {
return ((first & 3) << 7) | ((second & 3) << 5) | ((fourth & 0x100) >> 4) |
((third & 0x3ff) >> 6) | ((fourth & 0x200) << 2);
}