Files
Going-Commando/docs/roadmap.md
T
ReaperOfVeriod 48d9506cf1 Decode tfrag texture bindings, vertex colours and light records
Adds level_core.rs (the uncompressed core index header and texture table)
and extends tfrag.rs with the data a draw call needs beyond geometry.

An ad-gif is five A+D quadwords holding packed fields rather than GS
registers; the level load rewrites them in place before drawing. On disc
TEX0_1.data_lo indexes the level's texture table, TEX1_1.data_lo is the
LOD bias K and data_hi the minification filter, and CLAMP_1 holds both
wrap modes.

Three things are easy to get wrong here:

* K is a signed 12-bit value in 1/16 units, sign-extended by shifting the
  field up 4 and back down. Retail values are -137..-89 mip-scaled;
  reading it unsigned gives 1911 instead of -137.
* TEX1_1.data_hi is MMIN, and 4 decodes as LinearMipmapNearest, not
  LinearMipmapLinear as the raw bit layout suggests.
* RGBA and the light/normal records are not in the VIF lists at all. They
  sit at fixed offsets in the patch data block: rgba_size*4 colours at
  rgba_ofs, vert_count light records at light_ofs+0x10. The tfrag origin
  lives there too -- if no STROW row supplied one, the quadword at
  light_ofs is it.

The texture table checks out on its own terms: 93 entries at index offset
0x2900 on level 0, and 0x2900 + 93*16 lands exactly on the next range.

Verification: an ad-gif index outside the level's table would mean the
geometry and the textures were misread independently, so every one is
range-checked against real entries. All 27 levels: 47,950 ad-gifs over
18,102 patches, all in range, referencing 1429 of 1595 level texture
entries (90%). Each patch's light array is exactly vert_count records and
its RGBA array exactly rgba_size*4.

Still open: LOD selection and morphing, and the texel data itself.
2026-10-10 02:22:51 +02:00

12 KiB

Roadmap

Status as of 2026-10-09.

A from-scratch native Rust reimplementation of Ratchet & Clank: Going Commando (PS2). Each phase ends in a gate — something checkable, not a feeling that it is going well. Phases are ordered so that the riskiest unknowns are retired early, while they are still cheap to retire.

Where we are

  • Target disc identified and verified (SCUS_972.68 v1.01, NTSC 60 Hz)
  • Workspace builds (cargo check --workspace)
  • Repository hygiene in place (pre-commit guard, .gitignore, licence record)
  • Phase 1 complete. The disc pipeline runs end to end and is verified against a committed hash table: 5,135 files, 4,500,106,960 bytes, every one of the 92 asset containers parsing.
  • Phase 2 in progress. Level containers are parsed through their typed header, and the data extent is parsed as a container of byte ranges. The geometry decompresses: level 0 yields 16,969,296 bytes of level content from a 9.7 MB stream.
  • Next: tfrag LOD selection and morphing, then the first drawable frame.

The disc being the exact build the reference projects pin is the single biggest favourable fact so far: no disc conversion, no regional timing work, and no re-derivation of the reference baseline.


Phase 0 — Foundation ✅

Done.

  • Disc identified: SCUS_972.68 v1.01, USA Black Label, NTSC 60 Hz. Boot ELF SHA-1 bacda860cbd6723142e86db56f197e47e728970f, entry 0x00131AE8 — the entry point independently matches the value recorded by rac2-gc-decomp, which cross-confirms the identification.
  • tools/identify-disc.py reproduces the identification against any disc image and distinguishes a truncated dump from a non-PS2 image.
  • Five-crate workspace scaffolding, Bevy 0.19.1 pinned, builds clean.
  • .githooks/pre-commit blocks game data. Verified against seven cases: .iso, renamed disc image, WAD container, ADX container, extracted/, a 60 MiB file, and legitimate source.
  • AGENTS.md and PROVENANCE.md record conventions and licences.

Open from this phase

  • alsa-lib-devel is not installed, so Bevy's host audio backend is disabled in Cargo.toml. Audio is the last phase; revisit then.
  • Bevy feature set is deliberately minimal. Re-add features as needed rather than switching default-features back on wholesale.

Phase 1 — Disc pipeline ✅

