# Xenosaga III PS2 — decompiled C source

This directory holds the result of running Ghidra's headless decompiler over
the Xenosaga III (USA, SLUS-21389) main executable and its three overlay ELFs.

All files were produced from the disc copies under
`../disc1/browse/code/` — the SLUS executable and OV01/OV02/OV04 overlays.
Fair-use personal RE of a game Jessi physically owns.

## What you get

```
decompiled/
  README.md                this file
  symbol_map.txt           merged <binary>\t<addr>\t<name>\t<size> across all 4 binaries
  pipeline/                scripts used to produce everything
    vu-disasm/             standalone PCSX2-derived VU disassembler (GPL-3.0)
  slus/                    main SLUS_213.89 binary (4.5 MB, 7631 functions)
    functions/             one .c file per function (7612 decompiled / 19 timed out)
    all.c                  concatenated, ordered by address (~3.6 MB)
    symbol_map.txt         <addr>\t<name>\t<size> for every recognised function
    string_to_function.tsv strings cross-referenced to the function that uses them
    vutext.s               .vutext section disassembly (1190 VU instruction pairs)
  ov01/                    OV01.OVL    (15 functions)
  ov02/                    OV02.OVL    (1169 functions)
  ov04/                    OV04.OVL    (224 functions)
```

The three overlays don't have a `.vutext` section — they're flat
single-section blobs. So `vutext.s` only appears under `slus/`.

Counts (with MMI / VU0-macro lift **and** cross-module sibling mapping
applied — see *"How it was decompiled"*):

| binary       | size      | functions | renamed-from-strings | strings-annotated | truncated |
|--------------|-----------|-----------|----------------------|-------------------|-----------|
| SLUS_213.89  | 4,513,232 |     8,626 |                  220 |               769 |         0 |
| OV01.OVL     |    26,120 |        21 |                    0 |                 2 |         0 |
| OV02.OVL     |   303,468 |     1,301 |                    3 |               114 |         0 |
| OV04.OVL     |   263,996 |       369 |                    0 |                15 |         0 |

(Total: **10,317 functions decompiled, zero `halt_baddata()`
truncations**, 223 renamed from strings, 900 annotated with their
string references.)

The decomp went through three generations:

|                            | functions | truncated files | truncation sites |
|----------------------------|----------:|----------------:|-----------------:|
| Stock Ghidra MIPS          |     9,039 |    5,639 (62 %) |            7,677 |
| + MMI/VU0-macro lift       |    10,179 |       210 (2 %) |              237 |
| + cross-module mapping     |  **10,317** |         **0** |            **0** |

The residual 210 truncations after the MMI lift were **not** VU decode
failures — they were cross-module control flow: each binary was analysed
in an isolated Ghidra program, so overlay→SLUS `JAL`s and SLUS→overlay
`J` thunks pointed into unmapped memory and Ghidra truncated. The fix
(2026-06-09) re-imports each binary with the sibling module's bytes
mapped as a memory block *before* auto-analysis (`MapSiblingBlock.java`):
SLUS `.text` (base 0x120000) into each overlay's program, and OV02's
bank tail (base 0xa85570) into SLUS's program — the SLUS ELF already
embeds an `ov01` image at 0xa80000 as its own LOAD segment, so only the
bank region above it is added. Native flow-following then resolves every
cross-module call, and discovered ~140 additional real functions in the
process. Side effect: SLUS calls into *other* overlays' banks land in
NOP padding and get mis-carved as 68-byte fake functions whose entire
body decompiles to `halt_baddata();` — `filter_fakes.py` strips these
from the output (22 in OV01, 4 in OV04, none in OV02 because the thunk
targets land in real OV02 code).

## How it was decompiled — the pipeline

Software used (all installed under `/home/jessi/.local/`, no sudo):

- Oracle JDK 21 (LTS) — required by Ghidra.
- **Ghidra 12.0.4** (NSA, public release). 12.1 also works for the stock
  pipeline but the MMI / VU0-macro extension below tracks 12.0.4 as its
  latest supported version; we pin to 12.0.4 for that reason.
