Files
project-solana/games/rac1/docs/LEVELS.md
T
2026-10-13 20:04:31 -04:00

154 lines
9.2 KiB
Markdown

# Level programs
Each RAC1 level carries its own build of the game program. When a level
loads, ParseBin (`func_0012DA38`) copies it over the executable's `main`
segment (`.lit`, `.bss`, `.data`, the three level vtables and `.text`,
0x0015F000 up) and returns its entry point; main calls it (OpenRAC
docs/OVERLAYS.md, docs/ASSETS.md "Code overlays"). The wrapper compiles
level programs natively, like the executable: C from OpenRAC's
`src/overlays` where it exists, everything else translated ahead of time
from the level's own bytes by `tools/recomp.py`. Nothing is interpreted at
run time. `build.py --levels` picks the levels (default `00`, Veldin 1,
where the main menu is).
## Where the program comes from
- **At run time** the game itself reads the level's data section from the
disc into the buffer `D_0015EF4C` points at (exe code, before ParseBin).
The host's ParseBin (`host/src/rac1/levels.c`) walks the records exactly as
`src/core/boot.c` does and copies each to its address, so `lit`, `bss`,
`data` and the vtables land where the PS2 puts them. It then recognises
the program (entry point and text record against `gen/level_table.c`),
fills the executable's old text and the level's text with int3, installs
the level's trampolines and makes the level's table the one `rc_dispatch`
uses. A program that is not built in stops the run with a log line.
- **At build time** recomp.py needs the level's code bytes.
`tools/level_dump.py` extracts the seven records and a `manifest.json`
(OpenRAC's dump layout) to `build/levels/level_NN/` from the disc image
(`--iso`, `RAC1_ISO`, or `iso=` in `build/<platform>/rac1_wrapper.cfg`):
table of contents at LBA 1500, the level's header, its data range, whose
first word is the overlay's offset. An OpenRAC dump in the decomp repo
(`baserom/overlays/level_NN/`) is used instead when present. These are
game data; they stay in `build/`.
## Which code is which
`config/overlays/functions.tsv` lists every function of every level with
its places (`NN:ADDR`). For level NN the build uses those with a place in
NN, at that place:
| Function | Built from |
|---|---|
| shared / level (`func_LNN_X`) with C in `src/overlays/shared` or `src/overlays/lNN_*` | that C (gen_host.py `--level`) |
| the same, `INCLUDE_ASM`, or C with inline asm | the level's bytes (recomp.py `--level`) |
| shared, canonical level another one (`func_L00_X` in level 01) | the level's bytes (its C names level 00's data) |
| exe kind (`func_X`, a copy of an executable game function) | the level's bytes: the copy's relocations are the level's own (data at the level's addresses, calls to the level's functions), which the executable's C cannot know |
| exe kind with a host version (`host/overrides/func_X.c`: memcpy-likes, WAD unpack, movie player, sound init) | that host C, compiled again for the level |
| exe kind the host provides in `host/src` (`func_001F9988`) | the same host function |
| a table entry inside a function (InitCamera_7 at 0x2EDB68 in level 00), a `jal` target no catalogued function starts at | the level's bytes, from that address (`func_lNN_entry_ADDR`) |
| a host function the catalogue does not place (level 00's busy wait at 0x1FEFC8, catalogued as nops plus `func_L00_001FEFE0`) | found by the executable function's bytes; the host function |
| a function at several places | each place translated on its own (`func_X__lNNatADDR`); C is shared |
The level's C is only right for the level it was written for: a shared
function's C names its data and callees by its canonical level
(`D_L00_...`). Level 00 is the canonical level of everything it has, so all
its C applies. In another level, functions whose canonical level is not
that level come from its bytes (until C with relocations mapped from the
bytes, see Next).
Assembly-only functions have no definition to take a signature from, so
the generator makes one of their declarations the reference (the longest
plain `extern` prototype), puts every other prototype in its float/int
order and gives recomp.py's C entry the same signature: the EE passes
floats and integers in separate registers, so `f(float, void *)` in one
file and `f(void *, float)` in another are the same call there, not on
x86.
## Names: one header per level
The executable and every level link into one program, and their names
overlap (the same `func_X` is the executable's function and a different
copy in each level; a shared `func_L00_X` exists in 19 levels). So the
level pass gives every name it defines or places the suffix `_lNN`:
`gen/lNN/lNN_names.h` (`#define func_L00_X func_L00_X_l00`, the same for
each main-segment `func_X`, each level data name `D_L00_X`, each alias) is
forced into every C file of the level (`-include`), and asm labels
(`__asm__("D_L00_X")`, `SDATA(...)`) are rewritten to match. Names the
executable pass provides with the same value (the core segment, core data
below 0x15F000) are left alone and resolve to the executable's. The
generator's own passes work on the original names; only the compiler sees
the suffixes. Aliases from the generator's passes (`__h1`, `__imp`) carry
the level too (`__l00h1`).
Level outputs in `build/<platform>/gen/lNN/`:
| File | What |
|---|---|
| `overlays_*.c` | the level's C (other levels' functions left out) |
| `asm_stubs.c`, `missing.c` | stubs: the first are replaced by recomp.py; the second log calls to names the level's C uses that this level's program does not have |
| `rc_funcs_*.c`, `rc_wrappers.c`, `rc_thunks.c`, `rc_table.c` | translated functions (`rcNN_` prefix), their C entries, thunks into C, the dispatch table `rcNN_table` (with the core's translated functions and thunks) |
| `tramp_table.c` | `uya_tramp_table_lNN`: every function of the level at every place |
| `abs_syms.s` | the level's data and other by-address names (with the suffix) |
| `sigs.json`, `asm_entries.json`, `overrides.txt`, `funcs.tsv` | for recomp.py and build.py |
## Fixes made along the way (both passes)
- **Function values**: a function named as a value (callback argument,
pointer stored in a moby, `m->f74 = func_L00_0028F458`) is its PS2
address on the PS2. In level C it now is too (`((__typeof__(f) *)0xADDR)`),
so translated code and tables get an address `rc_dispatch` knows, and C
calls through it still work (trampoline). Level pass only.
- **Labeled prototypes** (`extern void f_x(...) __asm__("f")`) now get the
float/int order fix as well (only the order: they are deliberate views).
- **Thunks** pass the extra `uya_aN` arguments of extended definitions.
- **Quadword accesses** (`lq`/`sq`/`lqc2`/`sqc2`) of translated code keep the
EE's masking of the low 4 bits, except on the x86 stack, where C locals
are not 16-byte aligned (libvu0's matrix functions on C locals read the
wrong data before). A side effect: the executable's attract loop (a run
without `UYA_LEVEL`) now gets to its moby drawing (`func_001EE9F8`, a
hand-written loop over scratchpad data that toSPR DMA brings in) and
stays there after about 16 frames (not investigated yet); with
`UYA_LEVEL=0@1` the level is asked for before.
- **recomp.py**: a function whose last instruction has a delay slot reads it
from the next word; in level mode a function that runs off its end
continues into the next one; libgcc (0x11DF10-0x1207B8) is translated
(level code calls helpers the executable's C does not).
## Testing
`UYA_LEVEL="0@1"` asks for level 0 at vsync 1, as the title menu's "go to
level" (`func_0022F4A0`) does, so a run reaches ParseBin without the menu.
`UYA_VSYNCS=N` ends a run at vsync N with the list of missing functions.
```
RAC1_ISO=disc.iso UYA_SKIPMOVIES=20 UYA_LEVEL=0@1 UYA_VSYNCS=2500 ./rac1_wrapper SCES_509.16
```
State on 2026-10-10: ParseBin loads level 00 at vsync 769 (after the
level's three opening movies), the entry point (`func_L00_002465F8`,
translated) runs the level start-up (loaders, cameras, mobys: C and
translated code mixed) and enters the level's frame loop; up to vsync 2500
no level function traps. The first frame's moby updates then wait: the update
of moby class 834 (`func_L00_002DB278`) starts the scene's dialog stream
(`func_L00_00299B68`) and calls `sound_update` and vsync until the stream
state (the halfword at 0x0015182A) reaches 3. Since 2026-10-11 the host's 989snd
(host/src/rac1/snd989.c) plays the stream and answers "started" then "buffered", so the
state goes 1, 2, 3, the scene continues it (4) and the dialog plays to its end. Nothing is drawn (as in the executable: the
display set-up and VU1 are not there yet).
## Next
- **Exe-kind functions from C.** About 940 of level 00's functions are
copies of executable functions that have C in `src/game`; today they are
translated from bytes. Comparing each copy with the executable's bytes
instruction by instruction gives its relocation map (each `%hi/%lo` and
`$gp` reference, each `jal`): the executable's C can then be compiled for
the level with those names mapped (a per-function rename, as the header
does per level), and checked: a copy whose map is not consistent stays
translated. The same map makes shared C usable in levels other than its
canonical one.
- **Other levels**: `--levels 00,01,...` builds them (shared functions of
another canonical level from bytes); only level 00 has been run.
- The level's assembly-only functions shrink as OpenRAC matches them; the
build picks the C up by itself.