Repository navigation
Workflow
The loop for one function: pick it, read its assembly, write C until it matches, put it into its source file, and prove the full build still matches.
Every not-yet-decompiled function is an INCLUDE_ASM("asm/nonmatchings/text", func_XXXXXXXX); line in one of the source files in src/frontbin/ (the one whose address range in tools/src_files.txt contains it), and its retail assembly is asm/nonmatchings/text/func_XXXXXXXX.s. The header line gives the size: nonmatching func_0037D100, 0x20.
python tools/triage.py --tsv remaining.tsv sorts every remaining function into a bucket (plain, switch, vu0, mmi, handwritten, remnant, ...) with its size. Pick from plain, smallest first, or from switch and vu0 once you know those patterns.
Good first functions:
- Small ones (under about 0x100 bytes).
- Members of a family that is already matched: find a matched function with the same shape and copy its approach. Getters, setters,
isAchecks through a vtable, and the 8-slot hash lookups (HT_8) all come in families.
Not for C:
-
Already done, not C:
ASM_FUNC(...)lines in the source files are functions that were hand-written assembly in the original (their.sinasm/handwritten/is the source), andLINKER_REMNANT(...)lines are the leftover words of functions the original linker stripped (asm/remnants/). Both count as finished, including in objdiff. See Matching patterns. - odd: probably a bad split. Report it; don't write C for it yet.
- Functions listed in Matching patterns: known open problems.
Tell others what you're working on (open a draft PR early) so two people don't match the same function.
boot_elf works the same way: its functions are INCLUDE_ASM("asm/boot_elf/nonmatchings/core", ...) (the engine core, src/boot_elf/core/) and INCLUDE_ASM("asm/boot_elf/nonmatchings/text", ...) (the front end, src/boot_elf/text/), its file list and flags are in targets/boot_elf/, and every tool takes --target boot_elf. In localdecomp, pick it in the dropdown above the function list. Its front end is frontbin's code at other addresses: when you match a frontbin function, tools/seed_boot_elf.py carries it over (see Tools). Core functions with a switch can't be C yet (core.rdata isn't split; see docs/boot_elf.md). Most of the core is Sony library code built with other compilers (newlib likely 2.96, libkernl/libc, libgcc and the Sony libraries 2.9-ee-991111), which stays assembly until the toolchain can compile a file with those; Matching patterns lists the signs. The part that is SN code and matches now is 989snd's EE side, 0x13B330 to 0x13D420.
levels/common (the code the 51 level overlays share) is the last entry in the dropdown. It isn't a splatted executable: each function is its own file in src/levels/common/, listed in tools/common_c.json and checked by tools/build_common_c.py's gates against its canonical owner overlay (docs/common_level_c.md); it needs the private level inputs in --refs. The list holds every canonical owner. Pick a compiler profile next to Save (mode N or S, default assembler or Ps2EeAs; auto tries the catalogue's entry and then all four). A build at 0 also runs the gates (exact size, raw object body, no data sections, no assembly), and only a build that passes them can be saved: Save writes the file and its catalogue entry. A source in src/levels/common/ that isn't catalogued yet shows as unverified. Full check reports the gates' result (make objdiff rebuilds the shared code's base) and the levels/common numbers. The permuter doesn't support it yet.
i5bootn (the bootstrap launcher, src/i5bootn/, --target i5bootn) is set up the same way, but most of its compiled code is Sony library code built with Sony's ee-gcc 2.9-ee-991111 (libgcc, libc helpers), so expect near misses there until that compiler is in the toolchain. Its own code (main, func_008010F8) matches with ours. triage.py puts the clearest library cases in its sibcall bucket. See docs/i5bootn.md and docs/compiler_matrix_i5bootn.md.
- The calling convention is EABI: integer args in
$a0-$a3, $t0-$t3($4-$11), float args in$f12-$f19, return in$v0or$f0. -
$gpis0x1DC8B0. An access likelw $v0, -0x5b0c($gp)is address0x1DC8B0 - 0x5B0C = 0x1D6DA4, so the symbol isD_001D6DA4. - An access through
lui+%lo(...)names its symbol directly (%hi(D_00225980)). -
How each global is accessed matters as much as the code. Before writing C, list every global the function touches and whether it goes through
$gporlui. That list decides the declarations and the flags (Matching patterns).
Start the server (python localdecomp/server.py --project . --no-git-sync) and open http://127.0.0.1:8477.
-
The left pane is your C. It should hold the externs and typedefs the function needs, then the function. Types like
s32,u8andf32come fromcommon.hautomatically. -
Build compiles with the flags of that function's address range (from
tools/text_parts.txt, ortools/localdecomp_flags.txtif the function has an entry there) and shows the diff. A score of 0 is a match. -
Save stores your draft and also writes it into its source file, replacing the
INCLUDE_ASMline. Only save at score 0, or you will break the full build. If you saved a partial by mistake, put theINCLUDE_ASMline back before committing. -
Functions that are already C show "unverified" until you Build them once.
-
If a function needs a different assembler or address mode than its range while you're still working on it, add a line to
tools/localdecomp_flags.txt:func_0039BEC0 -mvu0-use-vf0-vf2 -O2 -G8 -fopt-stack -mno-check-zero-division @ps2asThis affects localdecomp and
try_func.pyonly, not the real build. The line replaces the range's flags, so include-mvu0-use-vf0-vf2like thetext_parts.txtlines do.
tools/try_func.py does the same compile and diff without a browser, and works on Linux through wibo:
python tools/try_func.py scratch/func_0039BEC0.c # flags from its range
python tools/try_func.py scratch/func_0039BEC0.c --all-modes # S/N x ee-as/Ps2EeAs
python tools/try_func.py scratch/f.c --mode S --as ps2as # force a combination
It prints func_X: MATCH or func_X: N diff with a side-by-side listing (left is yours, right is retail, ** marks differing lines). Relocated fields are masked, so lui $a0, 0 against lui $a0, 0x1e is not a difference. Keep scratch files outside src/ (for example in a gitignored scratch/ folder).
--all-modes is the fastest way to find out whether a function needs non-default flags. Run it whenever a function is close but the global accesses don't line up.
Two more checks before you spend time on a near miss:
-
--flags=-Werror-implicit-function-declarationlists every callee your snippet calls without a prototype. Each one silently becomesint f(), which passes floats as doubles and keeps$v0busy; several old drafts were measured that way.--flagsadds to the range's flags, it never replaces them. - Check the function's range in
tools/text_parts.txtbefore trusting a draft's header: some headers had the address mode wrong (S vs N), and drafts measured before 2026-10-07 were measured without-mvu0-use-vf0-vf2, which changes loop code (re-measure them).
If you used localdecomp's Save at score 0, this is done. By hand:
-
Replace the function's
INCLUDE_ASMline with the block, wrapped in markers so localdecomp can find it:/* localdecomp:start func_003E4790 */ typedef struct { u32 key; void *val; } HE_8; extern u8 D_001DAA89; void *func_003E4790(HT_8 *t, u32 key) { ... } /* localdecomp:end func_003E4790 */
-
If the function needs flags other than its range's, add a single-function override to
tools/text_parts.txt(see Toolchain and build) and delete its line fromlocaldecomp_flags.txt. Start from the neighbouring line's flags exactly as written (every frontbin and boot_elf line has-mvu0-use-vf0-vf2): lines with equal flag lists are built as one slice. -
Use plain names (
D_00142430). Only if another block in the same file declares that name with a different type, declare a per-function alias such asD_00142430_00396628and add it tosymbol_addrs_resolved.txtwith the real address (D_00142430_00396628 = 0x142430;). A plainD_name that's new to the build also needs its line there. -
If the block uses a prototype, typedef or extern that only another file declares, run
python tools/split_text.py --refresh. It adds what the file needs to its "declarations from other files" section (localdecomp's Save does this for you).pr_check.pyreports a file whose section is out of date.
python tools/pr_check.py
& "C:\tools\eegcc_2.95.3_sn_v1.36\bin\make.exe" (or: python3 tools/build.py)
pr_check.py catches the usual full-build failures (undefined aliases, conflicting typedefs, variable definitions, @ps2as ranges with asm stubs, and any file or slice that no longer compiles) and tells you the line to fix. Before that, python tools/try_in_context.py scratch/func_X.c on each new function shows both its diff inside its file and whether the rest of the build still compiles with its declarations. make must end with MATCH. Then open a PR (Pull requests).
-
Start over. A clean rewrite, or a copy of a matched sibling with the data changed, often beats tuning an old draft (
func_003A3FB0went from 46 diffs to a match with a plain rewrite). -
Search orders. Store order, statement order and prototype parameter order are cheap to brute-force with a script around
try_func.py; respect data dependencies, or the search changes what the code does. -
Permuter.
tools/permuter_setup.pywith the aligned scorer (tools/permuter_scorer.py, seedocs/permuter.md). Check every output for semantics: in one run about half of its improvements changed what the code does. -
Under load (several builds sharing a machine), wibo occasionally hangs in Ps2EeAs or the ee-gcc driver, and a full build can fail once without an error; a rerun passes. Wrap batch runs in
timeout, and don't run two builds in one checkout at once (they corruptbuild/). - Leave a header on a near-miss draft: flags, diff count, what is left, what you tried.
| Error | Cause | Fix |
|---|---|---|
multiple definition of D_... |
A variable was defined in a source file (no extern) |
Make it extern. Never define. |
relocation truncated to fit: R_MIPS_GPREL16 D_... |
Declared sized, so it went through $gp, but the address isn't in $gp range |
Retail uses lui: declare it extern T D_X[];
|
relocation truncated to fit: R_MIPS_LITERAL lit4 |
A float constant went to the .lit4 pool |
Retail builds it inline: needs @ps2as
|
undefined reference to D_..._suffix |
Alias missing from symbol_addrs_resolved.txt
|
Add it with the base address |
conflicting types for 'S_...' |
Two blocks define the same typedef name differently | Give one a unique name (e.g. suffix the function address) |
Ps2EeAs errors about macro.inc or unknown options |
An @ps2as range contains an INCLUDE_ASM stub |
Narrow the override to end after the C function |
implicit declaration of function or 'X' undeclared in a file that used to build |
The block uses something only another file declares | python tools/split_text.py --refresh |
conflicting types for 'func_X' (or D_X) in a later block or another file |
Your block declares a name with a different type than another declaration the compiler sees: a later block of the same file, or a later file that uses the name without declaring it (split_text.py --refresh gives it yours) |
Use the existing declaration (grep src/frontbin/), or a per-function alias in your block plus its line in symbol_addrs_resolved.txt. try_in_context.py and pr_check.py report this before make does |
NO MATCH with no compile error |
A function compiled but doesn't match, often a partial saved from localdecomp | Rebuild each changed function in localdecomp or try_func.py; restore INCLUDE_ASM for any that aren't at 0 |
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.