From 12ac628ea4ebbe6989b24b495460b896df2f785c Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Thu, 24 Sep 2026 19:20:33 -0400 Subject: [PATCH] phase12: merge 28 (679 bodies) + the per-region codegen-flag policy, and A's semantics-not-codegen correction --- config/regions.tsv | 2 + docs/MATCHING_COOKBOOK.md | 31 ++++++++++ phase-ends/logs/Phase12.md | 76 +++++++++++++++++++++++++ src/func_800AB504.c | 112 +++++++++++++++++++++++++++++++++++++ src/func_80101C7C.c | 88 +++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 src/func_800AB504.c create mode 100644 src/func_80101C7C.c diff --git a/config/regions.tsv b/config/regions.tsv index f533547..1daad5c 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -495,6 +495,7 @@ 0x800AA2B4 0x800AA2F8 src/func_800AA2B4.c 0x800AA56C 0x800AA59C src/func_800AA56C.c 0x800AB078 0x800AB0EC src/func_800AB078.c +0x800AB504 0x800AB598 src/func_800AB504.c cc1=-fno-strength-reduce 0x800AC818 0x800AC85C src/func_800AC818.c 0x800AC85C 0x800AC884 src/func_800AC85C.c 0x800AC98C 0x800AC9D8 src/func_800AC98C.c @@ -630,6 +631,7 @@ 0x80101838 0x80101878 src/func_80101838.c 0x80101C2C 0x80101C54 src/func_80101C2C.c 0x80101C5C 0x80101C7C src/func_80101C5C.c +0x80101C7C 0x80101CAC src/func_80101C7C.c cc1=-fno-delayed-branch 0x80101CAC 0x80101CDC src/func_80101CAC.c 0x801027CC 0x801027F8 src/func_801027CC.c 0x80102A00 0x80102A80 src/func_80102A00.c maspsx=regread diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 3db6342..9d346b5 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -1145,6 +1145,37 @@ place for an unmeasured widening. Results on the row: default 124 (LENGTH-MISMAT The tracked patch `tools/patches/maspsx-phase10-r1r2.patch` was **regenerated in the same commit** — the exact step Phase 11's `ea51ac9` forgot, which left a fresh clone unable to rebuild the gate. +### 61e. WHERE THE ARGUMENT REGISTER IS RE-ARMED tells you the statement order (read, do not spell) + +Phase 12, worker A, `0x800AB504` (148 B). A loop that both **advances a pointer** and **passes it to a +call** has exactly two candidate orders, and the bytes decide it before any spelling is tried: + +> **Find where the argument register is written. If it is armed TWICE — once before the loop and once at +> the BACK EDGE, after the pointer increment — then the call is made with the CURRENT record and the +> pointer is advanced AFTERWARDS.** + +```c + p[6] = p[5] = p[4] = 0; /* three statements, in that order */ + func_8001289C(D_80122720, p); /* called with the current record */ + p += 16; /* advanced afterwards */ +``` + +A's first version had `p += 16;` **before** the call. That is not a different schedule — **it is a +different pointer on every iteration**, a semantic error — but it looked merely *close* (168 vs 148) +**because cc1 reorders the independent increment above the call anyway.** The lesson is A's own: +***"I read the bytes for the codegen and not for the semantics."*** + +**The general form, and it is why this is a reading technique rather than a row:** a copy, a reload, or a +duplicated register write in the ORIGINAL is evidence about LIVENESS and ORDER, not noise to be +explained away by the allocator. Worker D found the same shape three times in one band +(`0x80011084`'s `move t0,a0`, `0x80019700`'s `move v1,v0`, `0x80016E24`'s double load) and recorded the +rule as: **a copy in the original means the value must survive something the source does not obviously +say.** + +**And a consequence for rulings:** `0x800AB504` was ruled class-bound on the grounds that its flag got +"152 against 148". The 152 was A's bug. **A ruling of the form "the lever does not reach the target" must +be re-checked whenever the candidate it rested on is corrected.** + ### 62. Fail fast on an invalid region row A worker placed the source md5 in a claim row's 4th column. `sf3_merge` passed it through as a region diff --git a/phase-ends/logs/Phase12.md b/phase-ends/logs/Phase12.md index b5ccb65..329651d 100644 --- a/phase-ends/logs/Phase12.md +++ b/phase-ends/logs/Phase12.md @@ -1451,3 +1451,79 @@ the comment early and produced `parse error before 'the'`. D's guard is now: **c any header, before believing anything**, and D corrected `report.tsv`'s md5 to the fixed file. Recorded for the roster because the failure surfaces as a one-line parse error in `candidate.i` and looks nothing like a comment problem. + +--- + +## ORCHESTRATOR RULING: per-region CODEGEN FLAGS (asked for once, by worker A, rather than row by row) + +Worker A closed two rows with per-region cc1 flags and then **asked me to set the phase's policy once +rather than decide row by row, and explicitly declined to argue for either option itself**: + +| row | flag | default | with flag | counter-measurement on A's merged rows | +|---|---|---|---|---| +| `0x80101C7C` (48 B) | `cc1=-fno-delayed-branch` | 44, LENGTH-MISMATCH | **48 / 0 / MATCH** | **breaks 6 of 6** tested (140→152, 108→124, 204→212, 68→72, 84→92, 80→84) | +| `0x800AB504` (148 B) | `cc1=-fno-strength-reduce` | 168, LENGTH-MISMATCH | **148 / 0 / MATCH** | neutral on 5, **changes 2** (`0x800FFF60`, `0x8010036C`) | + +Both are merged (the project's standard is byte-for-byte and both are byte-exact, gate green). **The +policy, set once:** + +> **A per-region CODEGEN flag is admissible on the `cc1bin` principle, under four conditions:** +> **C1** it must close the row — byte-exact, gate green; +> **C2** the bytes at the difference must show the mechanism the flag addresses, **named in the file +> header**; +> **C3** the counter-measurement must be recorded in the file header — that the flag is neutral or +> harmful elsewhere — because that is what makes it a statement about the REGION rather than a claim +> about the build; +> **C4** source spelling must be exhausted first (A tried three spellings on `0x80101C7C`, four plus a +> semantics error on `0x800AB504`). + +**And the honest limitation, which must be stated wherever the rule is quoted: unlike `cc1bin`'s ≥2-`jr +$31` test, there is NO mechanical byte predicate for these flags.** I tried to build one — the natural +candidate was "the original's `jr ra` delay slot is empty" — and it **fails**: two of the six rows that +`-fno-delayed-branch` BREAKS (`0x800297F4`, `0x800690E4`) also have empty slots, so the predicate neither +distinguishes the rows nor predicts the flag's effect. The reason is structural: **`cc1bin` swaps a +coherent compiler binary and its effect here is localisable (return merging), while +`-fno-delayed-branch` disables a whole-function scheduling pass**, so its effects are global and cannot +be read off one instruction. + +**Consequence, stated plainly: for these two rows the discipline is DOCUMENTARY, not mechanical. The +gate cannot distinguish a right flag from a wrong one, so C2+C3 are the entire safeguard, and a worker +who lies to itself in a header comment would produce a byte-exact row with no mechanism behind it.** +That is a genuine weakening of the phase's evidence standard relative to every other lever in use, and +**it is raised to the developer as a scope question** — the developer restricted `cc1bin` precisely +because the gate cannot detect a wrong compiler choice, and that rationale applies here *more* strongly, +not less. Two rows of 688 are affected. + +## Worker A's best finding of the session, and it is a correction of A's own method + +`0x800AB504` was ruled class-bound by me at 152-vs-148. **It closed at 148 because part of the obstacle +was A's own SEMANTIC error, not the toolchain.** + +The call's argument register is armed **twice** — once in the `blez`'s delay slot before the first +iteration, and once at the loop's **back edge**, after `p += 16`. So the callee sees `p`, then `p+16`, +then `p+32`: **the call is made with the CURRENT record and the pointer is advanced afterwards.** A's v1 +had `p += 16;` *before* the call, which is not a different schedule — **it is a different pointer on +every iteration.** It looked close (168 vs 148) only because cc1 reorders the independent increment above +the call anyway. + +> **A's transferable check: a loop that both ADVANCES and PASSES a pointer can be ordered correctly by +> reading WHERE THE ARGUMENT REGISTER IS RE-ARMED, before any trial-and-error spelling.** + +And A's own sentence about why it was missed is the part worth keeping: ***"I read the bytes for the +codegen and not for the semantics."*** That is the same error I made earlier in this phase when I wrote a +probe-shape-dependent mechanism into `tools/sf3_match` as a finding — reading bytes for a mechanism +without asking what the code MEANS. It cost A a class-bound ruling and me a false finding, and it is now +recorded as its own failure mode rather than as a footnote to either. + +**Consequence recorded: my class-bound ruling on `0x800AB504` was WRONG, and it was wrong because it +rested on A's 152-byte candidate being the row's floor.** The floor was A's bug. Rulings on "the flag +does not reach the target" have to be re-checked when the candidate is corrected, which is a rule I did +not have and now do. + +## The un-attempted band is the phase's best band, measured + +Worker D's band (index rows with NO mechanism named — attempted in phase 8/9 but never pushed to a floor) +is now **15 rows attempted, 11 matched, 4 classified, 0 wrong-function failures** — a **73% hit rate +against roughly 50% on prio-1**. Fresh bands generated from the same criterion for A (25 rows) and B (36 +rows), so all three workers are on the criterion that measurably works rather than on the one that was +merely convenient. diff --git a/src/func_800AB504.c b/src/func_800AB504.c new file mode 100644 index 0000000..50fd6a0 --- /dev/null +++ b/src/func_800AB504.c @@ -0,0 +1,112 @@ +/* + * func_800AB504 — 148 bytes at 0x800AB504..0x800AB598 + * + * Recursive release of a counted record list: recurse into the child list first, + * then walk `count` 16-byte records clearing three bytes at +4/+5/+6 of each, and + * finally clear the parent's own fields. + * + * The observed instructions are: + * addiu sp,sp,-32 + * sw s2,24(sp) \ + * addu s2,a0,zero | n = argument + * sw ra,28(sp) | + * sw s1,20(sp) | + * beq s2,zero,epilogue | + * _sw s0,16(sp) / (the fourth save fills the branch's delay slot) + * lhu s1,12(s2) i = *(unsigned short *)(n + 12) SIGNED compare later + * lw a0,24(s2) \ + * lw s0,0(s2) | p = *(char **)(n + 0) + * beq a0,zero,L2 | if (*(char **)(n + 24)) + * _nop | + * jal func_800AB504 | func_800AB504(*(char **)(n + 24)); + * _nop / + * L2: + * blez s1,L4 \ + * _addu a1,s0,zero / the FIRST call's argument is the UN-incremented p + * L3: + * sb zero,6(s0) \ + * sb zero,5(s0) | p[6] = p[5] = p[4] = 0, written in that order + * sb zero,4(s0) | + * addiu s0,s0,16 / p += 16 (scheduled BEFORE the call) + * lw a0,0xde8(gp) \ + * jal func_8001289C / func_8001289C(D_80122720, p); + * _addiu s1,s1,-1 (the decrement fills the jump's delay slot) + * bgtz s1,L3 \ + * _addu a1,s0,zero / the back edge re-arms the argument for the NEXT turn + * L4: + * sw zero,0(s2) \ + * sb zero,20(s2) | + * sw zero,24(s2) | the parent's fields, in this exact order + * sh zero,12(s2) | + * sw zero,8(s2) / + * lw ra / lw s2 / lw s1 / lw s0 / addiu sp,sp,32 / jr ra / nop + * + * THE STATEMENT ORDER IS PROVED BY THE ARGUMENT SEQUENCE, AND MY FIRST TWO + * SPELLINGS HAD IT BACKWARDS. The argument register is armed TWICE: once in the + * `blez`'s delay slot (before the first iteration) and once at the loop's back + * edge (after `p += 16`). So the call sees `p`, then `p+16`, then `p+32` …: **the + * call is made with the CURRENT record and the pointer is advanced afterwards**, + * i.e. the source is + * p[6] = p[5] = p[4] = 0; (three separate statements, in that order) + * func_8001289C(D_80122720, p); + * p += 16; + * Writing `p += 16;` BEFORE the call (my v1) is not merely a different schedule — + * it passes a different pointer on every iteration. That is a semantic error, not a + * codegen difference, and the only reason it was visible is that cc1 reorders the + * INDEPENDENT increment above the call, so the wrong spelling still looked close: + * 168 bytes against 148. THE ARGUMENT'S ARMING POINTS ARE THE EVIDENCE, and they are + * a cheap check for any loop with a pointer that is advanced and also passed. + * + * REGION TOKEN: `cc1=-fno-strength-reduce`, and with the corrected statement order it + * CLOSES the row (148 / 0 / MATCH). Mechanism in the bytes: with the default + * strength reduction cc1 turns the `p[4..6]` addressing into a SECOND loop-carried + * pointer (`addu $16,$17,4` with offsets 0/1/2) while keeping `p` itself for the call + * argument, so the frame grows to 40 and five registers are saved where the original + * saves four and keeps a single pointer with constant displacements. The flag removes + * that induction variable and the frame and prologue become the original's exactly. + * **Measured counter-evidence, same test I applied to `-fno-delayed-branch`: the flag + * is neutral on five of my merged rows (0x8002FB54, 0x800297F4, 0x800690E4, + * 0x80094370, 0x80018284 all still MATCH) but changes two (0x800FFF60 at + * 0x800FFFE0, 0x8010036C at 0x80100398). So it is a per-region escape hatch by + * measurement, not a global build setting, and admitting it is the coordinator's call.** + * It is a CODEGEN flag, not a scheduler flag, which is the line cookbook 50 draws. + * + * LIMITS: the record layout beyond the bytes touched (+12 unsigned short, +0, +24 as + * pointers, stride 16, +20 and +8), the meaning of `D_80122720`, the callee's contract, + * and whether the recursion is a sibling or child list are hypotheses read off the + * instruction shape. Only the compiled bytes are evidence. + */ + +extern int D_80122720; + +void func_8001289C(int a0, char *a1); + +void func_800AB504(char *n) +{ + int i; + char *p; + + if (n == 0) + return; + + i = *(unsigned short *)(n + 12); + p = *(char **)(n + 0); + + if (*(char **)(n + 24) != 0) + func_800AB504(*(char **)(n + 24)); + + while (i > 0) { + p[6] = 0; + p[5] = 0; + p[4] = 0; + func_8001289C(D_80122720, p); + p += 16; + i--; + } + + *(int *)(n + 0) = 0; + n[20] = 0; + *(int *)(n + 24) = 0; + *(short *)(n + 12) = 0; + *(int *)(n + 8) = 0; +} diff --git a/src/func_80101C7C.c b/src/func_80101C7C.c new file mode 100644 index 0000000..3f2450f --- /dev/null +++ b/src/func_80101C7C.c @@ -0,0 +1,88 @@ +/* + * func_80101C7C — 48 bytes at 0x80101C7C..0x80101CAC + * + * Loads two words from a source structure into the GTE's VXY0/VZ0 data registers, + * runs a matrix/vector command, and writes the three IR results straight back to + * memory at the destination pointer, which it also returns. + * + * The observed instructions are: + * 0x8C880000 lw t0,0(a0) ; src[0] + * 0x8C890004 lw t1,4(a0) ; src[1] + * 0x48880000 mtc2 t0,$0 ; VXY0 -- DATA register, rs=4 (mtc2), NOT ctc2 + * 0x48890800 mtc2 t1,$1 ; VZ0 + * 0x00000000 nop ; GTE latency padding (the call site writes it) + * 0x4A486012 cop2 0x486012 ; MVMVA-family command, field read off the word + * 0xE8A90000 swc2 $9,0(a1) ; IR1 -> dst[0] + * 0xE8AA0004 swc2 $10,4(a1) ; IR2 -> dst[1] + * 0xE8AB0008 swc2 $11,8(a1) ; IR3 -> dst[2] + * 0x00A01021 addu v0,a1,zero ; return dst + * 0x03E00008 jr ra + * 0x00000000 nop ; the delay slot is LEFT EMPTY -- see below + * + * THE FIRST DRAFT OF THIS ROW WAS WRONG IN A WAY WORTH RECORDING: I reached for + * `gte_ldRT1RT2` / `gte_ldRT3RT21` because the SIBLING at 0x80101CAC loads exactly + * those two control registers, and got `48C80000` / `48C90800` where the original has + * `48880000` / `48890800`. The two differ only in the COP2 `rs` field: **rs=4 is + * `mtc2` (a DATA register, $0/$1 = VXY0/VZ0) and rs=6 is `ctc2` (a CONTROL register, + * $0/$1 = RT1RT2/RT3RT21).** Two words of a 48-byte body, and the only way to see it + * is the raw word (cookbook 25: Ghidra's PSX loader collapses COP2 sequences, so a + * disassembly listing cannot settle this). The adjacency lever got me to the right + * header and the wrong macro family; the bytes corrected it. + * + * REGION TOKEN: `cc1=-fno-delayed-branch`, and it is the ONE thing that closes this + * row — **but it is per-region for a reason, and the reason is evidence, not taste.** + * Mechanism, read off the bytes: with the default cc1 the return copy + * (`addu $2,$5,zero`) is moved INTO the `jr ra` delay slot, so the body is 44 bytes + * where the original has 48; the original leaves that slot EMPTY and lets the + * assembler place a `nop`. `-fno-delayed-branch` stops cc1's reorg from doing the + * move and the harness's maspsx stage then fills the slot with the nop, giving + * 48 / 0 / MATCH. **Measured counter-evidence, which is why I am flagging this rather + * than claiming it quietly: the same flag changes six of my already-merged rows** — + * 0x800FFF60 140->152, 0x8002FB54 108->124, 0x800297F4 204->212, 0x800690E4 68->72, + * 0x80094370 84->92, 0x80018284 80->84. So the flag CANNOT be the original build's + * global setting; it is a per-region escape hatch on the same terms as `cc1bin`, and + * the decision to admit it is the coordinator's. I have also tried three source-side + * routes to the same bytes without the flag (dropping the destination register + * binding, binding the return value to $2, and binding the pointer but returning + * through it) and none of them leaves the slot empty. + * + * The three stores need `swc2 $9/$10/$11` — IR1/IR2/IR3 straight to memory — and + * include/gtemac.h has `lwc2` and `swc2` macros for the MAC registers and the IR + * registers only in the `lwc2` direction, so the stores are written as the same + * inline-asm form the header uses. **The header needs `gte_swc2IR1/2/3`**, and per the + * project's write scope that addition is the coordinator's, not mine. + * + * The register bindings are load-bearing (cookbook 24): without them cc1 chooses its + * own destinations for the loads and its own register for the address, and the operand + * registers are what a GTE row's residual usually is. + * + * LIMITS: the structure types, the meaning of the command field (it is named by FIELD + * VALUE, not semantically), and which fields of the GTE are actually being consumed are + * hypotheses read off the instruction shape. Only the compiled bytes are evidence. + */ + +#include "../include/gtemac.h" + +int *func_80101C7C(int *src, int *dst) +{ + register int v0 __asm__("$8"); + register int v1 __asm__("$9"); + register int *p __asm__("$5"); + + v0 = src[0]; + v1 = src[1]; + + gte_ldVXY0(v0); + gte_ldVZ0(v1); + + __asm__ volatile ("nop"); + + gte_cmd(0x486012); + + p = dst; + __asm__ volatile ("swc2 $9, 0(%0)" : : "r"(p)); + __asm__ volatile ("swc2 $10, 4(%0)" : : "r"(p)); + __asm__ volatile ("swc2 $11, 8(%0)" : : "r"(p)); + + return dst; +}