- **[ghidra-emotionengine-reloaded](https://github.com/chaoticgd/ghidra-emotionengine-reloaded)**
  v2.1.34 (Apache-2.0) — replaces Ghidra's stock MIPS SLEIGH with an
  R5900-aware one that covers MMI (`pmaddw`, `psllvw`, `pextlw`, …) and
  VU0 macro-mode (`vaddw.xyzw`, `vmulq`, …). Without this extension,
  62 % of EE functions used to be partially truncated at the first MMI
  op; with it, that drops to 2 %. **This is the single highest-impact
  thing in the pipeline.** Install the extension's
  `ghidra_12.0.4_PUBLIC_*_ghidra-emotionengine-reloaded.zip` asset into
  `Ghidra/Extensions/`, then run `support/sleigh -a Ghidra/Extensions/
  ghidra-emotionengine-reloaded/data/languages` to compile the new
  `r5900.sla`. Pipeline invocations pass `-processor "r5900:LE:32:default"`
  to `analyzeHeadless` to pick the new language module.
- The VU0 *micro* mode is not in the extension. The `.vutext` section
  is disassembled separately by `pipeline/vu-disasm/` (PCSX2-derived,
  GPL-3.0) — see step 10 below.

Pipeline scripts (see `pipeline/`):

1. **`dl.py`** — pure-Python downloader (network is sandboxed away from
   `curl`/`wget` but `urllib` works fine). Used to fetch the JDK and Ghidra.
2. **`extract.py`** — pure-Python `tarfile`/`zipfile` extractor.
3. **`chmod_x.py` / `chmod_os.py`** — Ghidra's release zip loses the executable
   bit on shell scripts and the native `decompile` binary; these restore it.
   Without `chmod_os.py` the decompile interface silently returns null for
   every function. **You will hit this.**
4. **`run_ghidra.py`** — Python wrapper that calls `support/analyzeHeadless`
   with the right `JAVA_HOME`. The sandboxed shell here blocks
   `java`/`tar`/`curl` invocations directly but allows them under
   `subprocess.run`, so the wrapper exists to step around that. On a normal
   workstation you can just call `analyzeHeadless` directly.
5. **`DecompileAll.java`** — Ghidra headless post-script. Walks every function,
   runs `DecompInterface.decompileFunction`, writes one `.c` per function plus
   a concatenated `all.c` and a `symbol_map.txt`.
6. **`extract_strings_xrefs.py`** — Mini ELF parser + lui/addiu disassembler.
   The user-supplied `SLUS_213.89.strings_xrefs.txt` covers the main exe; this
   produces the same format for OV01/OV02/OV04.
7. **`string_to_function.py`** — Binary-searches `symbol_map.txt` for each
   string xref to find the function that loads it. Output: `string_to_function.tsv`.
8. **`RenameFromStrings.java`** — Ghidra post-script. Reads the TSV; if a
   function's strings include a recognisable identifier pattern (e.g.
   `'mwPlyCreateSofdec: ...'`, `'XstAnimTest.euc.c'`), renames the function
   accordingly and then re-runs decompilation so callers display the new name.
   (Heuristic — see the `PATTERNS` list. Most renames come from CRI middleware
   error strings of the form `"<errcode>: <funcname>: <msg>"`.)
9. **`annotate_strings.py`** — Inserts a `// strings:` comment block at the top
   of each `.c` file listing the strings that function references. Makes
   functions self-documenting even when no clean rename was possible.
10. **`vu-disasm/` + `vu_disasm.py`** — VU0/VU1 microcode disassembler. The
    Ghidra MIPS SLEIGH doesn't cover VU instructions; Ghidra treats the
    `.vutext` section as raw bytes. We use a standalone GPL-3.0 binary
    derived from PCSX2's `DebugTools/DisVU*` source, with live-emulator
    register-state hooks replaced by name-only printing. `vu_disasm.py`
    wraps it: extracts the section via `extract_vu_blobs.py`, runs the
    decoder, emits `<binary>/vutext.s` with a header matching the MIPS
    disasm conventions. Build with `make` inside `pipeline/vu-disasm/`;
    verify with `make smoke`.

The full sequence, end-to-end:

