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 at 0x801CE810, return-scene labels at 0x801CE8AC, the dispatch table at 0x801D0A6C (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 mirror legaia_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..=8 range 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:

MV3.STR fmv_id 2 fmv_id 3 fmv_id 4 fmv_id 5 1 0xE2 0x1A5 0x27C 0x36A map01 chitei2 map02 caller scene after the segment ends, the dispatch writes the return scene and hands back to the field chain
One movie file, four cutscenes: the frame ranges abut exactly, and each slot names where play resumes.

Record layout (32 bytes)

OffsetSizeFieldMeaning
+0x004path_ptrPointer into the path string table at the overlay start
+0x044color_flagNon-0 = 24-bit colour (VRAM footprint width × 3/2, MDEC depth bit, DISPENV.isrgb24)
+0x084start_frame1-based; the loop seeks (start - 1) * 10 sectors in (the 15 fps cadence)
+0x0C4end_frameLast frame of the segment - the demux stops here
+0x104fb_xVRAM decode-target x (0 on retail slots)
+0x144fb_yVRAM decode-target y (8 on retail); the double-buffer sibling rect sits at fb_y + height
+0x184widthFrame width (320 retail)
+0x1C4heightFrame 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.

OffsetConsumed atFor
+0x00801cf0d0CdSearchFile on the path string
+0x04801cf100, 801cf27c, 801cf2f4, 801cf478the * 3/2 VRAM scaling, the MDEC depth bit, DISPENV.isrgb24
+0x08801cf1b8, 801cf9e0the (start - 1) * 10 seek, and StSetStream's armed seek
+0x0C801cf788 (in FUN_801CF740)the end-of-stream latch, tested per demuxed frame
+0x10801cf110, 801cf144the decode rect's VRAM x
+0x14801cf168, 801cf180the decode rect's VRAM y
+0x18801cf28cthe DISPENV width
+0x1C801cf168, 801cf2c8the 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_idMovieFramesPost-play hand-off
0\MOV\MV1.STR;11..0x53amenu / memory-card mode (the intro/attract movie)
1\MOV\MV2.STR;11..0xf4scene town0b, spawn word 0x0C
2\MOV\MV3.STR;11..0xe1scene map01, spawn word 0x55
3\MOV\MV3.STR;10xe2..0x1a4scene chitei2, spawn word 0x2C1
4\MOV\MV3.STR;10x1a5..0x27bscene map02, spawn word 0xF4
5\MOV\MV3.STR;10x27c..0x36aback to field mode with no scene-name write - play resumes in the caller scene
6\MOV\MV4.STR;11..0x152scene jou, spawn word 0x276
7\MOV\MV5.STR;11..0x288scene uru2, spawn word 0x1BC
8\MOV\MV6.STR;11..0x297scene town0e, spawn word 0x2E5
9 / 10MV1A.STR / MOV15.STR-dev slots - files not on the retail disc
11..=22\DATA\MOV.STR;11..0x64dev 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_idMovie segmentReturns to
0004_town011MV2.STRtown0b
0095_garmel2MV3.STR 1..0xe1map01
0606_deroa, 0706_chitei23MV3.STR 0xe2..0x1a4chitei2
0218_dohaty4MV3.STR 0x1a5..0x27bmap02
taiku (live-confirmed)5MV3.STR 0x27c..0x36acaller scene
0348_town0d6MV4.STRjou
0435_uru7MV5.STRuru2
0689_jouine8MV6.STRtown0e

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

SubjectEvidence
Selector: fmv_id * 32 into the tableFUN_801CEA3C, disasm 0x801CEC94..A4 (lh the global, sll v0,v0,0x5, addu onto 0x801D0A6C)
Dispatch-table stride + field layoutPROT 0970 disc bytes at base 0x801CE818 (asset overlay ...); byte-identical in the overlay_str_fmv.bin RAM capture
Play-loop field readsFUN_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 assignmentField-VM disassembler walk over all 88 scene MANs; disc-gated scene_fmv_triggers_disc
fmv_id 0..=8 retail rangeNine 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 globalRaw-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 discFmvTable::from_str_overlay; disc-gated fmv_dispatch_real
Directory-cache identificationTitle-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.

See also