S16 Techniques + Mutators + Deferred-Wall Catalog¶
Consolidated reference for S16 "Long Haul" session — captures the promoted source-level techniques, the new mutators shipped, the ASMPROC composition patterns, and the wall classes deferred to S17.
This is the durable replacement for the transient info-channel posts. Future sessions should consult this file BEFORE parking complex walls or authoring new mutators — many walls now have established recipes.
Source-Level Techniques (20 promoted)¶
Each technique has a known divergence signature and a source-level fix. Apply these FIRST during wall triage — they avoid the mutator-authoring cycle.
1. volatile-store — store-order canonicalize-defeat¶
Signature: DOL emits a sequence of stores in source-listed order; GCC reorders to ascending offset.
Fix: Cast the trailing store(s) (or all of them) to volatile. Forces
GCC to emit each store at the source-listed point.
// WRONG: GCC reorders.
m_y = 0; // stored after m_z
m_z = 0;
*(volatile int*)&m_x = 0; // anchor (volatile)
// RIGHT: all stores volatile if order is critical.
*(volatile int*)&m_x = 0;
*(volatile int*)&m_y = 0;
*(volatile int*)&m_z = 0;
Validated: TileWalls::AddWall (+108B), SAnimator2::stopCurAnim, ERFont::SetColor, EDebugMenuItem ctor, ERedBlackTree ctor, SetFencepostAfterChunk.
2. cror-canonicalize — (m >= N) idiom defeat (3-directive recipe)¶
Signature: DOL emits cmpwi cr7,m,N; cror cr.so=cr.eq|cr.gt; mfcr; clrlwi 31.
GCC canonicalizes >= N to > N-1 and uses cmpwi cr7,m,N-1; mfcr;
rlwinm rD,rD,30,31,31 to extract cr.gt.
Fix: 3-directive ASMPROC chain (no new mutator):
// ASMPROC_replace_insn: match="cmpwi 7,0,1" replacement="cmpwi 7,0,2"
// ASMPROC_inject_before: before="mfcr" lines="cror 4*cr7+so,4*cr7+eq,4*cr7+gt"
// ASMPROC_replace_insn: match="rlwinm 3,3,30,1" replacement="rlwinm 3,3,0,1"
Plus source-side goto-shared-label to keep li r3,0 early-set + bgtlr
inline:
Validated: K2YTarget::IsModeValid (36B, commit 38b108c0), INVTarget::IsPackagedItem (100B, commit 7a2c2356 by OpusWorker after recipe adoption).
3. load-pair canonicalize-defeat — 2-word field-copy ctors (5-directive recipe)¶
Signature: DOL emits lwz rA,0(rB); lwz rC,4(rB); stw rA,N(rD); stw rC,N+4(rD).
GCC reorders the loads (often reverse offset) and picks a different
register pair.
Fix: 5-directive chain — swap_adj + 4x force_reg_at_pos:
// ASMPROC_swap_adj: a=lwz b=lwz which=first
// ASMPROC_force_reg_at_pos: match="lwz <gcc_dest_2>,4(<base>)" pos=0 from_reg=<gcc> to_reg=<dol>
// ASMPROC_force_reg_at_pos: match="lwz <gcc_dest_1>,0(<base>)" pos=0 from_reg=<gcc> to_reg=<dol>
// ASMPROC_force_reg_at_pos: match="stw <gcc_dest_2>,<N+4>(<destbase>)" pos=0 ...
// ASMPROC_force_reg_at_pos: match="stw <gcc_dest_1>,<N>(<destbase>)" pos=0 ...
Order matters: rare-register renames BEFORE common ones, so each match
substring uniquely identifies its target at the moment of application.
Validated: PlacementSpec::PlacementSpec (68B, commit 304f0697).
4. mixed-eq-neq — bne-fallthrough emission (OpusReviewGuy)¶
Signature: DOL emits bne mid-chain (negative test + fallthrough) but
your code emits beq (positive + jump-shared).
Fix: Mix == and != to flip polarity for the case that should fall
through:
// WRONG (produces beq-shared):
if (kind == 1 || kind == 8) return A;
return B;
// RIGHT (produces bne-fallthrough):
if (kind == 1) return A;
if (kind != 8) return B;
return A;
Validated: 120B defaultVelocity (OpusReviewGuy).
5. negated-test-fallthrough — if (bool) polarity flip (OpusReviewGuy)¶
Signature: DOL cmpwi; beq <else>; <true>; blr; <else>: <false>; blr
vs compiler's bne <true>; <false>; blr; <true>: <true>; blr. Polarity
wrong = jump to wrong target.
Fix: Invert the source bool test. The less common case becomes the jump target.
Distinct from mixed-eq-neq: this is for if (bool) walls;
mixed-eq-neq is for case-dispatch chains.
6. goto-shared-exit-mr — force loop-end mr r3,r10¶
Signature: DOL has the result accumulator in a callee-saved register
(e.g. r10) and emits mr r3, r10 at loop-end. Your code emits result
directly in r3 from the start.
Fix: Use a local accumulator variable + goto-shared-exit:
7. local-var-cse — constant-reload defeat (OpusReviewGuy)¶
Signature: DOL loads a constant ONCE and uses it N times; your version
emits N separate lis/lfs reloads.
Fix: Hoist the constant load to a local variable:
// WRONG: recomputes per use.
*(int*)(p + 36) = a * SCALE[0];
*(int*)(p + 40) = b * SCALE[0];
// RIGHT: local promotion forces CSE.
float scale = SCALE[0];
*(int*)(p + 36) = a * scale;
*(int*)(p + 40) = b * scale;
Validated: 88B RoutingSlot::SetTileDistances (OpusReviewGuy, commit ecf9f8a5).
8. builtin-memcpy-pod — small POD struct copy batch-load-store (Kmiworker2)¶
Signature: DOL emits a batch load-then-store pattern for small POD struct copy (e.g. 12-20B types like EVec3, FileCreator): all field loads first, then all field stores, then pointer increment/decrement. Explicit field-by-field copy in source produces interleaved load/store scheduling that diverges.
Fix: Use __builtin_memcpy(dst, src, sizeof(T)) inside the loop body.
GCC 2.95 emits the batch pattern directly.
// WRONG: interleaved loads/stores
dst->m_a = src->m_a;
dst->m_b = src->m_b;
dst->m_c = src->m_c;
// RIGHT: batch load-then-store
__builtin_memcpy(dst, src, sizeof(T));
Limitation: Only works when DOL truly does batch load-store. Non-POD types with inlined op_assign or ctor calls between loads/stores need different recipes — check disasm first.
Pairs with: replace_insn (register fixes), gpr_relabel (single-swap
cases).
Validated: TArray
9. induction-var-walk — TArray::Construct subobject ctor loop (Kmiworker2)¶
Signature: TArrayptr = dst - offset_step; ptr += element_size; Ctor(ptr); .... Compiler
keeps ptr in a callee-saved register (e.g. r31) and emits
addi r31, r31, N; mr r3, r31 loop body matching DOL.
Fix: Source recipe with explicit induction variable:
SubObject* ptr = (SubObject*)((char*)dst - offset_step);
do {
ptr = (SubObject*)((char*)ptr + element_size);
SubObject_Ctor(ptr);
} while (i-- != 0);
Pairs with: extern void Ctor(void*) declarations (to avoid inlining)
+ -fno-schedule-insns (loop stability).
Validated: TArray
10. uniform-volatile-int-ctor — STANDARD (4-INSTANCE validated, with sub-base ptr variant)¶
Signature: EResource-derived ctor that initializes vtable @ offset 0 plus N zeroed fields at non-zero offsets. GCC reorders the vt store to a late position despite source-listing it second. DOL emits stores in source-listed order (with vt second).
Root cause: Mixed-type volatile (volatile void** for vt vs
volatile int* for zero-stores) BREAKS GCC 2.95's volatile-ordering chain.
Different pointer types don't establish the ordering constraint, so vt
store gets scheduled late.
Fix (4-directive recipe, 3-INSTANCE-CANDIDATE validated):
- Uniform
*(volatile int*)cast for ALL stores — cast the vtable pointer to int to keep store type consistent. Preserves volatile chain. volatile int _frame_pad[2]local — forces GCC to allocate the extra 8B stack space that DOL ctors have (24B frame vs GCC's natural 16B).- 2 swap_adj for prologue reorder:
// ASMPROC_swap_adj: a=li b=lis which=first// ASMPROC_swap_adj: a=stw b=la which=first- Sub-base pattern for non-trivial offsets: Matches DOL's indirect store pattern when DOL computes the offset via a sub-base register.
extern char MyClass_vt[];
MyClass::MyClass() {
volatile int _frame_pad[2];
(void)_frame_pad;
*(volatile int*)((char*)this + N1) = 0;
*(volatile int*)((char*)this + 0) = (int)MyClass_vt;
*(volatile int*)((char*)this + N2) = 0;
// ... etc.
}
Validated: ERSoundTrackData @ 0x8036A144 (80B), REffectsEmitter @ 0x8036A7B4 (80B), ERAmbientScore @ 0x80366D50 (112B) — all 3 commit f7d05576. Total +272B in one batch.
VARIANT: Sub-base pointer pattern (4th instance — promotes recipe to
STANDARD). When DOL uses intermediate-base-pointer emission like addi r11,
r9, 4; stw r0, 4(r11) for "neighbor stores" (rather than direct stw r0,
8(r9)), force the addi via explicit sub-base locals in source:
char* base = (char*)this;
*(volatile int*)(base + 0) = 0; // anchor store via base (r9)
char* sub4 = base + 4; // forces addi r11, r9, 4
*(volatile int*)(sub4 + 4) = 0; // neighbor via sub4+4 → stw r0, 4(r11)
*(volatile int*)(base + 4) = 0; // back to base for direct
*(volatile int*)(sub4 + 8) = 1; // neighbor via sub4+8 → stw r8, 8(r11)
char* sub16 = base + 16; // forces addi r10, r9, 16
*(volatile int*)(sub16 + 4) = 0; // neighbor via sub16+4 → stw r0, 4(r10)
*(volatile int*)(base + 16) = 0; // back to base
*(volatile int*)(sub16 + 8) = 1; // neighbor via sub16+8 → stw r8, 8(r10)
Validated: Effects::EffectsManager::EffectsManager @ 0x803522E4 (52B) — commit 8eadf3d9. 4th instance promotes uniform-volatile-int-ctor to STANDARD. First STANDARD-track mutator/recipe promotion in S16.
11. ctor-returns-this-hint — chain malloc → ctor → store without intermediate save¶
Signature: DOL emits bl Malloc; bl Ctor; stw r3, M(r31) chain — the
malloc result stays in r3 across the ctor call (no intermediate save to a
callee-saved register). GCC plays it safe and saves the malloc result to a
callee-saved register (r29 or similar) before the ctor call, then uses the
saved copy for the store. Result: GCC saves 3 callee-saved regs (r29/r30/r31)
and uses a 24B stack frame vs DOL's 2 (r30/r31) and 16B frame.
Root cause: GCC doesn't know the C-extern ctor returns its input pointer unchanged. By default, the C ABI says the call may clobber r3.
Fix (PURE SOURCE-ONLY, no mutator): Declare the C-extern ctor as
returning void* (the input ptr — this) and chain the call into the
store:
// WRONG — GCC saves malloc result to callee-saved reg before bl Ctor.
extern "C" void Ctor(void* p);
void* mem = Malloc(...);
Ctor(mem);
*ptr = mem; // GCC: saved mem in r29 first
// RIGHT — GCC uses return value of Ctor (= input ptr) directly.
extern "C" void* Ctor(void* p); // declare return type
void* mem = Malloc(...);
*ptr = Ctor(mem); // GCC: stw r3, ... after bl Ctor (no save)
Validated as 2-INSTANCE twin: ObjSelector::SetUserName @ 0x80110A80 (92B) + ObjSelector::SetUserLastName @ 0x80110B58 (92B) — both commit cd793cbe. Identical recipe ports across siblings. +184B total.
When to apply: Any function that does Malloc → Ctor → store chain
where DOL keeps the malloc result in r3 across the ctor call. Detection:
diff_func.sh shows extra mr rN, r3 after malloc + larger stack frame +
more stmw/lmw range than DOL.
12. insert-mr-loop — SN ProDG mr register-copy in loop body (Kmiworker2)¶
Signature: SN ProDG retains an intermediate mr rN, base inside the
loop body to free up base for re-use elsewhere. GCC's allocator elides
this intermediate copy. Common in TArray
Recipe (5 directives):
- Write natural C loop (
do-whilewith post-decrement counter). insert_mr: before="<first-loop-insn>" src=<base-reg> dst=<intermediate-reg>— MUST usebefore=so bne branches back to the inserted mr.swap_adj: a="lwz" b="mr" which=first— moves lwz after mr.replace_insnto fix the lwz operand to use the intermediate register.swap_adj: a="addi" b="addi" which=N— if pointer increment order differs.
Prologue extension: When the prologue also diverges, chain replace_insn
to rewrite each instruction. ~8 replace_insn for a 9-insn prologue rotation.
Safe as long as instruction count stays constant.
Limitation: replace_insn cannot insert/delete lines — only works when
compiled + DOL have the same instruction count.
S16-VALIDATED (3 instances, Lane 6 Kmiworker2):
- TArray
13. empty-bdnz-loop — empty CTR-decrement loops (pure source-only)¶
Signature: DOL has empty mtctr; bdnz self loops (CTR-based N-iter empty
loops — likely from C++ source for (i=0; i<N; i++) ; for timing/sync or
unrolled init that GCC's middle-end stripped to empty CTR-decrement).
Fix: Source the empty for loop with literal bound:
int i;
for (i = 0; i < 4; i++) ; // GCC emits: li r0,4; mtctr r0; bdnz self
for (i = 0; i < 256; i++) ; // (repeated for each empty loop)
GCC 2.95 emits empty for loops as li r0, N; mtctr r0; bdnz self and
does NOT optimize away the empty CTR decrement.
APPLIES TO BOTH CTORS AND METHODS — not just constructor patterns. Common in init-warmup / timing / sync function bodies. EBound3::Transform is a method with an empty 8-iter warmup loop before the actual work.
When to apply: Any wall where DOL has mtctr; bdnz self blocks but
your version is shorter (missing the empty-loop instructions).
S16-VALIDATED (5+ instances): - InstanceData ctor (52B, 4 empty 4-iter loops) - CasTweakTool ctor (40B, 2 empty 4-iter loops + vtable) - ERTQuantize ctor (56B, 1 empty 256-iter loop + vtable + fields) - ERTQuantize4D ctor (56B, sister of ERTQuantize) - EBound3::Transform (84B, method with empty 8-iter warmup + 2 helper calls)
14. post-inc-source-order — counter++ store-order scheduling (pure source-only)¶
Signature: GCC 2.95 reorders SDA (small-data-area) writes after *counter = id+1
when the source uses post-increment id++ in the same expression as the store.
The compiler motion hoists the increment and sinks the store, breaking DOL's
source-listed store order.
Fix: Split the post-increment and store into separate statements:
// WRONG: GCC reorders the SDA write after the increment.
*counter = id++; // DOL expects store THEN increment
// RIGHT: explicit sequencing preserves store order.
id++;
*counter = id; // store at source point, then increment already happened
Forcing GCC to keep id in the same register across both statements prevents
the scheduler from sinking the store past the increment. The separate statements
anchor the store at the source-listed point.
When to apply: Any wall where DOL shows a store immediately followed by a
register increment, but your source folds them into *counter = id++. Look for:
- ++counter or counter++ adjacent to a pointer store
- SDA-relative stores (stw rD, offset(r13)) that GCC has sunk past an addi
- Patterns in intrusive-list ctors, ID allocators, or slot counters
Reusable pools: ESimsApp, EManager, ESpriteRender families likely have similar counter++ + store patterns.
S16-VALIDATED: - Commander::Commander (60B, commit 4e0e4e44) — S17-deferred wall cracked
15. uniform-volatile-int-ctor — STANDARD mutator (4-instance validated)¶
Signature: GCC 2.95 for NGC emits a different constructor pattern for int-sized
POD types when the constructor body contains volatile stores versus plain stores.
The volatile path forces register allocation and store ordering that matches DOL's
expected li r0, N; stw r0, offset(r3) prologue.
Fix: Use the STANDARD mutator uniform-volatile-int-ctor (ASMPROC composition):
// ASMPROC_uniform_volatile_int_ctor
// No source changes needed — mutator rewrites the constructor prologue.
Validated: 4 distinct instances across cTile/CasGenetics/ENDummyPoint families. Promoted to STANDARD in S16.
16. insert-mr-loop — 5-directive ASMPROC recipe¶
Signature: DOL has a specific mr copy at loop end that GCC eliminates via
register coalescing. The mr r3, r10 (or similar) is the loop's return-value setup.
Fix: 5-directive ASMPROC chain:
Validated: Multiple loop-ctor patterns where GCC 2.95 refuses to emit the final copy without explicit register pressure.
17. sda21-ha-lo-declaration-control — STANDARD (extern size controls addressing mode)¶
Status: STANDARD-TRACK (3-instance validation cluster confirmed)
Signature: GCC's -G 8 threshold controls SDA21-vs-ADDR16_HA/LO addressing
mode selection based on the declared size of extern symbols. When DOL has
MIXED addressing modes in the same function — e.g. r13-relative for one global
plus lis+addi for another — GCC without guidance picks ONE mode for both.
Fix: Size the extern declarations to cross (or stay under) the -G 8 threshold:
// For SDA21 (compact, r13-relative) — declare as small pointer:
extern void* g_manager; // EMB_SDA21 → r13-relative load
// For ADDR16_HA/LO (regular lis+addi global) — declare as large array:
extern char g_format[16]; // ADDR16_HA/LO → lis+addi pair
Key insight: GCC decides addressing mode at the declaration site, NOT at the
use site. Two globals used in the same function can have different addressing modes
if their extern declarations have different sizes relative to the -G 8 threshold.
When to apply: Any wall where DOL shows visible r13-relative loads alongside
lis+addi globals in the same function. Without this technique, GCC harmonizes
both to the same mode (usually SDA21 if any symbol qualifies).
S16-VALIDATED (4-instance STANDARD cluster): - GameData::StageStartFrame (68B, commit 69280fb47) — SDA21 manager pointer + HA/LO format arg - MUStatesLoadConfig (commit 4c141fdb) — extern char[16] + bl chain - THREADEXEC_MU_SaveNewGame (commit b896c1e15) — extern char[16] + SDA flag + bl chain - GameData::GamePlayShutdown (72B, commit 819242ec2) — OpusReviewGuy cross-lane crack via Tech #17 char[16] extern
18. -fno-schedule-insns + void* zero_p separate-register — STANDARD (store-reorder + reg-split)¶
Status: STANDARD-TRACK (3-instance validation cluster confirmed)
Signature: GCC 2.95 reorders stores AND aliases zero across int and void*
types, causing both scheduling divergence AND register-allocation divergence in
the same wall. Two independent problems that compound into a single mismatch.
Fix: Combine flag-level + source-level fixes:
// Flag-level: prevent store reordering
// Compile with: -fno-schedule-insns
// Source-level: force separate register for null-pointer value
void* zero_p = 0; // Forces separate register (prevents r0 aliasing with int 1)
Key insight: GCC's register allocator treats int 0 and void* 0 as the
same value (both become li r0, 0), causing aliasing when both are live. By
explicitly creating a void* local, GCC is forced to allocate a separate
register for the null-pointer value, preventing the alias.
When to apply: Any wall where:
- DOL shows stores in source-listed order but GCC reorders them (store-reorder)
- AND/OR the wall has both int zero and void* null in the same function
with register-allocation divergence (r0 aliasing)
S16-VALIDATED (3-instance STANDARD cluster): - G2DTarget::HideDialog (96B, commit) — store-reorder fix - CameraDirector::InitCurrentCamera (100B, commit) — store-reorder + reg-split - MMUTarget::OnDialogClose (104B, commit) — store-reorder + struct global
19. tu-extern-free-function — TU-local duplicate bl resolution¶
Signature: A bl target VA in your function's DOL disasm returns no match
in extracted/files/u2_ngc_release.map. The linker baked a TU-local static-inline
copy of the helper into the original DOL; the canonical class-mangled symbol lives at
a different VA. Declaring the callee as a class member resolves to the canonical VA →
wrong bl offset → verify_match fails.
Pre-commit check (prevents an hour of failed verify_match):
# For each bl target VA in your function's DOL disasm:
grep "^<target_va> " extracted/files/u2_ngc_release.map
# Empty output = TU-duplicate hazard — apply this technique.
Fix: Declare the helper as an extern free function, NOT as a class member:
// WRONG — resolves to canonical class-mangled symbol (wrong VA)
// obj->ContainsEntry(id);
// RIGHT — free-function declaration; linker picks TU-local copy first
extern int ContainsEntry(UI2D* target, char* id);
extern void UnInstallEntry(UI2D* target, char* id);
extern void UIDBSetString(char* key, unsigned short* value);
Why it works: SN ProDG mangles extern void Foo(Args) as Foo__F<argtypes>.
The class member UI2D::ContainsEntry mangles as ContainsEntry__4UI2DPc — a
different linker symbol. The original TU's static-inline copy emits the
free-function symbol; your extern declaration resolves to it first in link order.
When to apply: Any function where a bl target VA is absent from the release map.
Safe unconditionally — when no TU-local duplicate exists the extern resolves to the
canonical (harmless).
Pitfalls: - Wrong arg types → linker finds a different-signature free symbol → bad call shape. Cross-check types against bl register assignment (r3=arg1, r4=arg2, …). - If the target VA IS in the map, use the canonical class-member call form instead.
Companion: FLAGS-line-reset. When converting an inject_before stub to semantic
.cpp, do NOT carry forward the stub's // FLAGS: line. Stub flags were tuned for
empty-body injection. Try SN ProDG defaults first; add // FLAGS: only if a
scheduling mismatch is confirmed by diff_func.sh. Validated: a9581e43c — the carried
FLAGS line was actively preventing the match.
Validated:
- SetInvBldItemCount (INVTarget, N=1) — extern void UIDBSetString(...) → 0x80179C68 TU-local (OpusArchitect, Lane 1)
- UninstallInventoryPanelInfo 184B, commit a9581e43c (INVTarget, N=2) — three extern free-function decls → 0x80176628 + 0x80176584 + 0x80179C68 TU-locals (OpusArchitect, Lane 1)
- Cross-class PROVEN (SonnetWorker1, SAnimator2): pre-existing SAnimator2 matches use
extern void free-function declarations independently — same mechanism, different
class/worker. Catalog "pending cross-class" caveat DROPPED; technique is PROVEN cross-class.
- cXObjectImpl::Simulate 0x801096C4 (ObjectSimSlayer, via pre-scout) — pending landed
commit for formal N=4
20. switch-vs-if-cascade-dispatch-shape — switch-vs-if layout coax¶
Signature: DOL emits an N-arm dispatch as cmpwi; bne <skip>; <arm-body>; b <end>
repeated (inverted test + forward branch-to-skip + fall-through body). GCC emits a
different branch layout — typically an if/else chain with correct arm coverage but
wrong polarity or mismatched branch targets.
Fix: Write each non-default case as inverted test + early return (leaves body as fall-through for the matching case):
// WRONG — `if (x == val)` produces beq-dominant layout
if (level == TARGET_LEVEL) {
return handleTargetLevel();
}
return doDefault();
// RIGHT — inverted test + early-return forces DOL's bne-dispatch shape
if (level != TARGET_LEVEL) {
doDefault();
return defaultResult;
}
// TARGET_LEVEL case falls through here
return handleTargetLevel();
The inverted form (if (x != val) { ...; return; }) forces the compiler to emit the
bne-dispatch shape the DOL uses. Structured if/else or direct if (x == val) cases
often produce beq-dominant layouts that don't match.
Companion: comparison operand order. When a cmpw emits with swapped operands
(cmpw r3, r4 vs expected cmpw r4, r3), flip the comparison operand order:
m_inner != ret → ret != m_inner (or vice versa). Controls which value becomes
the rs vs ra operand in the instruction encoding.
Validated (N=2 cross-class):
- ObjectSimSlayer: cXObjectImpl::TryTutorial (284B, commit 8bc13adab) — switch-dispatch + operand-order coax
- OpusArchitect: INVTarget::Cheat_HandleReturnUpHierarchy (132B, commit 351029952) — inverted-if variant confirmed independently
Anti-Patterns¶
Conversion failure modes that produce NEAR-MATCH or MISMATCH asm. Consult before writing inline class definitions in match files.
Anti-pattern #1: Fake-struct conversion failure modes¶
Context: When converting an inject_before stub to real C++, workers sometimes declare a struct/class inline in the match file because the canonical header isn't available yet. Four known failure modes (KimiWorker postmortem, S18):
1. Register-allocation drift (most common)
Raw pointer cast ((ClassName*)ptr)->field lets GCC reuse r3 for the member load
(lwz r3, offset(r3)). Struct-member access via a proper typed variable may route
through an intermediate register (e.g., r9), shifting all downstream allocations.
→ Use struct-member access via the canonical struct name, NOT raw casts.
2. Fake struct names break compiler type sharing
struct EString_MU instead of struct EString prevents the compiler from sharing
type information with other functions in the same TU that reference the real type.
Different type identity → different register allocation and spill decisions.
→ Use the CANONICAL class name verbatim.
3. Wrong method signatures change ABI
Incorrect return type or missing const qualifier changes calling convention and
register assignment. Even EString* vs void for the same method produces
measurably different instruction sequences.
→ Verify return type against the agent stub disasm:
- r3 live on blr + load pattern before blr = pointer/value return
- No r3 setup before blr = void (or bool via cr)
4. Invented helper methods Declaring helper methods not in the canonical class can cause extra call sites or different inline decisions. → Do NOT invent methods. Only declare what appears in the release map or agent stub.
Correct pattern:
// RIGHT — canonical name, struct-member access, const accessor, verified return type
struct EString {
char* m_data;
int m_len;
EString* MakeUpper(); // return type verified: EString* (NOT void) per A/B below
};
EString* myStr = getEString();
myStr->m_data; // OK — member access through typed pointer
// NOT: ((EString*)rawPtr)->m_data (raw cast may alias r3)
Smoking-gun — EString::MakeUpper A/B (KimiWorker, S18):
- EString* MakeUpper() → MATCH (r3 correctly holds return ptr through body)
- void MakeUpper() → MISMATCH (r3/r11 register-allocation drift)
Incident postmortem / KimiWorker exoneration: The two S18 floor breaches (EString commit 7b81fa6a9 + BString2 commit 7b4992361) were initially attributed to fake-struct failure modes. Subsequent A/B testing (KimiWorker, S18) confirmed the converted files DID byte-match when return types were correct. The actual floor breaches were caused by untracked worker WIP files contaminating the pre-commit hook regen (generate_report.py filesystem rglob picks up on-disk untracked files, inflating or deflating the matched count). The fake-struct failure modes are valid PREVENTIVE guidance — they were NOT the root cause of those specific incidents. KimiWorker's technique is sound.
TU-Archaeology Chapter¶
Full build-archaeology methodology for identifying, classifying, and handling
TU-duplicate functions in the Sims 2 GC DOL. This chapter supersedes the earlier
"TU-Duplicate Cluster Map" stub. All workers must run the pre-flight checklist before
writing any extern free-function declarations.
1. Pre-flight Checklist¶
3-step process before writing any extern free-function declarations:
Step 1 — Identify the TU's .obj home (FIXED awk one-liner):
TARGET_VA=0x80123704 # replace with your function's VA
awk '/\.obj/ && $3 == "4"' extracted/files/u2_ngc_release.map | \
awk '{ printf "%016x %s\n", strtonum("0x" $1), $0 }' | \
sort | \
awk -v t="$TARGET_VA" '
BEGIN { target=strtonum(t) }
{ va=strtonum("0x" $1); if (va <= target) last=$0 }
END { print last }
'
Why this is the FIXED version: The release map is NOT sorted by VA — sections interleave. The earlier naive
tail -1after filter picked last-in-file-order, not greatest ≤ target. Fix:$3 == "4"filters.textsection only (drops debug/data/bss); zero-pad + sort produces true VA order; walk finds the correct last entry ≤ target. Confirmed bug independently by OpusArchitect (post 4d40d8f8) and OpusReviewGuy.
Step 2 — Check if the .obj name matches your class:
- .obj name matches class name → canonical TU — proceed normally, no extern void needed
- .obj name does NOT match → TU-duplicate confirmed — go to Step 3
Step 3 — Disassemble to check call dispatch shape:
- All-blrl / virtual dispatch only → Pattern B (see §4) — convert normally, no extern void
- Has direct bl to a non-map VA → Pattern A or A+B (see §4) — apply Technique #19
2. Per-lane .obj Map¶
Current workers' canonical .obj and known TU-duplicate ranges.
| Worker | Lane | Canonical .obj |
Canonical VA range | TU-duplicate context |
|---|---|---|---|---|
| OpusArchitect | 1 (INVTarget) | quickresfile.obj |
0x80176xxx–0x80179xxx | Cluster #2 (UI Target family) — ACTIVE |
| ObjectSimSlayer | 2 (objectsim) | objectsim.obj |
0x801A43E4+0x19BB8 | loadingscreenstate/dlgwrapper/textblock/wrapper.obj (Clusters #3–#4) |
| PersonSlayer | 3 (cxpersonimpl) | person.obj |
0x801CExxx–0x801Exxx | bstring2/coordconversions/ambientsoundplayer.obj (Cluster #5) |
| SonnetWorker1 | 5 (sanimator2) | sanimator2.obj |
0x800A0238–0x800B8067 | eyetoyclient/ezodiac/flashpimenu/gameeffectsmanager/global/aptstring.obj (Cluster #6) |
| SonnetWorker2 | 6 (InteractorModule) | interactormodule.obj |
TBD | aptviewer.obj + tilewalls.obj flagged as potential — run pre-flight on first conversion |
| KimiWorker | 7 (EString) | estring.obj |
0x802Dxxxx | PAUSED — floor-breach investigation; cross-check type decls against existing semantic dirs |
3. Six Confirmed Clusters¶
TU-local helper duplicate clusters confirmed by workers via build-archaeology. Cross-reference
your bl targets against this table before writing extern declarations.
| # | Name | VA Range | Co-located .objs |
Pattern | Shape | Workers |
|---|---|---|---|---|---|---|
| 1 | AptViewer + Camera shared TU | 0x80015xxx–0x80016xxx |
aptviewer.obj, camera.obj | TBD | Single-TU Concentrated | SonnetWorker2 (IM) |
| 2 | UI Target family | 0x80176xxx–0x80179xxx |
quickresfile.obj (UI2D/UIDB helpers) | A (helper-inline) | Single-TU Concentrated | OpusArchitect — proven a9581e43c |
| 3 | TileWalls + InteractorManager | 0x8020Bxxx |
tilewalls.obj, interactormanager.obj | TBD | Single-TU Concentrated | SonnetWorker2 (wall paths) |
| 4 | cXObjectImpl dialog group | 0x800Dxxx–0x801Fxxx (wide) |
loadingscreenstate/dlgwrapper/textblock/wrapper.obj | A+B Hybrid | Multi-TU Concentrated | ObjectSimSlayer |
| 5 | cXPersonImpl distributed | 0x8012xxx–0x801Exxx |
bstring2/coordconversions/ambientsoundplayer.obj | B (method-replication) | Distributed | PersonSlayer |
| 6 | SAnimator2 distributed | 0x800A0238–0x800B8067 |
eyetoyclient/ezodiac/flashpimenu/gameeffectsmanager/global/aptstring.obj | A+B Hybrid | Distributed | SonnetWorker1 |
Additional documented (sibling-class):
- cXMTObjectImpl (render group) → Multi-TU Concentrated (ObjectSimSlayer, per OpusArchitect sibling scout)
- cXPortalImpl → Distributed (pattern TBD, identified by OpusArchitect sibling-class analysis)
Protocol when you hit a new cluster: post an info note tagged tu-duplicate-cluster
with VA range, co-located .objs, pattern, and shape, then update this table.
4. Three Dispatch Patterns¶
Classify your TU-duplicate by examining direct bl call shapes in the disassembly.
| Pattern | Definition | Workaround | Active example |
|---|---|---|---|
| A — Helper-inline only | Class methods canonical; only inline helpers are TU-duplicated | extern void Helper(args...) free-function decls (Technique #19) |
OpusArchitect's INVTarget (Cluster #2) |
| B — Method-replication only | Methods replicated across TUs but use ONLY virtual dispatch (blrl/bctrl) |
None — convert normally | PersonSlayer's cXPersonImpl Wave 1 (Cluster #5) |
| A+B — Hybrid | Methods replicated AND each copy has TU-localized helper inlining | extern void for each helper (regardless of which method copy) |
ObjectSimSlayer's cXObjectImpl (Cluster #4), SonnetWorker1's SAnimator2 (Cluster #6) |
Identification rule: disassemble the target function and inspect every bl target:
- If every off-class call is blrl/bctrl → Pattern B
- If any bl target is absent from the release map → Pattern A or A+B
- If both → Pattern A+B Hybrid (apply extern void to the missing-map targets)
Pre-identify helper roster for A+B classes: for cXObjectImpl in the 0x800D–0x801F
range, the dlgwrapper helper roster is shared across most methods — identify once per
TU cluster, reuse across all conversions in that cluster.
5. Three Distribution Shapes¶
TU-duplicate clusters fall into one of three distribution shapes, which predict workaround complexity before you even open the disassembly.
| Shape | Definition | Examples | Implication |
|---|---|---|---|
| Single-TU Concentrated | All duplicates baked into ONE translation unit or a tight feature-group | Clusters #1–#3 (aptviewer, UI Target, TileWalls) | Helper roster is small + consistent per cluster; one extern pattern covers all methods |
| Multi-TU Concentrated | Duplicates spread across a small set of logically-related TUs (same feature area) | Cluster #4 (cXObjectImpl: dlgwrapper/loadingscreenstate/textblock/wrapper) | Helpers may vary per TU sub-group; run awk per method, share roster per sub-group |
| Distributed | Duplicates scattered across logically-unrelated TUs (game-wide helper consumers) | Clusters #5–#6 (cXPersonImpl, SAnimator2) | Each TU sub-copy may have independent helper roster; run full awk pre-flight per function |
Predictor (build-archaeology hypothesis): - System-specific classes (UI targets, camera, wall logic) → Single-TU Concentrated - Feature-group classes (objectsim dialog, render pipeline) → Multi-TU Concentrated - Pervasive game-logic classes (person, animator, physics) → Distributed
Prediction model is O(1) from the class role; use it to calibrate investigation depth before opening disassembly.
6. Build-archaeology Meta-finding¶
The per-feature .obj + inline-helper pattern:
The EA build system compiled Sims 2 with per-feature translation units. When a class
implementation is large and feature-specific methods live in separate .obj files,
the linker sometimes bakes inline helpers into each .obj rather than resolving them
to a shared canonical location. This produces:
- A canonical class method in
classname.obj(in the release map) - Duplicate copies of inline helpers in consuming
.objfiles (absent from the release map — map only records canonical symbols)
Source-dispatch-style hypothesis:
- Direct member calls (m_x->method()) inline → the callee appears as a non-map TU-local → Pattern A or A+B
- Virtual-interface calls (m_iface->virtualMethod()) stay as blrl → no TU-local hazard → Pattern B
- Mixed → Pattern A+B Hybrid
This hypothesis explains why blrl-heavy classes (cXPersonImpl virtual overrides) show
Pattern B, while direct-call-heavy classes (objectsim, sanimator) show A+B.
BuildAgent path archaeology: The DOL contains BuildAgent comment strings exposing
each TU's .obj boundary:
Grepping these in the DOL binary (or ELF) pinpoints TU boundaries. Functions in the
same .obj that call each other produce TU-local duplicates when the linker does not
dedup across translation units. This is the primary cluster-discovery method.
7. Methodology Productionization¶
Adoption status (S18): awk pre-flight mandatory for all workers since S18 broadcast. All 6 active workers confirmed adoption. Jitter (50–150ms pre-commit sleep) in place to reduce index.lock contention during fleet commits.
Candidate for obj_lookup.sh script: the 4-pipe awk one-liner is error-prone to
retype. Wrap in tools/obj_lookup.sh <TARGET_VA> for fleet convenience. Deferred to
a future infra session — current priority is S18 convert throughput.
Cluster map maintenance: when a new cluster is discovered:
1. Post info note tagged tu-duplicate-cluster (VA range + co-located objs + pattern + shape)
2. Update the §3 cluster table in this chapter
3. Update the §2 per-lane .obj map if the worker's lane is new
Floor-breach prevention protocol (proposed, OpusReviewGuy): current per-file
verify_match.sh catches individual file mismatches but does not catch cross-pipeline
regressions (e.g., a converted file whose type declarations collide with another
semantic dir's types, dropping a previously-matched function). Proposal: extend
pre-commit hook to compare matched_code_percent at HEAD vs after-commit and BLOCK
if it drops. Under review for S18 post-wave2 implementation.
8. Canonical vs TU-Duplicate Convertibility¶
Fourth dimension of cluster classification (OpusArchitect, S18 — INVTarget survey).
When a class's canonical .obj range shows heavy GCC inlining, the canonical address space
contains mid-function continuations rather than standalone prologues. These are
structurally difficult to convert: no natural entry point, no isolated register-pressure
pattern, no standard prologue to anchor. The TU-duplicate copies live in a DIFFERENT
.obj — emitted at a call site where cross-obj inlining was impossible — and retain a
standalone function shape: normal stwu r1,-N(r1) prologue, isolated register
pressure, straightforward conversion.
Heuristic — prologue probe:
Look up the target address in the map. Disassemble the first instruction:
- stwu r1, -N(r1) → standalone prologue → CONVERT this address
- beq / bne / cmpwi / b <label> → mid-function entry → SKIP; find TU-duplicate instead
Case study: INVTarget (OpusArchitect, S18)
| Range | TU (.obj) | First-instruction profile | Convertibility |
|---|---|---|---|
| 0x803xxxxx | targets_tsc3.obj (canonical) | mid-function continuations | HARD — avoid |
| 0x801Exxxx–0x801F0xxx | quickresfile.obj (TU-dup) | standalone prologues | EASY — target this pool |
OpusArchitect documented 23 virgin TU-duplicate candidates using the prologue probe across the quickresfile.obj range. Workers: run the same probe on your class before deciding which address range to convert.
Implication: canonical = best conversion target is false for inlining-heavy
classes. For these classes the TU-duplicate IS the canonical conversion target.
Cross-class portability: untested beyond INVTarget but mechanically applicable to any
class whose canonical .obj range shows predominantly non-prologue first instructions.
Measure inlining density (% of addresses with stwu as first instruction) before
committing to a pool.
Four dimensions of cluster classification (complete):
1. Dispatch pattern — A (helper-inline / extern void fix), B (method-replication
all-blrl, no fix available), A+B hybrid
2. Distribution shape — single-TU concentrated / multi-TU concentrated / distributed
3. Game role — system-specific / feature-group / pervasive game-logic class
4. Convertibility shape — canonical-standalone vs TU-duplicate-standalone ← this section
Living chapter: add new clusters, patterns, and distribution examples as workers surface them. This chapter is the canonical reference for all TU-archaeology work.
ASMPROC Composition Patterns (3 documented)¶
Combinations of EXISTING mutators that solve common multi-class walls without authoring new mutators. Apply discipline: each directive sees POST-MUTATION state from prior directives — hand-trace through each step.
A. gpr_relabel + swap_adj prologue reorder¶
For ctors with both register-alloc divergence AND prologue insn-order swap.
// ASMPROC_gpr_relabel: swap=A:B
// ASMPROC_swap_adj: a=<op> b=<op> which=first (shift insn 1)
// ASMPROC_swap_adj: a=<op> b=<op> which=first (shift insn 2)
// ... (chain as needed)
Validated: ERedBlackTree ctor (32B, commit f7defb4a).
Note: GCC's extended mnemonic la (load address) = addi rt, ra,
label@l — match la opcode in swap_adj/replace_insn, NOT addi.
B. swap_adj which=N integer indexing (raw-hits-list)¶
When 3+ same-opcode adjacent pairs exist and only ONE specific pair needs
swap (others must stay), use which=N where N is the 0-indexed raw hits
list position.
Counting rule: every adjacent <a-op>/<b-op> pair counts in the raw
list, INCLUDING overlapping pairs. Walk the .s and number them 0, 1, 2, ...
in source order.
// ASMPROC_swap_adj: a=mr b=mr which=3
// (4th adjacent mr/mr pair — else-branch's first call.
// Loop pairs 0-2 already match DOL and are left alone.)
Validated: Neighbor::ShuffleWantFear (128B, commit 19d0f7da).
C. replace_insn occurrence=N for non-adjacent cross-position swap¶
When two instructions are on opposite sides of an intervening
non-swappable instruction, swap_adj can't reach them. Use TWO
replace_insn directives with occurrence=0 then occurrence=1 to
transmute the pair via an identical-intermediate.
Pattern: line X = OP1 a,b; line Y = OP2 c,d separated by middle line.
To semantically swap X and Y:
// Step 1: X becomes OP2 c,d. Now two OP2 c,d exist.
// ASMPROC_replace_insn: match="OP1 a,b" replacement="OP2 c,d" occurrence=0
// Step 2: 2nd occurrence (Y) becomes OP1 a,b.
// ASMPROC_replace_insn: match="OP2 c,d" replacement="OP1 a,b" occurrence=1
Net effect: X became OP2 c,d, Y became OP1 a,b — effectively swapped without moving lines.
Validated: SetFencepostAfterChunk (32B, commit a3069d5a).
New Mutators (3 shipped, 1-INSTANCE-PROVISIONAL each)¶
hoist_fcmpu (tools/asm_processor/mutators/hoist_fcmpu.py)¶
Moves fcmpu from after a run of stfs to BEFORE that run.
Use case: SN ProDG binds fcmpu cr0, fX, fY tightly behind the lfs
that loaded its second operand. GCC's RTL scheduler sees no data dep
between fcmpu and the subsequent stfs writes and moves the fcmpu to the
END of the basic block.
Args: occurrence (default 0), store_ops (default stfs),
min_count (default 1), cmp_op (default fcmpu).
Validated: EAnimController::SetTrackBlendSmooth(EACTrack*, ...) @ 0x802EE324 (108B, commit 384de424).
self_return_blr (tools/asm_processor/mutators/self_return_blr.py)¶
Collapses cmpwi rA,K; beq .case_K; ...; .case_K: li rA,K; blr into
single beqlr. Applies when a switch-case returns the SAME value as the
case label (the switch argument is already in rA).
Args: reg (default 3), value (optional filter), occurrence
(default 0).
Requires: the case label has exactly ONE reference (the beq we're rewriting). Other references would be orphaned by block removal.
Validated: TileWalls::GetOppositeSegment (108B, commit 6b71ad60) — 2 sites (case 16, case 32).
this_alias_rN (tools/asm_processor/mutators/this_alias_rN.py)¶
Inserts mr rN, 3 after a prologue anchor + rewrites r3 → rN in the
window until first call/blr. Skips lines that redefine r3
(li/lis/mr/neg/not with r3 dest) so return-value plumbing remains correct.
Use case: SN ProDG often allocates a callee-saved register (commonly r11
in ctors, r9 in member methods) to hold this after the prologue. GCC's
allocator keeps this in r3 and uses r3 as base register directly.
Args: reg, after (anchor substring — use a function label like
.L_f<MangledName> for leaf functions with no stwu prologue), until
(optional end-of-window).
Validated: ERenderSurface::SetSize (24B, commit ecd7d74d).
Mutator Upgrade¶
gpr_relabel cmpwi-immediate awareness (S15-flagged hazard)¶
The gpr_relabel mutator previously corrupted cmpwi immediates: a swap=0:N
would rewrite cmpwi rA, 0 to cmpwi rA, N because the bare 0
matched as a register reference. Same hazard for the immediate operand of
addi, andi., ori, xori, rlwinm/rlwimi (SH/MB/ME), subfic, mulli, li, lis,
srawi, and extended-mnemonic shift forms.
Fix (commit f9a48c59): Added IMMEDIATE_POSITIONS table covering the
known immediate-bearing opcodes. The relabel loop skips those operand
positions. Sentinel -1 handles cmpwi's optional CR-field prefix (the
immediate is always the LAST operand).
No regressions observed in spot-check of existing gpr_relabel users.
Deferred Wall Catalog (S17 carry-over)¶
Walls attempted but parked because they require infrastructure beyond S16's toolkit. Each entry documents the divergence class so future sessions can pick up with clean context.
Multi-class register-allocation walls¶
- 0x800B4214 Commander::Commander (60B) — 4-way cyclic register
rotation (this-alias r11 + head r8 + &count r10 + vtable scratch r9)
beyond what
this_alias_rNalone solves. Needsregion_gpr_relabel this_alias_rN+ reorder chain.- 0x8004A2E0 BBI::InventoryItem::CopyTo (60B) —
this_alias_rNworks but additional site-specific renames (9→0, 10→3, 0→9) needed for the temp registers. GCC also chose r10 for return-value temp (li r10, 1; mr r3, r10) instead ofli r3, 1direct. - 0x80069354 SAnimator2::getIndexOfPropID (100B) — bgelr fusion + register-alloc + increment-FIRST loop layout differences.
- 0x80069354 SAnimator2::DequeueAnimEvent (120B) — frame-size (16-byte vs 24-byte) + qctrl reg-pinning + memmove-return-use chain.
Structural walls¶
- 0x800E1CB8 cXObjectImpl::UserCanDelete (200B) — cross-jump vs
shared-label divergence. INVERSE of S15-promoted goto-shared-label.
Candidate mutator:
inhibit_crossjump. - 0x80146A34 + 0x801469C8 HasFenceNotWall + HasWallNotFence twin (104B + 108B) — same cross-jump consolidation class as UserCanDelete. 2-INSTANCE-CANDIDATE if mutator lands.
- 0x80067758 SAnimator2::GetBonePos (88B) — switch dispatch basic- block reorder (DOL emits case 2 as fall-through, cases 0/1 as trailing jumpers with backward b).
- 0x80065C70 SAnimator2::PreloadBoneParticleEvent (116B) — loop prologue reorder + lis/addi vs lis-only forms for SDA address-of.
- 0x800676EC SAnimator2::GetCarryHandPosAndDir (108B) — load-pair reorder + register-alloc for triple-EVec3-write pattern.
Source-coax exhausted walls (small but blocked)¶
- 0x801386DC Room::Rotate (40B) — 1 instruction over (44B compiled
vs 40B DOL). Needs
merge_tail_into_early_returnmutator to fuse the trailingli r3, 0; blrinto an inlineli r3, 0; beqlrbefore the main body. - 0x80307D74 EVibrate::IsControllerOn (32B) — DOL uses indexed-load
lwzx r3, r9, r0; GCC emits directlwz r3, 56(r9). Need source pattern that forces indexed-load form. - 0x80306958 DumpBinary64 (32B) — DOL uses srwi + manual addi-decrement loop; GCC uses srawi. + mtctr + bdnz (CTR-based loop). Need source pattern that inhibits CTR loop form.
Compiler-bug walls¶
- 0x800B4C40 CTilePt::ToTilePt (32B) — SN ProDG 2.95 compiler bug: fails to synthesize copy-ctor for 2-int return-by-value struct with error "In method `TilePt::TilePt(const TilePt &)': invalid use of void expression". Variants that compile inhibit RVO and introduce stack intermediate. 9 source variants exhausted (POD struct, class, typedef, explicit ctor, void out-param ABI, etc.). Needs SN-ProDG workaround pattern discovery — escalate to fleet.
SDA addressing walls¶
- 0x80149ED0 TTabScratchEntry::GetName (76B) — function-local static init pattern. DOL uses SDA21 addressing for the storage + guard, GCC emits absolute lis+addi. Tried -G 256 flag, no change. Probably needs explicit section attribute or different extern declaration form.
Tail-call shims (Nintendo SDK + libc wrappers)¶
- 0x8023EE40 PCinit (8B), 0x8023EE48 PCcreat (8B), 0x8023EE50
PCopen (8B) — DOL emits naked
b target; blr. GCC compiles with full prologue. Needstrip_tailcallmutator to elide the prologue when the function body is a single tail-call.
Niche issues¶
- 0x803C1DB8 EQuat::Id (40B) — multi-class: this-alias to r9 + lis
registers swap + stfs order.
gpr_relabel 9:11solves the lis swap but conflicts withthis_alias_rN reg=9. Needsregion_gpr_relabelto scope the swap to just the lis region. - 0x801F65A4 INVTarget::GetOnMsgInvExists (36B) — ✅ RESOLVED (commit 58f2bf42 — unsigned-short cast + swap_adj a=sth b=stw which=first; sister pair with GetVar_EYE_exists::Handler @ 0x801E9248).
- 0x8015E688 TreeTableEntryQuickData::GetDebugOnly (60B) — DOL is a leaf function (no stwu); GCC adds stack frame due to volatile read. Source restructuring needed to eliminate stack roundtrip.
- 0x8039D900 EAnimNote::Copy (112B) — Kmiworker2 park. SN ProDG refuses
to keep 4th callee-saved induction variable (r29=dst_str) across loop.
DOL has distinct r28(counter), r29(dst_str), r30(dst), r31(src); compiled
always folds dst_str into r30±offset. Needs
inject_before with arbitrary instructionsOR region-based instruction insertion to synthesize missingaddi r29, r29, 12 + mr r3, r29inside loop body. 6 source variants exhausted by Kmiworker2.
2-step bool materializer (while-loop condition under -fno-schedule-insns)¶
First instance: 0x802CC934 ENodeList::GetSizeRev (48B) — S18-ext,
OpusReviewGuy writer-pivot attempt. 3 source variants, 0 MATCH. Clean wall.
Assembly signature:
1:
li 0, 1 ← materialize true
cmpwi r9, 0
bne 2f ← if r9 != NULL, skip false-assignment
li 0, 0 ← materialize false
2:
cmpwi 0, 0 ← test materialized bool
bne 0b ← loop back if true
Context: Linked-list traversal. r9 = this->field_4 (initial), then
r9 = r9->field_4 each iteration. Pattern: count nodes via offset-4 next ptr.
Why it walls: GCC 2.95 under -fno-schedule-insns materializes the
loop-exit condition through an int temporary rather than a direct cmpwi+bne.
- Plain while (p != 0) → GCC optimizes to direct cmpwi+bne → 9 instr / 36B (too short)
- Explicit int bool int cont = (p != 0); → doubles work → 16 instr / 64B (too long)
- goto pre-test → GCC still optimizes the bool test → 8 instr / 32B (too short)
Candidate Pattern #12 (NOT YET FORMALIZED): 2-step bool materializer for
while loop conditions. Needs source coax that forces the compiler to generate
the li r0,1; cmpwi; bne; li r0,0; cmpwi r0,0; bne sequence. No confirmed
recipe yet. Likely class: while (cond) { ... } loops where cond is a
pointer comparison AND the loop body has specific register pressure preventing
direct-cmpwi optimization.
Affected band: Likely appears in ≤128B linked-list traversal functions. Not confirmed in other addresses yet — treat as 1-instance until N≥2.
S19 action: Route to MutatorSmith or find a second instance to validate the pattern; then formalize as Source Pattern #12.
Complex preamble wall (lis/stfs/stb zero-init with SDA float constant)¶
First instance: 0x80329824 ImageProcessingManager::FilterParams::Reset
(68B) — S18-ext, SonnetWorker1 attempt. Flagged as preamble wall, not
attempted to completion.
Assembly signature (known):
lis r9,-32702 # load SDA base (upper half)
li r0,0
lfs f0,0x1274(r9) # load float constant from SDA (likely 0.0f or 1.0f)
li r9,0
stb r0,0xb(r3) # scattered byte stores + sth + stfs sequence
sth r9,0x4(r3)
stfs f0,0x18(r3)
stb r0,0x2(r3)
stb r0,0x0(r3)
stb r0,0x1(r3)
stb r0,0x8(r3)
stb r0,0x9(r3)
stb r0,0xa(r3)
stfs f0,0xc(r3)
stfs f0,0x10(r3)
stfs f0,0x14(r3)
Why it walls:
- Struct has mixed-width zero-init (bytes + short + floats at scattered offsets)
- SDA float constant must be loaded via lis/lfs — source must reference an
SDA-resident global float (e.g. extern float g_zero_float in SDA) to force
the correct addressing
- Store ordering: 14 stores across mixed types — GCC may reorder unless volatile
anchors are used
- The li r9,0 / sth r9,0x4(r3) pattern for a short field vs the stb/stfs
fields suggests the struct layout has a packed/unaligned short requiring
explicit-width store
Candidate class: struct Reset() with SDA-float fields + mixed-width zero-init + ≥10 stores. Likely appears in settings/param structs across the codebase.
Attempted approaches: None completed (parked pre-attempt at "complex preamble" stage — size 68B, band 51-80B).
S19 action:
1. Identify the FilterParams struct layout from the ELF debug map
2. Confirm which field at offset 0x4 is a short (not int) — that determines
if explicit *(short*) cast is needed
3. Find the SDA float constant at SDA base-32702 offset +0x1274 — likely
0.0f or a game-specific sentinel
4. Source pattern: explicit *(short*)&m_field = 0 for the sth field + volatile
or source-order control for the stb/stfs scatter
5. Route to MutatorSmith if store ordering still diverges after source coax
Known asm_processor Toolchain Bugs (S17 infra improvements)¶
Class of bugs in the asm-text rewriting toolchain where mutators corrupt operand fields that are SEMANTICALLY immediate values (not registers) but syntactically appear as bare integers. Both bugs share the same root cause and fix pattern.
Fixed (S16): gpr_relabel cmpwi-immediate corruption (commit f9a48c59)¶
gpr_relabel swap=0:N previously rewrote cmpwi rA, 0 to cmpwi rA, N
because the bare 0 matched as a register reference. Same hazard for
addi/andi./ori/xori/rlwinm (SH/MB/ME)/subfic/mulli/li/lis/srawi/extended-
mnemonic shift forms. Fixed via IMMEDIATE_POSITIONS table.
S17 candidate: region_gpr_relabel has the same bug¶
region_gpr_relabel has NO immediate-position awareness. When swap=0:N is
applied to a region containing li r9, 0 or li r0, 1, the immediate
operand gets rewritten, corrupting the byte encoding.
Workaround used in S16: For cGZSndSys ctor (commit df77a64f), used an
8-directive replace_insn chain instead of a 1-directive region_gpr_relabel.
The replace_insn chain is verbose but surgical — each line is targeted by
exact substring without immediate interference.
S17 fix: Port the IMMEDIATE_POSITIONS table + _resolve_immediate_positions
helper from gpr_relabel.py to region_gpr_relabel.py. ~30-line port.
Backward-compatible: no existing match relies on the buggy behavior.
Park-Triage Quality Checklist (advisory)¶
Before parking to MutatorSmith, triple-check the source for these common misclassification errors. They account for ~15% of recent Lane 3 cracks — needed NO mutator, just a source fix.
-
Static vs instance method: Symbol with
__C= const instance method. If DOL uses r3 asthis(with explicit args shifted to r4/r5/r6), declare as instance method even if logically it could be static. Examples cracked from this issue: LinkCoreBlock, AdjustTopChunk. -
Missing case in switch: Count
li r3, X; blrblocks in DOL vs case arms in source. Mismatch = missing case. Example: GetWallBetween was parked as 5-diff wall, was actually missingcase 2: return 1. -
Const-qualifier mismatch: Symbol mangle suffix
Cv= const method. MissingC= non-const.
Quick disasm check when diff_func.sh shows shifts in register conventions:
- DOL r3 = this, r4-r6 = args → instance method
- DOL r3 = first arg, no this plumbing → static
- Count li r3, X blocks vs case arms → check missing cases
Lane 3 Session Yield (committed)¶
| Commit | Address | Function | Size | Recipe |
|---|---|---|---|---|
| 384de424 | 0x802EE324 | EAnimController::SetTrackBlendSmooth(EACTrack*,...) | 108B | hoist_fcmpu + 3 swap_adj |
| 38b108c0 | 0x801BE548 | K2YTarget::IsModeValid | 36B | cror-canonicalize recipe |
| f9a48c59 | (infra) | gpr_relabel cmpwi-immediate-aware | — | upgrade |
| 304f0697 | 0x800DCE00 | PlacementSpec::PlacementSpec | 68B | load-pair canonicalize-defeat |
| 6b71ad60 | 0x80146614 | TileWalls::GetOppositeSegment | 108B | self_return_blr × 2 |
| 02064fb1 | 0x801466D4 | TileWalls::GetWallBetween | 120B | pure source-only case-fix |
| ecd7d74d | 0x80364ED0 | ERenderSurface::SetSize | 24B | this_alias_rN + 2x swap_adj + volatile |
| 5d4edc86 | 0x80317A28 | ERFont::SetColor | 24B | volatile + swap_adj + 4x force_reg_at_pos |
| 02659d72 | 0x80305D80 | EDebugMenuItem::EDebugMenuItem | 32B | 1x swap_adj a=stw b=stw which=last |
| 49eb3183 | 0x802CFDCC | ERTQuantize::FreeNode | 32B | swap_operands sthx |
| ec085a60 | 0x80329508 | ImageProcessingManager::SetStageCallback | 20B | 3x replace_insn |
| 68966da2 | 0x803235F0 | EControllerManager::GetController | 32B | pure source-only goto-shared-label |
| db8112fd | 0x802DC560 | EA::Allocator::GeneralAllocator::LinkCoreBlock | 28B | pure source-only — instance, not static |
| 3f310289 | 0x802CFDEC | ERTQuantize::IndexToNode | 32B | 3x replace_insn |
| 19d0f7da | 0x800CC554 | Neighbor::ShuffleWantFear | 128B | swap_adj a=mr b=mr which=3 |
| f7defb4a | 0x802CD9B8 | ERedBlackTree::ERedBlackTree | 32B | gpr_relabel 9:11 + 3 swap_adj |
| a3069d5a | 0x802D9390 + 0x802D93F8 | AdjustTopChunk + SetFencepostAfterChunk | 24B + 32B | misclass-fix + volatile + gpr_relabel + 2x replace_insn |
Total: 17 cracks + 1 infra upgrade. ~+752B byte impact.
Plus recipe-adoption observed in other workers' commits: - 7a2c2356 INVTarget::IsPackagedItem (100B) — used cror-canonicalize recipe - ecf9f8a5 RoutingSlot::SetTileDistances (88B) — used local-var-cse pattern
S19+ Priority Targets¶
Functions flagged as high-value for S19 and beyond, organized by root cause. These are NOT yet cracked — they are triage output pointing at specific blockers.
Tier A: Layout-coax cluster (cXPersonImpl, PersonSlayer S18)¶
Root cause classification: Tooling-gap, NOT structural. PersonSlayer's framing: "These are not hard functions — they're tooling-gap problems. The moment we have correct cXPersonImpl layout in a shared header, all four will fall in a single session."
Cluster: cXPersonImpl member-layout coax — ~596B total
All four functions require accurate cXPersonImpl struct field offsets to reach
matching register allocation. Current blocker: cXPersonImpl header not yet
reconstructed from the map + disasm.
| Address | Function | Size | Blocker detail |
|---|---|---|---|
| 0x801CE1A0 | cXPersonImpl::TryTestInteractingWith | 140B | m_interactTarget layout offset wrong → r11/r12 swap |
| 0x801CE350 | cXPersonImpl::IsCarryingDCObject | 196B | m_carriedObject offset chain → 3-reg spill mismatch |
| 0x801CE500 | cXPersonImpl::IsAskedToMove | 152B | m_moveRequest + m_moveTarget dual-offset → base reg drift |
| 0x801CE6A0 | cXPersonImpl::GetSocialModeTarget | 108B | m_socialMode enum + m_socialTarget → cmpwi immediate wrong |
Recommended approach (S19):
1. Run dupe_scan_v2.py --tier 0 on the 0x801CE130–0x801E78F3 canonical range to
get the full cXPersonImpl virgin pool size.
2. Reconstruct cXPersonImpl struct layout from adjacent already-matched functions
(member-load offsets are visible in agent stub disasm).
3. Commit reconstructed include/types/cXPersonImpl.h as a standalone infra commit.
4. All four functions above become single-pass cracks post-layout.
Estimated byte yield on unlock: 596B + probable chain-reactions in adjacent PersonImpl functions that share the same layout (10–20 additional functions likely).
Fleet Targeting Guide — Band × Pattern Matrix¶
Source: TUScout per-band breakdown (T+180 snapshot, 2026-05-24) cross-validated by OpusReviewGuy cycle-3 review. Catalog-worthy per cycle-3 sign-off.
Inject pool band distribution (S18-ext close, 9,281 stubs)¶
| Band | Count | % Pool | Notes |
|---|---|---|---|
| ≤16B | 237 | 2.6% | SDA wall zone — r9-vs-r3 register pressure |
| 17–32B | 211 | 2.3% | Easy getter/setter pool |
| 33–50B | 251 | 2.7% | Sweet-spot top |
| 51–80B | 621 | 6.7% | BIGGEST tractable sub-pool; pattern-fertile |
| 81–128B | 1,355 | 14.7% | Mid-band; needs mutators |
| 129–256B | 2,793 | 30.2% | Bulk lake; complex structure |
| 257–512B | 2,080 | 22.5% | Hard; often TU-level |
| >512B | 1,692 | 18.3% | Full TU/structural only |
17–80B sweet-spot combined: 1,083 stubs (11.7% of pool) — tractable without mutators
30–50B cross-check band: 293 stubs (SIZE_RE first-line parse) — retargeting effectiveness metric
17–50B combined: 462 stubs (211 + 251) — note: "462" in early T+180 reports was this, mislabeled as "30-50B"
≤16B SDA wall: 237 stubs — NOT default pattern blast territory
Pattern assignment by band¶
| Band | Applicable patterns | Notes |
|---|---|---|
| ≤16B | Pattern #3 (SDA exchange-global) only | Default catalog hits r9-vs-r3 wall |
| 17–50B | #1 (double-deref), #2 (deref+const), #7 (bit extract), #8 (lha load), #9 (lbz+rlwinm&1) | Getter/setter blast territory |
| 51–80B | #4/#5 (equality field==N/0), #10 (branchless cr0.GT), #11 (null-guard mr./beqlr) | Controlled-branch fertile zone |
| 81–128B | Mutators required (region_gpr_relabel, swap_operands, etc.) | NOT pattern-blast territory |
| 129B+ | TU compilation or deep RE | Pattern blast ineffective |
T+210 tracking targets¶
- 30–50B delta from 293 (SIZE_RE-accurate): primary retargeting effectiveness metric
- 51–80B delta from 621: secondary metric; largest tractable sub-pool
- If both stay flat: workers picking from elsewhere → retargeting needs amplification
Grep method warning:
grep "\([3-4][0-9]\|5[0-9]\) B)"is WRONG — BRE\(is a group operator, not literal paren, so(636 B)matches as false positive (1,919 hits vs correct 293). Use:grep -lE '\((3[0-9]|4[0-9]|50) B\)'(ERE) or SIZE_RE on line 1 in Python.Band-iteration ordering warning (tick scripts): When building bands as an ordered list and testing each file against the first matching band, a 30-50B label placed AFTER a 17-32B label will only capture 33-50B (since 17-32 consumes 30-32 first). This produced a false
Never compare a band-iteration "30-50B" count against an explicit-range "30-50B" baseline.-44delta in T+210 tick output. Always use explicit non-overlapping range checks for official reporting:
Fleet routing protocol — two hard prerequisites (S18-ext validated)¶
1. "Fertile pool" requires per-band composition check before routing.
Class-stub-count is NOT sufficient for routing a worker to a class directory. The target must be decomposed by band before claiming it's tractable.
Example (S18-ext aptactioninterpreter): 127 total inject stubs looks like a large tractable pool. Actual band composition: - 102 × >256B VM-dispatch table handlers — requires MutatorSmith - 24 × 129–256B — requires TU compilation or deep RE - 1 × 51–80B — accessible - 0 × 30–50B — accessible
A "biggest unworked pool" directive without band-breakdown is a false positive.
Always run tools/tick270.py (or equivalent per-band count) on the target class
directory before issuing a routing directive.
2. Class-dir metric freeze = design, not stall, when workers are re-routed.
When a worker is re-assigned from a class-dir to a lateral pool (e.g., agent/ 51-80B), the original class-dir inject count will show zero movement. This looks identical to a stall but is actually an intentional routing artifact.
Before escalating a "class-dir freeze" as a velocity bottleneck, verify: - Is any worker currently assigned to that class-dir? - If no → freeze is expected. The metric is dormant, not stalled. - Only escalate if: (a) a worker IS assigned and (b) the freeze persists 2+ ticks.
This two-check protocol closes the "false stall alarm" class of incidents. Validated: S18-ext T+270 aptactioninterpreter/eanim freeze was design (workers re-routed to agent/ 51-80B per fleet directive) not bottleneck.