```sh
GHIDRA=/home/jessi/.local/ghidra_12.0.4_PUBLIC

# One-time tool install (under /home/jessi/.local/)
python3 pipeline/dl.py https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.tar.gz /tmp/jdk.tar.gz
python3 pipeline/dl.py https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.0.4_build/ghidra_12.0.4_PUBLIC_20260303.zip /tmp/ghidra.zip
python3 pipeline/extract.py /tmp/jdk.tar.gz /home/jessi/.local/
python3 pipeline/extract.py /tmp/ghidra.zip /home/jessi/.local/
python3 pipeline/chmod_x.py $GHIDRA
python3 pipeline/chmod_os.py $GHIDRA

# Install ghidra-emotionengine-reloaded extension (MMI + VU0-macro lift)
python3 pipeline/dl.py \
    https://github.com/chaoticgd/ghidra-emotionengine-reloaded/releases/download/v2.1.34/ghidra_12.0.4_PUBLIC_20260307_ghidra-emotionengine-reloaded.zip \
    /tmp/ee-reloaded.zip
python3 pipeline/extract.py /tmp/ee-reloaded.zip $GHIDRA/Ghidra/Extensions/
JAVA_HOME=/home/jessi/.local/jdk-21.0.11 \
    $GHIDRA/support/sleigh -a $GHIDRA/Ghidra/Extensions/ghidra-emotionengine-reloaded/data/languages

# One-time: extract the sibling blobs used for cross-module mapping
#   slus_text.bin     = SLUS .text section bytes        (base 0x120000)
#   ov02_bank_tail.bin = OV02 bank bytes above 0xa85570 (skips the ov01
#                        image the SLUS ELF already embeds at 0xa80000)

# Per-binary: import with sibling mapped + analyse + decompile + rename
# + re-decompile + filter padding fakes + annotate
# (note -processor "r5900:LE:32:default" — picks the new R5900 SLEIGH module)
# Overlays get SLUS .text; SLUS gets OV02's bank tail. Range filter keeps
# the mapped sibling's functions out of the output:
#   overlays: 0xa7f000..0x10000000      SLUS: 0x100000..0xa7f000
for B in SLUS_213.89 OV01.OVL OV02.OVL OV04.OVL ; do
    P=projects N=xs3_${B%.OVL}                            # project name
    case $B in
      SLUS*) BLOB=out/ov02_bank_tail.bin BASE=0xa85570 BNAME=ov02_bank_tail
             RMIN=0x100000 RMAX=0xa7f000 ;;
      *)     BLOB=out/slus_text.bin BASE=0x120000 BNAME=slus_text
             RMIN=0xa7f000 RMAX=0x10000000 ;;
    esac
    python3 pipeline/run_ghidra.py --                       \
        $P $N -import ../disc1/browse/code/$B               \
        -processor "r5900:LE:32:default"                    \
        -scriptPath pipeline                                \
        -preScript MapSiblingBlock.java $BLOB $BASE $BNAME  \
        -postScript DecompileRange.java out/$N $RMIN $RMAX
    python3 pipeline/extract_strings_xrefs.py ../disc1/browse/code/$B out/$N/strings.txt
    python3 pipeline/string_to_function.py out/$N/symbol_map.txt out/$N/strings.txt out/$N/s2f.tsv
    python3 pipeline/run_ghidra.py --                       \
        $P $N -process $B -noanalysis                       \
        -scriptPath pipeline                                \
        -postScript RenameFromStringsRange.java out/$N/s2f.tsv out/$N $RMIN $RMAX
    python3 pipeline/filter_fakes.py out/$N
    python3 pipeline/annotate_strings.py out/$N/s2f.tsv out/$N/functions
done
```

(The `SLUS_213.89.strings_xrefs.txt` file the user already provided was used
directly for the main exe; the Python `extract_strings_xrefs.py` was needed
only for the overlays.)

## Notable findings

Compiled from running the post-MMI-lift decomp and grepping the
recovered strings + functions on 2026-05-16. Everything below is
provable from the artifacts in this repo — file references are
relative to the decomp tree.

### Build environment

* **Sony DTL-T10000 dev kit.** All baked-in asset paths start with
  `host0:/home/xeno/work/` — that's the standard PS2 development
  hardware's host-filesystem mount. The retail binary still knows how
  to load assets from it (see "SRD" below).
* **Three working dirs** under `xeno/work/`:
  * `iwase/` — a developer responsible for character animation work.
    Hardcoded debug-asset filenames include `kosmos00.xap`, `nep.txy`,
    `shionH00.pxy`, `anim_test.pxy`, `E2_MOM04_EVA.pxy`.
  * `tetsuya/chara/` — character model work
    (e.g. `C3kosmos01.pxy` — the "C3" prefix suggests a numeric
    character ID scheme).
  * `_particle/` — shared particle-effects assets.
