Reusing this repo for other PS1 games
Most of this project describes one game, but a fair share of it describes the PlayStation itself: the disc's sector layout, Sony's file formats (TIM, VAB, SEQ, XA, STR), the GTE and SPU hardware, emulator save states. This page sorts the crates and scripts by how far they carry to another title, names every Legend of Legaia assumption that would trip a second game, and points at the working method the project runs on, which transfers whole.
Three classes
The classification is read off each crate's source, not its README.
- Generic PS1
- Works on any PSX disc, or any file of that format.
- Generic, Legaia quirks
- Sony's format or hardware, but the code bakes in something Legaia does. Each quirk is named below.
- Legaia-only
- Legaia's own containers, data or runtime. A worked example, not a dependency.
The package name follows the class: the standalone generic crates are named psx-* (lib psx_*), everything else legaia-*. The two engine crates with a generic core (legaia-engine-audio, legaia-render-kernels) keep the engine's prefix. The directory under crates/ carries no prefix either way.
isoxabytesretail-exectimvabseqlzsmdecmednafenpcsx-reduxram-imageengine-audiorender-kernels::gtetmdcontainer-formatsprotformat familiesengine-*Depending on a crate
Nothing here is on crates.io. Take a crate as a git dependency and turn off its default cli feature, which adds the command-line binary and its dependencies (for some crates, other Legaia crates):
psx-tim = { git = "https://github.com/AndrewAltimit/legend-of-legaia-re", default-features = false }Every crate is MIT OR Unlicense. The binaries build with cargo build --release -p <crate>. None embeds Sony bytes, and none needs a Legaia disc to run on another game's files unless noted.
The crates at a glance
| Crate | Class | Entry point | CLI |
|---|---|---|---|
psx-iso | Generic | RawDisc::open, iso9660::walk_files, write::patch_file_logical | disc-extract |
psx-xa | Generic | demux::demux_raw_sectors, decode, StreamingDecoder | xa |
psx-retail-exec | Generic | Cpu::call, Cpu::hook, Cpu::calls, gte::Gte (the hardware-exact GTE); the Legaia comparisons are its tests alone (method) | - |
psx-bytes | Generic | u16_le / u32_le returning Option | - |
psx-tim | Quirks | parse, parse_strict, decode_rgba8, encode::encode_replacement | tim |
psx-vab | Quirks | parse, find_vabs, decode_vag | vab |
psx-seq | Quirks | Seq::parse | seq |
psx-lzs | Quirks | decompress, compress | lzs-decode |
psx-mdec | Quirks | str_av::demux_str_av, MdecDecoder | mdec |
psx-mednafen | Quirks | SaveState::main_ram_structured, gpu::PsxGpu, spu::PsxSpu | mednafen-state |
psx-pcsx-redux | Quirks | SaveState::from_path_structural, gpu::PcsxGpu, fixture::SyntheticState | pcsxr-state --game generic |
psx-ram-image | Quirks | diff::diff_ram, bisect::bisect_first_bad, prim_pool::decode, GameProfile | - |
legaia-engine-audio | Quirks | spu::Spu, Sequencer::new, vab_bind::VabBank::upload | note-trace |
legaia-render-kernels | Generic module | gte::Gte, gte::gte_divide, vram_capture | - |
legaia-tmd | Legaia-only | parse (Legaia variant) | tmd |
legaia-container-formats | Legaia-only | parse_streaming, pack; tim_scan is generic | - |
Four more generic modules sit inside Legaia crates: the memory-card walker legaia_save::card, the PPF 3.0 writer and applier legaia_disc_patch::ppf, the R3000 encoders and small simulator legaia_code_hooks::{mips, mips_sim}, and the GameShark and Mednafen .cht parsers in legaia-cheats. For disc-patch and code-hooks, which depend on Legaia format crates, copying the module is lighter than depending on the crate.
Generic PS1 crates
psx-iso
A raw Mode 2/2352 .bin (or .cue) reader, an ISO9660 walker that resolves names the way the console does (case-insensitive, ;1 stripped), and the write side: EDC and P/Q ECC re-encode by the ECMA-130 math, and patch_file_logical to rewrite bytes inside a file. Four things name Legaia:
extract_prot_dat,extract_cdname_txtandextract_scusare shorthands for Legaia's root files; useread_file_in_imageorfind_path_in_image.disc-extract verifyknows only Legaia's image hash; pass--expected.relayout::grow_prot_datgrows one file and renumbers every reference after it. Its safety rests on a fact proved for Legaia only - no executable seeks a later file by absolute LBA (relayout). Check another game's executable first.grow_prot_dat_by_namelooks upPROT.DATby name.
psx-xa
CD-XA ADPCM in 4- and 8-bit, mono and stereo, decoded bit-identically to jPSXdec's reference. The one Legaia-shaped helper, demux_disc_all, visits only files named *.XA; for XA interleaved in .STR files or an archive, feed demux_raw_sectors the raw sectors.
psx-bytes
Bounds-checked little-endian readers returning Option; no dependencies.
Generic crates with Legaia quirks
| Crate | The quirk | What to do on another game |
|---|---|---|
tim | Vram::upload_tim lands a CLUT block as a linear run along row fb_y (palette i at fb_x + 16*i), as Legaia's loader does - not the rectangle a stock LoadImage writes. | Upload CLUTs with Vram::write_block until a capture shows the loader's layout. |
vab | Legaia carries every VAB as two chunks of a stream, with a 4-byte header between the header part and the bodies. | parse on a standalone VAB; concatenate split .VH / .VB files first. |
seq | u32 big-endian version (15-byte header); meta events with no length byte (FF 51 tt tt tt, FF 2F). Both are what the statically linked libsnd reader consumes, so other libsnd titles most likely match - untested. | Check one tempo event by hand; SMF-style length bytes would misparse. |
lzs | Common LZSS parameters (4 KB ring, cursor at 0xFEE, 18-byte match), but the ring starts zero-filled, not with spaces; no size header; the .lzs container is Legaia's. | Compare the game's decompressor; magic-check decoded bytes, since junk decodes without error. |
mdec | The decoder is the rare Iki bitstream. A STRv2 frame shares its 0x3800 word and comes back blank or garbage with no error. The STRv2/v3 path needs a VLC table unpacked from Legaia's STR overlay. | Reuse the sector parser and A/V demux; check frame[6..8] before decoding. |
mednafen | Library generic; a Legaia string search is only the fallback, and main_ram_with_anchors takes another game's. The CLI pulls in Legaia crates, and identify, display-list, clut-trace, prim-dispatch-* read Legaia structures. | Use the library, or extract / diff / bisect / vram-dump / spu. |
pcsx-redux | Main RAM, VRAM and scratchpad are all found by walking the state's protobuf; only the scene / mode / player accessors read Legaia's globals, and Legaia's string search is the fallback for a payload the walk does not recognise. | SaveState::from_path_structural or pcsxr-state --game generic; identity_with takes your own profile. |
ram-image | Game knowledge enters as a GameProfile (anchor strings, identity globals, manifest defaults); the Legaia profile is the default of every older API. prim_dispatch and source_hunt are Legaia's. | Implement GameProfile or use GenericGame; diff, bisect, ram_slice, prim_pool take any 2 MiB image. |
engine-audio | The SPU has no pitch modulation, noise or FM (Legaia never enables them); battle-voice, duck and slot modules are Legaia's runtime; legaia-prot is a plain dependency though only note-trace uses it. | spu::Spu plus Sequencer play any libsnd SEQ + VAB pair. |
render-kernels | gte is a register-level GTE (every command, FLAG bits, cycle costs, the UNR divide) inside a crate that depends on Legaia's engine. Its divide, NCLIP, AVSZ, SQR, OP and MVMVA match the hardware; its RTPS keeps its own q19.12 scale and its colour ops are not hardware-exact - psx-retail-exec's gte is. | Vendor src/gte/ without camera_relative.rs. |
A worked example
crates/pcsx-redux/examples/bring_your_own_game.rs runs the generic crates on inputs built in code, none of them from a game: a 4-bit TIM with its CLUT through parse_strict, decode_rgba8 and write_png; a SEQ through Seq::parse; a two-directory ISO9660 image mastered as Mode 2/2352 sectors and read back by both the in-memory walker and RawDisc (it also lists and extracts under xorriso); and a made-up game's PCSX-Redux and mednafen states, opened structurally, diffed and bisected. A test runs the same code in CI, with no disc.
cargo run -p psx-pcsx-redux --example bring_your_own_game -- <out_dir>
The SEQ step is not evidence about other games
It builds the shape psx-seq parses: meta events without a length byte, which is what the libsnd reader in Legaia's executable consumes. No other title's SEQ is tested here. The step also shows the trap: an SMF-style tempo event (FF 51 03 tt tt tt) does not read as the right tempo.
Legaia-only crates
legaia-tmd reads Legaia's TMD variant, not Sony's. parse requires bit 31 of the id word (Legaia's 0x80000002) and rejects a stock 0x00000041 TMD, and the primitive walker reads Legaia's grouped primitives, not stock packets (TMD). The object table and vertex layout are shared, so it is a starting point for a stock reader, not one.
legaia-container-formats is Legaia's container layer (DATA_FIELD streams, the pack format, sound bundles); its tim_scan is generic but brings the crate's dependencies. prot, mes, anm, the format families and every engine-* crate describe Legaia's own data and runtime.
Emulator automation scripts
PCSX-Redux. scripts/pcsx-redux/lib/probe/ is a Lua library over the emulator's Lua API, and most of it knows nothing about Legaia: RAM reads, breakpoints, pad overrides, save-state loading, the boot / arm / capture / quit driver, instruction tracing and write attribution, watchpoints, CSV output, declarative .probe.toml specs. The Legaia parts are the USA-build fingerprint guard, the battle-state reader, the Ghidra-generated symbol table and the LEGAIA_* variable names. run_probe.sh stages the disc image in a private directory so a .ppf beside it is never applied by accident. See PCSX-Redux automation.
Mednafen. run-mednafen.sh, state-diff.py and watchpoint-bisect.py wrap the generic mednafen-state subcommands; the rest of scripts/mednafen/ reads Legaia structures. See Mednafen automation.
Fingerprint before you arm
A probe with hard-coded addresses, pointed at the wrong build or region, returns a CSV of plausible garbage with no error. Check a few words of always-resident code first, the way the version guard does.
The transferable method
The parts most worth taking to another game are practices, not crates. The retail-oracle method writes them up engine-neutral, in the order to adopt them. Each page below documents one for Legaia; nothing in them depends on Legaia beyond the examples.
disassembly, capture, decompiled-C or inference. The decompiled-C grade is a re-audit worklist: a claim nobody has confirmed against instructions.Related pages: byte accounting, address reference scans and the soak harness.
Traps any PS1 project meets
- A save state replays the RAM of the disc that booted it: a state loaded onto a patched image still runs the old code. Verify disc edits from a cold boot or a memory-card load.
- PCSX-Redux silently applies a
.ppfsitting beside the image (patched-disc taint). - Ghidra does not resolve MIPS
lui/addiupairs as references, so xref queries miss heavily used globals. - "No static caller in the executable" means "look in the overlays", not "dead code" (overlay capture).