mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 05:56:00 -04:00
chore(phase-27): preserve the Fable5 wave-1/2 recon (R20) — 112K, not 260M
The Fable5 discovery sprint's irreplaceable output, banked before the distillation (that's the
pending Max task). Each is the product of a ~250k-token Fable5 pass; the bulk beside them (260M of
RTL dumps + bisection .s under pincrash/) is regenerable and stays ignored (R33).
- wave-1 crack recon: func_8014D820 (block-0 cracked pin-free 261->110, "reused-load-temp
serialization" lever), func_801670E4 (RC-6-not-S3 reclassification + the reg_renumber-swap oracle),
func_8016CBC0 (root-A cracked byte-zero, "coalescing knife-edge" refuted, density-dial lever) +
the workhorse variant.
- pin_crash_sigabrt.md + pincrash/{minimal_repro.c, *.gdb}: the §42e pin-crash wall CHARACTERIZED and
REFUTED as a compiler wall — it's the extract_unit macro-drop (fixed in T5); sched.c:2725
create_reg_dead_note abort; pinned families stage 133/133 clean once macros ride along.
All three wave-1 seeds produced oracle-proven reclassifications refuting cookbook §44-Lever-5 wall
names + new pin-free levers (Fable5 discovers, cheap-Opus applies). Distillation -> cookbook is next.
This commit is contained in:
@@ -60,6 +60,15 @@
|
||||
!/.run/backlog.jsonl
|
||||
!/.run/fuel_manifest.json
|
||||
|
||||
# Phase-27 pin-crash characterization: the minimal repro + gdb harness (the irreplaceable ~KB
|
||||
# core; pin_crash_sigabrt.md above is already covered by giants/*.md). The 260M of RTL dumps +
|
||||
# bisection .s/.c beside them are regenerable via abort.gdb -> stay ignored (R33).
|
||||
!/.run/giants/pincrash/
|
||||
/.run/giants/pincrash/*
|
||||
!/.run/giants/pincrash/minimal_repro.c
|
||||
!/.run/giants/pincrash/*.gdb
|
||||
!/.run/giants/pincrash/*.md
|
||||
|
||||
# Regenerated build output (deterministic churn)
|
||||
/build/
|
||||
/expected/
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#include "common.h"
|
||||
// func_8014D820 -- "attach/track to parent entity" mover (304 ins, ov_SC01_077_after).
|
||||
// param_1 = self entity (s32 base), param_2/param_3 = u16[] pos snapshots (prev/cur).
|
||||
// Delta -> validate tracked parent (e=*(param_1+0x170)) -> ratan2/func_80012A60 heading
|
||||
// gate (<0x400) -> either the 0x3008 special case (func_800139C8 fan-of-3 + func_8014DCE0
|
||||
// collide loop) or the generic func_80135888 loop -> func_80133784 -> write back the pose.
|
||||
// Returns s32 (1 = applied, 0 = detach: *(param_1+0x170)=0).
|
||||
//
|
||||
// §43 return-type-flip pair: the target materializes v0=1 / v0=0 at the two exits, so the
|
||||
// def MUST be s32. engine_core.h declares it void (via DEFINE_func_8014D790, invoked at
|
||||
// split line 1572 -> in scope), so cc1 hard-errors "conflicting types" unless flipped.
|
||||
// Byte-neutral: the sole caller (func_8014D790, engine_core.h:9765) discards the result.
|
||||
// There is NO split-scope self-decl to //@EDIT (func_8014D820 is INCLUDE_ASM'd; the ov077
|
||||
// canon-sig layer lists only its callees). So the ONLY pre-edit is the engine_core.h flip:
|
||||
//
|
||||
// EC_EDIT (src/shared/engine_core.h line 9752, in the DEFINE_func_8014D790 body):
|
||||
// old: extern void func_8014D820(s32 a0, void *a1, void *a2);
|
||||
// new: extern s32 func_8014D820(s32 a0, void *a1, void *a2);
|
||||
//
|
||||
// PINS? NO. Fully block-scoped (typedefs + externs in-body), void*/s32 params (no
|
||||
// s16-by-value -> no K&R param form, no param //@EDIT) -> ×134-clean via family_sweep
|
||||
// --edit-remap (contrast the old pinned seed, which cc1-SIGABRTs in sibling TUs).
|
||||
//
|
||||
// STATUS: STRUCTURALLY COMPLETE, NOT byte-identical. rtu_match: 304/304 ins, all calls/
|
||||
// constants/control-flow/two-pointer loops/unaligned Blk8 copy/apply+detach branches
|
||||
// byte-correct; LCS 237/304 (close=67), pin-free -> beats the prior pinned seed (LCS 228).
|
||||
// Residual = gcc-2.7.2 INTRINSIC sched1/regalloc knife-edge (§31 RC-6 / S3): prologue
|
||||
// deferred param_1->$s4 copy + serialized-vs-parallel dx/dz load temps; sched1 store/load
|
||||
// ORDER in the e3/e5/e7 setup, the func_800139C8 out0/1/2 combine, and the apply block;
|
||||
// dead-dx/dz $s0/$s1 reuse for the ratan2#2 operands. 4 structural probes (reused-t temp,
|
||||
// d2 hoist, alias-removal, out2 store-swap) each cascaded 20+ ins (the RC-6 tell) -> NOT
|
||||
// hand-steerable. Route: permuter (this pin-free seed is permuter-viable) / Fable5.
|
||||
//
|
||||
// OPUS INVESTIGATION (2026-07-11, cheap-Opus giant-cracker, Lever A/B pass): CONFIRMED RC-6.
|
||||
// This IS the best pin-free seed (match_one 261 positional / masked-LCS 226/304; every probe <=226).
|
||||
// Uses the EXACT target hard-reg set ($v0,$v1,$a0-$a3,$t0,$s0-$s5,$ra) + all 304 correct opcodes ->
|
||||
// pure schedule/alloc-ORDER permutation (§31 RC-6 tell: "regs+opcodes exact, placement off, edits explode").
|
||||
// * PROLOGUE ROOT CAUSE (new): the deferred param_1->$s4 copy is PINNED early by sched.c:3199
|
||||
// ("don't delay getting parameters" -> INSN_REF_COUNT=1 on the leading (set pseudo hardreg) run).
|
||||
// The target UN-pins it because it SERIALIZES the dx/dz loads (2-at-a-time thru $v0/$v1), opening
|
||||
// a load-delay slot the copy fills (t12). The serialize-vs-parallel choice is sched1's list-sched
|
||||
// tie-break (rank_for_schedule sched.c:2385/2414) -> NO clean C-lever. Probes: s32 `self=param_1`
|
||||
// copy FOLDS (cse) back into the pin (neutral); `u8* self=(u8*)param_1` cast still lands early.
|
||||
// * LEVER A PARTIALLY VALID but pressure-blocked: merging dz->d1 / dx->dzz is right in spirit
|
||||
// (v2 d2-hoist DID land d1->$s1, t60 exact), but a long-lived dzz allocno then EVICTS e->$s2 and
|
||||
// param_1->$s4 to $s3/$s5 (K4/K2 cascade). The target keeps dzz SHORT-lived by computing it in
|
||||
// ratan2#1's DELAY SLOT (t62) -> not C-expressible w/o disturbing the 6-callee-saved balance.
|
||||
// Source-reorder merge (v1) cascades 226->199. K8: global.c has no coalescing, so the $s0/$s1
|
||||
// reuse must come from sched1 placement, not a C variable. Full evidence: func_8014D820.opus.md.
|
||||
s32 func_8014D820(s32 param_1, void *param_2, void *param_3)
|
||||
{
|
||||
typedef struct { u16 x, y, z, w; } V8;
|
||||
typedef struct { u8 b[8]; } Blk8; /* align-1 -> unaligned lwl/lwr/swl/swr block copy */
|
||||
typedef struct {
|
||||
V8 out0; V8 out1; V8 out2; V8 pvec; V8 dvec;
|
||||
u8 gap[0x10];
|
||||
s32 z0; s32 z1; s32 z2;
|
||||
} Ctx;
|
||||
extern s16 currentLocationId;
|
||||
extern u8 D_801202A0[];
|
||||
extern u8 D_8018876C[];
|
||||
extern u16 D_801152A8, D_801152AC;
|
||||
extern s32 ratan2(s32 a0, s32 a1);
|
||||
extern s32 func_80012A60(s32 a0, s32 a1);
|
||||
extern s32 func_80135A4C(s32 a0, s32 a1, s32 *a2, s32 a3);
|
||||
extern s32 func_80135888(s32 a0, s32 a1, s32 a2, s32 a3);
|
||||
extern void func_800139C8(s32 a0, void *a1, void *a2);
|
||||
extern s32 func_8014DCE0(s32 arg0, s32 arg1, s32 arg2);
|
||||
extern s32 func_80133784(s32 a0, void *a1, s32 a2);
|
||||
|
||||
Ctx f;
|
||||
u8 *e;
|
||||
u8 *p;
|
||||
u16 *p2 = param_2;
|
||||
u16 *p3 = param_3;
|
||||
s32 dx, dz, d1;
|
||||
s32 a, b;
|
||||
s32 e3, e5, e7;
|
||||
s32 ang, r;
|
||||
|
||||
dx = p3[0] - p2[0];
|
||||
dz = p3[2] - p2[2];
|
||||
if ((s16)(dx | dz) == 0) goto fail;
|
||||
|
||||
f.dvec.x = dx;
|
||||
f.dvec.y = p3[1] - p2[1];
|
||||
f.dvec.z = p3[2] - p2[2];
|
||||
|
||||
e = *(u8 **)(param_1 + 0x170);
|
||||
if (*(u16 *)e == 0) goto fail;
|
||||
if ((*(u16 *)(e + 0x5C) & 0x1000) == 0) goto fail;
|
||||
if (*(s32 *)(e + 0x58) == 0) goto fail;
|
||||
if (func_80135A4C(*(s32 *)(e + 0x20), *(s32 *)(e + 0x58), (s32 *)param_2, (s32)param_3) == 0) goto fail;
|
||||
|
||||
d1 = *(u16 *)(e + 0x6) - *(u16 *)(param_1 + 0x6);
|
||||
a = ratan2((s16)dz, (s16)dx);
|
||||
b = ratan2((s16)(*(u16 *)(e + 0xE) - *(u16 *)(param_1 + 0xE)), (s16)d1);
|
||||
a = (s16)func_80012A60((s16)a, (s16)b);
|
||||
if (a >= 0x400) goto fail;
|
||||
|
||||
f.z2 = 0;
|
||||
f.z1 = 0;
|
||||
f.z0 = 0;
|
||||
|
||||
e3 = *(u16 *)(e + 0x6);
|
||||
e5 = *(u16 *)(e + 0xA);
|
||||
e7 = *(u16 *)(e + 0xE);
|
||||
f.dvec.x = e3 + f.dvec.x;
|
||||
f.pvec.x = e3;
|
||||
f.dvec.y = e5 + f.dvec.y + 8;
|
||||
f.pvec.y = e5;
|
||||
f.dvec.z = e7 + f.dvec.z;
|
||||
f.pvec.z = e7;
|
||||
|
||||
if (currentLocationId != 0x3008) {
|
||||
for (p = D_801202A0; p < D_801202A0 + 0x6480; p += 0x10C) {
|
||||
if (p == e) continue;
|
||||
if (*(u16 *)p == 0) continue;
|
||||
if (*(u16 *)(p + 0x5C) == 0) continue;
|
||||
if (*(s32 *)(p + 0x58) == 0) continue;
|
||||
func_80135888(*(s32 *)(p + 0x20), *(s32 *)(p + 0x58), (s32)&f.pvec, (s32)&f.dvec);
|
||||
}
|
||||
} else {
|
||||
ang = ratan2((s16)f.pvec.x - (s16)f.dvec.x, (s16)f.pvec.z - (s16)f.dvec.z) & 0xFFF;
|
||||
func_800139C8(ang, D_8018876C, &f.out0);
|
||||
func_800139C8((s16)(ang - 0x155), D_8018876C, &f.out1);
|
||||
func_800139C8(ang + 0x155, D_8018876C, &f.out2);
|
||||
f.out0.x += f.pvec.x;
|
||||
f.out0.z += f.pvec.z;
|
||||
f.out0.y = f.pvec.y;
|
||||
f.out1.y = f.pvec.y;
|
||||
f.out2.y = f.pvec.y;
|
||||
f.out1.x += f.pvec.x;
|
||||
f.out1.z += f.pvec.z;
|
||||
f.out2.x += f.pvec.x;
|
||||
f.out2.z += f.pvec.z;
|
||||
for (p = D_801202A0; p < D_801202A0 + 0x6480; p += 0x10C) {
|
||||
if (p == e) continue;
|
||||
if (*(u16 *)p == 0) continue;
|
||||
if (*(u16 *)(p + 0x5C) == 0) continue;
|
||||
if (*(s32 *)(p + 0x58) == 0) continue;
|
||||
if (func_8014DCE0((s32)p, (s32)&f.pvec, (s32)&f.out0) != 0) {
|
||||
*(Blk8 *)&f.dvec = *(Blk8 *)&f.pvec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r = func_80133784(0, &f.pvec, (s32)&f.dvec);
|
||||
if (r == 0 || (r == 0x2000 && (D_801152A8 | D_801152AC) == 0)) {
|
||||
*(u16 *)(e + 0x6) = f.dvec.x - f.z0;
|
||||
*(u16 *)(e + 0xA) = f.dvec.y;
|
||||
*(u16 *)(e + 0xE) = f.dvec.z - f.z2;
|
||||
f.dvec.x = f.dvec.x - f.pvec.x;
|
||||
f.dvec.z = f.dvec.z - f.pvec.z;
|
||||
f.dvec.y = f.dvec.y - f.pvec.y;
|
||||
*(u16 *)(param_1 + 0x6) = p2[0] + f.dvec.x;
|
||||
*(u16 *)(param_1 + 0xA) = p2[1] + f.dvec.y + 0x10;
|
||||
*(u16 *)(param_1 + 0xE) = p2[2] + f.dvec.z;
|
||||
} else {
|
||||
*(u16 *)(param_1 + 0x6) = p2[0];
|
||||
*(u16 *)(param_1 + 0xA) = p2[1] + 0x10;
|
||||
*(u16 *)(param_1 + 0xE) = p2[2];
|
||||
}
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
*(s32 *)(param_1 + 0x170) = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// func_8014D820 -- "attach/track to parent entity" mover (304 ins, ov_SC01_077_after).
|
||||
// param_1 = self entity (s32 base), param_2/param_3 = u16[] pos snapshots (prev/cur).
|
||||
// Delta -> validate tracked parent (e=*(self+0x170)) -> ratan2/func_80012A60 heading gate
|
||||
// (<0x400) -> either the 0x3008 special case (func_800139C8 fan-of-3 + func_8014DCE0 collide
|
||||
// loop) or the generic func_80135888 loop -> func_80133784 -> write back the pose.
|
||||
// Returns s32 (1 = applied, 0 = detach: *(self+0x170)=0).
|
||||
//
|
||||
// EC_EDIT (src/shared/engine_core.h line 9752, DEFINE_func_8014D790 body): void->s32 return
|
||||
// flip (unchanged from the opus seed; byte-neutral, the sole caller discards the result).
|
||||
//
|
||||
// PINS? NO. Fully block-scoped; the only asm is ABSENT (this draft is asm-free). `self`/`t0`/`t1`
|
||||
// are plain s32 locals; no `register __asm__("$N")`, no generic-constraint asm, zero file-scope
|
||||
// footprint -> ×134-clean via family_sweep --edit-remap.
|
||||
//
|
||||
// ============================================================================================
|
||||
// FABLE5 gdb-on-cc1 PASS (2026-07-15) -- match_one 261 -> **110 positional** (mine=305/target=304).
|
||||
// The opus "RC-6 whole-function permutation, pin-blocked" verdict is REFUTED at the mechanism
|
||||
// level; the block-0 residual is CRACKED pin-free. Details in func_8014D820.fable.md.
|
||||
//
|
||||
// ROOT-CAUSE CORRECTION (gdb oracle, .run/giants/fable_d820/nopin.gdb):
|
||||
// The opus blamed the sched.c:3199 "don't delay getting parameters" pin (INSN_REF_COUNT=1 on
|
||||
// the leading param copy). FALSE. Patching cc1 to skip BOTH the pin store AND the head-advance
|
||||
// (so insn 4 = `reg72=a0` re-enters the schedulable range) leaves the schedule BYTE-IDENTICAL:
|
||||
// the copy still lands at the block head, the 4 loads still PARALLELIZE. The pin is a red herring.
|
||||
// The real block-0 divergence: the target SERIALIZES the dx/dz load temps (load-pair -> subu ->
|
||||
// load-pair -> subu, reusing $v1/$v0), which opens the load-delay slot the param_1->$s4 copy
|
||||
// fills. A direct-expression draft (`dx = p3[0]-p2[0]`) gives 4 DISTINCT load pseudos -> the
|
||||
// scheduler batches them (parallel, 4 scratch regs) -> no slot for the copy.
|
||||
//
|
||||
// THE CRACK (261 -> 110, PIN-FREE):
|
||||
// (1) SERIALIZE via reused load-temps -- the `{ s32 t0,t1; t0=p3[0]; t1=p2[0]; dx=t0-t1;
|
||||
// t0=p3[2]; t1=p2[2]; dz=t0-t1; }` block. K8/no-coalescing: reusing ONE pair of temp
|
||||
// VARIABLES forces one pseudo each -> the read-before-2nd-write anti-dependence forces the
|
||||
// serial schedule AND the $v1/$v0 reuse -- an exact block-0 load structure + temp regs.
|
||||
// (This is §45 Lever A / the func_801670E4 SPLIT<->MERGE duality applied to LOAD TEMPS.)
|
||||
// (2) ROUTE param_1 through `self` -- `self=param_1;` then all base accesses via `self`. This
|
||||
// moves param_1's callee-saved copy INTO block 0 (it participates in scheduling) instead of
|
||||
// the copy-at-head that param_1-direct produces. (param_1-direct + serial = 287; this = 110.)
|
||||
//
|
||||
// RESIDUAL 110 = ~5 LOCALIZED knife-edges (NOT a whole-function permutation -- see fable.md):
|
||||
// (a) the `self` copy sinks (S2 birthing boost) to block-0 slot2 (dz-delay); target has it in
|
||||
// slot1 (dx-delay). Source position is invariant (sinks to the lowest slot with a live
|
||||
// successor use; both branch-successors use self). 2-set re-tie to stop the sink
|
||||
// MATERIALIZES a move (307/266). INTRINSIC -> permuter.
|
||||
// (b) beqz-delay dvec.x temp reg (a0 vs v1) -- CASCADES from (a): a0 is freed for the temp only
|
||||
// if the copy took slot1.
|
||||
// (c) ratan2 d1/dzz callee-saved reuse ($s0/$s1 order) -- RC-3/Lever-A callee tie; the reuse
|
||||
// form cascaded (129). INTRINSIC -> permuter.
|
||||
// (d) e3/e5/e7 + out0/1/2 combine + writeback store/load ORDER -- coupled schedule (source
|
||||
// reorder cascaded 231).
|
||||
// (e) return-1 const: mine materializes `li v0,1` standalone (the +1 ins); target CROSS-JUMPS
|
||||
// the `sh 0xE(s4); j; li v0,1` tail (§5a). goto-ret1 ADDS an insn (134). Cross-jump-shaped.
|
||||
//
|
||||
// ROUTE: this pin-free 110 seed -> decomp-permuter. Block 0 is now STRUCTURALLY EXACT (serial
|
||||
// loads + correct temp regs), which the old opus seed was not; the prior ILS walled at masked-38
|
||||
// from the worse (parallel-block-0) seed, so a warm restart from here should go lower.
|
||||
s32 func_8014D820(s32 param_1, void *param_2, void *param_3)
|
||||
{
|
||||
typedef struct { u16 x, y, z, w; } V8;
|
||||
typedef struct { u8 b[8]; } Blk8; /* align-1 -> unaligned lwl/lwr/swl/swr block copy */
|
||||
typedef struct {
|
||||
V8 out0; V8 out1; V8 out2; V8 pvec; V8 dvec;
|
||||
u8 gap[0x10];
|
||||
s32 z0; s32 z1; s32 z2;
|
||||
} Ctx;
|
||||
extern s16 currentLocationId;
|
||||
extern u8 D_801202A0[];
|
||||
extern u8 D_8018876C[];
|
||||
extern u16 D_801152A8, D_801152AC;
|
||||
extern s32 ratan2(s32 a0, s32 a1);
|
||||
extern s32 func_80012A60(s32 a0, s32 a1);
|
||||
extern s32 func_80135A4C(s32 a0, s32 a1, s32 *a2, s32 a3);
|
||||
extern s32 func_80135888(s32 a0, s32 a1, s32 a2, s32 a3);
|
||||
extern void func_800139C8(s32 a0, void *a1, void *a2);
|
||||
extern s32 func_8014DCE0(s32 arg0, s32 arg1, s32 arg2);
|
||||
extern s32 func_80133784(s32 a0, void *a1, s32 a2);
|
||||
|
||||
Ctx f;
|
||||
u8 *e;
|
||||
u8 *p;
|
||||
s32 self;
|
||||
u16 *p2 = param_2;
|
||||
u16 *p3 = param_3;
|
||||
s32 dx, dz, d1;
|
||||
s32 a, b;
|
||||
s32 e3, e5, e7;
|
||||
s32 ang, r;
|
||||
|
||||
{
|
||||
s32 t0, t1;
|
||||
self = param_1;
|
||||
t0 = p3[0]; t1 = p2[0]; dx = t0 - t1;
|
||||
t0 = p3[2]; t1 = p2[2]; dz = t0 - t1;
|
||||
}
|
||||
if ((s16)(dx | dz) == 0) goto fail;
|
||||
|
||||
f.dvec.x = dx;
|
||||
f.dvec.y = p3[1] - p2[1];
|
||||
f.dvec.z = p3[2] - p2[2];
|
||||
|
||||
e = *(u8 **)(self + 0x170);
|
||||
if (*(u16 *)e == 0) goto fail;
|
||||
if ((*(u16 *)(e + 0x5C) & 0x1000) == 0) goto fail;
|
||||
if (*(s32 *)(e + 0x58) == 0) goto fail;
|
||||
if (func_80135A4C(*(s32 *)(e + 0x20), *(s32 *)(e + 0x58), (s32 *)param_2, (s32)param_3) == 0) goto fail;
|
||||
|
||||
d1 = *(u16 *)(e + 0x6) - *(u16 *)(param_1 + 0x6);
|
||||
a = ratan2((s16)dz, (s16)dx);
|
||||
b = ratan2((s16)(*(u16 *)(e + 0xE) - *(u16 *)(param_1 + 0xE)), (s16)d1);
|
||||
a = (s16)func_80012A60((s16)a, (s16)b);
|
||||
if (a >= 0x400) goto fail;
|
||||
|
||||
f.z2 = 0;
|
||||
f.z1 = 0;
|
||||
f.z0 = 0;
|
||||
|
||||
e3 = *(u16 *)(e + 0x6);
|
||||
e5 = *(u16 *)(e + 0xA);
|
||||
e7 = *(u16 *)(e + 0xE);
|
||||
f.dvec.x = e3 + f.dvec.x;
|
||||
f.pvec.x = e3;
|
||||
f.dvec.y = e5 + f.dvec.y + 8;
|
||||
f.pvec.y = e5;
|
||||
f.dvec.z = e7 + f.dvec.z;
|
||||
f.pvec.z = e7;
|
||||
|
||||
if (currentLocationId != 0x3008) {
|
||||
for (p = D_801202A0; p < D_801202A0 + 0x6480; p += 0x10C) {
|
||||
if (p == e) continue;
|
||||
if (*(u16 *)p == 0) continue;
|
||||
if (*(u16 *)(p + 0x5C) == 0) continue;
|
||||
if (*(s32 *)(p + 0x58) == 0) continue;
|
||||
func_80135888(*(s32 *)(p + 0x20), *(s32 *)(p + 0x58), (s32)&f.pvec, (s32)&f.dvec);
|
||||
}
|
||||
} else {
|
||||
ang = ratan2((s16)f.pvec.x - (s16)f.dvec.x, (s16)f.pvec.z - (s16)f.dvec.z) & 0xFFF;
|
||||
func_800139C8(ang, D_8018876C, &f.out0);
|
||||
func_800139C8((s16)(ang - 0x155), D_8018876C, &f.out1);
|
||||
func_800139C8(ang + 0x155, D_8018876C, &f.out2);
|
||||
f.out0.x += f.pvec.x;
|
||||
f.out0.z += f.pvec.z;
|
||||
f.out0.y = f.pvec.y;
|
||||
f.out1.y = f.pvec.y;
|
||||
f.out2.y = f.pvec.y;
|
||||
f.out1.x += f.pvec.x;
|
||||
f.out1.z += f.pvec.z;
|
||||
f.out2.x += f.pvec.x;
|
||||
f.out2.z += f.pvec.z;
|
||||
for (p = D_801202A0; p < D_801202A0 + 0x6480; p += 0x10C) {
|
||||
if (p == e) continue;
|
||||
if (*(u16 *)p == 0) continue;
|
||||
if (*(u16 *)(p + 0x5C) == 0) continue;
|
||||
if (*(s32 *)(p + 0x58) == 0) continue;
|
||||
if (func_8014DCE0((s32)p, (s32)&f.pvec, (s32)&f.out0) != 0) {
|
||||
*(Blk8 *)&f.dvec = *(Blk8 *)&f.pvec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r = func_80133784(0, &f.pvec, (s32)&f.dvec);
|
||||
if (r == 0 || (r == 0x2000 && (D_801152A8 | D_801152AC) == 0)) {
|
||||
*(u16 *)(e + 0x6) = f.dvec.x - f.z0;
|
||||
*(u16 *)(e + 0xA) = f.dvec.y;
|
||||
*(u16 *)(e + 0xE) = f.dvec.z - f.z2;
|
||||
f.dvec.x = f.dvec.x - f.pvec.x;
|
||||
f.dvec.z = f.dvec.z - f.pvec.z;
|
||||
f.dvec.y = f.dvec.y - f.pvec.y;
|
||||
*(u16 *)(self + 0x6) = p2[0] + f.dvec.x;
|
||||
*(u16 *)(self + 0xA) = p2[1] + f.dvec.y + 0x10;
|
||||
*(u16 *)(self + 0xE) = p2[2] + f.dvec.z;
|
||||
} else {
|
||||
*(u16 *)(self + 0x6) = p2[0];
|
||||
*(u16 *)(self + 0xA) = p2[1] + 0x10;
|
||||
*(u16 *)(self + 0xE) = p2[2];
|
||||
}
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
*(s32 *)(self + 0x170) = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# func_8014D820 (304 ins, ×134 core, ov_SC01_077_after) — Fable5 gdb-on-cc1 pass
|
||||
|
||||
**Result:** `match_one` **DIFF 110 positional** (mine=305 / target=304), down from the opus seed's
|
||||
**261**. **PIN-FREE, ×134-safe** (asm-free; `self`/`t0`/`t1` plain s32 locals; zero file-scope
|
||||
footprint). Draft: `.run/giants/func_8014D820.fable.c`. **NOT byte-identical → hand off to the
|
||||
decomp-permuter** (this seed is far better than the prior one — block 0 is now structurally exact).
|
||||
|
||||
**Headline: the opus "RC-6 pin-blocked whole-function permutation" verdict is REFUTED.** The
|
||||
sched.c:3199 pin is a red herring (oracle-disproven), and the block-0 residual — the largest,
|
||||
which the opus called intrinsic — is **CRACKED pin-free**. What remains is ~5 *localized* knife-edges,
|
||||
not a whole-function permutation.
|
||||
|
||||
## Verified starting point (not inherited)
|
||||
- `match_one` on the opus seed = **261 mismatched** (matches the ledger; not stale).
|
||||
- Full 261-diff is one cascade from block 0: the target **serializes** the dx/dz load temps through
|
||||
`$v1/$v0` and fills the resulting load-delay slot with the `param_1→$s4` copy; the seed
|
||||
**parallelizes** 4 distinct load pseudos (`$a1/$v1/$a0/$v0`) with the copy floating to the head.
|
||||
Allocation is byte-identical in both (dx→$s0, dz→$s1, param_1→$s4) — pure schedule.
|
||||
|
||||
## The gdb oracle (the decisive experiment) — the pin is NOT the blocker
|
||||
`.run/giants/fable_d820/nopin.gdb` patches cc1 (i386, symbols, no line info) to neutralize the
|
||||
sched.c:3199 "don't delay getting parameters" pin. **Key mechanic:** the pin works by
|
||||
`INSN_REF_COUNT(head)=1` (0x817b3c1) AND `head=NEXT_INSN(head)` (advances head *past* the pinned
|
||||
insn, removing it from the schedulable range `[head,tail]`). Setting ref_count=0 alone is a NO-OP
|
||||
(head already advanced past it). The working patch jumps from the pin store straight to the loop exit
|
||||
(0x817b3c1 → 0x817b43c), skipping BOTH the store and the advance → insn 4 (`reg72=a0`) re-enters the
|
||||
ready list (verified: `insn[ 4]: priority = 1` now appears in the block-0 trace).
|
||||
|
||||
**Outcome: the schedule is byte-identical with or without the pin.** insn 4 has the *lowest LUID*, so
|
||||
`rank_for_schedule`'s stable-sort tie-break (`sched.c:2428 INSN_LUID`) always sorts it to be placed at
|
||||
the block head; the 4 loads still parallelize. **⇒ the deferred-param-copy residual is governed by
|
||||
LUID + the S2 sink, NOT the pin. The opus root cause is wrong.**
|
||||
|
||||
## The block-0 crack (261 → 110, byte-verified ladder), PIN-FREE
|
||||
|
||||
| step | change | mismatch | note |
|
||||
|---|---|---|---|
|
||||
| seed | opus baseline | **261** | parallel loads, copy at head |
|
||||
| v1 | reused load-temps, `param_1` direct | 287 (+2) | **serial loads achieved**, but copy at head + 2 nops |
|
||||
| v2 | + asm-copy of param_1 mid-block | 286 | asm has unknown latency → not a clean slot filler + extra $s5 |
|
||||
| **v3/v_before** | **reused temps + route param_1 through `self`** | **110** | **serial loads + correct temp regs ($v1,$v0) + copy IN block 0** |
|
||||
| v_merge | + Lever-A reuse dz/dx for d1/dzz | 129 | ratan2 callee-order tie cascades |
|
||||
| v_ld | + §45 Lever-D goto-ret1 | 134 | ADDS an insn here (tail is a cross-jump, not a shared goto) |
|
||||
| v_ro | + e3/e5/e7 store reorder | 231 | coupled schedule cascades |
|
||||
|
||||
**The two levers that landed 261→110:**
|
||||
1. **SERIALIZE via reused load-temps.** `{ s32 t0,t1; t0=p3[0]; t1=p2[0]; dx=t0-t1; t0=p3[2];
|
||||
t1=p2[2]; dz=t0-t1; }`. K8 (no coalescing): reusing ONE pair of temp *variables* forces one
|
||||
pseudo each → the **read-before-2nd-write anti-dependence** on the reused pseudo forces the serial
|
||||
schedule (load-pair→subu→load-pair→subu) AND the `$v1/$v0` reuse — reproducing the target's exact
|
||||
block-0 load structure and temp registers. A direct-expression draft gives 4 distinct pseudos → the
|
||||
scheduler batches the loads (parallel, 4 scratch regs) with no delay slot for the copy.
|
||||
2. **ROUTE param_1 through `self`.** `self=param_1;` then all base accesses via `self`. This puts
|
||||
param_1's callee-saved copy INSIDE block 0 (it participates in scheduling) rather than the
|
||||
copy-at-head that `param_1`-direct produces. (Measured: param_1-direct+serial = 287; +self = 110.)
|
||||
|
||||
## Residual 110 — five LOCALIZED knife-edges (per-cluster verdict)
|
||||
|
||||
- **(a) copy in block-0 slot2 (dz-delay) not slot1 (dx-delay) — INTRINSIC (sink) → permuter.**
|
||||
The `self=param_1` body copy SINKS (S2 birthing boost, `sched.c` `birthing_insn_p`) toward its
|
||||
far first use, landing in the *lowest* block-0 slot with a live successor use. Both branch
|
||||
successors use `self` (not-fail: `e=*(self+0x170)`; fail: `*(self+0x170)=0`), so it sinks to the
|
||||
block end = slot2. Source position is invariant (tested `before`/`mid`: both 110). The 2-set
|
||||
dead-re-tie to disable the sink (`__asm__("":"=r"(self):"0"(self))`) MATERIALIZES a move (307/266).
|
||||
The target's copy is *intermediate* (slot1, not head, not slot2) — not source-reachable as a
|
||||
sink-eligible body copy.
|
||||
- **(b) beqz-delay dvec.x temp reg (a0 vs v1) — CASCADES from (a).** The target uses `a0` for the
|
||||
`f.dvec.x = dx` store temp because a0 is freed once param_1's copy took slot1; mine can't free a0
|
||||
as early. Falls out with (a).
|
||||
- **(c) ratan2 d1/dzz $s0/$s1 reuse — INTRINSIC (RC-3 callee tie) → permuter.** The target reuses
|
||||
dz's reg ($s1) for d1 and dx's reg ($s0) for dzz after the first ratan2 (a merged-accumulator).
|
||||
Reproducing it via variable reuse (v_merge) cascaded to 129 — the callee-saved allocno birth order
|
||||
is a density tie, not a coalescing choice.
|
||||
- **(d) e3/e5/e7 + out0/1/2 combine + writeback store/load ORDER — coupled schedule.** Source
|
||||
statement reorder (v_ro) cascaded to 231; these are §T2/S3 ties that only settle once the upstream
|
||||
block-0/ratan2 schedule is exact (permuter territory).
|
||||
- **(e) return-1 const (the +1 instruction) — §5a cross-jump.** Mine materializes `li v0,1`
|
||||
standalone; the target CROSS-JUMPS the two apply branches' common tail (`sh <v>,0xE(s4); j
|
||||
epilogue; li v0,1` in the `j` delay slot). §45 Lever-D (goto-ret1) ADDS an insn here (134) because
|
||||
the shared tail is a cross-jump, not a plain shared block.
|
||||
|
||||
## Cookbook-worthy findings (the highest-value output)
|
||||
|
||||
1. **NEW LEVER — reused-load-temp SERIALIZATION (the MERGE direction, for load temps).** When the
|
||||
target serializes two independent load-fed ops (load-pair → op → load-pair → op, reusing 2 scratch
|
||||
regs) but a direct-expression draft PARALLELIZES them (N distinct scratch regs, all loads batched),
|
||||
**reuse ONE set of named temp variables across both computations.** The read-before-second-write
|
||||
anti-dependence on the reused pseudo (K8, no coalescing) forces the serial schedule AND the
|
||||
2-register reuse in one move. This is **§45 Lever A applied to LOAD TEMPS**, and the MERGE pole of
|
||||
the func_801670E4 SPLIT↔MERGE duality: *a value the target holds in reused scratch across serial
|
||||
regions wants MERGED temp variables; a value it holds in one scratch across disjoint regions wants a
|
||||
per-region FRESH local.* Byte-proven: func_8014D820 block 0, 261→110, PIN-FREE. Cheap-Opus can
|
||||
apply this directly (it's a source rewrite, no asm).
|
||||
|
||||
2. **The sched.c:3199 leading-param-copy pin is a RED HERRING for deferred-param-copy residuals**
|
||||
(gdb-oracle-disproven). A param-copy's schedule position is governed by its LUID + the S2 sink, not
|
||||
the pin. To test any "pinned" hypothesis: patch cc1 to skip BOTH the `INSN_REF_COUNT=1` store AND
|
||||
the `head=NEXT_INSN` advance (setting ref_count=0 alone is insufficient — head is already advanced
|
||||
past the pinned insn). Do NOT attribute a deferred-param-copy diff to sched.c:3199.
|
||||
|
||||
3. **Route a param through a plain `self=param` local to land its callee-saved copy INSIDE the entry
|
||||
block** (vs param-direct → copy at head). The copy survives cse because `self` has real later uses.
|
||||
Useful whenever the target schedules a param→callee-saved copy into an entry-block delay slot. Its
|
||||
exact slot is sink-governed (lowest slot with a live successor use) — so this gets the copy into the
|
||||
block but the permuter/Fable5 must place the final slot.
|
||||
|
||||
## Method / reproducibility
|
||||
- Draft + all variants: `.run/giants/fable_d820/{v1,v2,v3,v_before,v_merge,v_ld,v_ro,v_2set}.c`.
|
||||
- gdb oracle: `.run/giants/fable_d820/nopin.gdb` (pin-neutralizer; the decisive experiment).
|
||||
- Dumps: `.run/giants/dump.sh <c> <prefix>` (pinned triple + `-dS` sched trace); block-0 ready-list
|
||||
trace in the `.i.sched` handed over the parallel-vs-serial mechanism directly (read it before
|
||||
hand-modeling, per §34).
|
||||
- Gate: `match_one func_8014D820 --c … --asm-subdir asm/ov_SC01_077/nonmatchings/ov_SC01_077_after`.
|
||||
|
||||
## Recommendation for the next tier
|
||||
Feed `func_8014D820.fable.c` (110, pin-free, ×134-safe, block-0 structurally exact) to the
|
||||
decomp-permuter as a warm restart. The prior ILS walled at masked-38 from the *worse* (parallel
|
||||
block-0) seed; the four remaining clusters (copy-slot, ratan2 callee-order, store-order, cross-jump
|
||||
tail) are exactly the localized schedule/allocation perturbations the permuter explores. A byte match
|
||||
is plausible from here; if the permuter also walls, the copy-slot (a) and the ratan2 callee-tie (c)
|
||||
are the two genuinely-intrinsic residuals to characterize.
|
||||
@@ -0,0 +1,196 @@
|
||||
/* func_801670E4 (279 ins, ov_SC01_077_jr_8015AE2C) — Fable5 gdb-on-cc1 pass (2026-07-15).
|
||||
* STATUS: match_one DIFF 23/279 (NOT a MATCH), PIN-FREE, x134-safe. Body == the Opus seed
|
||||
* (.run/giants/func_801670E4.opus.c); I could not beat 23 pin-free. The value of this pass is
|
||||
* the RECLASSIFICATION + oracle PROOF of the dominant residual (see .fable.md).
|
||||
*
|
||||
* Residual 23 = region A (idx 19-24, 6: block0 fence-lock, a scheduling permutation) +
|
||||
* region B (idx 112-133, 17, DOMINANT: the pos-subtraction v0/v1 swap).
|
||||
*
|
||||
* ==== KEY FABLE5 FINDING (corrects the seed's S3 verdict) ====
|
||||
* Region B is NOT an S3 scheduling-priority wall. The FINAL schedule already matches the target
|
||||
* byte-for-byte; the ENTIRE residual is register IDENTITY ($v0<->$v1 swapped on the t/r1 family).
|
||||
* PROVEN by gdb oracle: patching reg_renumber at reload entry (0x0815d4d7) to swap
|
||||
* reg78(t)$v1->$v0, reg82(r1)$v0->$v1, reg132/136/140 (the D_ loads), reg127 (0x10 const)
|
||||
* makes cc1 emit region B BYTE-EXACT to the target, INCLUDING the col[1].g-store-before-A4-load
|
||||
* reordering (that reorder is a sched2 CONSEQUENCE of the register identity, not its cause).
|
||||
*
|
||||
* ROOT CAUSE: `t` (reused for pos[0]+pos[1], 2 deaths) is a GLOBAL allocno; `r1` (pos[2], 1 death)
|
||||
* is a LOCAL allocno. local-alloc runs before global-alloc, so the {AC,pos2} local qty grabs $v0
|
||||
* first-fit; this makes the global `t` conflict with $v0 -> forced to $v1. The target needs the
|
||||
* opposite coloring (t=$v0, r1=$v1). It is a self-consistent 2-coloring whose tie the allocator
|
||||
* resolves the wrong way; no pin-free C construct flips it (7 byte-verified failures, see .md).
|
||||
* The seed cracked it only with `r1 __asm__("$3")` (x1-only pin, unusable).
|
||||
*
|
||||
* NEXT TIER: decomp-permuter (may stumble on a globalizing perturbation), OR accept the x1 pin
|
||||
* for this ONE location if x134 propagation is later abandoned. Region A is likewise fence-locked.
|
||||
*/
|
||||
s32 func_801670E4(s32 param_1, s32 param_2, s32 param_3, s32 param_4) {
|
||||
typedef struct { s16 x, y, z, pad; } SVEC;
|
||||
typedef struct {
|
||||
SVEC center; /* 0x00 -> sp+0x10 */
|
||||
SVEC diff; /* 0x08 -> sp+0x18 */
|
||||
s32 pos[3]; /* 0x10 -> sp+0x20 */
|
||||
s32 _pad1c; /* 0x1C -> sp+0x2C */
|
||||
SVEC v[4]; /* 0x20 -> sp+0x30 */
|
||||
struct { u8 r, g, b, pad; } col[4]; /* 0x40 -> sp+0x50 */
|
||||
u32 code; /* 0x50 -> sp+0x60 */
|
||||
u32 _pad54; /* 0x54 -> sp+0x64 */
|
||||
} Fr;
|
||||
extern u8 D_80189A20[];
|
||||
extern u8 D_80189A0C[];
|
||||
extern u8 D_80189A34[];
|
||||
extern s32 D_801269A4;
|
||||
extern s32 D_801269A8;
|
||||
extern s32 D_801269AC;
|
||||
extern u16 D_80126CE0;
|
||||
extern void func_80017E68(void *a0, void *a1);
|
||||
extern void func_800D20C0(void *a0, void *a1, s32 a2);
|
||||
extern void func_800D23D0(void *a0);
|
||||
extern void RotMatrixYXZ(void *a0, void *a1);
|
||||
extern s32 func_80017758(void *a0, void *a1);
|
||||
extern s32 func_80017DC4(void *a0, void *a1);
|
||||
extern void func_80048EAC(void *a0, void *a1);
|
||||
extern s32 func_80149350();
|
||||
|
||||
Fr fr;
|
||||
u8 m1[0x20];
|
||||
u8 m2[0x20];
|
||||
u8 *p;
|
||||
s32 i;
|
||||
s32 t;
|
||||
s32 t1;
|
||||
s32 t2;
|
||||
s32 t3;
|
||||
s32 r1;
|
||||
s32 arg0;
|
||||
s32 d1;
|
||||
s32 d2;
|
||||
s32 c12;
|
||||
u16 h;
|
||||
s32 c12b;
|
||||
|
||||
arg0 = *(s32 *)(param_1 + 0x34);
|
||||
fr.v[1].z = 0;
|
||||
fr.v[1].y = 0;
|
||||
fr.v[1].x = 0;
|
||||
fr.v[3].z = 0;
|
||||
fr.v[2].z = 0;
|
||||
fr.v[0].z = 0;
|
||||
fr.col[1].b = 0x70;
|
||||
c12 = *(u8 *)(param_1 + 0x12);
|
||||
fr.center.x = param_2;
|
||||
fr.center.y = param_3;
|
||||
__asm__ __volatile__("");
|
||||
p = D_80189A20;
|
||||
i = 0;
|
||||
fr.col[0].b = 0;
|
||||
fr.col[0].g = 0;
|
||||
fr.col[0].r = 0;
|
||||
fr.col[2].b = 0;
|
||||
fr.col[2].g = 0;
|
||||
fr.col[2].r = 0;
|
||||
fr.col[3].b = 0;
|
||||
fr.col[3].g = 0;
|
||||
fr.col[3].r = 0;
|
||||
fr.code = 0x50000000;
|
||||
fr.center.z = param_4;
|
||||
fr.col[1].r = c12;
|
||||
fr.col[1].g = c12;
|
||||
((void (*)(s32, void *, void *))func_80149350)(arg0, &fr.center, &fr.center);
|
||||
|
||||
h = *(u16 *)&fr.center.x;
|
||||
*(s16 *)(param_1 + 6) = h;
|
||||
*(s32 *)(param_1 + 0x4C) = (s16)h;
|
||||
h = *(u16 *)&fr.center.y;
|
||||
*(s16 *)(param_1 + 0xA) = h;
|
||||
*(s32 *)(param_1 + 0x50) = (s16)h;
|
||||
h = *(u16 *)&fr.center.z;
|
||||
*(s16 *)(param_1 + 0xE) = h;
|
||||
*(s32 *)(param_1 + 0x54) = (s16)h;
|
||||
func_800D20C0(&fr.center, &fr.diff, 7);
|
||||
func_80017E68(&fr.center, m1);
|
||||
func_800D23D0(&fr.diff);
|
||||
RotMatrixYXZ(&fr.diff, m1);
|
||||
|
||||
L1:
|
||||
fr.v[0].x = (s8)*p++;
|
||||
fr.v[0].y = (s8)*p++;
|
||||
fr.v[2].x = (s8)*p++;
|
||||
fr.v[2].y = (s8)*p++;
|
||||
fr.v[3].x = (s8)*p++;
|
||||
fr.v[3].y = (s8)*p--;
|
||||
func_80017758(fr.v, m1);
|
||||
t1 = i + 1;
|
||||
__asm__("addu %0,%1,$zero":"=r"(i):"r"(t1));
|
||||
if ((s16)t1 < 4) goto L1;
|
||||
|
||||
p = D_80189A0C;
|
||||
fr.col[1].g = 0xA0;
|
||||
fr.col[1].b = 0x10;
|
||||
fr.col[1].r = 0x10;
|
||||
t = D_801269A4 - fr.center.x;
|
||||
fr.pos[0] = t;
|
||||
i = 0;
|
||||
fr.v[3].y = 0;
|
||||
fr.v[2].y = 0;
|
||||
fr.v[0].y = 0;
|
||||
r1 = D_801269AC - fr.center.z;
|
||||
fr.pos[2] = r1;
|
||||
t = D_801269A8 - fr.center.y;
|
||||
fr.pos[1] = t;
|
||||
L2:
|
||||
fr.v[0].x = (s8)*p++;
|
||||
fr.v[0].z = (s8)*p++;
|
||||
fr.v[2].x = (s8)*p++;
|
||||
fr.v[2].z = (s8)*p++;
|
||||
fr.v[3].x = (s8)*p++;
|
||||
fr.v[3].z = (s8)*p--;
|
||||
func_80017758(fr.v, (void *)(param_1 + 0x38));
|
||||
t2 = i + 1;
|
||||
__asm__("addu %0,%1,$zero":"=r"(i):"r"(t2));
|
||||
if ((s16)t2 < 4) goto L2;
|
||||
|
||||
if (*(s32 *)(param_1 + 0x30) > 0) {
|
||||
d1 = (s16)*(u16 *)&fr.diff.x >> 6;
|
||||
fr.center.x = *(u16 *)&fr.center.x + d1;
|
||||
fr.center.y = *(u16 *)&fr.center.y + d1;
|
||||
fr.center.z = *(u16 *)&fr.center.z + d1;
|
||||
func_80017E68(&fr.center, m1);
|
||||
p = D_80189A34;
|
||||
i = 0;
|
||||
d2 = (*(u32 *)(param_1 + 0x30) >> 1) + D_80126CE0 * 0x20;
|
||||
fr.center.z = d2;
|
||||
fr.center.y = d2;
|
||||
fr.center.x = d2;
|
||||
func_80017DC4(&fr.center, m2);
|
||||
func_80048EAC(m2, m1);
|
||||
fr.v[3].z = 0;
|
||||
fr.v[2].z = 0;
|
||||
fr.v[0].z = 0;
|
||||
c12b = *(u8 *)(param_1 + 0x12);
|
||||
__asm__("" :: "r"(c12b));
|
||||
fr.col[1].b = 0x20;
|
||||
fr.col[1].r = 0x20;
|
||||
fr.col[3].b = 0;
|
||||
fr.col[3].r = 0;
|
||||
fr.col[2].b = 0;
|
||||
fr.col[2].r = 0;
|
||||
fr.col[0].b = 0;
|
||||
fr.col[0].r = 0;
|
||||
fr.col[3].g = 0;
|
||||
fr.col[2].g = 0;
|
||||
fr.col[0].g = 0;
|
||||
fr.col[1].g = c12b + 0x60;
|
||||
L3:
|
||||
fr.v[0].x = (s8)*p++;
|
||||
fr.v[0].y = (s8)*p++;
|
||||
fr.v[2].x = (s8)*p++;
|
||||
fr.v[2].y = (s8)*p++;
|
||||
fr.v[3].x = (s8)*p++;
|
||||
fr.v[3].y = (s8)*p--;
|
||||
func_80017758(fr.v, m1);
|
||||
t3 = i + 1;
|
||||
__asm__("addu %0,%1,$zero":"=r"(i):"r"(t3));
|
||||
if ((s16)t3 < 8) goto L3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# func_801670E4 (279 ins, ov_SC01_077_jr_8015AE2C) — Fable5 gdb-on-cc1 pass
|
||||
|
||||
**Result:** `match_one` **DIFF 23/279** (NOT a MATCH), **PIN-FREE, ×134-safe** — unchanged from the
|
||||
Opus seed. I could **not** beat 23 pin-free. The deliverable of this pass is the **oracle-proven
|
||||
RECLASSIFICATION** of the dominant residual (region B), which corrects the seed's S3 verdict and
|
||||
retargets the next tier. Per the brief: *a wall verdict that names the exact compiler decision is a
|
||||
first-class deliverable.* Draft: `.run/giants/func_801670E4.fable.c` (body == the seed).
|
||||
|
||||
Residual 23 = **region A** (idx 19-24, 6: block0 fence-lock) + **region B** (idx 112-133, 17,
|
||||
DOMINANT: the pos-subtraction `$v0`/`$v1` swap).
|
||||
|
||||
---
|
||||
|
||||
## Region B (17, DOMINANT) — VERDICT: intrinsic **register 2-coloring tie**, NOT S3
|
||||
|
||||
### The oracle proof (the headline)
|
||||
|
||||
The seed called region B an **S3** wall ("load/mul-fed chain sinks below stores; C cannot LOWER a
|
||||
priority; sched1 emits the D_801269A4 load before the col[1].g store"). **This is wrong.** I proved
|
||||
it with a gdb-on-cc1 reg_renumber patch:
|
||||
|
||||
- Harness: break at `reload` entry (`*0x0815d4d7`), patch `reg_renumber` (a `short*` at
|
||||
`0x082d4330`, deref then index) to swap the $v0↔$v1 family:
|
||||
`78→2 82→3 132→2 136→3 140→2 127→3`. (`.run/giants/fable_670E4/swap2.gdb`.)
|
||||
- Result: cc1 emits region B **BYTE-EXACT to the target** — 17→0 — **including** the
|
||||
`col[1].g`-store-before-`D_801269A4`-load reordering the seed attributed to sched1 priority.
|
||||
|
||||
**Conclusion:** the FINAL schedule already matches the target. The col-store-vs-load order is a
|
||||
**sched2 (post-reload) CONSEQUENCE of the register identity**, not a sched1 priority decision. The
|
||||
entire 17-diff residual is one thing: **register identity — `$v0`/`$v1` swapped on the `t`/`r1`
|
||||
family.** (`.i.sched` even shows the seed's premise inverted: the col[1].g store *is* emitted before
|
||||
the A4 load in sched1 RTL; the swap in the final `.s` is reload+sched2 reacting to the reg map.)
|
||||
|
||||
### Root cause (from the disposition dump + find_reg trace)
|
||||
|
||||
`.i.greg` dispositions (v0=2, v1=3): `78 in 3, 82 in 2, 132 in 3, 136 in 2, 140 in 3, 127 in 2`
|
||||
(all $v0↔$v1 opposite the target). Everything cascades from **one allocno**:
|
||||
|
||||
- **`t`** (`s32 t`, reused for pos[0] AND pos[1] → 2 deaths) = pseudo **reg78**, a **GLOBAL**
|
||||
allocno (one of the 5 in `;; 5 regs to allocate`). It ties the three D_ loads
|
||||
(pos[k]=D-center reuses the load reg destructively): reg132/140 (A4/A8) follow reg78.
|
||||
- **`r1`** (`s32 r1`, pos[2], 1 death) = pseudo **reg82**, a **LOCAL** allocno (merged qty
|
||||
{AC(136), pos2(82)}).
|
||||
|
||||
`local-alloc` runs **before** `global-alloc`. The local qty {AC,pos2} grabs `$v0` by first-fit; this
|
||||
records the conflict `78 conflicts: … 2 …` (reg78 ⟂ `$v0`), so when global-alloc's `find_reg`
|
||||
(traced: allocno 3 = pseudo 78, `hard_reg_preferences=0`, `hard_reg_conflicts` has bit 2) runs, `$v0`
|
||||
is blocked → `t`→`$v1`. The A4/A8 loads and pos results all cascade to `$v1`; the `0x10` const and
|
||||
pos[2] cascade to `$v0`.
|
||||
|
||||
The target needs the **opposite, equally-valid coloring** (`t`=$v0, `r1`=$v1). It is a
|
||||
**self-consistent 2-coloring**: A4=$v0 ⇐ t=$v0 ⇐ r1=$v1 ⇐ (A8 in $v0 overlaps pos2) ⇐ A8=$v0 ⇐
|
||||
t=$v0. Both colorings close; the allocator's deterministic tie-break (**local-before-global +
|
||||
first-fit** on the source-order-invariant sched1 order) picks mine. There is no external anchor in
|
||||
the block to break the symmetry.
|
||||
|
||||
Why source order can't touch it (the seed's true "S3-like" observation): the sched1 **backward list
|
||||
scheduler** always emits `pos1subu` before `pos2subu` regardless of C statement order, which makes
|
||||
the {AC,pos2} qty **live longer** than the A8 qty (`qty_compare`, `local-alloc.c`), so {AC,pos2}
|
||||
always wins `$v0`. Confirmed: statement reorder pos1↔pos2 is a byte no-op.
|
||||
|
||||
### Pin-free levers tried — all fail (byte-verified)
|
||||
|
||||
| # | lever | result | why |
|
||||
|---|---|---|---|
|
||||
| m1 | split `t` → t/tb (both local 1-death) | **24** | breaks the schedule (the reused `t` is load-bearing for sched1 order) |
|
||||
| m3 | inline all three pos (no temps) | **24** | same schedule shift |
|
||||
| m4 | reorder pos1 before pos2 (source) | **23** (no-op) | sched1 re-derives the same order (the true invariance) |
|
||||
| m5 | pos1 & pos2 share `r1` | **24** | wrong reg group |
|
||||
| m6 | globalize `r1` via block3 reuse | **34** | becomes **call-crossing** → `$a2` (wrong class) |
|
||||
| m7 | pre-load A8 into a var spanning pos2 | **28** | adds a copy / schedule shift |
|
||||
| m8 | `r1` in-out re-tie (2nd set → global) | **29** | `#APP` perturbs; r1 chases `$a1` |
|
||||
| m9 | input-only anchor on `r1` | **29** | `#APP` perturbs |
|
||||
| m2 | scheduling fence between pos2/pos1 | **169** | bare `asm("")` mid-block = cse flush + maspsx nop-killer |
|
||||
|
||||
The mechanism that WOULD work — **make `r1` a block2-confined 2-death global** (so it competes with
|
||||
`t` in global-alloc by density, where `t`'s higher ref count wins `$v0` first, leaving `r1`→`$v1`) —
|
||||
has no pin-free spelling: the only ways to add a 2nd death are (a) a cross-block reuse → call-crossing
|
||||
→ wrong class (m6), or (b) an in-out/anchor `asm` → `#APP` maspsx perturbation (m8/m9). The seed
|
||||
cracked it only with `register s32 r1 __asm__("$3")` — the ×1-only pin (§42e), unusable at ×134.
|
||||
|
||||
**VERDICT (region B):** intrinsic **RC-6-class local-alloc qty-tie** (downgraded from the seed's S3).
|
||||
Route: **decomp-permuter** (its temp-split/merge randomizers may stumble on a globalizing form the
|
||||
directed C-lever space can't reach), or — if ×134 propagation is ever abandoned for this core —
|
||||
accept the one `r1 __asm__("$3")` pin for this ×1 location.
|
||||
|
||||
---
|
||||
|
||||
## Region A (6) — VERDICT: intrinsic **fence-lock scheduling permutation**
|
||||
|
||||
The 6 diffs are a pure permutation of `{addiu a1=¢er, sh center.y, addu a2=a1, lui/addiu p, addu
|
||||
i=0}`. Target floats the func_80149350 arg-addresses (`a1=&fr.center`, `a2=a1`) **above** `p`/`i=0`
|
||||
(a1 reuses param_2's freed `$a1` right after `sh $a1,0x10`). The mandatory empty `__volatile__("")`
|
||||
fence keeps `p` (single-set → S2 birthing-boost) from floating to idx4 (else +250) but simultaneously
|
||||
**walls the args** (def at the call, high LUID) from floating up past it. Levers tried:
|
||||
|
||||
| # | lever | result |
|
||||
|---|---|---|
|
||||
| m10 | drop fence + `p` in-out re-tie (kill boost) | **41** (`#APP` + p position) |
|
||||
| m11 | `pc=&fr.center` var before the fence | **280 ins / 248** (pc → callee-saved, crosses call) |
|
||||
| m12 | `pc=&fr.center` between the center stores | **280 / 248** (same) |
|
||||
|
||||
Consistent with the seed. The tension (barrier must block `p`'s upward boost yet not the args'
|
||||
upward float, but both move up through the same point) has no pin-free C resolution I found.
|
||||
|
||||
---
|
||||
|
||||
## Cookbook-worthy findings (the highest-value output)
|
||||
|
||||
1. **The reg_renumber-swap oracle: prove a residual is pure register-identity in ONE run.** When a
|
||||
whole diff block is the same opcodes with two registers transposed, break at `reload` entry
|
||||
(`*0x0815d4d7`), patch `reg_renumber` (`*(short**)0x082d4330`, index by pseudo) to the target
|
||||
assignment, and re-emit. If the block goes byte-exact, the residual is **100% allocation, 0%
|
||||
scheduling** — even if the final *order* differs, because sched2 re-derives order from the reg
|
||||
map. This **cheaply discriminates S3 (scheduling) from RC-6 (allocation)** and stops you hunting
|
||||
scheduling levers for an allocation problem (the trap this function was in for a whole seed pass).
|
||||
Pseudos come from the `.i.greg` dispositions; `reg_renumber` is a `short*` **pointer**, not an
|
||||
array (deref first — same gotcha as `qty_first_reg`).
|
||||
|
||||
2. **"Store-before-load reordering" can be a CONSEQUENCE of register identity, not a cause.** A
|
||||
target that stores a dying value (freeing its reg) immediately before a load that reuses that reg
|
||||
looks like a scheduling decision. It often is not: once the allocator gives the load that reg,
|
||||
**sched2 (post-reload) produces the reorder for free** to satisfy the WAR/reuse. Verify with the
|
||||
swap oracle before invoking S3/S4 filler levers.
|
||||
|
||||
3. **The local-vs-global allocation tie is a distinct RC-6 sub-class with a NAMED failure mode.**
|
||||
When the target's coloring is the *global-first* solution of a self-consistent 2-coloring but
|
||||
yours is the *local-first* solution: a **reused scalar** (2 deaths → global, e.g. a temp written
|
||||
in two disjoint regions) loses `$v0` to a **single-use sibling** (1 death → local) because
|
||||
`local_alloc` runs first. The fix wants the sibling **globalized while staying block-confined**
|
||||
(so global-alloc density order — `t` outranks `r1` — decides), but pin-free that is unreachable:
|
||||
cross-block reuse → call-crossing (wrong class); `asm` re-tie/anchor → `#APP` maspsx perturbation.
|
||||
**Tell:** `.i.greg` shows the reused temp in `;; N regs to allocate` (global) while its sibling is
|
||||
absent (local), and the sibling's hard reg is the lower one. **Recognize → route to permuter; do
|
||||
not spend C-lever budget** (this pass byte-verified 7 forms to zero effect).
|
||||
|
||||
4. **Corollary — the reused-temp split is double-edged.** §44-Lever-3 / §45-A say "split a reused
|
||||
value into per-region locals to get first-fit." Here the reuse is **load-bearing for the
|
||||
schedule** (splitting `t` shifts sched1 → +1). Check the schedule delta before splitting a reused
|
||||
temp; when the reuse feeds the sched1 order you want, the split is a regression, and the residual
|
||||
it leaves is the local-vs-global tie above (finding #3), not something the split can fix.
|
||||
|
||||
---
|
||||
|
||||
## Method / reproducibility
|
||||
- Oracle workspace: `.run/giants/fable_670E4/` — `orc.i` (the seed's preprocessed input),
|
||||
`swap2.gdb` (the byte-exact reg_renumber-swap proof), `probe2.gdb` (reg_renumber deref pattern),
|
||||
`freg.gdb`/`ffr3.gdb` (find_reg / find_free_reg traces), `m0..m12.c` (the 12 byte-verified
|
||||
variants above). Dumps `m0.i.{sched,lreg,greg,…}` via `.run/giants/dump.sh`.
|
||||
- cc1 symbols (i386, `tools/bin/gcc-2.7.2-psx/cc1`): `reload 0x0815d4d7`, `find_reg 0x0814c8ca`,
|
||||
`find_free_reg 0x08149ad0`, `local_alloc 0x0814575b`, `global_alloc 0x0814a579`;
|
||||
data `reg_renumber 0x082d4330 (short*)`, `qty_first_reg 0x082c5404 (live copy)`,
|
||||
`allocno_reg 0x082c5448`, `hard_reg_preferences 0x082c5470`, `hard_reg_conflicts 0x082c546c`.
|
||||
- Gate: `python3 tools/match_one.py func_801670E4 --c <c> --asm-subdir asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_8015AE2C`.
|
||||
|
||||
## Next-tier hand-off
|
||||
- **Bank candidate?** No — draft is 23, not a MATCH.
|
||||
- **Permuter:** point at region B specifically; its temp-introduction/merge randomizers are the only
|
||||
automated path to a block-confined `r1` globalization. Seed the permuter with the seed draft.
|
||||
- **Cheap-Opus:** nothing new to apply here (the 7-lever space is byte-exhausted); use findings #1/#2
|
||||
as a general discriminator on OTHER giants that were tagged S3 — some may be mislabeled RC-6.
|
||||
@@ -0,0 +1,178 @@
|
||||
/* func_8016CBC0 — trail/afterimage RENDERER over the 0xC4-stride records at D_801D9CA0.
|
||||
* ×134 per-location family core (28,006 templatable ins). Sibling of func_80166994/func_8016CF04.
|
||||
*
|
||||
* ===== STATUS: PIN-FREE, match_one 46/209 (positional, CORRECT length 209). ROOT A CRACKED. =====
|
||||
*
|
||||
* This is the LOWEST-positional pin-free draft (the best next-tier seed by the gate metric).
|
||||
* Companion draft `func_8016CBC0.workhorse.c` (variant e4) cracks the harder root-B WORKHORSE
|
||||
* residual (scoped-merge) at the cost of an RC-6 secondary coupling — read `.fable.md` for the
|
||||
* full ladder and which base the next tier should take.
|
||||
*
|
||||
* PIN-FREE: YES. Zero `register __asm__("$N")` pins, zero hard-reg names. The only asm are
|
||||
* generic-constraint zero-byte optimisation barriers (counter fences `__asm__("":"=r"(t):"0"(t))`
|
||||
* + one input-only density anchor `__asm__("":: "r"(mat))`) — all ×134-safe (the §42e SIGABRTs
|
||||
* are from FIXED-reg pins, which this has none of). Block-scoped decls, no file-scope footprint.
|
||||
*
|
||||
* WHAT LANDED vs the prior seed (m7, was 151/208):
|
||||
* • ROOT A — mat/i $s4/$s5 swap: CRACKED with a PURE DENSITY DIAL (not coalescing!). mat(pseudo
|
||||
* 76) and i(77) both had live_length 163; mat had 7 refs, i had 9. `allocno_compare`
|
||||
* (global.c:594) pri = floor_log2(n_refs)*n_refs/live_length: mat 2*7=14→858, i 3*9=27→1656 —
|
||||
* i wins $s4 (the 7→8 ref jump crosses the floor_log2 boundary at 8, DOUBLING the multiplier).
|
||||
* Fix: `__asm__("":: "r"(mat))` inside the outer-loop `if(active)` body (loop_depth 2 →
|
||||
* reg_n_refs += 2, flow.c:2067) → mat 7→9 refs → ties i at 27 → mat wins $s4 by the creation-
|
||||
* order tie-break (76<77, global.c:616). Byte-zero. This ALSO fixed the 208→209 length.
|
||||
*
|
||||
* RESIDUAL (root B, the ~46 masked): the vertex-block WORKHORSE. w0 loads to $a0 (mine) vs $v1
|
||||
* (target); w3 to $a2/$a3 vs $a0. The target holds the whole w0→(short)w0→sum→av chain in ONE
|
||||
* workhorse $v1 (a per-coord merged variable). Here w0 is a SEPARATE, MULTI-death (reused across
|
||||
* 3 coords) → GLOBAL allocno (local-alloc.c:472 gate REG_N_DEATHS==1 fails) → no destructive tie
|
||||
* (K8: gcc-2.7.2 has NO global coalescing) → gets its own reg $a0. See `.fable.md`: this is
|
||||
* CRACKABLE (workhorse.c does it via per-coordinate scoped-merge) but the crack trades into an
|
||||
* RC-6 register-contention on $a1 (pv vs the coord-z accumulator) — the honest wall.
|
||||
*
|
||||
* Best pin-free draft (variant e1) follows.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
short func_8016CBC0(void) {
|
||||
typedef struct { u16 v[6]; } VtxR;
|
||||
typedef struct { u8 active, nv, cnt, col; VtxR verts[16]; } BlkR;
|
||||
typedef struct { s32 w[8]; } Blk20;
|
||||
extern BlkR D_801D9CA0[];
|
||||
extern Blk20 D_800AE620;
|
||||
extern s32 func_80017758(void *a0, void *a1);
|
||||
|
||||
u8 buf[0x90];
|
||||
u8 col;
|
||||
int uv;
|
||||
int cnt;
|
||||
u8 *vb;
|
||||
void *mat;
|
||||
int i;
|
||||
short ret;
|
||||
|
||||
*(Blk20 *)(buf + 0x70) = D_800AE620;
|
||||
ret = 0;
|
||||
i = 0;
|
||||
mat = buf + 0x70;
|
||||
*(s32 *)(buf + 0x68) = 0x50000000;
|
||||
*(s32 *)(buf + 0x30) = 0x50000000;
|
||||
*(u8 *)(buf + 0x66) = 0;
|
||||
*(u8 *)(buf + 0x65) = 0;
|
||||
*(u8 *)(buf + 0x64) = 0;
|
||||
*(u8 *)(buf + 0x5E) = 0;
|
||||
*(u8 *)(buf + 0x5D) = 0;
|
||||
*(u8 *)(buf + 0x5C) = 0;
|
||||
*(u8 *)(buf + 0x2A) = 0;
|
||||
*(u8 *)(buf + 0x29) = 0;
|
||||
*(u8 *)(buf + 0x28) = 0;
|
||||
*(u8 *)(buf + 0x22) = 0;
|
||||
*(u8 *)(buf + 0x21) = 0;
|
||||
*(u8 *)(buf + 0x20) = 0;
|
||||
do {
|
||||
BlkR *e = &D_801D9CA0[(short)i];
|
||||
if (e->active != 0) {
|
||||
int tcnt;
|
||||
vb = (u8 *)e + 4;
|
||||
uv = e->nv;
|
||||
cnt = e->cnt;
|
||||
col = e->col;
|
||||
ret = ret + 1;
|
||||
__asm__("" :: "r"(mat));
|
||||
tcnt = cnt - 1;
|
||||
__asm__("" : "=r"(tcnt) : "0"(tcnt));
|
||||
cnt = tcnt;
|
||||
if (tcnt != -1) {
|
||||
do {
|
||||
s32 uvm;
|
||||
u8 *pv;
|
||||
{
|
||||
s32 po = (short)uv * 0xc;
|
||||
s32 w0, w3, av;
|
||||
u8 c8c;
|
||||
pv = vb + po;
|
||||
w0 = *(u16 *)(pv + 0x0);
|
||||
*(short *)(buf + 0x00) = w0;
|
||||
w3 = *(u16 *)(pv + 0x6);
|
||||
*(short *)(buf + 0x40) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x38) = av;
|
||||
*(short *)(buf + 0x08) = av;
|
||||
w0 = *(u16 *)(pv + 0x2);
|
||||
*(short *)(buf + 0x02) = w0;
|
||||
w3 = *(u16 *)(pv + 0x8);
|
||||
c8c = col >> 3;
|
||||
uvm = uv - 1;
|
||||
__asm__("" : "=r"(uvm) : "0"(uvm));
|
||||
uv = uvm;
|
||||
*(short *)(buf + 0x42) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x3A) = av;
|
||||
*(short *)(buf + 0x0A) = av;
|
||||
w0 = *(u16 *)(pv + 0x4);
|
||||
*(short *)(buf + 0x04) = w0;
|
||||
w3 = *(u16 *)(pv + 0xa);
|
||||
*(u8 *)(buf + 0x59) = col;
|
||||
*(u8 *)(buf + 0x25) = col;
|
||||
*(u8 *)(buf + 0x5A) = c8c;
|
||||
*(u8 *)(buf + 0x26) = c8c;
|
||||
*(u8 *)(buf + 0x58) = c8c;
|
||||
*(u8 *)(buf + 0x24) = c8c;
|
||||
*(short *)(buf + 0x44) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x3C) = av;
|
||||
*(short *)(buf + 0x0C) = av;
|
||||
}
|
||||
if ((int)(uvm << 16) < 0) uv = 0xf;
|
||||
{
|
||||
s32 po = (short)uv * 0xc;
|
||||
s32 w0, w3, av;
|
||||
u8 c84;
|
||||
pv = vb + po;
|
||||
col = col - 0xf;
|
||||
c84 = col >> 3;
|
||||
w0 = *(u16 *)(pv + 0x0);
|
||||
*(short *)(buf + 0x10) = w0;
|
||||
w3 = *(u16 *)(pv + 0x6);
|
||||
*(short *)(buf + 0x50) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x48) = av;
|
||||
*(short *)(buf + 0x18) = av;
|
||||
w0 = *(u16 *)(pv + 0x2);
|
||||
*(short *)(buf + 0x12) = w0;
|
||||
w3 = *(u16 *)(pv + 0x8);
|
||||
*(short *)(buf + 0x52) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x4A) = av;
|
||||
*(short *)(buf + 0x1A) = av;
|
||||
w0 = *(u16 *)(pv + 0x4);
|
||||
*(short *)(buf + 0x14) = w0;
|
||||
w3 = *(u16 *)(pv + 0xa);
|
||||
*(u8 *)(buf + 0x61) = col;
|
||||
*(u8 *)(buf + 0x2D) = col;
|
||||
*(u8 *)(buf + 0x62) = c84;
|
||||
*(u8 *)(buf + 0x2E) = c84;
|
||||
*(u8 *)(buf + 0x60) = c84;
|
||||
*(u8 *)(buf + 0x2C) = c84;
|
||||
*(short *)(buf + 0x54) = w3;
|
||||
av = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x4C) = av;
|
||||
*(short *)(buf + 0x1C) = av;
|
||||
}
|
||||
func_80017758(buf + 0x00, mat);
|
||||
func_80017758(buf + 0x38, mat);
|
||||
tcnt = cnt - 1;
|
||||
__asm__("" : "=r"(tcnt) : "0"(tcnt));
|
||||
cnt = tcnt;
|
||||
} while ((short)tcnt != -1);
|
||||
}
|
||||
}
|
||||
{
|
||||
int ti = i + 1;
|
||||
__asm__("" : "=r"(ti) : "0"(ti));
|
||||
i = ti;
|
||||
if ((short)ti >= 8) break;
|
||||
}
|
||||
} while (1);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
# func_8016CBC0 (209 ins, ×134 family core) — Fable5 gdb/dump-oracle pass
|
||||
|
||||
**Result:** the "coalescing knife-edge" verdict is **REFUTED as named**. gcc-2.7.2 has NO global
|
||||
coalescing (map key K8), so nothing here is a coalescing decision. The residual is two SEPARATE,
|
||||
precisely-located compiler decisions — one **CRACKED**, one **crackable-but-RC-6-coupled**:
|
||||
|
||||
| root | what | mechanism (exact gcc file:line) | verdict |
|
||||
|------|------|---------------------------------|---------|
|
||||
| **A** | mat/i `$s4/$s5` swap | `global.c:594 allocno_compare` density gap at a `floor_log2` boundary | **CRACKED** — pure density dial, byte-zero |
|
||||
| **B** | vertex-block workhorse (w0→`$a0` vs target `$v1`) | `local-alloc.c:472` single-death gate + destructive tie (`combine_regs`) | **workhorse CRACKED** (scoped-merge) but exposes an RC-6 `$a1` contention |
|
||||
|
||||
**Ledger reconciliation (task asked):** BOTH recorded numbers were stale — they were measured
|
||||
against the now-renamed subdir `ov_SC01_077_after`. Against the CURRENT subdir
|
||||
`ov_SC01_077_jr_8016AB6C`, the seed `m7`/opus.c scores **match_one positional 151 (length 208), real
|
||||
aligned-diff 111**. Neither "close=148" (backlog) nor "153 masked" (.opus.md) reproduces; the backlog's
|
||||
prose ("166/209 aligned, edit-dist ~44") was the honest figure. The `match_one` positional count is
|
||||
**cascade-inflated whenever the length is wrong** — always cross-check with the aligned differ
|
||||
(`.run/giants/opus_8016CBC0/adiff.py <t.o> <target.s>`).
|
||||
|
||||
## Deliverable drafts (both PIN-FREE, ×134-safe — generic-constraint zero-byte asm only)
|
||||
|
||||
- **`func_8016CBC0.fable.c`** (variant **e1**) — **match_one 46/209, correct length**. Root A cracked;
|
||||
ONE clean residual (root B, w0-load register). Best next-tier seed by the gate metric.
|
||||
- **`func_8016CBC0.workhorse.c`** (variant **e4**) — match_one 105/209 (length 210, +1) but **real
|
||||
aligned-diff 81** (best) and it **CRACKS the root-B workhorse** (w0 chain lands in `$v1`). Its 105 is
|
||||
cascade-inflated by the +1 length. Recommended base for a permuter finish — see root B below.
|
||||
|
||||
## The byte-verified lever ladder (match_one positional / real aligned-diff, current subdir)
|
||||
|
||||
| variant | change | pos | real | note |
|
||||
|---------|--------|-----|------|------|
|
||||
| e0 (=m7 seed) | prior pin-free seed | 151 | 111 | length 208 |
|
||||
| **e1** | + `__asm__("":: "r"(mat))` density anchor | **46** | 92 | **root A cracked + length→209** |
|
||||
| e2 | e1 + merge w0/av (vertex-block scope) | 52 | 110 | merge → whole chain to `$a0` (wrong dir) |
|
||||
| e3 | e1 + per-coord block scope, no merge | 96 | 182 | fragments — scope-without-merge is wrong |
|
||||
| **e4** | e1 + per-coord **scoped MERGE** | 105 | **81** | **workhorse w0→`$v1` (cracked)**; +1 ins, pv→`$a3` |
|
||||
| e9 | e4 but coord-z left plain (hybrid) | 96 | 120 | mixing scoped/plain fragments worse |
|
||||
|
||||
## ROOT A — CRACKED. The `$s4/$s5` swap is a density gap, NOT coalescing.
|
||||
|
||||
From the `m7` `.greg`: callee-saved pseudos `72..78` = col,uv,cnt,vb,**mat(76),i(77)**,ret. Seed
|
||||
disposition `76 in 21 ($s5) 77 in 20 ($s4)` — i wins `$s4`; target wants mat=`$s4`, i=`$s5`.
|
||||
|
||||
`.lreg`: **mat(76) = 7 refs / 163 live; i(77) = 9 refs / 163 live** (identical live_length). Plug into
|
||||
`allocno_compare` (`tools/reference/gcc-2.7.2/global.c:594`)
|
||||
`pri = floor_log2(n_refs)*n_refs / live_length * 10000`:
|
||||
- mat: `floor_log2(7)*7 = 2*7 = 14` → **858**
|
||||
- i: `floor_log2(9)*9 = 3*9 = 27` → **1656**
|
||||
|
||||
i allocates first (higher density) → takes the lower reg `$s4`. The gap is NOT a tie and NOT
|
||||
coalescing — it is the **`floor_log2` step**: 7→8 refs crosses `2^3=8`, doubling the multiplier from
|
||||
×2 to ×3. To flip, mat needs `floor_log2*refs ≥ 27`; mat=8 gives 24 (still loses!), mat=9 gives 27 = a
|
||||
TIE, which mat then wins by the creation-order tie-break (`global.c:616 return v1-v2`, and 76<77).
|
||||
|
||||
**The dial:** `__asm__("" :: "r"(mat));` placed inside the outer-loop `if (e->active)` body. That block
|
||||
is at **loop_depth 2** (function=1, outer do-while=2; inner do-while=3), and `flow.c:2067`
|
||||
`reg_n_refs[regno] += loop_depth` → mat's ref count rises **7→9**. Zero bytes (input-only asm), ×134-
|
||||
safe. Result: `76 in 20 ($s4) 77 in 21 ($s5)` — exact. Side benefit: it also repaired the 208→209
|
||||
length (the swap had been perturbing the tail schedule).
|
||||
|
||||
**This retires the "coalescing knife-edge" name for root A**: it is the same `allocno_compare` density
|
||||
dial as cookbook §47/§48-A, just gated by the `floor_log2` boundary rather than an int-truncation tie.
|
||||
|
||||
## ROOT B — the workhorse. Crackable, but the crack is RC-6-coupled.
|
||||
|
||||
The target keeps `w0 → (short)w0 → sum → av` all in ONE workhorse `$v1` for every coordinate (a
|
||||
per-coord in-place accumulator). The seed/e1 keeps `av` in `$v1` (correct) but the raw `w0` load in
|
||||
`$a0` (wrong). **Why:** `w0` is ONE C variable reused across the 3 unrolled coordinates → `.lreg`
|
||||
"Register 109 … dies in 3 places" → `REG_N_DEATHS = 3 ≠ 1` fails the local-alloc gate
|
||||
(`local-alloc.c:472`) → `w0` becomes a **GLOBAL allocno**. gcc-2.7.2 global-alloc has **no coalescing
|
||||
(K8)**, so `w0` cannot tie into the `$v1` av-chain; it gets its own reg. Confirmed from the conflict
|
||||
matrix: `109` conflicts with hard regs `2 (v0)` and `3 (v1)` (scratch pressure across its multi-coord
|
||||
range) → first-fit `$a0`.
|
||||
|
||||
**The crack (workhorse.c / e4):** give each coordinate its own block-scoped, MERGED accumulator —
|
||||
`{ s32 w0 = *(u16*)(pv+K); s32 w3; store w0; w3=…; store w3; w0 = (u32)((short)w0+(short)w3)>>1;
|
||||
store w0; store w0; }`. Now each coord's accumulator is **single-death → a LOCAL qty**, and the store-
|
||||
before-sign-extend order makes `w0` die into its own in-place update → the local-alloc **destructive
|
||||
tie fires** → the whole chain sits in one workhorse reg = **`$v1`, matching the target** (verified: e4
|
||||
loads are `lhu v1,0(a1)`). This is **§48-A3 (block-scoped per-arm temps) + §45-Lever-A (merged
|
||||
accumulator) COMBINED on loop-unrolled coordinates** — a new composite.
|
||||
|
||||
**Why e4 is not yet a match — the RC-6 coupling:** the per-coord scoped-merge, applied to the LAST
|
||||
(coord z, highest-pressure — it also carries the 6 colour stores + the `uvm<<16` bgez test), makes
|
||||
coord-z's accumulator a high-priority local that grabs **`$a1`** (its `.greg` pri = `4*18/18 = 40000`),
|
||||
**evicting `pv`** (pseudo 101, pri `5*42/63 = 33333`) to `$a3`. So all 12 vertex loads read `(a3)`
|
||||
instead of the target's `(a1)`, plus one delay-slot fill flips (+1 ins). Every attempt to relieve it
|
||||
moved 20+ instructions discontinuously (e2/e3/e9 above; a `pv` density anchor needs +9 refs to out-
|
||||
rank the coord-z accumulator — 3 depth-3 anchors, itself perturbing). This is textbook **RC-6 / §31
|
||||
S11 coupling at `global.c` `find_reg` first-fit** — NOT a coalescing decision. It is the genuine wall
|
||||
for the single-C-lever tier; a **decomp-permuter seeded from workhorse.c** (localized `$a1` contention,
|
||||
workhorse already correct) is the right next move, OR a gdb patch of `find_reg`'s grant for the pv vs
|
||||
coord-z-accumulator allocnos to confirm the minimal steer.
|
||||
|
||||
## Cookbook-worthy findings (the highest-value output — "Fable5 DISCOVERS, cheap-Opus APPLIES")
|
||||
|
||||
1. **DENSITY DIAL ACROSS A `floor_log2` BOUNDARY (new, byte-proven, root A).** When two equal-
|
||||
live_length call-crossing allocnos tie-break wrong, don't reach for pins — compute
|
||||
`floor_log2(refs)*refs` for each. If the loser is one `floor_log2` step below (e.g. 7 refs vs 9:
|
||||
`2*7=14` vs `3*9=27`), you cannot win by matching refs alone (8 refs = `3*8=24` STILL loses); you
|
||||
must **reach the next integer of `floor_log2*refs` and then win the creation-order tie**. The dial
|
||||
is an **input-only `__asm__("":: "r"(v))` placed at the loop depth that supplies the needed
|
||||
`reg_n_refs += loop_depth` increment** (`flow.c:2067`): depth-2 gives +2 per anchor, depth-3 gives
|
||||
+3. Zero bytes, ×134-safe. This generalises §47's ±1 live-length slider to the ref-count axis and
|
||||
subsumes the "coalescing knife-edge" mislabel whenever the residual is a callee-saved swap.
|
||||
|
||||
2. **SCOPED-MERGE = §48-A3 ⊕ §45-Lever-A, for LOOP-UNROLLED coordinates (new composite).** A workhorse
|
||||
register the target holds across an in-place `load→store→transform` chain, but which your draft
|
||||
splits because the driving variable is reused across N unrolled iterations (→ multi-death → GLOBAL
|
||||
→ no tie): **wrap each iteration in its own `{ }` with a fresh merged accumulator** (`s32 acc =
|
||||
load; store acc; acc = f(acc,…); store acc;`). Single-death per iteration → LOCAL qty → the store-
|
||||
before-transform order fires the destructive tie → the whole chain collapses into the one workhorse
|
||||
reg. Verified to move `w0` from `$a0` to the target's `$v1` across all coordinates. **Caveat
|
||||
(also proven):** applying it to the highest-pressure iteration can make its accumulator out-price a
|
||||
held pointer for a shared reg (RC-6). Apply, then read `.greg` dispositions for collateral before
|
||||
claiming the match.
|
||||
|
||||
3. **"MERGE" HAS A DIRECTION, AND IT IS THE OPPOSITE OF NAIVE (root B negative result).** Merging at
|
||||
the WRONG scope backfires: merging `w0/av` at *vertex-block* scope (multi-death, still GLOBAL) drags
|
||||
the WHOLE chain onto `w0`'s reg `$a0` (e2, regressed) — the reverse of the goal. The merge only
|
||||
helps when it is ALSO single-death-scoped. Confirms §44/§45's "each giant is its own class": the
|
||||
merge and the block-scope are ONE lever, not two.
|
||||
|
||||
4. **`match_one` positional count is untrustworthy at wrong length — gate on the aligned differ.**
|
||||
e4's real closeness (81 aligned) beats e1's (92) yet e4's positional (105) looks far worse than e1's
|
||||
(46), purely from a +1 length cascade. The ledger disagreements on this function trace to exactly
|
||||
this. Rule: read `adiff.py`, not the positional headline, when the length is off by any amount.
|
||||
|
||||
## Method / reproducibility
|
||||
- Iterate: `bash .run/giants/fable_8016CBC0/chk.sh <c> <tag>` (prints match_one positional + the
|
||||
aligned real-diff). Variants preserved in `.run/giants/fable_8016CBC0/e{0..10}_*.c`.
|
||||
- Dumps: `bash .run/giants/dump.sh <c> <prefix>` (pinned triple + `-dS -dR -dj -dc -dg -dl -dd`) →
|
||||
read `.i.lreg` (per-pseudo refs/live_length/deaths) and `.i.greg` (dispositions + conflict matrix).
|
||||
Root A and root B were both settled from these dumps + `tools/reference/gcc-2.7.2/{global,flow,
|
||||
local-alloc}.c` — NO gdb run was needed (the dumps carried the decision). The gdb oracle
|
||||
(`find_reg @ 0x0814c8ca`, `find_free_reg @ 0x08149ad0` in this cc1) remains the fallback to confirm
|
||||
the pv-vs-coord-z `find_reg` grant for a permuter-free root-B close.
|
||||
- Gate reminder: a `match_one` result is a CANDIDATE; finish any bank on the whole-binary byte-gate.
|
||||
@@ -0,0 +1,202 @@
|
||||
/* func_8016CBC0 — trail/afterimage RENDERER over the 0xC4-stride records at D_801D9CA0
|
||||
* (sibling of func_80166994/func_8016CF04; same Blk struct: u8 f0..f3 header + 16x {6 verts}).
|
||||
* Loops entries 0..7; for each active entry, walks its `count` vertices building two GPU
|
||||
* primitive packets per step (pktA@buf+0x00, pktB@buf+0x38, shared mat@buf+0x70 <- D_800AE620)
|
||||
* and calls func_80017758(pkt, mat) twice. Returns (short) count of active entries.
|
||||
*
|
||||
* ===== STATUS: PIN-FREE NEAR-MISS 153/209 (masked) — ESCALATE TO FABLE5 (RC-6 qty-birth lock) =====
|
||||
*
|
||||
* PIN-FREE: YES. Zero `register __asm__("$N")` pins, zero hard-reg asm. The only asm are FOUR
|
||||
* empty-string opaque-identity fences `__asm__("" : "=r"(t) : "0"(t))` (generic constraints,
|
||||
* NO opcode) — pure optimisation barriers, milder than the flagship's in-out `lh`, fully
|
||||
* ×134-safe (the §42e cc1-SIGABRTs come from FIXED-reg pins, which this has none of).
|
||||
* Block-scoped typedefs/externs, no file-scope footprint, no //@EDIT (canon-sig already
|
||||
* `extern short func_8016CBC0(void)`), so it splices + family_sweeps cleanly IF/when cracked.
|
||||
*
|
||||
* WHAT THE FLAGSHIP (func_80133CD4) LEVERS FIXED HERE (measured, match_one masked):
|
||||
* • Lever B / opaque-copy fence (§36): the 3 counter chains (cnt initial + loopback, uv, i) now
|
||||
* emit the target's compute-in-caller-temp-then-copy K8-no-coalesce form
|
||||
* (`addiu vN,sN,-1; addu sN,vN,zero`) instead of the seed's in-place `addiu sN,sN,-1`.
|
||||
* Root: `t = x-1; fence(t); x = t; test t` — the fence blocks cse/copy-prop from folding t
|
||||
* into x, so x (a call-crossing GLOBAL allocno, global.c K8) keeps the copy. (The dB seed's
|
||||
* PINS FORCED in-place counters — a pinned var is a hard reg, no coalescing decision at all.)
|
||||
* • uv-split cured: the pin-free baseline scattered uv to a reload pseudo (201); the counter
|
||||
* fence lands it back on a clean early pseudo -> $s1.
|
||||
* • SINGLE reused inner-scope `pv` pointer (NOT block-local — the OPPOSITE of naive scoping):
|
||||
* makes pv a GLOBAL allocno that global-alloc gives a1 AFTER local w3 grabs a0 — this fixed
|
||||
* the whole first-vertex-block spine: (short)uv->v1, po->v0, pv-base->a1 (all == target).
|
||||
*
|
||||
* RESIDUAL (the ~56 masked mismatches — ONE interlinked RC-6 qty-birth/priority lock, §31/§27):
|
||||
* 1. w0-LOAD register: mine `lhu a0,0(a1)` vs target `lhu v1,0(a1)`. At the IDENTICAL register
|
||||
* state (v0=po dead, v1=(short)uv dead, a1=pv live) gcc first-fits the w0 load to a0 (mine)
|
||||
* vs v1 (target). Because the load lands in a0, its (short)/sum/av chain then lives in v1 —
|
||||
* the target keeps the whole chain in v1 (workhorse). Merging w0/av (var reuse, Lever A)
|
||||
* DOES pull w0->v1 but REGRESSES av in the 3rd unrolled vertex (v1->a0), net −13 (m8 144).
|
||||
* 2. w3 register: mine a2/a3 vs target a0 (target's 3rd w3 then merges into the dead pv-base
|
||||
* a1: `lhu a1,0xa(a1)`). Pure local-alloc qty-birth order — w3 should be born early enough
|
||||
* to win a0 and share it with uvm; mine's w3 qty is born late -> a2/a3.
|
||||
* 3. mat/i $s4/$s5 SWAP: mine mat=$s5,i=$s4; target mat=$s4,i=$s5. Both are call-crossing
|
||||
* globals; both have 4 refs. gcc's allocno_compare (global.c:594) ranks i's priority
|
||||
* (floor_log2(refs)*freq/live_length) strictly above mat's, so i takes $s4 first. Declaration
|
||||
* order (col,uv,cnt,vb,mat,i,ret -> pseudos 72..78) WOULD give the target by the `v1-v2`
|
||||
* tie-break IF priorities tied — they don't. NOT sensitive to: i-fence on/off (m6),
|
||||
* break-vs-while outer loop (m10), pointer split (m5). Needs mat.pri >= i.pri, uncontrollable
|
||||
* from C without a byte-visible mat ref (the block-copy is sp-relative; mat-relative mismatches).
|
||||
* 4. dbr: target steals `addiu v0,s5,1` (i+1) into the outer beqz delay slot (mine: `li v0,-1`),
|
||||
* because the -1 test-constant is materialised AFTER the lbu loads in the target, freeing the
|
||||
* slot. Downstream of the counter/pressure equilibrium.
|
||||
* 5. mine=208 vs target=209 (one load-delay nop the target leaves, mine's tighter schedule fills).
|
||||
*
|
||||
* LEVERS TRIED (match_one masked-match / 209): dB-seed pinned 155 · pin-free structural 140 ·
|
||||
* +counter fences (m4) 140 · +single-pv spine (m7, THIS) 153 · +w0/av merge (m8) 144 ·
|
||||
* no-uvm-fence (m7a) 146 · clean-while outer (m10) 153 · block-scoped pv (m5)=m4 · siblings
|
||||
* func_80166994/CF04 are switch-shaped (no transferable vertex-loop idiom).
|
||||
* => genuine gdb-on-cc1 ORACLE territory (§34): the w0-load/w3 qty-birth order + the mat/i
|
||||
* allocno_compare priority are the two roots. Patch find_free_reg's qty pick (ffr2.gdb) +
|
||||
* the allocno priority mid-compile to confirm each, then hunt the pin-free C form
|
||||
* (flagship method). Confirms the prior agent's Fable5 verdict, now from a pin-free base.
|
||||
*
|
||||
* Best pin-free draft (m7, 153/209 masked in match_one) follows verbatim.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
short func_8016CBC0(void) {
|
||||
typedef struct { u16 v[6]; } VtxR;
|
||||
typedef struct { u8 active, nv, cnt, col; VtxR verts[16]; } BlkR;
|
||||
typedef struct { s32 w[8]; } Blk20;
|
||||
extern BlkR D_801D9CA0[];
|
||||
extern Blk20 D_800AE620;
|
||||
extern s32 func_80017758(void *a0, void *a1);
|
||||
|
||||
u8 buf[0x90];
|
||||
u8 col;
|
||||
int uv;
|
||||
int cnt;
|
||||
u8 *vb;
|
||||
void *mat;
|
||||
int i;
|
||||
short ret;
|
||||
|
||||
*(Blk20 *)(buf + 0x70) = D_800AE620;
|
||||
ret = 0;
|
||||
i = 0;
|
||||
mat = buf + 0x70;
|
||||
*(s32 *)(buf + 0x68) = 0x50000000;
|
||||
*(s32 *)(buf + 0x30) = 0x50000000;
|
||||
*(u8 *)(buf + 0x66) = 0;
|
||||
*(u8 *)(buf + 0x65) = 0;
|
||||
*(u8 *)(buf + 0x64) = 0;
|
||||
*(u8 *)(buf + 0x5E) = 0;
|
||||
*(u8 *)(buf + 0x5D) = 0;
|
||||
*(u8 *)(buf + 0x5C) = 0;
|
||||
*(u8 *)(buf + 0x2A) = 0;
|
||||
*(u8 *)(buf + 0x29) = 0;
|
||||
*(u8 *)(buf + 0x28) = 0;
|
||||
*(u8 *)(buf + 0x22) = 0;
|
||||
*(u8 *)(buf + 0x21) = 0;
|
||||
*(u8 *)(buf + 0x20) = 0;
|
||||
do {
|
||||
BlkR *e = &D_801D9CA0[(short)i];
|
||||
if (e->active != 0) {
|
||||
int tcnt;
|
||||
vb = (u8 *)e + 4;
|
||||
uv = e->nv;
|
||||
cnt = e->cnt;
|
||||
col = e->col;
|
||||
ret = ret + 1;
|
||||
__asm__("" :: "r"(mat));
|
||||
tcnt = cnt - 1;
|
||||
__asm__("" : "=r"(tcnt) : "0"(tcnt));
|
||||
cnt = tcnt;
|
||||
if (tcnt != -1) {
|
||||
do {
|
||||
s32 uvm;
|
||||
u8 *pv;
|
||||
{
|
||||
s32 po = (short)uv * 0xc;
|
||||
u8 c8c;
|
||||
pv = vb + po;
|
||||
{ s32 w0 = *(u16 *)(pv + 0x0); s32 w3;
|
||||
*(short *)(buf + 0x00) = w0;
|
||||
w3 = *(u16 *)(pv + 0x6);
|
||||
*(short *)(buf + 0x40) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x38) = w0;
|
||||
*(short *)(buf + 0x08) = w0; }
|
||||
{ s32 w0 = *(u16 *)(pv + 0x2); s32 w3;
|
||||
*(short *)(buf + 0x02) = w0;
|
||||
w3 = *(u16 *)(pv + 0x8);
|
||||
c8c = col >> 3;
|
||||
uvm = uv - 1;
|
||||
__asm__("" : "=r"(uvm) : "0"(uvm));
|
||||
uv = uvm;
|
||||
*(short *)(buf + 0x42) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x3A) = w0;
|
||||
*(short *)(buf + 0x0A) = w0; }
|
||||
{ s32 w0 = *(u16 *)(pv + 0x4); s32 w3;
|
||||
*(short *)(buf + 0x04) = w0;
|
||||
w3 = *(u16 *)(pv + 0xa);
|
||||
*(u8 *)(buf + 0x59) = col;
|
||||
*(u8 *)(buf + 0x25) = col;
|
||||
*(u8 *)(buf + 0x5A) = c8c;
|
||||
*(u8 *)(buf + 0x26) = c8c;
|
||||
*(u8 *)(buf + 0x58) = c8c;
|
||||
*(u8 *)(buf + 0x24) = c8c;
|
||||
*(short *)(buf + 0x44) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x3C) = w0;
|
||||
*(short *)(buf + 0x0C) = w0; }
|
||||
}
|
||||
if ((int)(uvm << 16) < 0) uv = 0xf;
|
||||
{
|
||||
s32 po = (short)uv * 0xc;
|
||||
u8 c84;
|
||||
pv = vb + po;
|
||||
col = col - 0xf;
|
||||
c84 = col >> 3;
|
||||
{ s32 w0 = *(u16 *)(pv + 0x0); s32 w3;
|
||||
*(short *)(buf + 0x10) = w0;
|
||||
w3 = *(u16 *)(pv + 0x6);
|
||||
*(short *)(buf + 0x50) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x48) = w0;
|
||||
*(short *)(buf + 0x18) = w0; }
|
||||
{ s32 w0 = *(u16 *)(pv + 0x2); s32 w3;
|
||||
*(short *)(buf + 0x12) = w0;
|
||||
w3 = *(u16 *)(pv + 0x8);
|
||||
*(short *)(buf + 0x52) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x4A) = w0;
|
||||
*(short *)(buf + 0x1A) = w0; }
|
||||
{ s32 w0 = *(u16 *)(pv + 0x4); s32 w3;
|
||||
*(short *)(buf + 0x14) = w0;
|
||||
w3 = *(u16 *)(pv + 0xa);
|
||||
*(u8 *)(buf + 0x61) = col;
|
||||
*(u8 *)(buf + 0x2D) = col;
|
||||
*(u8 *)(buf + 0x62) = c84;
|
||||
*(u8 *)(buf + 0x2E) = c84;
|
||||
*(u8 *)(buf + 0x60) = c84;
|
||||
*(u8 *)(buf + 0x2C) = c84;
|
||||
*(short *)(buf + 0x54) = w3;
|
||||
w0 = (u32)((short)w0 + (short)w3) >> 1;
|
||||
*(short *)(buf + 0x4C) = w0;
|
||||
*(short *)(buf + 0x1C) = w0; }
|
||||
}
|
||||
func_80017758(buf + 0x00, mat);
|
||||
func_80017758(buf + 0x38, mat);
|
||||
tcnt = cnt - 1;
|
||||
__asm__("" : "=r"(tcnt) : "0"(tcnt));
|
||||
cnt = tcnt;
|
||||
} while ((short)tcnt != -1);
|
||||
}
|
||||
}
|
||||
{
|
||||
int ti = i + 1;
|
||||
__asm__("" : "=r"(ti) : "0"(ti));
|
||||
i = ti;
|
||||
if ((short)ti >= 8) break;
|
||||
}
|
||||
} while (1);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
# The cc1-2.7.2 "pin-crash" SIGABRT — characterized (Fable5 discovery, 2026-07-15)
|
||||
|
||||
**Verdict up front: the §42e "CC1-CRASH WALL" is a misdiagnosis. The SIGABRT is real and now
|
||||
fully characterized (a gcc-2.7.2 sched1 REG_DEAD-note conservation bug), but it was triggered
|
||||
by the PROPAGATION STAGING dropping file-scope `#define` dependencies — not by "ov077-TU-context
|
||||
/ fixed-size allocator tables". All four backlogged pin families compile CLEAN in sibling TUs
|
||||
when properly staged: func_80133AB0 133/133 (era AND today), func_8013D9B0 133/133 (today,
|
||||
fleet-swept), func_8016DF5C + func_80156044 (era TU, spot-proven). The pin-×1 ceiling is a
|
||||
staging-tool artifact. P31's pin-propagation route is OPEN.**
|
||||
|
||||
All artifacts referenced below live in `.run/giants/pincrash/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. The abort site (exact)
|
||||
|
||||
**`gcc-2.7.2/sched.c:2725-2728`, in `create_reg_dead_note()`:**
|
||||
|
||||
```c
|
||||
/* The number of registers killed after scheduling must be the same as the
|
||||
number of registers killed before scheduling. ...
|
||||
We carefully remove REG_DEAD notes from the dead_notes list, so that
|
||||
there will be none left at the end. If we run out early, then there
|
||||
is a bug somewhere in flow, combine and/or sched. */
|
||||
if (dead_notes == 0)
|
||||
{
|
||||
#if 1
|
||||
abort (); /* <-- the SIGABRT / make Error 134 */
|
||||
```
|
||||
|
||||
`dead_notes` is a per-basic-block free list: `schedule_block` strips every REG_DEAD note off the
|
||||
block's insns onto it (sched.c:3581-3582, reset at 3255), reschedules, then re-attaches death
|
||||
notes from the recount (`attach_deaths_insn → attach_deaths → create_reg_dead_note`). The pool
|
||||
runs DRY when the post-schedule recount needs more deaths in the block than flow ever noted
|
||||
there. (The mirror-image surplus abort is sched.c:3967.)
|
||||
|
||||
**gdb backtrace (shipped i386 cc1, `tools/bin/gcc-2.7.2-psx/cc1`):**
|
||||
|
||||
```
|
||||
#0 0x08049160 in abort ()
|
||||
#1 0x08179ff1 in create_reg_dead_note ()
|
||||
#2 0x0817a79b in attach_deaths ()
|
||||
#3..#4 in attach_deaths () (recursive walk of the pattern)
|
||||
#5 0x0817ac12 in attach_deaths_insn ()
|
||||
#6 0x0817cd60 in schedule_block ()
|
||||
#7 0x0817f2f7 in schedule_insns ()
|
||||
#8 0x08073335 in rest_of_compilation ()
|
||||
```
|
||||
|
||||
**Pass: sched1** (`reload_completed == 0` at the abort — the before-reload scheduler,
|
||||
`-fschedule-insns`, on at -O2). Repro scripts: `abort.gdb`, `detail.gdb`, `min.gdb`.
|
||||
|
||||
At the abort on the real body (era staging of func_8013D9B0), the reg being killed was
|
||||
`(reg/v:SI 11 t3)` — the `register u16 *puVar10 __asm__("$11")` pin — at a plain
|
||||
`(zero_extend (mem:HI (reg 11)))` load. On the 12-line minimal repro it is `(reg/v:SI 4 a0)`
|
||||
at the outer `if (a != b)` `jump_insn`.
|
||||
|
||||
## 2. The minimal repro (12 lines, `minimal_repro.c`, ddmin-reduced from the real crasher)
|
||||
|
||||
```c
|
||||
typedef unsigned int u32;
|
||||
typedef int s32;
|
||||
extern void g();
|
||||
void f(int param_1)
|
||||
{
|
||||
register u32 a __asm__("$3"); /* caller-saved pin */
|
||||
register u32 b __asm__("$4"); /* caller-saved pin */
|
||||
g(); /* a CALL the pins' values must cross */
|
||||
if (a != b) { /* use after the call ... */
|
||||
if ((s32)a < (s32)b) a = a + 1; /* ... + branch-dependent partial update */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker minimal_repro.c` → SIGABRT
|
||||
(exit 134; Python subprocess sees returncode -6).
|
||||
|
||||
**Why (visible in `minimal_repro.c.combine`, the RTL fed to sched1):** flow places the single
|
||||
REG_DEAD note for each pinned reg at the last use on the FALL-THROUGH path (the inner `slt`
|
||||
insn, a later basic block). The uses at the outer branch (same block as the call) carry NO
|
||||
notes. sched1's per-block recount, walking the block that contains the CALL, sees the
|
||||
call-clobber kill the caller-saved regs and therefore demands a REG_DEAD note for their
|
||||
use-after-call inside THAT block — whose harvested pool is empty → abort. Flow's death
|
||||
placement and sched's clobber-aware recount disagree exactly and only for this shape.
|
||||
(sched.c's own comment names the class: "a bug somewhere in flow, combine and/or sched.")
|
||||
|
||||
## 3. The predicate — which pins are fatal vs safe (probe matrix, all byte-run)
|
||||
|
||||
| probe | shape | exit |
|
||||
|---|---|---|
|
||||
| minimal_repro | $3+$4 pins, call, nested-if partial update after | **134** |
|
||||
| p1 | same, NO call | 0 |
|
||||
| p2 | ONE pin ($3), call, nested-if | **134** |
|
||||
| p3 | pins initialized before the call (live-across, not uninit) | **134** |
|
||||
| p4 | same shape, callee-saved pins ($17/$18) | 0 |
|
||||
| p5 | one-level `if (a!=b) a=a+1;` after call | 0 |
|
||||
| p6 | non-arg caller-saved pins ($8/$9) | **134** |
|
||||
| p7 | single `if ((s32)a<(s32)b) a=a+1;` after call | 0 |
|
||||
| p8 | use-only after call (`if (a!=b) h();`) | 0 |
|
||||
| minimal_repro `-fno-schedule-insns` (or -O1) | sched1 off | 0 |
|
||||
|
||||
**FATAL:** `register T x __asm__("$N")` where $N is CALL-CLOBBERED ($2–$15, $24, $25), the
|
||||
value is used AFTER a CALL_INSN, and the post-call uses have a branch-dependent
|
||||
use-then-conditionally-set (nested-condition) shape.
|
||||
**SAFE:** callee-saved pins ($16–$23, $30) in any shape; caller-saved pins whose live range
|
||||
never crosses a call; use-only or single-level-conditional shapes.
|
||||
$0 (`zr`) pins: never observed to abort (fixed register; 133/133 clean, §5).
|
||||
|
||||
## 4. Why "compiles in ov077, dies in the sibling" — the REAL mechanism
|
||||
|
||||
`family_remap.extract_unit` carries a function's contiguous preceding `extern`/comment/
|
||||
single-line-`typedef` lines — **it does NOT carry `#define` lines** (and, in the Phase-25-era
|
||||
version, no typedef lines either; and its "definition" regex false-matches a one-line
|
||||
`extern ...; int wrapper(){...}` construct). ov_SC01_077's TU supplies each body's file-scope
|
||||
dependencies; the staged sibling body arrives WITHOUT them:
|
||||
|
||||
- **func_8013D9B0 (GTE 20-pin, THE one genuine crasher):** ov077 defines
|
||||
`#define gte_ldIR0z()/gte_ldrgb/gte_ldIRGB/gte_dpcl/gte_stORGB` as inline `__asm__ __volatile__`
|
||||
right above the body (ov_SC01_077.c:435-439, present in the era file too). The macro-less
|
||||
staged sibling body turns those five GTE ops into **implicit-declaration CALLS** — putting the
|
||||
caller-saved pins ($2,$3,$4,$6,$7,$8,$9,$10,$11,$13,$14) live across calls in the fatal shape
|
||||
→ the sched1 abort → `make` Error 134. Era-exact replication: `era_d9b0.c` → **exit 134**
|
||||
(the historical crash, reproduced). With the macros carried: **exit 0 in the era TU and in
|
||||
ALL 133 sibling TUs today** (`d9b0_fleet.py`: clean 133/133; 4 SC07 overlays have no image =
|
||||
not siblings). In the REAL body no pin crosses a real call (the GTE ops are asm; the one true
|
||||
call, func_800599B8, is crossed only by callee-saved pins) — which is why ov077 banked it.
|
||||
- **func_80133AB0 (zero-reg pin, "crashed 3/3"): NEVER crashed.** Era staging dropped its
|
||||
multi-line `typedef struct Map_80133AB0` (era extract_unit couldn't carry it) →
|
||||
`Map_80133AB0 undeclared` → **exit 33** (plain error), both era layouts tested (`histA/histB`).
|
||||
Properly staged (typedef + the sibling's `extern int func_80133AB0(int,s16,s16,int);` →
|
||||
s16-form decl flip): **exit 0 in 133/133 sibling TUs today** (`sweep.sh`) **and 133/133 at the
|
||||
era commit commit:0510** (`era_sweep.sh`). The Error-134 attribution: in the era one-big-split
|
||||
layout its stub shared `ov_SC01_XXX.c` — and therefore the (overlay,split) gate compile —
|
||||
with func_8013D9B0's stub; d9b0's genuine abort killed the shared TU compile and the 134 was
|
||||
blamed on every family staged there.
|
||||
- **func_8016DF5C (GTE-pin):** era staging dropped its single-line
|
||||
`typedef struct { s32 w[8]; } Blk20_8016DF5C;` → **exit 33** (`era_df5c.c`). With the typedef
|
||||
(today's extract_unit carries it): **exit 0** against the era sibling TU (`era_df5c_td.c`).
|
||||
Its pins are $17 (callee-saved) + block-scoped $3 not crossing calls → safe class.
|
||||
- **func_80156044 (trampoline):** extract_unit false-positive — the def-search regex matches the
|
||||
ONE-LINE `extern void func_80156044(int,int); int func_80155FF8(...) { __asm__(...); }`
|
||||
wrapper (the trailing `/* dedup */` comment defeats the era `;`-endswith guard), so the staged
|
||||
"body" was func_80155FF8's WRAPPER → `redefinition of func_80155FF8` → **exit 33**
|
||||
(`era_x6044.c`). The REAL body + the engine_core int→void flip: **exit 0** against the era
|
||||
sibling TU (`era_x6044_flip.i`). Its $2/$3 pins are used before the first call → safe class.
|
||||
|
||||
So: "the pins are ov077-TU-context-specific" is REFUTED. The pins were never the problem in 3 of
|
||||
4 families, and in the 4th the problem was the staging deleting the asm macros around them. No
|
||||
fixed-size-table, no register-pressure, no surrounding-function-set effect exists — decls-only
|
||||
context reproduces every result (ddmin `mask_min.txt`: the masking "TU context" reduced to the
|
||||
5 basic typedefs + 3 externs, i.e. to nothing).
|
||||
|
||||
## 5. Repro commands
|
||||
|
||||
```sh
|
||||
# the genuine SIGABRT (minimal):
|
||||
tools/bin/gcc-2.7.2-psx/cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker \
|
||||
.run/giants/pincrash/minimal_repro.c -o /dev/null # exit 134
|
||||
# the historical crash, era-exact (staged macro-less GTE body in the commit:0510 sibling TU):
|
||||
tools/bin/gcc-2.7.2-psx/cc1 <same flags> .run/giants/pincrash/era_d9b0.i -o /dev/null # exit 134
|
||||
# the same body properly staged (macros carried) — clean:
|
||||
tools/bin/gcc-2.7.2-psx/cc1 <same flags> .run/giants/pincrash/d9b0_000.tu3.i -o /dev/null # exit 0
|
||||
# gdb: gdb -batch -x .run/giants/pincrash/min.gdb tools/bin/gcc-2.7.2-psx/cc1
|
||||
```
|
||||
|
||||
Fleet evidence: `sweep_results.txt` (80133AB0 today, 133×exit=0), `era_sweep_results.txt`
|
||||
(80133AB0 at commit:0510, 133×exit=0), `d9b0_fleet.py` output (8013D9B0 today, 133×exit=0).
|
||||
|
||||
## 6. Implications for Phase 31 (the pin-×1 endgame)
|
||||
|
||||
1. **The ×1 ceiling is not a compiler wall — it is a fixable staging gap.** To propagate the four
|
||||
backlogged families, the per-sibling unit must carry: (a) file-scope `#define` blocks the body
|
||||
uses (extract_unit currently drops ALL `#define` lines — add `"#define"` to its prefix-walk,
|
||||
or stage macros like the single-line-typedef carry), (b) multi-line typedefs (route through
|
||||
the engine_types.h lift or flatten to single-line), (c) the per-sibling decl flips
|
||||
(`//@EDIT`-class: the s16-decl flip for 80133AB0, `(void)`→`()` + the hoisted
|
||||
`extern s16 *D_...;` for 8013D9B0, the engine_core int→void flip for 80156044), and (d) a fix
|
||||
for the extract_unit one-line-wrapper false-positive (require the def-line's brace-block, or
|
||||
reject lines starting `extern`). With (a)-(d), cc1 compiles all four in every sibling TU.
|
||||
Byte-identity per sibling remains for the whole-binary gate to decide — but the "cc1-crash"
|
||||
barrier does not exist.
|
||||
2. **The fatal-pin predicate is checkable at draft time** (§3): flag any caller-saved
|
||||
`register __asm__` pin whose variable is referenced after a function call. Such drafts are the
|
||||
only ones that can ever hit this abort — and even they are fine if the "call" was really a
|
||||
macro/asm that staging must preserve.
|
||||
3. **Diagnostic signature:** cc1 exit 134 + this backtrace (abort ← create_reg_dead_note ←
|
||||
attach_deaths) = this bug, ALWAYS. An exit-33 is ordinary decl/typedef plumbing — the R14
|
||||
lesson recursed: the era experiment's "SIGABRT, universal, 3/3" folded three distinct exit-33
|
||||
plumbing failures into one crash bucket because the shared-TU gate reported one code.
|
||||
4. `-fno-schedule-insns` (or -O1) suppresses the abort — useless for matching (changes schedule),
|
||||
but confirms the sched1 locus and is a safe probe for "is this the dead-notes bug?".
|
||||
|
||||
## 7. What was NOT determined
|
||||
|
||||
- The exact flow.c line placing the fall-through REG_DEAD (the mechanism is proven from the
|
||||
combine dump + the abort state; the flow-side walk was not source-traced line-by-line).
|
||||
- Whether the four families byte-MATCH their siblings once staged correctly (compile-clean is
|
||||
proven; matching is the byte-gate's question, out of scope for this characterization).
|
||||
@@ -0,0 +1,12 @@
|
||||
set pagination off
|
||||
set confirm off
|
||||
set height 0
|
||||
set width 0
|
||||
set debuginfod enabled off
|
||||
break abort
|
||||
break fancy_abort
|
||||
break fatal_insn
|
||||
run -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker /home/musashi/bfm-decomp/.run/giants/pincrash/era_d9b0.i -o /home/musashi/bfm-decomp/.run/giants/pincrash/era_d9b0.s
|
||||
echo \n=== BACKTRACE ===\n
|
||||
bt 25
|
||||
quit
|
||||
@@ -0,0 +1,25 @@
|
||||
set pagination off
|
||||
set confirm off
|
||||
set height 0
|
||||
set width 0
|
||||
set debuginfod enabled off
|
||||
break abort
|
||||
run -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker /home/musashi/bfm-decomp/.run/giants/pincrash/mini_call.c -o /dev/null
|
||||
echo \n=== reload_completed (0=sched1, 1=sched2):\n
|
||||
print (int)reload_completed
|
||||
echo \n=== BACKTRACE ===\n
|
||||
bt 8
|
||||
echo \n=== create_reg_dead_note args (frame 1): reg rtx ===\n
|
||||
frame 1
|
||||
info frame
|
||||
echo \n=== the reg being killed (debug_rtx of arg0) ===\n
|
||||
# cdecl: args at ebp+8 (reg), ebp+12 (insn)
|
||||
set $cr_ebp = $ebp
|
||||
frame 0
|
||||
# walk to frame 1's saved ebp: at abort entry, esp points at return addr; caller ebp unknown -> use frame cmd
|
||||
frame 1
|
||||
x/2wx $ebp+8
|
||||
call (void)debug_rtx(*(void**)($ebp+8))
|
||||
echo \n=== the insn being processed ===\n
|
||||
call (void)debug_rtx(*(void**)($ebp+12))
|
||||
quit
|
||||
@@ -0,0 +1,12 @@
|
||||
set pagination off
|
||||
set confirm off
|
||||
set debuginfod enabled off
|
||||
break abort
|
||||
run -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker /home/musashi/bfm-decomp/.run/giants/pincrash/minimal_repro.c -o /dev/null
|
||||
bt 8
|
||||
print (int)reload_completed
|
||||
frame 1
|
||||
x/2wx $ebp+8
|
||||
call (void)debug_rtx(*(void**)($ebp+8))
|
||||
call (void)debug_rtx(*(void**)($ebp+12))
|
||||
quit
|
||||
@@ -0,0 +1,12 @@
|
||||
typedef unsigned int u32;
|
||||
typedef int s32;
|
||||
extern void g();
|
||||
void f(int param_1)
|
||||
{
|
||||
register u32 a __asm__("$3");
|
||||
register u32 b __asm__("$4");
|
||||
g();
|
||||
if (a != b) {
|
||||
if ((s32)a < (s32)b) a = a + 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user