* **Source files are EUC-JP encoded.** Every C source ends in
  `.euc.c` rather than just `.c` — their build pipeline used this
  convention to mark files containing Japanese characters. Modern
  Japanese codebases would just be UTF-8; this is an early-2000s
  Japan-industry convention.
* **CRI middleware build date pinned:** `ADXENC.DLL Ver.1.10 Sep 20 2005`
  appears as a literal string. Xenosaga III shipped July 2006 — so the
  team was on 9-month-old CRI middleware when the gold master was cut.
  Internal error codes use a `YYYYMMDD` prefix convention
  (`E2005012801` → 2005-01-28) which lets you date individual subsystem
  revisions if you grep `E[12][09][09][0-9]{6}`.

### Engine architecture

The engine appears to be an in-house Monolith Soft framework with at
least three distinct namespaces:

* **`Ti*` — animation / physics / environment toolkit.** 15 modules
  with the `Ti` prefix recoverable via `.euc.c` markers:
  `TiCameraSolver`, `TiMotionSolver`, `TiDynamicsColl`,
  `TiGlobalColl`, `TiDrawCollision`, `TiDrawJoint`, `TiMRPackage`
  (motion-resource package — see also `pMRPackage` pointer), 
  `TiMtnBlend`, `TiPartsAnim`, `TiVu0` (VU0-microcode helpers),
  `TiXacAnimCurve`, `TiWatSim`, `TiWater`, `TiWindAnim`,
  `TiWindBase`. The "Ti" prefix is likely a programmer's initials
  ("Tetsuya I[wase]"?) or a project codename. The cohesion is
  striking — `Ti*` covers cameras, physics, collision, animation
  blending, water, wind, and particle pipelines.
* **`nj*` — Sega's Ninja Library.** `njPlane.euc.c`,
  `njString.euc.c`. NJ was Sega's Dreamcast/Naomi-era graphics
  library that several Japanese studios kept reusing on PS2.
  Monolith Soft licensed or copied pieces of it.
