In-RAM STR FMV file tables Confirmed
When the game plays a movie - the intro, the Mist rolling over Rim Elm, the Genesis Tree reviving - which file does it open, which stretch of it plays, and where does the player land afterwards? A single 23-slot FMV dispatch table inside the cutscene overlay answers all three. Each numbered cutscene maps to a movie file path, a frame range - one .STR file (the PlayStation's streaming-movie format) can carry several cutscenes back-to-back - and a return scene. Nine slots cover every movie on the retail disc; the rest are leftover developer test slots.
At a glance
- In the game
- Every full-motion video: the attract intro and the eight story movies, plus which field scene the game returns to when each ends.
- Magic / marker
- None - static initialised data in the cutscene overlay's data section (an overlay is a chunk of code + data loaded into RAM on demand)
- Lives in
- PROT 0970 (the STR / MDEC overlay) at link base
0x801CE818: path strings at0x801CE810, return-scene labels at0x801CE8AC, the dispatch table at0x801D0A6C(23 × 32 B) - Retail reader
- Selector in the master dispatch + the play loop (how we know)
- In the port
legaia_game_tables::fmv_dispatch- Confidence
- Confirmed - stride and layout pinned from the PROT 0970 disc bytes, byte-identical in the resident RAM capture; every field cross-validated against the play loop's reads; the retail
fmv_id 0..=8range pinned by nine trigger save states.
The dispatch table
The field-VM FMV-trigger op (see the cutscene subsystem) writes an s16 fmv_id to a global; the master dispatch multiplies it by 32 and indexes the table. The play loop opens the slot's file, seeks to start_frame, and streams until the demuxed frame number reaches end_frame - which is how one MVn.STR carries several cutscenes by frame range:
Record layout (32 bytes)
| Field | Meaning |
|---|---|
| path_ptr | Pointer into the path string table at the overlay start |
| color_flag | Non-0 = 24-bit colour (VRAM footprint width × 3/2, MDEC depth bit, DISPENV.isrgb24) |
| start_frame | 1-based; the loop seeks (start - 1) * 10 sectors in (the 15 fps cadence) |
| end_frame | Last frame of the segment - the demux stops here |
| fb_x / fb_y | VRAM decode target (0 / 8 on retail slots); the double-buffer sibling rect sits at fb_y + height |
| width / height | Frame size (320 × 240 retail) |
Because the table is static initialised data in PROT 0970, it decodes straight from the disc. The +0x18/+0x1C pair is the one place the record can be overruled: it sizes the decode rects once, then from the first demuxed frame onward the play loop overwrites the rects' width and height from the STR sector header's own fields. A slot whose dimensions disagree with the movie loses.
Every word of the record is a play-loop input
The record has no field reserved for another subsystem and no undecoded margin: FUN_801CF098 reads all eight words itself, each traceable to the instruction that consumes it.
| Offset | Consumed at | For |
|---|---|---|
+0x00 | 801cf0d0 | CdSearchFile on the path string |
+0x04 | 801cf100, 801cf27c, 801cf2f4, 801cf478 | the * 3/2 VRAM scaling, the MDEC depth bit, DISPENV.isrgb24 |
+0x08 | 801cf1b8, 801cf9e0 | the (start - 1) * 10 seek, and StSetStream's armed seek |
+0x0C | 801cf788 (in FUN_801CF740) | the end-of-stream latch, tested per demuxed frame |
+0x10 | 801cf110, 801cf144 | the decode rect's VRAM x |
+0x14 | 801cf168, 801cf180 | the decode rect's VRAM y |
+0x18 | 801cf28c | the DISPENV width |
+0x1C | 801cf168, 801cf2c8 | the sibling rect at fb_y + height, and the display rect's h = height * 2 |
The sector-header overwrite of width/height is at FUN_801CF740 (cached in DAT_801D0D50 / DAT_801D0D54); the one-time rect sizing at FUN_801CF8B0.
The nine retail movies (and the dev slots)
fmv_id | Movie | Frames | Post-play hand-off |
|---|---|---|---|
| 0 | \MOV\MV1.STR;1 | 1..0x53a | menu / memory-card mode (the intro/attract movie) |
| 1 | \MOV\MV2.STR;1 | 1..0xf4 | scene town0b, spawn word 0x0C |
| 2 | \MOV\MV3.STR;1 | 1..0xe1 | scene map01, spawn word 0x55 |
| 3 | \MOV\MV3.STR;1 | 0xe2..0x1a4 | scene chitei2, spawn word 0x2C1 |
| 4 | \MOV\MV3.STR;1 | 0x1a5..0x27b | scene map02, spawn word 0xF4 |
| 5 | \MOV\MV3.STR;1 | 0x27c..0x36a | back to field mode with no scene-name write - play resumes in the caller scene |
| 6 | \MOV\MV4.STR;1 | 1..0x152 | scene jou, spawn word 0x276 |
| 7 | \MOV\MV5.STR;1 | 1..0x288 | scene uru2, spawn word 0x1BC |
| 8 | \MOV\MV6.STR;1 | 1..0x297 | scene town0e, spawn word 0x2E5 |
| 9 / 10 | MV1A.STR / MOV15.STR | - | dev slots - files not on the retail disc |
| 11..=22 | \DATA\MOV.STR;1 | 1..0x64 | dev multi-window display-test slots (varying fb_x/fb_y/width) |
MV3.STR's four segments abut exactly. The scene + spawn word hand-offs write the next-scene name global (from the label table of seven CDNAME-shape names: town0b, map01, chitei2, map02, jou, uru2, town0e) plus a spawn/door word, then set field mode - so each mid-game FMV returns to a specific scene rather than the trigger scene. Every movie on the retail disc is dispatched. The path strings live in a packed null-terminated table at the overlay start; three of the nine (MOV.STR, MOV15.STR, MV1A.STR) are dev-only.
Which scene fires which movie
The fmv_id each scene fires is inline script data, not a runtime value: the trigger op carries its id as a literal i16 operand, so walking every scene MAN's scripts with the field-VM disassembler recovers the full assignment straight from the disc:
| Scene (extraction entry) | fmv_id | Movie segment | Returns to |
|---|---|---|---|
0004_town01 | 1 | MV2.STR | town0b |
0095_garmel | 2 | MV3.STR 1..0xe1 | map01 |
0606_deroa, 0706_chitei2 | 3 | MV3.STR 0xe2..0x1a4 | chitei2 |
0218_dohaty | 4 | MV3.STR 0x1a5..0x27b | map02 |
taiku (live-confirmed) | 5 | MV3.STR 0x27c..0x36a | caller scene |
0348_town0d | 6 | MV4.STR | jou |
0435_uru | 7 | MV5.STR | uru2 |
0689_jouine | 8 | MV6.STR | town0e |
One trigger op per scene; no other scene MAN carries one. fmv_id 0 (the intro) fires from the title/new-game path rather than a scene script. Slot 5 appears in no scene MAN partition-1 script; a live playthrough capture pins taiku as the scene (it enters the movie mode with id 5 and resumes in taiku itself, exactly per the slot-5 hand-off), while the byte-level carrier of its trigger op sits in taiku's uncompressed scene structures outside partition 1 - the one open detail.
The look-alike table: libcd's directory cache
A second, unrelated table shares the overlay's data section and has been mistaken for the FMV table: libcd's CdSearchFile per-directory file cache (libcd is the CD-ROM library of PsyQ, the PlayStation SDK) at 0x801CAE08 - one 24-byte CdlFILE record per file of the last-searched directory: 4-byte BCD location, 4-byte size, 16-byte name. It starts with the . / .. entries; the contents change with whatever directory was searched last (the XA directory at the title screen, MOV during a movie). It describes the disc filesystem, not the cutscenes.
In the port
legaia_game_tables::fmv_dispatch::FmvTable::from_str_overlay decodes the table from the PROT 0970 entry bytes; entry(fmv_id) gives the movie path and frame range, and engine_path() declines the dev slots whose files are not on the disc. The player builds its decode rects in legaia_mdec::str_player.
How we know
Evidence by claim
| Subject | Evidence |
|---|---|
Selector: fmv_id * 32 into the table | FUN_801CEA3C, disasm 0x801CEC94..A4 (lh the global, sll v0,v0,0x5, addu onto 0x801D0A6C) |
| Dispatch-table stride + field layout | PROT 0970 disc bytes at base 0x801CE818 (asset overlay ...); byte-identical in the overlay_str_fmv.bin RAM capture |
| Play-loop field reads | FUN_801CF098; funcs/str0970_801cf098.txt |
| Return-scene hand-offs (scene label + spawn word + mode) | FUN_801CEA3C; funcs/overlay_cutscene_str_0970_801cea3c.txt |
| Per-scene trigger assignment | Field-VM disassembler walk over every scene MAN |
fmv_id 0..=8 retail range | Nine save states, one per id, each captured just before playback |
| Only two writers of the id global | Raw-byte scan of all 1233 PROT entries, every MIPS addressing form and alignment phase: the trigger-op handler and the title-attract tick; the debug menu writes it through a register-pointer editor, invisible to static scans |
| Table decode from disc | FmvTable::from_str_overlay against the disc entry |
| Directory-cache identification | Title-screen + FMV-overlay RAM captures; PSX-SPX libcd CdlFILE definition |
Full path-table offsets and the trigger-corpus save-state details are in the source of record: docs/formats/str-fmv-table.md.