phase12: merge 28 (679 bodies) + the per-region codegen-flag policy, and A's semantics-not-codegen correction

This commit is contained in:
Christopher Williams
2026-09-24 19:20:33 -04:00
parent 656e78d9f1
commit 12ac628ea4
5 changed files with 309 additions and 0 deletions
+2
View File
@@ -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
1 # Code-region registry: one C region per matched function.
495 0x800AA2B4
496 0x800AA56C
497 0x800AB078
498 0x800AB504
499 0x800AC818
500 0x800AC85C
501 0x800AC98C
631 0x80101838
632 0x80101C2C
633 0x80101C5C
634 0x80101C7C
635 0x80101CAC
636 0x801027CC
637 0x80102A00
+31
View File
@@ -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
+76
View File
@@ -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.
+112
View File
@@ -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;
}
+88
View File
@@ -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;
}