Repository navigation
Matching Patterns
What makes SN ee-gcc 2.95.3 produce retail's exact code, collected from the ~1200 functions matched so far. The research behind it (compiler comparisons, how the assemblers were identified) is in docs/compiler_matrix_findings.md.
Every range already builds with -O2 -G8 -fopt-stack -mno-check-zero-division, so $s registers saved with sd in 8-byte slots and div without the trap come out right on their own. What varies per function is how globals are declared, split vs no-split addresses, and which assembler runs.
-G8 lets the compiler address any object of 8 bytes or less relative to $gp. Retail only did that for some variables. Match each global's declaration to how retail reaches it:
| Retail access | Declaration | Use |
|---|---|---|
lw $v0, -0x3e40($gp) |
extern s32 D_001D8A70; (sized) |
D_001D8A70 |
lui $v0, %hi(D_X) + lw $v0, %lo(D_X)($v0)
|
extern s32 D_001DA0D0[]; (no size) |
D_001DA0D0[0] |
Struct or table through lui/addiu
|
extern S_Foo D_00142430[]; or extern S_Foo D_00142430; (size > 8) |
p = D_00142430; p->x |
The address of a $gp access is 0x1DC8B0 + offset (offsets are negative). Name the symbol D_ plus 8 hex digits: -0x5b0c($gp) is D_001D6DA4.
An unsized array is never small data, so it always gets lui. To keep existing code readable you can alias the element:
extern u8 D_00142734[];
#define D_00142734 (D_00142734[0])Sometimes retail reads a variable through lui and writes it through $gp in the same function, or uses $gp only for the access that sits in a branch delay slot. No single declaration does that with the default assembler. The cause is SN's own assembler, Ps2EeAs, which retail was built with:
- It is single-pass. It uses
$gpfor a symbol only if it already knows the symbol is small at that point in the file. gcc writes its.externsize hints at the end of the file, too late. - In a branch delay slot it can't expand a two-instruction
luiaccess, so it uses$gpthere regardless.
Option A (preferred): natural C with @ps2as. Declare everything with its natural sized type and assemble with Ps2EeAs. For the variables retail does reach through $gp everywhere in the function, tell the assembler their size before the function:
/* Ps2EeAs only uses $gp for these if it knows they are small before the use. */
__asm__(".extern D_001D62FC, 4");
__asm__(".extern D_001D6308, 2");
extern void (*D_001D62FC)(void);
extern u16 D_001D6308;.extern only declares a size. It creates no storage. Examples: func_003969B8, func_003D3050. The bitfield writes to D_001D4CEC (flags.b5 = 0;) and the GIF packet writers (p[0] = ...; p += 4;) are this pattern too.
Option B: two declarations with the default assembler. An unsized array for the lui accesses plus a sized alias for the $gp ones:
extern s32 D_001D4CE8[]; /* lui reads */
extern s32 D_001D4CE8_g; /* $gp writes */The alias needs an address in symbol_addrs_resolved.txt (D_001D4CE8_g = 0x1D4CE8;) or the link fails.
Use the plain name (D_00142430, func_003E8320). A source file is one translation unit: every block sees the earlier blocks' declarations in its file, plus, at the top, the declarations from earlier files of names the file uses without declaring them itself. Other files' declarations never reach a file that declares the name itself.
An alias is needed only when two blocks in the same file need different types for one symbol (a different struct type, sized vs unsized, a function used once as an array to take its address), or as one of the code-generation techniques on this page (a second symbol at the same address to get lui where the plain name gets $gp, and the like). Then don't change the shared declaration; declare a per-function alias named after the function address:
typedef struct { u8 pad[0x18]; u16 h18; ... } S_3969B8;
extern S_3969B8 D_00142430_003969B8[];and add D_00142430_003969B8 = 0x142430; to symbol_addrs_resolved.txt (and a D_00142430 = 0x142430; line for the plain name if it's the first use of the address). pr_check.py flags any alias without an address, and try_in_context.py / pr_check.py report a clash that needs one.
Until 2026-10-04 aliases were the default, a leftover of the single text.c, where every declaration stayed in scope to the end. That day 646 of the 941 were renamed back to plain names; the 295 left are the ones where a block in the same file really uses another type, or where the alias changes the generated code.
To pass a function's address, use the function's own name (func_003E8320), declared the way the sources already declare it, not an extern u8 func_003E8320_x[]; array alias.
Name typedefs uniquely the same way (S_3969B8, S_142430x). Two blocks defining the same typedef name with different bodies is the most common full-build error.
The source files must only produce .text. Every variable is extern:
-
s32 D_X;,static s32 D_X = 3;andvoid (*D_X)(void);withoutexternare definitions. They create.data,.sdataor.bss, which shifts the retail layout or fails the link withmultiple definition. - String literals and float constants that the compiler puts in
.rodataor.lit4have the same problem. See floats below. -
pr_check.py --obj build/src/text.c.olists any data section that sneaks in.
Retail was built from many source files, and some used -mno-split-addresses (N) while others used the default (S). They form address runs, recorded in tools/text_parts.txt:
-
S:
lui $v0, %hi(X)+lw $v0, %lo(X)($v0), and array bases aslui+addiu. -
N: the compiler emits
lw $v0, Xand the assembler expands it, so you seelui $atpairs and loads that fold the offset differently.
A function's mode only shows when it touches a global. If the diff is all about how addresses are formed, run try_func.py --all-modes. Known N-only shapes: constructors that return p, destructors that save ra with sd, and the "no-split global load" class.
| Assembler | Selected by | When |
|---|---|---|
bin/ee-as.exe (Aug 2000) |
default | Almost everything. It inserts the nop after mtc1 that retail has. |
ee/bin/Ps2EeAs.exe (SN ps2eeas 1.9.25) |
@ps2as |
Mixed gp/lui access (above); float constants built inline with lui/ori/mtc1; mtc1 hazard nops that depend on whether the next instruction uses the register |
ee/bin/as.exe (May 2001) |
@newas |
Rare; try it when both of the others are one instruction off |
Ps2EeAs can't read the GNU macro.inc, so a range assembled with it must not contain any INCLUDE_ASM stub. Give the function its own single-function range (the build then compiles it as a separate slice of its file) (see Toolchain and build).
Ps2EeAs can select lui/lo instead of $gp when GCC places its scalar
.extern symbol, size declaration after the first access. An isolated probe
with four real four-byte scalars and a float constant demonstrated both
GPREL16 access and inline constants once the same size metadata was made
visible early. Alignment attributes alone did not change that result.
For a scoped candidate, tools/try_func.py accepts repeated
--early-extern-size SYMBOL=SIZE options, for example:
python tools/try_func.py scratch/candidate.c --mode N --as ps2as --early-extern-size D_001DB6DC=4
The helper requires a positive size exactly present in the compiler's own
generated assembly. It duplicates only those declarations immediately before
the first .ent, preserving the initial header, instructions, and late
declarations. Missing symbols, different/conflicting sizes, malformed requests
and missing function entries are rejected. No source inline ASM, register
constraint, storage or instruction substitution is introduced.
This is an opt-in compiler-metadata experiment, not a matching result. Recheck the full function's symbol type/size, payload, all resolved retail bytes and raw object encoding. The current full-project builder does not automatically enable this command-line option; successful candidates need a reproducible build integration before promotion. Never guess a different data width merely to influence GP selection.
-
li.sconstants: retail usually builds them inline (lui $at, 0x3f80/ori/mtc1). Only Ps2EeAs does that. With the default assembler, the constant goes to a.lit4pool and the link fails withR_MIPS_LITERAL lit4. Use@ps2as. - Read the constant's value from the asm:
lui $at, 0x3f46+ori $at, $at, 0x6666is0x3F466666, which is0.775f. Write it with enough digits that it rounds to the same bits. - Float arguments need prototypes. Without one, a
floatargument is promoted todouble(you'll seecvt.d.sand the wrong registers). Declare it (extern f32 func_00388A28(f32, f32);) or cast at the call (((void (*)(void *, f32))func_00388830)(a, f)). - 64-bit values (
sd,ld,dsll32,dsra32) arelongorunsigned longin this compiler, nots32.
A float loaded through $gp (lwc1 $f12, -0x33a4($gp)) is a small-data global in frontbin's .lit segment (0x1D5680 to 0x1D9900), not a literal. Declare it sized, like any $gp variable:
extern f32 D_001D950C; /* 0x1DC8B0 - 0x33A4 */A few such loads reach below 0x1D5680, into the main executable's small data; declare those the same way. Constants written in the source come out inline (lui/ori/mtc1) and need @ps2as, as above.
switch works in C. The jump tables used to sit inside the data blob; since tools/migrate_jtbls.py they come from the source files, in function order, exactly where retail has them (the start of .data). Each asm function with a table has an INCLUDE_RODATA(...) line right after its INCLUDE_ASM. When you convert the function, delete both lines: gcc's table takes the same place. pr_check.py catches a leftover INCLUDE_RODATA.
Getting the table to start at the right index: if retail's table has entries for 0 and 1 that go to the default code, list them with the default at the end, as in func_003B0FC8:
switch (D_00143A07[0]) {
case 2: return 0x151;
...
case 6: case 7: return 0x150;
case 0: case 1: default: return 0x150;
}Insomniac wrote VU0 math as inline assembly inside C functions, so that is the matching form too. Use explicit $vfN registers in the template and pass pointers as "r" operands:
void func_00388698(void *o, void *a, void *b) {
__asm__ __volatile__(
"lqc2 $vf1, 0(%1)\n"
"lqc2 $vf2, 0(%2)\n"
"vmini.xyzw $vf1, $vf1, $vf2\n"
"nop\n"
"sqc2 $vf1, 0(%0)\n"
: : "r"(o), "r"(a), "r"(b) : "memory");
}Keep any nop retail has between VU0 instructions in the template.
Most VU0 functions are pure assembly leaves: a few lqc2 loads, vector math, an sqc2 store. For those, transcribe every instruction into one __asm__ __volatile__ block in retail's order and let gcc emit the return. The details that decide whether it matches:
-
Raw
.wordlines.tools/fix_quadword_ops.pywriteslqc2,sqc2,lqandsqas raw words so the GNU assembler can't pad them. Decode them back to real instructions in your C (.word 0xD8A10000islqc2 $vf1, 0($5)); a.wordinside inline asm assembles fine but is unreadable. -
The delay slot decides the assembler. gcc ends the function with its own
j $31. With the default assembler in reorder mode, the assembler moves your block's last instruction into that delay slot. When retail has that instruction in the delay slot, the default assembler is right; when retail hasjr $rafollowed bynop, use@ps2as, which leaves the order alone. Try both. -
Branches inside the block need
.set noreorderaround the part that contains the label, or the assembler re-pads the branch. - Functions that end by moving a value into
$f0(mtc1 $at, $f0) still don't match: retail has that instruction in the delay slot and neither assembler puts it there, since gcc, not the assembler, owns the return value. Those need the value returned from C instead of from the asm block.
-
Trailing padding is part of the layout. A function's
.sfile also holds the padding words that follow it (func_0039BD08is 0x24 bytes with 5nops after it, because the next function starts at 0x39BD40). Converting such a function to C drops that padding and shifts everything after it, which changes everyjaltarget in the build even though the function itself is byte-exact andtry_func.pysays MATCH. Before converting, check the gap to the next function's address; if there is one, the padding has to be reproduced or the function left as asm. -
Linker remnants (the
remnantbucket intriage.py, about 200 entries). Retail has about 620 single instructions, each followed by anop, between functions. Nothing references them, and 449 of them areaddiu $sp, $sp, N, a function epilogue. They are what the original linker left behind when it stripped unused functions: the final odd instruction plus its alignmentnop. They are not source code: they live inasm/remnants/and are included withLINKER_REMNANT(...). Don't write C for them.A C function whose whole body is one
asm volatile("addiu $sp, $sp, 0x50")looks like a match in localdecomp but is not one: gcc still appendsj $31and anop, which shifts every later function. 26 of these were scored 0 before localdecomp stopped trimming real instructions past the target size;pr_check.pynow warns when its count runs ahead of the sources. -
Handwritten assembly (the
handwrittenbucket, 100 functions). spimdisasm flags them (addi,$at, unusual registers). The original was a.sfile, so their.sinasm/handwritten/is the source, included withASM_FUNC(...). The same goes for VU0 leaves whose last instruction sits in thejr $radelay slot after a dependent instruction (mtc1 $4, $f0afterqmfc2 $4,ppacbafterppach): neither assembler nor gcc will put it there, even when gcc emits themtc1itself, so the original wrote the whole function,jrincluded, in assembly.
When the instructions are right but their order or registers aren't:
-
Independent stores get rotated by the scheduler. gcc emits the last store of a run to the same base first. For retail's order A, B, C, D write B, C, D, A (
func_003A61D0,func_003A6888). The scheduler issues at most one memory access per cycle; between equal priorities it prefers the instruction with more dependents, then the one that frees a register, then source order (found by rac1-decomp). -
adduoperand order is picked by the access form, not by the order you write the+. gcc canonicalises the addition.p = (u8 *)T + i * 4andb = table + igive index first;T[i].field,S.arr[i]andtable[i]give base first (rac1-decomp). A field at a fixed offset from an indexed table (sw $a0, 0x34($v0)afteraddu base, idx) is an array member of a struct:extern T D_X; D_X.arr[i] = v;(func_00395958). -
Two registers holding the same pointer. Retail computing
base + i * 4into one register and copying it to another before the second store (addu $v0, $a0, $a1; move $a0, $v0) is two arrays in one struct:s->a[i] = x; s->b[i] = y;(func_003A7FC8). Pointer variables,volatileand return tricks don't produce it. -
Callee prototypes decide argument code. Use the declaration the sources already have instead of an alias with guessed types. A
long/unsigned longparameter builds its constant as one macro that can move (func_003A3EF0,func_003D47A0); a callee declared with too many arguments leaves extramoves (func_0037EAA0). A function that ignores its argument in retail may still be called with one:func_003E16B8(&D_001DA9B8)is what keeps the pointer in$v0infunc_003E2D90. -
volatilepins accesses. reorg never moves a volatile access into a delay slot, and volatile stores keep their order against the epilogue (rac1-decomp). Try it before an__asm__fence when retail leaves a slot empty. -
fabsfthat isn't scheduled. When retail'sabs.ssits beforejr $rainstead of in its delay slot, write__asm__("abs.s %0, %1" : "=f"(r) : "f"(x))(func_003BEBF8,func_0037E920). -
Check m2c's pointer arithmetic. It writes
D_X + 0x40orp->f4 + 0x20on struct ors32 *types, which scales the offset. Cast tou8 *first (func_0039D510,func_003E16B8). -
Early
returnvsif/elsechanges block layout and which branch is likely (beql/bnel). -
x = c ? a : bvsifproduce different code (movz/movnvs branches). Try both. - Declaration order of locals can swap registers.
-
Signedness picks
sltvssltuandsravssrl.u8vss32for a flag changesandimasks. -
A copy of a parameter (
s32 id = arg;) sometimes changes which register holds it. -
Pointer vs index loops (
p++vsa[i]) produce different induction code.
Stop after a handful of attempts on the same register difference. Leave it as a partial and move on: breadth gets more done.
When every instruction is right but a variable sits in another register, python tools/regalloc.py scratch/func_X.c prints gcc's global allocation order: for each variable its reference count, live length, priority (about refs * log2(refs) / live) and the register it got. gcc allocates in decreasing priority, and a variable takes the first register not used by anything live at the same time that was already allocated; a parameter prefers its incoming register but loses it to a higher-priority local that overlaps it. So to move a parameter, change what outranks it or what overlaps it:
- Give the competing local a shorter live range (compute it later, or inline it into its single use), or one more or fewer use.
- Declare a temporary in a different block, or turn an
ifthat sets it into a conditional expression: both change the live length and reference count the priority uses. - Declaration order of locals only matters as a tie-break when priorities are equal.
Change the C, re-run the tool, and compare the -> registers with retail before spending a build on it.
m2c (--target mipsee-gcc-c --valid-syntax) gets the logic right for many functions but needs the fixes below before it matches. Each was confirmed on a real function.
-
m2c names parameters by register, C by position. A function that only uses
$a0and$a3must still declarearg1andarg2, or its arguments land in the wrong registers (func_003ACED0). -
Virtual calls pass the object as the first argument. m2c drops it when
$a0already holds it:(*vtbl[0x10])(obj, D_x), not(*vtbl[0x10])(D_x)(func_003E7028,func_003E7D68). The branch is also usually positive:if (call(...) != 0) { ...; return 1; } return 0;. -
Early return versus a result variable changes the branch layout. If only the branch shape differs, try
if (!cond) return 0; ...; return 1;(func_003B4220). -
Narrow argument types. A
s8parameter makes gcc sign-extend at entry. If retail only stores the byte, declare the parameters32and let the store narrow it (func_003B4220). -
Keep one base register with a struct pointer. m2c repeats
M2C_FIELD(&D_x, ..., off), which gcc folds into the address. Declare a struct with the fields at their offsets, takeS *p = D_x;once and writep->field(func_003B8700,func_0039C710). -
(x > -1 ? x : x + 0xFF) >> 8 << 8is(x / 256) * 256(signed). The same for 1024 (func_003AAD40,func_003AAEC8). -
A callee's parameter order sets how its arguments are scheduled. Registers are assigned by type independently (ints and floats separately), so a prototype can list parameters in a different order than their registers, and that changes which argument setup lands in the
jaldelay slot (func_0038C840,func_0038C718). -
128-bit copies (
lq/sq):typedef int u128_t __attribute__((mode(TI)));and*(u128_t *)p = *(u128_t *)q;. -
Unaligned 16-byte copies (
ldl/ldr/sdl/sdr): assign a struct ofu8 b[16](alignment 1):V16 v = D_x;(func_003E5D08and its four siblings, with-O2 -G8split addresses). -
Singleton lookup template (
func_003E24B0and six siblings). Retail is split-address (-G8, no-mno-split-addresses), the singleton is a struct object with a local pointer, and the check is a conditional expression:p = D_001DA9B8_x; /* typedef struct { s32 x0, x4, x8, xC; } S; extern S D_...[]; */ if (p->x4) q = p; else q = func_003E16B8(p); t = (void *)func_003E0E28(func_003E1898(q), arg0); v = (t != 0 && vcall(t, g) != 0) ? t : 0;
Separate
tandvare needed here: retail keeps the tested object in$a0. -
Pointer-count loops over a table of function pointers keep one outer struct pointer:
p->q->fn[i]rather than cachingp->q(func_003E15D8). -
Loop padding no assembler mode reproduces (retail has
jal; nop; nop; nop; bnez):do { r = f(); __asm__ volatile("nop\n\tnop\n\tnop"); } while (r);(func_003B6488,func_003AB100). This is an inline-asm hack.tools/asm_filter.pynow countsjal/jalrwhen it pads short loops (see "Loop padding with a call in the body" below), so try the plain loop first; it may make the hack unnecessary. -
64-bit constants and
lwu: declare the callee parameter and the struct field asunsigned long(func_00386608, needs@ps2as). -
Under
@ps2as, a scalarextern s32 X;gets the non-gp form (lui; lw, orlui $at; sw). Add__asm__(".extern X, 4");only for symbols retail reaches through$gp(func_003A3220,func_003D3C80). -
Check the raw words for
lq/sq(.word 0x78../0x7C..) before rewriting a copy loop (func_0037F550). -
Singleton-style load through a different address form: a second symbol alias at the same address (
D_001D52FC_003B42D0, unsized array) gives retail'slui/lwwhere the plain name gives$gp(func_003B42D0).
Each of these fixed a real near miss.
-
Toggle a callee between
voidands32. Retail's register choice depends on whether the callee's result is live in$v0. A call whose result is discarded but declareds32kept$v0busy and moved a followingluifrom$v0to$v1(func_003AE098,func_003B12C8,func_003CB890,func_003BC0F0,func_003B62E8). If another block in the same part already declares itvoid, call through a cast (((s32 (*)(s32))func_0039D6C8)(1)) instead of redeclaring. -
Scalar versus array decides who splits the address. Under
@ps2as,extern s32 X;becomes the assembler macro (lui $a0; lw $a0, lo($a0), destination reused) andextern s32 X[]/X[0]becomes gcc's split form (lui $v1; lw $v0, lo($v1)). When a near miss differs only in which register holds the high half, flip that global (func_003958A0,func_00396F18,func_003ADF88). Add.extern X, 4for the symbols retail reaches through$gp. -
Singleton vtable wrappers (the
D_001DA9B8family) match in plainSmode (no-mno-split-addresses). The key is a temporary for the virtual call's result:t = vcall(p); cb = lookup(table, t); r = cb(p, ...). Twelve functions follow this template (func_003E2728,func_003E2808,func_003E28E0,func_003E1E50,func_003E2028,func_003E2C88,func_003E22D0,func_003E23C0,func_003E2B98,func_003E21F8,func_003E2618,func_003E2A90), and the permuter found the same template forfunc_003E2118,func_003E29B8andfunc_003E30C8. The lookup functions are defined later in the same part, so call them through an alias symbol and a cast. -
Index first in an address sum.
(i << 2) + (s32)basegivesaddu idx, base, the order retail has;base + i * 4gives the other (func_003E8EC8,func_00392108). -
A callee that returns its argument. Use the result (
b = f(b, 1)), or the pointer needs its own callee-saved register (func_003DFB40family). -
Separate return variable, single return.
func_003ABE98andfunc_003AD520only match with onereturn r;at the end and no early return inside the else branch; early returns let gcc foldrto a constant and free the register. -
Hash-style update
off = (key & 1) + (off + 1)gets reassociated; a temporary (t = off + 1; off = (key & 1) + t;) keeps retail's order. With it plus the init-order fix below, the lookupsfunc_003E4890,func_003E4918andfunc_003E4DA0match; the insertsfunc_003E5F00,func_003E6680andfunc_003E67B8are still open (setup moves hoisted in a different order). -
Chains of
!= 0tests combined with&(func_003E34F0,func_003E3580,func_003E3630):r = f(a, b) != 0; t = f(a, c) != 0; r = r & t; .... A single expression gives conditional moves instead. -
A switch whose table starts at 0 needs
case 0:sharing thedefault:body (func_003AE368); without it gcc subtracts 1 and builds a shorter table. -
A reload that retail really performs after a store to the same global needs
volatileon that global (func_00395FF0,D_001D4CEC); a second alias symbol is scheduled above the store instead. -
Object-array initialisers (
if (flag == 0) { for (...) init(p++); flag = 1; } return &array[i];): retail has onenopin the loop that neitherSnor@ps2asreproduces, so they needi--; __asm__ volatile("nop"); p++;(func_003E13D0,func_003AD820,func_003AEDF8, as infunc_003B46F0). That is an inline-asm hack. -
Order of independent stores can only be found by search. A statement-order hill climb on the store lines found
func_003AC0D8. decomp-permuter finds the same kind of change; it also tends to introducedo { ... } while (0)andnew_vartemporaries, which are harmless, but check that it did not change what the code does (it once moved an assignment into anif).
Thirteen retail functions (func_003869E8, 0038C888, 0038C9D8, 003A6C30, 003A9E60, 003AAF88, 003B3DB8, 003B82C0, 003BA5B8, 003C0B10, 003C1130, 003D1B10, 003D2370) save $ra with sq in a 16-byte slot. gcc 2.95.3 writes sd, and when the function also saves $s registers it puts $ra in the lowest slot, where retail puts it in the highest ($s0 lowest, ascending). No compiler or flag we have gives retail's layout, so tools/asm_filter.py rewrites it: list the function in tools/sq_ra_funcs.txt and every callee-saved save and restore is moved to retail's slot (same slot set, same frame size) as sq/lq. Write the C normally.
- A function with
$sregisters also needs the-fopt-stackflag removed (a single-function override intools/text_parts.txt), because retail's$ssaves aresq, notsd. -
func_0038C888andfunc_0038C9D8are done this way.func_003A9E60has the right slots and call; only its argument-copy registers differ (retail gives$a0the first temp, gcc gives it the last). - A 64-bit argument that is moved with
daddumust belong, nots64(s64produced a 128-bitpor).
-
Read a struct-typed global through its fields, with locals for the early reads.
func_003A13B0only matched once the two reads ofD_00225780(f30,f6C) were locals at the top and the indexed table was a struct array (D_160C40.e[idx].v[a], entry type 0x14C bytes) instead of pointer arithmetic. Retail's base-register reuse ($a1holds one global, then the other) follows from that. -
Do not cache a field chain if retail re-reads it (
func_003C0B10):obj->set->nandobj->set->arr[j]->ewritten out each time matched; a local forobj->setdid not. -
A base pointer that is hoisted to the top (
addiu $a1, $a0, 0x70before anything else) is a localu8 *b = p->b;declared first (func_003932B0). -
Assign first, then test (
func_003932B0):p->f74 = t; if (a < t) p->f74 = a; else if (t < 0) p->f74 = 0;puts the first store in the branch's delay slot the way retail has it. The if/else form with the store inside each arm does not. -
Boolean return from a comparison: write
if (r < 0) return 0; return 1;(func_003AAAC8).return r >= 0;(and!(r < 0),(r < 0) ^ 1) compile tonor/srl, retail hasslti+xori 1. -
Search loops (
func_0038EC80,func_00395BC0):for (i = 0; i < N; i++) if (tab[i].k == key) break;followed byif (i == N) return;/if (i < N)gives retail's rotated loop. They need a struct with the exact element stride; the function itself is easy once the stride is right. -
Loop over an index range (
func_0039B0F8):for (i = lo; i < hi; i++) { dst[i].x = ...; }withlo/hiread from a table matched; the pointer-walking version withn = hi - lodid not. -
Float immediates and
$gpfloats together (func_003A3028):@ps2asplus__asm__(".extern X, 4");for each$gpfloat/pointer global; initialise a loop offset inside theifthat guards the loop (off = 0;before theifmoves themoveabove the branch). -
Typedef and extern names collide within a file. A block's typedefs and
externs stay visible to every later block in the same source file, so give each function's types a suffix (S_395BC0), and give a global an alias symbol (D_160C40_00395BC0plus a line insymbol_addrs_resolved.txt) only when another block of the file declares it differently. -
VU0 code that is not a pure leaf (
func_003BFD10): C around one__asm__block withlqc2/vadd.xyz/sqc2matched in no-split mode; a callee that other matched code declares with fewer arguments needs an alias symbol (func_003BFC18_003BFD10). -
div.swithnops in front (func_00393380): matched as plain C oncetools/divs_nops.txthandled the padding (see thediv.sentry under Known open problems). Don't use the old inline-asmnop; nop; div.sworkaround; it changes the register choice.
-
Switch functions work now. Write the
switch; the compiler emits its own jump table in.rodata, so delete theINCLUDE_RODATA(... jtbl_XXXXXXXX)line after the function (func_003A0178,func_0039BD48). Case numbers must follow the retail table, not the order of the code: readasm/nonmatchings/text/rodata/jtbl_*.s, find which index points at each body, and put the bodies in address order. gcc only builds a table when at least five non-merged case nodes exist and it deletes cases that lead to the same place asdefault; to get retail's long table add cases thatbreakbut are not adjacent (case 0: case 2: case 4: case 18: break;), which keeps a 19-entry table. -
A store that gcc pulls into a branch delay slot but retail does not (
func_003ADBB0): make that store volatile,*(volatile s32 *)&D_001D9F40 = 0;. Retail put the epilogue's first load there instead. -
A reload of a pointer global after every store (
func_00385570,func_00387B10, GIF packet writers): declare the pointer globals32and write through casts of it (*(s32 *)(D + 0x64) = a;). Ans32store may alias ans32global, so gcc reloads it; a store through a typed struct pointer does not reload. -
Boolean ANDs of call results (
func_003E3A80):t = f() != 0; ok = ok & t;with a temporary matchessltu+and. Writingok &= f() != 0makes gcc usemovz. -
Two arms where retail's branch-likely goes to the later block (
func_003AAC70): put the!=case first (if (b->f4 != 4) {A} else {B}), and the arm that falls through first in the secondif. -
Pointer arithmetic order (
func_003AAC70):(s32)((u8 *)b + 8 + b->f30)givesaddiubeforeaddu;(s32)b + b->f30 + 8gives the opposite. -
Loop padding with a call in the body (
func_003BD360):tools/asm_filter.pynow countsjal/jalras one instruction when it pads short loops to 6. -
A call with no argument moves (
func_003E11D0,func_003E5F00): retail'sjalfollows the prologue with$a0/$a1untouched even though the callee takes them. The original called it with the incoming registers still live, so call it through a zero-argument declaration:extern void *func_003E1150_003E11D0();(alias symbol, K&R, no prototype) and writefunc_003E1150_003E11D0(). gcc then emits nomove $a0, $s1. -
Boolean from a compare (
func_003AAAC8):if (r < 0) return 0; return 1;, notreturn r >= 0;.
Five functions between 0x600 and 0x96C bytes (func_0039DB38, func_003E3F08, func_003DF038, func_003DE8F0, func_00384420), all S mode with @ps2as.
-
Small objects get the assembler's
la, not gcc's split. Under@ps2as, gcc treats an object of 8 bytes or less as small data and emits onela/lwmacro, which Ps2EeAs expands tolui/addiu(adjacent, same register) when it has no.externhint. A larger or unsized object gets gcc's split form instead (luiandaddiuscheduled apart,%hiCSE'd). So when retail'slui/addiufor an address are adjacent, and the same address is rebuilt at every use instead of kept in a register, declare the object with its real small size:extern s32 D_001DA868[2];(an 8-byte buffer cleared withfunc_00388440(p, 0, 8)) or the 0x20-spaced hash tables at 0x1DA9C8 infunc_003E3F08. -
Which addresses stay in
$sregisters is gcc's allocation priority. In a long run of calls that reuse table addresses (func_003E3F08), each repeated address is one pseudo; the ones with many uses over a short span win the callee-saved registers and the rest are rebuilt withlui/addiuat each use. If the wrong ones are cached, look for an object that is really small data (previous point) before reordering calls. -
volatileon a global that is saved and restored around a call (t = D_001D9F40; f(); D_001D9F40 = t;infunc_00384420). Retail keeps both accesses out of the call delay slots, which only happens for a volatile access. -
A hardware register read with the full address in a register (
lui; ori 0x800; lw 0($v0)):*(volatile u32 *)0x10000800. Withoutvolatilegcc folds the low half into the load offset. -
A byte constant address that must not land in a delay slot:
*(u8 *)0x1D5477instead of a declared global (func_0039DB38). A declared scalar is small data, so reorg puts its load in a delay slot and Ps2EeAs turns it into$gp. -
Loop-invariant copies come from member access in the loop. Retail's
move $t0, $t2before each loop (func_0039DB38) is loop.c hoistingo + 0x38Cout ofo->an[o->idx][i]. A local pointer (an = o->an) makes it disappear. Likewise a pointer that loop strength reduction creates (lbu 4($a2)with$a2stepping) comes from indexing (raw[i + 4]), not from a pointer variable in the source. -
One variable per loop counter. A counter shared by a loop with calls and a loop without moves both into a callee-saved register; give each loop its own variable. The same goes for a temporary reused for unrelated values (a save/restore temp and a swap temp in
func_00384420). -
Stores to one struct in retail's order A..J: the "write B, C, ..., A" rotation applied to
func_0039DB38's ten stores only after a statement-order search; try permutations of the stores before anything else. -
(x ^ 1) == 0and(x ^ 1) & 1give retail'sxoritest wherex == 1or!(x & 1)giveli/bneorandi/bnez(func_003DE8F0,func_00384420). Bit 0 of a flags word read withlbuis*(u8 *)&flags. -
Blocks of the same function with their own locals: two branches that each pass
&wto a callee use two stack slots in retail (sp0,sp4), so each branch declares its ownf32 w;(func_003DE8F0). -
div.sright aftermtc1under@ps2as: Ps2EeAs adds its ownmtc1hazardnop, so thetools/divs_nops.txtcount for thatdiv.sis one less than retail'snopcount (func_00384420). -
sqrt.sthat spimdisasm prints asc1 0x504is missed bygen_divs_nops.py; check the table line has one count persqrt.stoo (func_0038F3F8,func_00390730). With the default flags,sqrtf()adds an errno check and a fallback call. The existing inline form is__asm__("sqrt.s %0, %1" : "=f"(r) : "f"(sum));with a separate input variable. For a plain-C candidate, see the native compiler probe below before adding assembly.
SN ee-gcc 2.95.3 v1.36 can emit sqrt.s from plain C. An isolated probe on
2026-10-03 compiled sqrtf(value) and __builtin_sqrtf(value) with the existing
-O2 -G8 -mno-split-addresses -fopt-stack -mno-check-zero-division flags:
| Additional flags | Function size | Native sqrt.s
|
Calls in that function |
|---|---|---|---|
| none | 44 bytes | 1 | 1, errno/fallback path |
-ffast-math |
12 bytes | 1 | 0 |
-O3 -ffast-math |
12 bytes | 1 | 0 |
The source used a correctly typed extern f32 sqrtf(f32); declaration and no
inline assembly, forced register or approximation. The correctly typed double
sqrt probe still needed conversions/calls, not the same single-precision leaf.
The spelling __builtin_sqrt remained an undefined symbol in a separate probe;
no supported double-precision builtin ABI is inferred from that result.
-fno-math-errno is not accepted by this
compiler's cc1.
This proves compiler capacity, not a matching game function or numerical
equivalence for arbitrary inputs. -ffast-math may also alter surrounding
floating-point evaluation and NaN/unordered comparisons. Try it only as a
scoped candidate configuration; compare the complete compiled function,
including padding, all branches and fully resolved relocations, against retail.
Never remove an errno/libm call by relabeling it as a native instruction or
replace square root with an approximate routine to obtain a match.
A function sees the declarations at the top of its file (from earlier files), every earlier block of its own file, and nothing from later files. A file with mixed flags is compiled in slices, and a slice only sees the declarations of the file's earlier blocks, never their definitions. So:
- A call to a function that is defined in another file, or in another slice of the same file, still needs its own
extern. - An
externin your block must agree with that function's definition if both end up in the same file (or slice). When they differ, use the definition's types and cast at the call, or align the otherexternlines to yours (callers that ignore the result are unaffected when the return type changes fromvoidtos32). - A K&R declaration
extern s32 f();followed by a prototyped definition withu8ors16parameters is a conflict; use a prototype. - A function called with a different argument count than its existing prototype needs a cast call:
((s32 (*)())f)(a, b).
Many functions are near-copies. Once one is matched, the rest usually go fast:
-
isAchecks through a vtable at+8:if ((*(VT_E **)(p + 8))->isA(p, D_001D97D0_g)) { func_X(p, a, b); return 1; } return 0; -
8-slot hash tables (
HT_8/HE_8):h = ((key & 7) + ((key % 7) * i + i)) & 7;over 8 probes, with a sentinel value. -
Getters/setters on the big state struct at
0x142430, each with its own aliased declaration.
If you find a family, say so in your PR. It helps the next person.
Leave these for now, or open an issue if you crack one:
-
Resolved (hand-written):
lwc1 $fN, off($gp)followed bynop(func_003882D0,func_00388308,func_00388340,func_00388378,func_00388388). Not a C problem. Retail has the same load-then-jump-then-use shape unpadded 19 other times, so thenopis not something a compiler or assembler adds: SN gcc's-Soutput has none, and neither GNUas(any-mips/-mcpu) nor Ps2EeAs inserts one. The five sit in the hand-written math range (0x3882D0 to 0x388388) between functions already classified as hand-written, so they areASM_FUNCnow instead of C plus an__asm__("nop")hack. -
Resolved: 64-bit constant synthesis
li 0x8000; dsll 24(func_00383B08): pass the constant as anunsigned longliteral with@ps2as. -
Resolved:
div.s/sqrt.snoppadding (func_003E1D18and 69 other functions). Retail pads mostdiv.s/sqrt.swith 0 to 3nops; no rule predicts the count, and both assemblers delete explicitnops in reorder mode.tools/asm_filter.pynow puts them back as raw.word 0lines, driven bytools/divs_nops.txt(one line per function:func_0037E568 2, one count perdiv.s/sqrt.sin order). Runpython tools/gen_divs_nops.pyto add lines for every function that is stillINCLUDE_ASM; existing lines are kept so a function keeps its padding after it is decompiled. If the number ofdiv.s/sqrt.sin your C differs from the table, the function is left unpadded. First function matched this way:func_0037E568. Functions that were already in C without the table are not in it. -
Register allocation where retail keeps an argument in a temporary (
move $t3, $a0at entry) while$a0holds something else (func_0039BEC0): solvable in C, mostly. The cause is gcc's priority order (seetools/regalloc.py): a block-local temporary outranks the parameter and overlaps it, so the parameter is pushed out of its incoming register. Two changes reproduce retail's exact allocation forfunc_0039BEC0(screenId$t3, the-2constant$a0,result$t2,flag$t1): declareD_001A7430unsized (extern s32 D_001A7430[];, read asD_001A7430[0]), because retail reaches it with a compilerlui, and write the flag ass32 flag = (screenId >= 0) ? D_001D5B78[screenId] : 0;. That takes the diff from 1,680 to 210; what is left is the branch shape (which instruction lands in thebltzdelay slot). The forms that get the registers right change the branch layout, and the forms that get the layout right change the registers, so this function is logged for decomp-permuter. -
Resolved: "a match in isolation can still differ in the full build".
func_003AEDC8andfunc_003AED08passedtry_func.pybut failed the full build with two$gpstores swapped. The cause was the tool, not the build: it masked every relocated field, and two stores through$gpdiffer only in their relocated offsets, so the wrong order looked identical.try_func.pynow fills relocations in with the symbols' real addresses and compares them in full (checked against all 617 previously matched blocks: no false failures). The#define D_001D5B34 ...hack macros above some functions still replace a laterexternof the same name;#undefit in your block (seefunc_00389908). - About 20 matched functions use inline
__asm__or a hand-rolled$gpregister. They count, but plain-C rewrites of them are welcome. -
Init order before a loop: loop-invariant constants gcc hoists (a divisor
3, a&symbol) land between the loop variable's init and the other inits. To reproduce retail's order, name them as locals and assign in retail's order (i = 0; three = 3; sent = &sym; off = 0;, thenfor (; i < 3; i++)). Matched the three 3-slot hash lookups func_003E4890/4918/4DA0. -
A quad store that the scheduler moves behind a narrow store: make that one
sqstore volatile (*(volatile u128_t *)(d + 0) = ...) and keep the narrow stores plain; retail'ssq, sb, shorder then holds (func_0037FEE8). Also: a down-countingnthat retail keeps in the callee-saved register ahead of a copy ofq + 0x41DCwantsn--in the loop instead ofm = n - 1; n = m(func_003E0478). Ado { h = f(&s, 1); } while (h == 0);poll loop followed bydo {} while (g(h) >= 0);matches the retail pair of raw-branch loops (func_003AB180). -
D[idx].fieldagainst a cached pointer:&D[idx].f380folds the field offset into the symbol (lui; addiu D+0x380; addu idx*size), whilep + 0x380from a cachedp = &D[idx]adds it to the register. When retail shows an extra copy of the element address, or an argument built as(D + off) + idx*size, write that access asD[idx]...with a struct type for the element instead of reusingp(func_003809F0; also matched withu128_tfields for thelq/sqpairs). A 16-byte struct ofu8[16]assigned through a local gives theldl/ldr, sdl/sdrpairs (func_003E0930). Passing the callee's float arguments before its integer ones in the prototype moved amov.sinto the right slot (func_0038CA08).
Two shapes showed up while trying to match the smallest functions still filed as plain.
Neither is a C problem: if your target's .s contains one of them, move on to another
function rather than rewriting the C.
-
lq $at-$atas a data register. Compiling the same three-quadword copy (*(u128_t *)at 0x00/0x10/0x20) with the range's flags allocates$2,$3,$6;$atis not something this compiler hands to a value. Infrontbin.elfthe only files containinglq $atare the nine underasm/handwritten/, plusfunc_00388E58andfunc_00388E38- both stillINCLUDE_ASMinsrc/frontbin/388B40.c. -
sq $zero- a 128-bit zero store.*(u128_t *)p = 0;,(u128_t)0and a namedregister u128_t z = 0;all compile topor $2,$zero,$zerofollowed bysq $2,0($a0): the TImode zero is materialised in a register first, so retail's singlesq $zerois out of reach from C here.
A third shape from the same pass, sq $31 / lq $31, has a workaround rather than a dead
end - list the function in tools/sq_ra_funcs.txt (Functions that save $ra with sq
above); tools/pr_check.py now warns when a function that is C in src/frontbin/ has the
signature in retail and is missing from that list. What the pass did settle is that no flag on this compiler produces it, which is
what the next section is about.
Reproduce any of them from the repo root:
python tools/try_func.py <your attempt>.c func_XXXXXXXX # the ordinary judge
python tools/try_func.py <your attempt>.c func_XXXXXXXX --all-modes # 4 flag/assembler combos
Two more things worth knowing before a long session: sizeof(long) is 8 here, so the
64-bit type is unsigned long (long long, and therefore u64, is 16 bytes and any
arithmetic on it fails with unsupported wide integer operation), and the literal suffix
ULL is rejected by this compiler - use UL or a cast.
The sq $31 layout is not only reachable through asm_filter: the SN ProDG 2.0
package's ee-gcc295.exe (gcc 2.95.2, SN build v2.73a - a separate download, not the
3.01 package this repo builds with) emits sq $31 / lq $31 by default, puts $ra in
the highest callee-saved slot with $s0 lowest, and does not know -fopt-stack at all
(cc1.exe: Invalid option).
Measured on the C bodies of func_0038C888 and func_0038C9D8, assembled and compared
word for word rather than read off the .s: 2.0's output for both is byte-identical to
retail, the jal operand aside, while 3.01 at the project flags differs in the two $ra
accesses only. Nothing in 3.01's flag space moves those two: -O1/-O2/-O3/-Os,
-G0/-G8/-G16/-G24, -mgp64, -mips3, -mips4 and -fopt-stack/-fno-opt-stack all
keep sd $31. On this compiler -fopt-stack only governs the $s slots (16-byte
sq without it, 8-byte sd with it), never $ra - which is why the layout the filter
reproduces was never a flag away.
The same test on the two larger matched functions, func_003AAF88 and func_003C0B10,
splits the claim in two. 2.0 gets retail's frame right with no filter at all: same
frame size, $sN in the 16-byte sq slot at 0x10*N, $ra highest - where 3.01 needs
-fopt-stack removed and the filter's rewrite to get there. But it does not emit them in
retail's order: retail saves the registers ascending by slot, 2.0 descending (on
func_003AAF88, 2.0 saves $s3, $s1, $s0, retail $s3, $s0, $s1). Those two bodies are
the C that makes 3.01 match, so a compiler swap would mean re-tuning them, and re-tuning is
what the filter avoids. 2.0 also emits the lq $at bodies above exactly as 3.01 does, and
the sq $zero body too (por $2,$zero,$zero + sq $2,0($a0), unchanged), so the package
is a stack-slot signature and nothing else.
One of those tests is done: func_003A9E60 (0x8C, reconstructed from its retail
disassembly alone, flags -O2 -G8 -mno-split-addresses -mno-check-zero-division plus the
unsized D_001D5C78[] declaration the range needs) comes out with retail's frame and slots
under 2.0 and with 3.01's layout under 3.01 - but both compilers leave the same 22 of 35
words different, all of them the argument-copy registers (a0 lands in $s1 where retail
uses $t5, and the lui takes $2 where retail takes $12). So 2.0 does not fix this
one's schedule either; that half looks like allocation, not a compiler version.
So the layout is a real version signature and the arithmetic is unsettled. It is four
functions, and only the two wrappers are byte-exact; nine of the 13 still have no C block
at all. Most of the file still matches 3.01 at the project flags, so retail only
possibly looks like an intermediate SN release - 2.95.3-era instruction selection with
the older 16-byte stack slots - rather than a mix of two packages. Writing C for more of the
13 and running the 2.0 package against them is the cheap next test; the compiler/flag
matrix in docs/compiler_matrix_findings.md (15 builds) does not include this package.
Each was confirmed by a function that now matches in the full build.
-
Pass the incoming parameter to a K&R call that doesn't need it. In
func_003B7568,cb(a)instead ofcb()raises the parameter's priority, which flips which of two saved registers it gets. -
Two loops, two counters. Reusing one
ifor both loops offunc_003B2958swapped$s0/$s1. A separatejfor the second loop, indexing a[2]-sized$gparray asD_arr[j], gives retail's pointer plus count-down loop (bgezl). -
Table walk with an end marker:
for (p = arr; *p >= 0; p++)givesmove; lw; bgez; addiuwith the increment in the delay slot (func_003D46E0). A do-while withv = *p++doesn't. -
lui $v0; lh $v1, %lo(X)($v0)(the load into a different register than thelui) comes from reading through an unsized array alias,X[0], not a raw*(s16 *)0x1CD018(func_003A3430). -
Reuse the incoming pointer as the walking pointer (
arg += 8; ... func(arg, arg + b, ...)) instead of newp/qlocals, which add amoveand change register numbers (func_003972A0). -
A select assigned to a new local (
q2 = q - w) changes which instruction fills thejaldelay slot (func_003ABB60). -
Two reads of the same
luiglobal around calls need a scalarextern s32 X;under@ps2as; an unsized array shares oneluiacross the calls (func_003ABB60). -
(flag >> 24)gives a baresrawhere retail has one; the callee's parameter order sets the order of float register setup (func_003A4DC8). -
Declaring a callee
s32instead ofvoidchanges the$v0/$v1choice around the call; a K&R zero-argument declaration leaves the argument registers untouched (func_003B8968). -
Index an array in a loop instead of walking a pointer (
func_003C1950):e[i].t,e[i].idlet gcc strength-reduce to a pointer whose start sits in a temp and ismoved into the saved register, as retail does. A hand-written walking pointer gets other registers. -
Count-down loops written
for (i = N; i != -1; i--), one counter per loop (func_003A9768,func_003A8060): this decides which register holds the-1constant. -
Store order sets delay-slot filling (
func_003A5A90): zero stores writtent[1],t[2],t[3],t[0]matched; ascending order did not. -
K&R definition when callers use an unprototyped
extern void f();with another argument count (func_003A5A90); a prototyped definition makes those callers fail with "too few arguments". -
64-bit values are
long/unsigned long, notu64/s64, or gcc reports an unsupported wide integer operation (func_003D14D0). -
Float arguments need a prototype for the callee, or gcc promotes them to double (
func_003A5DF0went from 33 to 63 diffs without one).
Each was confirmed by a function that now matches in the full build.
-
The callee's return type changes the caller.
extern void f();versusextern s32 f();(or the reals32 *) moves registers and scheduling even when the result is unused. It was the deciding change forfunc_003B0608,00397380,003C8D50,00384C98and003E0FC8. Try the callee's real prototype, ors32, before anything else. -
Size hints choose the address form of a scalar extern:
__asm__(".extern X, 4");before first use gives$gp;.extern X, 16gives the macroluiform even for a scalar; an unsized array read asX[0]gives the split%hi/%loform (func_003C8D50,003B0608,0039AAF0,0039C028). -
One variable, two address forms: when retail reads the same address through both
$gpandlui, declare two names for it and give only one the.extern X, 4hint (func_003968F8,00396120,00396680,0038EB10). This is one of the cases where an alias is a real technique, not a leftover. -
Adjacent
$gpobjects need separate names: an array base for several of them gets shared by gcc (batch 15). -
A switch tree with
slti: aswitchwith onlycase 1andcase 2givesbeq; beq; retail'sbeq 1; slti 2; ...tree appears when the switch also has acase 0:(func_003A6520). -
A one-element array declaration (
u8 D_X[1], or a sizedT D_X[2]) instead ofu8 D_X[]changes whether the address is built before the call or split around its delay slot (func_003E1F40). -
Calling a K&R extern with fewer arguments than it reads matches retail where it passes only some registers; adding the extra arguments adds instructions (
func_003BE418,func_0039C028). -
Reuse the parameter as the working pointer (
a0 = ...; a0[0] = ...) to keep it in$a0; a fresh local lands in$v1(func_0039C028). -
Early returns: a
bnelfollowed directly by the "return 0" block comes fromif (x == 0) return 0;, not from nesting (func_003822C8). -
Stores of different constants to one global in several branches: write the separate stores with no result variable; gcc tail-merges them into one
swwith retail's registers (func_00396680). -
Index a struct-array member each time (
D.row[a].f) rather than taking a row pointer once; the pointer form folds the member offset into the base register (func_00395AC8). -
Same family, different data: adapt a matched sibling and change only the callee and the table global (
func_003E1F40fromfunc_003E1E50). -
Extra compiler flags are a per-file question, not a per-function trick. The original build set flags per source file, so a flag a function needs must also leave every other function in its file matching. Check that before adding one: compile the file's other C functions with it (
try_func.py --flags="-fno-..."on each).-
-fno-force-mem(needed byfunc_0039A040): all 88 C functions in3958F0.cstill match with it, and 1069 of all 1123 do. It's now the flag for the whole of3958F0.c, which is most likely what the original used. -
-fno-schedule-insns(needed byfunc_003AD650,func_003C8D50,func_00384C98): 524 of the 1123 C functions change with it, so it wasn't a project-wide flag. Around those three, the C functions that still match with it form runs of only 3, 3 and 1, so each would have to be its own small source file. That is possible (our file boundaries are estimates, andfunc_003AD650is in a different address mode from both neighbours, which is what a file boundary looks like), but unproven. They stay single-function overrides; if more neighbours turn out to need the flag, that's evidence for a small file there.
-
-
A
longworking variable keeps amovzchain: withint, gcc foldsr = 1; if (x == -2) r = 0;intoxor/sltu; withlong(and alongreturn) it emits retail'smovzchain. -
t = &A; p = &B; if (a0 == 0) p = t;loads the two addresses in the same order as retail'smovz $a2, $a1, $a0; a ternary or if/else swaps them. -
Statement-order search pays off early: the order of a function's first few statements alone moved results between 5 and 46 differing words; a brute-force search over them matched
func_00394B38andfunc_00394F78, and a random-swap search over store order matchedfunc_0039C7B0andfunc_0039C8A8. -
forversusdo { } whilecan change which registers a loop counter and its copy get (func_003D98D0neededfor). Try both. -
A guarded hardware wait loop:
if (r & 0xF0) { do {} while (r & 0xF0); }withravolatilehardware register read gives retail's shape; a plainwhilegives the unguarded form (func_003AC5E0). -
Callees with float arguments: if retail loads
$f13before a call, the callee's real prototype has more float parameters than the call seems to need. Check its definition. -
No
#definein a function block:split_text.py --refreshcopies a block's macros into later files like any declaration. -
-fno-schedule-insnsnever helped a near miss: tried on about 25 near misses, it made all but one worse and fixed none. The remaining differences in near misses are register allocation and statement order, not scheduling.
UYA decomp
- Home
- Setup
- Workflow
- Matching patterns
- Cross-repository resources
- Tools
- Toolchain and build
- Pull requests
- Credits
Edit these pages in docs/wiki/ in the repo.