From 6ef29e45e945af2c207a67c498c280f7d46fdd4b Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Wed, 23 Sep 2026 23:26:49 -0400 Subject: [PATCH] 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. --- config/regions.tsv | 5 ++++ docs/MATCHING_CONVENTIONS.md | 47 +++++++++++++++++++++++++++++++- src/func_80026180.c | 46 +++++++++++++++++++++++++++++++ src/func_8003768C.c | 46 +++++++++++++++++++++++++++++++ src/func_800681A4.c | 53 ++++++++++++++++++++++++++++++++++++ src/func_80089D14.c | 46 +++++++++++++++++++++++++++++++ src/func_800F75D0.c | 34 +++++++++++++++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 src/func_80026180.c create mode 100644 src/func_8003768C.c create mode 100644 src/func_800681A4.c create mode 100644 src/func_80089D14.c create mode 100644 src/func_800F75D0.c diff --git a/config/regions.tsv b/config/regions.tsv index ced0808..6d9c3c7 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -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 diff --git a/docs/MATCHING_CONVENTIONS.md b/docs/MATCHING_CONVENTIONS.md index ac679ff..5d2f96f 100644 --- a/docs/MATCHING_CONVENTIONS.md +++ b/docs/MATCHING_CONVENTIONS.md @@ -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=` 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. diff --git a/src/func_80026180.c b/src/func_80026180.c new file mode 100644 index 0000000..ef5e104 --- /dev/null +++ b/src/func_80026180.c @@ -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; +} diff --git a/src/func_8003768C.c b/src/func_8003768C.c new file mode 100644 index 0000000..7d45aca --- /dev/null +++ b/src/func_8003768C.c @@ -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; +} diff --git a/src/func_800681A4.c b/src/func_800681A4.c new file mode 100644 index 0000000..d61cb4d --- /dev/null +++ b/src/func_800681A4.c @@ -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; +} diff --git a/src/func_80089D14.c b/src/func_80089D14.c new file mode 100644 index 0000000..890b8c8 --- /dev/null +++ b/src/func_80089D14.c @@ -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; +} diff --git a/src/func_800F75D0.c b/src/func_800F75D0.c new file mode 100644 index 0000000..b9a5d9f --- /dev/null +++ b/src/func_800F75D0.c @@ -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); +}