Two harness gaps were closed as per-region overrides, both found by the workers
and implemented by the coordinator:
1. maspsx=off. Worker A isolated the $at-macro-store-in-the-jr-delay-slot shape
with a decisive experiment: cc1 emits an empty delay slot, maspsx fills it with
a nop, and GNU as in reorder mode fills it properly by moving the last half of
the expanded store. Six regions need this, including 0x800F3160 -- Phase 5's
first recorded bounded negative, unreachable for three phases -- and 0x800F8AEC,
0x800F8B58, 0x800F8B6C, 0x800F8FE4, 0x80102B10. Worker B reverse-engineered the
same mechanism independently from the cc1 output.
2. gp=-NAME. The gp marker is per symbol, but the original's access form is per
SITE: 0x80121F84 is read gp-relative at 0x800A80BC and written absolutely at
0x8002D288, in the same function. A byte search confirmed 6 gp-relative
accesses at d=0x64c, so worker B's census row was right and worker A's source
was right; only a per-region exclusion can express both. This makes cookbook
finding 10 incomplete.
Both overrides are validated (199 tests, 10 added), and sf3_merge gained
--skip-registered because a worker's claims file is naturally cumulative.
Cycle-2 merge: 66 claims, 58 accepted, 8 skipped, 0 rejected. Candidate gate:
c_regions=115, 0 differing bytes, SHA-1 e173426c. Promoted, then make check green:
199 tests, regions=115 disagreements=0, c_regions=115 MATCH.
Distinct matched bodies: 51 -> 109, past the phase milestone of 70.
P8-T2's protocol record plus worker B's cycle-1 merge.
Worker B verified 8 claims (7 bodies: g0029 is a two-address duplicate group).
It also classified its whole 946-row partition off one objdump and found that 226
rows (24%) access gp-relative globals, staging 335 distinct addresses. Those were
checked before use: every row is arithmetically exact under the list's decimal d
column (335 rows, 0 errors), and four sampled globals were confirmed against my
own Ghidra disassembly (0x564(gp) -> 0x80121E9C and three others). A risk check
for whether a new gp marker could change an already-matched region found only
worker A's unclaimed in-progress files, so no registered region is affected.
323 rows were new (12 already present), taking config/symbols.tsv to 354 rows and
unblocking a quarter of worker B's partition.
Candidate gate: c_regions=57, 0 differing bytes, SHA-1 e173426c. Promoted, then
make check green: 189 tests, regions=57 disagreements=0, c_regions=57 MATCH.
Distinct matched bodies: 34 -> 51.
Worker B also deferred 0x8001D98C (g0053, 436B, GTE 3x3 transform) into the
bounded GTE class, and recorded two byte-proven cookbook findings: BCD nibble
pairs must be their own parenthesised subexpression (a pure association tell, 9
shifted bytes), and a constant absolute address in a load folds the
carry-adjusted low half into the load displacement rather than using lui+ori.
P8-T2 plus the first verified cycle-1 merge.
tools/sf3_merge validates worker claims before anything tracked changes: a claim
is accepted only if its extent exists in the derived extents table and is graded
exact with exactly the claimed end, its source is a repo-relative
src/func_XXXXXXXX.c that exists, and it overlaps neither the registry nor another
worker's claim. Rejections are reported with reasons and nothing is written.
The workflow it enables is stronger than the plan's wording: merge to a CANDIDATE
registry, gate the candidate, and promote only on MATCH, so the tracked registry
never contains an unverified claim.
Worker A reported 10 claims (target 8) and correctly refused to edit the shared
symbol registry itself, instead requesting 9 gp-marked rows; each was checked
arithmetically as gp + d. Candidate gate: c_regions=49, 0 differing bytes, SHA-1
e173426c. Promoted, then make check green: 189 tests, regions=49
disagreements=0, c_regions=49 MATCH.
Worker A also reported a significant blocker: two of the remaining duplicate
groups are GTE (COP2) bodies -- 0x80018CB0 (3 addresses) and 0x8001084C (2
addresses, the 712-byte shared body) -- plus 0x80103A94 and 0x80103B60. That is
four GTE functions, which raises the value of the bounded SDK-shape
investigation in P8-T5.
Both batch targets are met in one pass: 22 new bodies (23 including the P7-T5
match), taking the project from 11 distinct bodies to 34 -- past the phase
milestone of 30 -- across 39 registered regions.
The batch is dominated by the small leaf shapes the worklist ranks first:
empty function (4 duplicate addresses), byte/halfword/word setters, field
getters, a word-buffer clearer, and eight gp-relative getters/setters. Every one
was byte-identical on the first or second attempt, and every extent came from
tools/sf3_extents rather than a hand-derived end.
Three shapes needed diagnosis, and two were solved:
- 0x8003636C was 16 bytes instead of 12 because `return 1` forced a second
`li v0,1` into the delay slot; the original leaves 1 in v0 as scratch, so the
function is `void`.
- 0x800F7FB4 needed the parameter reused as the loop counter.
Four candidates are recorded as bounded negatives or deferrals rather than
guesses:
- 0x800F7FB4: the loop body reproduces exactly, but the original has no frame
and every tested formulation allocates 8 bytes (five C forms, and an
-O1/-O2/-O3/-fomit-frame-pointer matrix). Stopped after two distinct attempts.
- 0x800F8AEC: the original schedules `lui` / `jr ra` / `addiu` with the low half
in the delay slot; the reconstruction emits `lui` / `addiu` / `jr ra` / `nop`.
- 0x80010810: GTE (COP2) code needing the SDK's GTE macros, which the build has
no headers for.
- 0x800FB5D4 (`move v0,sp`) and 0x80103FCC/0x80103FEC (BIOS `syscall` wrappers)
and 0x8001EAFC (a shared jump block, not a standalone function).
make gate: c_regions=39, 0 differing bytes, SHA-1 e173426c. Extents verify agrees
on all 39 regions. 168 synthetic tests.
Every cross-reference used to need a hand-written config/symbols.tsv row, which
does not scale to a batch. A symbol whose name is an address now resolves to that
address with no row: func_XXXXXXXX, D_XXXXXXXX, g_XXXXXXXX, lbl_XXXXXXXX. This is
the convention the registry already used, applied without the row.
The names come from the object's own undefined-symbol list (nm -u), not from a
guess about the source, so a name the source defines but never references is
never mistaken for one needing resolution. A registry row still wins, which is
how a gp marker or a real name is attached. Anything neither registered nor
address-shaped now fails before the link with a message naming the symbol and the
fix, instead of a bare ld diagnostic.
First match from the worklist: func_800F8F9C (36 bytes, worklist rank 3), a
duplicate-group representative with a frame and a call. It matched on the first
attempt and is registered twice (0x800F8F9C, 0x80109314) against one source, so
two functions were matched for one body. Its callee needed no registry row.
Ghidra's independent body agrees with the derived extent.
make gate: c_regions=14, 0 differing bytes, SHA-1 e173426c. 168 synthetic tests.
P5-T5. Part A: the entry [0x800FB368,0x800FB410) is not compiler output. The
return address is round-tripped through an absolute global around the first
call, the range ends in break, the stack pointer is built from linker globals,
and the clear loop falls through with no jr ra. Two bounded compile experiments
failed and were stopped; the entry stays fallback and no C is claimed.
Part B: first byte-identical C match -- func_80017AD4 at 0x80017AD4..0x80017AE8
(20 bytes), src/func_80017AD4.c, registered in config/regions.tsv. make gate
reports c_regions=1, 0 differing bytes, SHA-1 e173426c157384ebf1b6caf8c6fea18a85a14af9.
Body is unique with one caller at 0x80014C14; a shared-tail near-miss was
checked and rejected as a duplicate. The Phase 3 baseline is unaffected.
Records six codegen findings, notably that GNU as expands the la macro with ori
while the original assembler (ASPSX 2.81) uses addiu, so la-using functions will
need maspsx. No ROM-derived material is tracked.