Done. Gate met: every extracted file matches the committed hash table.

  • iso9660 reader (R&C2 lists assets as real files, unlike R&C1)
  • G/*.WAD container header parsing — 92 containers, three table shapes
  • WAD LZ77 decompression (wad.rs, four packet classes)
  • Unpack every container, bounds-checking each extent
  • SHA-1 table for all 5,135 files (hashes only, never content)
  • verify stage, plus integration tests against a real disc
  • Format specs written: disc-layout.md, global-wad.md, wad.md
  • Work record with the negative results: plan/disc-pipeline.md
gc-extract identify disc.iso        -> SCUS_972.68 v1.01 NTSC, ELF SHA-1 matches
gc-extract unpack   disc.iso out/   -> 5135 files, 4,500,106,960 bytes, 10.8s
gc-extract verify   out/ table.tsv -> OK: 5135 files match

Not done, deliberately: the table of contents at sector 1001 is documented but not read. Enumerating the ISO filesystem is simpler and sufficient; the ToC matters when per-level overlay loading is understood in Phase 3.

Two wrong turns are recorded in plan/disc-pipeline.md, because both would be easy to repeat: the container pair table read backwards, and the LZ77 stream mistaken for a bit stream. Both produced plausible-looking wrong output before failing, which is the failure mode worth guarding against.


Phase 2 — Format parsers

Goal: every asset type parses into plain Rust values, tested against real disc data.

Phase 1 hands over the extents; this phase works out what each one contains.

Done so far:

  • level_wad.rs — the 0x60-byte header on all 27 level containers, naming four extents plus three chunk descriptors. Verified: extents tile each container in file order (27 of 27).
  • Two facts the format documentation does not state, both measured here: sizes in this header are sector counts where the generic G/ table uses byte counts, and LEVEL21.WAD reports level id 30, so container order is not level identity.

In progress:

  • data — the extent header locates core_index and core_data; the geometry decompresses and is verified on level 0.

  • core_index class tables — the header base is confirmed (the compressed and decompressed sizes of core_data sit at the same offsets in all 27 levels), and every offset lands inside core_data. But the class-table fields do not decode under any field order or stride tried. Ruled out and written up in plan/level-assets.md. Next: identify tfrag by its own structure instead of through the header.

  • occlusion and sky offsets located and their first bytes read.

  • Region layout of core_data established: tfrag, occlusion, sky, collision and textures_base are offsets, strictly increasing on all 27 levels. Terrain is [0, occlusion) and carries inline texels.

  • tfrag vertex format: recovered. The tfrags block header gives a table offset (0x40) and a patch count; each patch is a 0x40-byte header whose data offset is relative to the table, not to the block. Positions are V3_16 payloads inside a VIF1 command list, split across three lists per patch (common, LOD 0+1, LOD 0). The whole vector format is encoded in the command byte and every element size follows from it. Verified on all 27 levels: 18,102 patches, 821,439 vertices, each patch yielding exactly its own vert_count. See plan/level-assets.md and gc-formats::tfrag.

  • tfrag topology: recovered. Vertex-info (UVs plus LOD parenting), the strip command lists and the index arrays, classified by unpack format and destination VU address. Strip indices address the vertex-info array rather than positions. Every patch's strips expand to exactly its own tri_count: 18,102 patches, 809,879 triangles, all indices in range. See plan/level-assets.md and gc-formats::tfrag.

  • tfrag texture bindings and per-vertex data. The ad-gifs decode to a texture index, wrap modes, minification filter and LOD bias; the level index gained a texture table (level_core.rs) for those indices to land in. Per-vertex RGBA and light/normal records recovered from their fixed offsets. All 27 levels: 47,950 ad-gifs, every index in range, 90% of the level texture tables referenced.

  • tfrag LOD selection: the switch and morph rules in TfragProc.

  • gameplay — begins WAD + a per-level byte, and does not decode with the WAD LZ77 decoder. All 27 levels fail identically, so the input is wrong rather than the decoder. Ruled out: bit order, header length, the LZ77 framing. Hypotheses queued in plan/level-assets.md.

  • occlusion — 2 to 25 sectors, small enough to read whole.

  • sound_bank — SBlk and DAW signatures found in all 27.

  • data — the large one: 4.2 to 21.9 MB of geometry per level.

  • tfrag — terrain: LOD chains, strips, morph targets

  • tie — instanced environment geometry, fat vertices

  • shrub — foliage and decorative meshes

  • moby — dynamic actors; moby_anim — skeletal animation

  • collision — cells, packed vertices, faces

  • occlusion — the PVS grid

  • sky, volumes, textures (CSM1 CLUT + PS2 alpha)

  • level_overlay — the seven-section parser and relocation from Phase 3

Gate: every parser has a golden test against bytes from the real disc, and reports mismatched header counts as failures rather than warnings.

Discipline: bound every read. Typed errors, no panics, no reads past the buffer.


Phase 3 — Overlay system

Goal: make 27 level overlays tractable.

Every overlay carries the same seven sections (.lit, .bss, .data, lvl.vtbl, lvl.camvtbl, lvl.sndvtbl, .text) and the same engine code, linked at different addresses. ReRAC solves this by masking relocations — j/ jal targets, lui immediates of main-RAM addresses, the %lo that completes them, and $gp-relative operands — then matching the masked words to find a reference function's copy on another overlay.

  • Port the masking and code-identity logic
  • Relocation::func and Relocation::data
  • $gp for this game (0x001AEFF0, against R&C1's 0x166c00)
  • Extend the level table from 19 to 27

Gate: every level-01 function's relocated copy is found on all 27 overlays; cross-checked against an independent cluster hash.

Why this is high-value: it turns "27 overlays of unknown code" into a table lookup, and it is the main thing inherited from ReRAC.


Phase 4 — Engine core

Goal: draw a frame.

  • Graphics Synthesizer in wgpu, with the GS's own rules: its round(log2(z/n) + K) mip selection, blend and alpha-test behaviour, alpha 0x80 meaning 1.0, CLAMP wrap
  • VU0/VU1 skinning and lighting on the PS2 float model
  • LOD/morph/cull per TfragProc and TieProc
  • Occlusion culling, fog, shadows
  • GIF/VIF packet stream interpretation

Gate: CPU and GPU paths agree on every vertex of a reference level — the same identity check ReRAC runs.

Risk: VU1 microcode is binary in the game's .vutext section, so it is reverse engineered rather than parsed. This is the hardest single item in the project and the main threat to the timeline. Note that ReRAC's own moby VU1 analysis trips their disassembler on two opcodes.


Phase 5 — Veldin vertical slice

Goal: one planet, playable start to finish.

  • Ratchet's controller on a spherical planet
  • Chargeboots: hold/release timing, chained jumps, dash
  • Wrench and the camera
  • Collision and the HUD

Gate: Veldin is completable and visually matches PCSX2 frame-for-frame under replayed input.

This is the go/no-go for the project. If Veldin works, the other 26 levels are content. If it does not, the reason is discovered while it is still cheap. Spherical gravity is the thing most likely to break, because it invalidates R&C1's movement and camera assumptions wholesale.


Phase 6 — Remaining overlays and the shell

  • Per-level moby class dispatch via lvl.vtbl
  • Remaining 26 levels
  • Planets, travel, the Gadgetron vendor, RPG upgrades
  • Weapon modification bitmasks (lock-on, shock, acid, …)
  • Minigames and challenges
  • The 25-state memory-card FSM and its UI
  • IFF chunk saves, including import of a R&C1 save to unlock legacy weapons

Phase 7 — Audio and video

  • 989snd v2 audio decode and playback (not CRI ADX — a different codec)
  • SPU2 mixing, reverb zones, level sounds
  • IPU/MPEG-2 video for the title screen and cutscenes

Phase 8 — Fidelity pass

Differential testing against PCSX2 across every level:

  • Recorded-input frame comparison, per planet
  • Geometry, fog, blend, alpha, texture sampling
  • Audio sync and 60 Hz timing
  • Behavioural comparison: traversal routes, boss timing, RNG

Principles

  • A gate is pass/fail, not a percentage. "Looks fine" is not progress.
  • Negative results are written down. A ruled-out approach saves the next attempt as much as a working one.
  • The float model is not negotiable. Approximate is not the same as faithful, and the difference is visible in how the game feels.
  • Fidelity over tidiness. Where the original does something that looks like a bug, reproduce it and say why in a comment. See AGENTS.md.