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)
- Parser
legaia_asset::fmv_dispatch::FmvTable::from_str_overlay; engine mirrorlegaia_engine_core::cutscene::fmv_index_to_str_filename- 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)
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | path_ptr | Pointer into the path string table at the overlay start |
+0x04 | 4 | color_flag | Non-0 = 24-bit colour (VRAM footprint width × 3/2, MDEC depth bit, DISPENV.isrgb24) |
+0x08 | 4 | start_frame | 1-based; the loop seeks (start - 1) * 10 sectors in (the 15 fps cadence) |
+0x0C | 4 | end_frame | Last frame of the segment - the demux stops here |
+0x10 | 4 | fb_x | VRAM decode-target x (0 on retail slots) |
+0x14 | 4 | fb_y | VRAM decode-target y (8 on retail); the double-buffer sibling rect sits at fb_y + height |
+0x18 | 4 | width | Frame width (320 retail) |
+0x1C | 4 | height | Frame height (240 retail) |
Because the table is static initialised data in PROT 0970, it decodes straight from the disc; the windowed player uses the frame range to seek to the right segment (cutscene_av::fmv_segment_window). 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 |
FmvEntry keeps six of the eight; fb_x / fb_y land in legaia_mdec::str_player::FmvSlot, which builds the two decode rects from them. 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.
History: the 64-byte-stride reading
An earlier reading used a 64-byte stride (a sll v0,v0,6 transcription error), pairing wrong slot halves - it concluded MV2/MV5 were never referenced and that several triggers pointed at cut movies. The disc bytes and the resident RAM capture both encode sll v0,v0,0x5; under the 32-byte stride every movie is dispatched. Superseded; see do not re-walk.
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.
History: the phase-shifted "compact MV-file table at 0x801CAE40"
Earlier captures read this cache as a name-first 24-byte table starting at 0x801CAE40. That parse was phase-shifted 8 bytes, pairing each name with the next record's location - manufacturing an apparent one-entry shift ("MV1 points at disc MV2"). At the CdlFILE phase every record is self-consistent. The legaia_asset::str_fmv_table parser still reads the historical name-first window; treat it as a capture-forensics helper, not a format decoder. Convert CdlLOC to LBA with LBA = ((M×60)+S)×75 + F − 150; a third copy of the six MV files appears nearby in raw ISO9660 directory-record form (0x801CCA80, 56-byte stride).
Rust API
use legaia_asset::fmv_dispatch::FmvTable;
// Decode the dispatch table straight from the PROT 0970 entry bytes.
let overlay = std::fs::read("extracted/PROT/0970_xxx_dat.BIN")?;
let table = FmvTable::from_str_overlay(&overlay).expect("dispatch table");
// fmv_id (the value the field VM writes) -> movie + range.
let e = table.entry(1).unwrap();
assert_eq!(e.engine_path(), "MOV/MV2.STR");
assert_eq!((e.start_frame, e.end_frame), (1, 0xf4));
// Dev slots decode too but engine_path() declines them.
assert_eq!(table.engine_path(10), None); // MOV15.STR, not on the retail disc
How we know
| 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; labels match legaia_engine_core::scene::FMV_TRIGGER_FIELD_SCENES |
| Per-scene trigger assignment | Field-VM disassembler walk over all 88 scene MANs; disc-gated scene_fmv_triggers_disc |
fmv_id 0..=8 retail range | Nine save states, one per id, each captured just before playback (capture_observations::cutscene_trigger_corpus; disc-gated cutscene_trigger_corpus_pins_fmv_id_across_nine_saves) |
| 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; disc-gated fmv_dispatch_real |
| 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.