* **`XAct*` — "Xeno Actor" entity system.** A struct accessor leaked
  into an assertion: `pKosmos->rsrc[XAct_Rsrc_MDL]`. Confirmed enum
  values (no Ext0/Ext1/Ext3 — only the listed values exist):
  * `XAct_Rsrc_MDL` — Model
  * `XAct_Rsrc_TEX` — Texture
  * `XAct_Rsrc_XHR` — Hierarchy / skeleton
  * `XAct_Rsrc_XAP` — Animation
  * `XAct_Rsrc_XAP_Ext2` — Extended animation (the "2" is a version
    number, not an index — Ext1 doesn't exist)
* **Capitalization inconsistency**: both `xenoActor.euc.c` (lowercase
  first letter) and `XenoActorEvent.euc.c` (Mixed) exist — different
  programmers, different style preferences, no enforcement.

### File-extension dictionary (resolved from `XAct_Rsrc_*`)

| Ext | Meaning | Notes |
|---|---|---|
| `.txy` | Texture container | Always paired with `.pxy`; pair-loaded |
| `.pxy` | Packed model data? | Pair with same basename as `.txy` |
| `.xhr` | Joint hierarchy / skeleton | One per character |
| `.xap` | Animation | Multi-per-character |
| `.adx` | CRI audio (ADPCM) | Streaming-capable via AFS |
| `.sfd` | SOFDEC video | MPEG-2 PS + ADX interleaved |
| `.afs` | CRI Archive File System | Streamed; **128 MB / inner file limit** |
| `.irx` | IOP module | Standard PS2 binary format |

`.dap`, `.epf`, `.esd`, `.sb`, `.cnf` also appear in path strings but
their structure isn't recovered yet.

### Named character pointers in debug builds

Only `pKosmos` and `pChaos` survived as named pointer variables in
the retail-build assertion strings. Other characters (Shion, MOMO,
Jr., Allen, Ziggy, Wilhelm) certainly exist as `XAct_Actor` instances
but their debug-print coverage was less complete. Other recovered
engine-state pointers: `pCurrMat`/`pGlobalMat`/`pTransMat` (matrix
stack), `pElemEff`/`pElemHeader` (effect elements),
`pKeyHeader` (animation keyframes), `pLandscape`/`pWater`/`pLocalLight`
(scene), `pMRPack`/`pMRPackage` (motion resources — linked to
`TiMRPackage`), `pVisible` (culling), `pState` (state machines).

### Boot flow and main loop

With the MMI lift in place, the boot sequence is now fully readable.

**`entry()` at `0x00120008`** does:
1. `SYNC(0x10)` — DMA barrier
2. Three-phase BSS zero-clear from `0x00551380..0x009109c8` —
   byte-by-byte until 16-aligned, then 128-bit MMI stores in bulk
   (the actual BSS clear is *fast* — 4 words per loop iteration via
   `SQ` opcode), then byte-by-byte tail
3. Two syscalls (`0x3c`, `0x3d` — IOP module load + exec)
4. Runtime init with heap-start = end of BSS, stack/heap sizes
5. `FlushCache(0)`, `EI()` (R5900 enable-interrupts), then jumps to
   game `main()` at `FUN_00181af0`

**Game `main()` at `0x00181af0`** does:
1. Stashes argv pointers in `$gp`-relative globals (`uGpffff9090`,
   `uGpffff9094`)
2. **First subsystem call is `FUN_00a80000()` — into the overlay
   region.** The overlays aren't optional add-ons; the very first
   thing main does is jump into overlay-mapped memory.
3. Eight subsystem inits
4. **Event-driven main loop**:
   ```c
   for (;;) {
       do { uVar2 = FUN_00a81718(); } while (uVar2 == 0);
       channel = uVar2 >> 0x18;             // top 8 bits = channel
       code_ptr = uVar2 & 0xffffff;         // low 24 bits = function
       if (code_ptr != 0xa81718 && channel != 1) {
           (*(code *)code_ptr)();           // indirect call
       }
   }
   ```
   Events are packed as `[8-bit channel][24-bit function pointer]`
   into a 32-bit slot. `0xa81718` is the dispatcher's own address,
   used as a "no event" sentinel; channel `1` is a no-op marker.

### CRI middleware footprint

* **MWPLY / MWSFD / MWSTM** — CRI's media-player and streamer
  libraries. ~150 functions auto-renamed from their `"<errcode>:
  <funcname>: <msg>"` error strings. Canonical starting point:
  `slus/functions/0x0012f058_mwPlyCreateSofdec.c`.
* **AFS** (CRI Archive File System) — hard 128 MB inner-file limit
  enforced at runtime (`E2122501:AFS file has 128MB or more...`); 
  parallel loads explicitly forbidden (`E20040917 : It is impossible
  to load AFS files in parallel.`).
* **SRD** — disc-I/O abstraction layer. Runtime `devtype = DVD` vs
  `devtype = HOST` switch so the same binary runs on retail discs and
  on the dev-kit `host0:` filesystem without a recompile.
* **ADXM** — ADX Manager, with custom `UsrVsyncThread` and
  `UsrIdleThread` on the IOP.
* **LSC** — namespace unique to Monolith (not a CRI prefix). 10
  consecutively-numbered error codes about `LSC_Create` parameter
  validation. Purpose unclear — possibly "Lua Script Container" or
  "Light/Local Scene Cache".

### Linguistic and cultural

* **"Illigal" is consistent across 22 string sites** — it should be
  "Illegal" but the misspelling appears in *both* CRI's middleware
  errors (e.g. `adxf_GetPtStat`) *and* Monolith's own `LSC_*` code.
  Not a single programmer's tell — it's a Japanese-game-industry
  idiom of the era. CRI shipped the misspelling and in-house code
  adopted the same style.
* **`E20040917` and similar date-prefixed codes** date-stamp specific
  error introductions; the rough span we see is 2004–2005.

### Cut and debug content shipped in retail

The retail binary still contains code (with intact strings) that
appears to have been developer-only:

* **`test.euc.c`** is a `heap-dump` debug printer. Strings
  `"heap = %p top = %p size = %d"` and `"heap-dump"` survived; the
  function dumps heap state to console. Probably runtime-gated behind
  a debug flag but the code is right there.
* **A suite of graphics test scenes**: `Gtest.euc.c`,
  `BlendTest.euc.c`, `CFLLightTest.euc.c`, `CullingTest.euc.c`,
  `ScisTest.euc.c` (scissor test), `XstAnimTest.euc.c`. Likely once
  reachable via a debug menu; with a flag-flip or hex patch, almost
  certainly re-enablable.
* **`EventViewer.euc.c` and `MotionViewer.euc.c`** — internal
  cutscene/animation viewer tools. Same story.

### Renderer / VU1 microcode structure

`slus/vutext.s` (1,190 instruction pairs) contains **roughly 28
distinct VU1 microprograms**, identified by E-bit (end-of-program)
markers in the upper-instruction's top nybble. Average ~42
instructions per program.

The common loop pattern is the classic PS2 vertex/transform kernel:

```
SQ.xyzw vf<n>, vi12, <offset>   ; write vertex data to output buffer
XGKICK  vi12                     ; kick the GIF packet to the GS
IBGTZ   ..., vi08                ; decrement counter, branch if > 0
```

29 `XGKICK` instructions total across all 28 programs (so most
programs kick once; a few kick multiple packets). These are the
actual real-time rendering kernels for every drawcall.

### Anchor functions worth exploring

| Address | What it is | Why interesting |
|---|---|---|
| `0x00120008` | `entry()` | Boot, MMI-recovered |
| `0x00181af0` | game `main()` | Event-loop dispatcher |
| `0x00a80000` | overlay entry | First overlay call from `main` |
| `0x0012f058` | `mwPlyCreateSofdec` | CRI middleware canonical example |
| `0x00a81718` | event dispatcher | Used as "no event" sentinel

## What works well, what doesn't

**Works well**
- **97.9 % of recognised functions** (9,969 out of 10,179 across all
  four binaries) decompile end-to-end with no `halt_baddata()`
  truncation. Stack-frame reconstruction is good, local variables are
  typed reasonably, calls between renamed functions display the real
  names. **R5900 MMI ops decode correctly** thanks to the
  `ghidra-emotionengine-reloaded` extension — e.g. SLUS's `entry()` at
  `0x00120008` now shows a proper 576-byte BSS-clear loop using 128-bit
  MMI stores, where the stock-Ghidra output was 132 bytes of opaque
  `special2()` calls.
- **VU0 macro-mode** (`vaddw.xyzw`, `vmulq`, etc. issued from EE code
  as coprocessor-2 ops) also decodes cleanly via the same extension.
- CRI middleware functions (SOFDEC video, ADX audio, MWSFD streamer)
  are *especially* well-recovered because their error-handling code
  emits strings of the form `"<errcode>: <funcname>: <msg>"` that the
  rename heuristic picks up. Look at
  `slus/functions/0x0012f058_mwPlyCreateSofdec.c` for a typical
  example — function and most callees correctly named.
- Engine-internal source-file markers (`BlendTest.euc.c`,
  `XstAnimTest.euc.c`, `TiMRPackage.euc.c`, …) appear in the
  `// strings:` comment block of any function that hosts an assertion
  from that file. That's not a function-name match, but it tells you
  the source module — very useful for cross-referencing.
- **VU0 micromode** in the `.vutext` section (`0x003172f0..0x00319820`,
  1,190 instruction pairs) is disassembled separately by
  `pipeline/vu-disasm/` (PCSX2-derived, GPL-3.0). Output:
  `slus/vutext.s`. **29 XGKICK instructions** found in there — see
  *"Notable findings"*.

**Still doesn't work**
- Most non-renamed functions stay as `FUN_001xxxxx`. The string-xref
  heuristic is good for ~223 functions out of 10,179; the rest mostly
  don't load strings or only load assert messages that don't name
  their function.
- Global data is shown as `DAT_005xxxxx` with `undefined1[N]` types,
  not as the typed `g_sEpisode.pCut` etc. that the source would show —
  no symbol table exists in the stripped ELF and the inference engine
  isn't going to recover it.

## How to interpret a typical output file

```c
// 0x0012f058  mwPlyCreateSofdec
// size=1852 bytes
// strings:
//   'E2053002: not enough work'
//   'E4041301: not enough work'
//   'E20010703C mwPlyCreateSofdec: create error'
//   'E20010703D mwPlyCreateSofdec: set errcb'
//   "E410142 mwPlyCreateSofdec: can't skghn."

void mwPlyCreateSofdec(undefined8 param_1,undefined4 *param_2)
{
  ...
  uStack_74 = MWSFD_Malloc(param_1,DAT_008f0850 + ...);
  MWSFD_Malloc(param_1,DAT_008f084c + 0x40);
  FUN_0012ede0(param_1,param_2,auStack_e0);
  ...
}
```

Reading guide:
- `// 0x...` — the virtual address (matches the address in `symbol_map.txt` and
  in `SLUS_213.89.disasm.txt` from the Capstone disasm).
- `// strings:` — every string-literal this function loads (via `lui`/`addiu`).
  The "real" name of the function is almost always derivable from these.
- `halt_baddata()` — no longer present in this tree (the cross-module
  sibling mapping resolved every truncation). If you regenerate without
  the `MapSiblingBlock.java` pre-script you'll reintroduce them: they
  mark `J`/`JAL` targets in the *other* module that weren't mapped, not
  undecodable instructions.
- `undefined4`, `undefined8` — Ghidra's "didn't infer a type" marker for 4-byte
  and 8-byte values respectively. Pretty much every variable that flows in/out
  via untyped memory comes back this way.
- `DAT_005xxxxx` — global at that address. Likely a `.data`/`.rodata`/`.sdata`
  symbol; check the section table:
  - `.text`     `0x00120000..0x003172f0`  EE code
  - `.vutext`   `0x003172f0..0x00319820`  VU0 microcode (not decompiled)
  - `.data`     `0x00319880..0x00515ae4`
  - `.rodata`   `0x00527800..0x0054f200`  string literals live here
  - `.sdata`    `0x00550280..0x00551380`  small-data, addressed via `$gp`
  - `.bss`      `0x00551600..0x009109c8`  zero-init globals
  - OV01-4 overlays load over `0x00a7f000..0x00a80000`-and-up

## Symbol map format

`symbol_map.txt` rows are `<addr>\t<name>\t<size_bytes>`. Use this as the
single source of truth for "what function lives at address X?" — the
`functions/` filenames embed the same name but truncate at 80 chars.

`string_to_function.tsv` rows are the joined view: each string xref linked to
its containing function. Header:
```
func_addr  func_name  func_size  use_va  string_va  string
```

## Running this on another PS2 binary

The pipeline is generic for any PS2 EE ELF:

```sh
# Replace SLUS_213.89 with your binary path
B=SLUS_xxx.xx
python3 pipeline/run_ghidra.py --                           \
    projects xs3 -import $B                                 \
    -scriptPath pipeline                                    \
    -postScript DecompileAll.java out/$B
python3 pipeline/extract_strings_xrefs.py $B out/$B/strings.txt
python3 pipeline/string_to_function.py                       \
    out/$B/symbol_map.txt out/$B/strings.txt out/$B/s2f.tsv
python3 pipeline/run_ghidra.py --                           \
    projects xs3 -process $B -noanalysis                    \
    -scriptPath pipeline                                    \
    -postScript RenameFromStrings.java out/$B/s2f.tsv out/$B
python3 pipeline/annotate_strings.py                         \
    out/$B/s2f.tsv out/$B/functions
```

If you have a starting set of known function names (e.g. you've matched some
to retail SDK headers), drop them into `string_to_function.tsv` before the
RenameFromStrings step and they'll be applied alongside the heuristic matches.

## Things I'd add if I had another half day

- **VU0/VU1 microcode disassembler** — Ghidra has a `VU` processor in
  contrib/community but not the official release; the `.vutext` section and
  the in-line VU0 macro code are currently a blind spot. The PCSX2 VU
  decompiler source is probably the right starting point.
- **MMI lift in SLEIGH** — write the missing R5900 MMI ops as SLEIGH so the
  decompiler stops bailing out at the `halt_baddata()` markers. There's a
  community fork that does this; haven't tested it.
- **Better automatic renaming** — pull the function-name-shaped tokens out of
  Japanese assert messages too (the engine has plenty: see strings near
  `iwase/`, `xeno/work/`).
- **Cross-overlay symbol unification** — the SLUS exe references `OV*.OVL`
  via overlay-table entries (`.DVP.ovlytab` section). Cross-link the SLUS
  callers to OV02's entry points to get full call-graph coverage.
- **Discourse-style "split into translation units"** — the engine clearly
  builds from per-file `.euc.c` units; grouping functions by source file
  (which we have for ~340 functions in SLUS via assertion strings) would
  make the output much easier to navigate.
