At a glance

What it really is
chunk header (0x01 << 24) | payload_len + an container-formats::pack of TIMs
Where on disc
each scene's texture pack at raw-TOC +4 of its CDNAME block; raw TOC entries 0 and 1 (boot system UI, GAME OVER page)
Offset math
byte_offset = word_index * 4 + 4
Parser
legaia_prot::timpack (is_tim_pack, detected_ext); boot bundle legaia_ui_formats::system_ui_bundle
Confidence
Confirmed - retail uploader traced, layout pinned on the real disc
Used by
boot sequence, pause menu glyphs, asset viewer

Layout

Read as a TIM-pack, the first eight bytes are a discriminator and a member count; read as what they are, they are a chunk header whose top byte is the TIM_LIST type (0x01) and whose low 24 bits are the payload length, followed by the pack's own u32 count.

+0x00payload_len (low 24 bits)u8 × 3
+0x030x01type
+0x04tim_numu32
+0x08word_index[tim_num]u32 each
…members back to backTIMs
  • Detection (is_tim_pack): byte[3] == 0x01 and byte[2] < 0x10 (the type byte, and the top of a payload length under 1 MB), tim_num positive, and the offset table inside the blob. byte[2] is not the count.
  • Offsets are word indices relative to the pack, which starts four bytes in - hence byte_offset = word_index * 4 + 4. The pack reader computes the same offsets from the pack's own start.
  • Member typing (detected_ext): first byte 0x10 (TIM magic) → "TIM", else "BIN". The boot bundle below is why the “else” branch exists.

Raw TOC entries 0 and 1: the boot system UI and the GAME OVER page

The two entries at the head of PROT.DAT's TOC are both TIM-packs. They sit in the region the extraction index space skips - the game's own TOC counts entries 2 higher than this project's extraction, so raw entry p + 2 = extraction entry p (see PROT TOC). They are CDNAME's two head names, and they have different loaders. Raw entry 0 (init_data) is the system UI: the cursors, menu glyphs and sprite sheets that the boot image init.pak (PROT 0895) uploads once and never evicts, resident in video memory from the title screen through every scene and mode. Raw entry 1 (gameover_data) belongs to the GAME OVER screen and only that overlay loads it (loaders).

The coordinates below are framebuffer positions in the PSX's 1024×512 video memory (VRAM), where images and palettes both live as pixels. A CLUT is such a palette: a 16- or 256-colour lookup table stored as a row of framebuffer pixels.

Raw entry 0 (sectors 3..55) declares 20 members:

PROT.DAT offsetMemberVRAM placement
0x01858boot cursor-
0x018E0system-UI sprite sheetimage (896,256) 64×192
0x07B00, 0x07F40UI elements-
-four small TIMsCLUTs at (896, 498..=501)
0x11218menu-glyph / interior-page atlas (extractors legaia_ui_formats::interior_page / menu_glyph_atlas)image (960,256) 64×256, declared CLUT (0,510,16,16)
0x19438UI sprite stripimage (960,400) 60×24
0x1A018..0x1AA7Csix bare row-patch members (below)rows 456..462 of the atlas
0x1AC90..0x1AED0four cursor-part TIMsimages at (976,256..)

Raw entry 1's single TIM is the GAME OVER page at image (640,0) with a 256-entry CLUT declared (0,480,256,1), followed by a pack of 29 TMDs. No retail field, battle or menu save state holds its palette.

Because the bundle never leaves VRAM, field environment meshes can reference palette cells on row 510 that no scene TIM ever uploads (town01 env slots 21/26/74, rikuroa slots 50/51/63). In the port, the scene VRAM pre-pass uploads raw entry 0 the same way (SceneResources with BuildOptions::system_ui, through upload_boot_resident_to_vram), byte-exact against retail VRAM, and leaves raw entry 1 out.

Two member shapes that deviate from a plain TIM list
  • Flat-strip CLUT upload. The retail per-TIM uploader writes every member TIM's CLUT block as a flattened (clut_x, clut_y, w*h, 1) strip, NOT the declared w × h rect - which for the row-510/511 banks would overflow VRAM at y >= 512. The bundle's strips populate CLUT rows 510/511 (fb_x 0..320) plus the (896, 498..=501) / (976, 304..=307) side cells; raw entry 1's row-480 strip lands only when the GAME OVER overlay loads it. See npc-palette for the row layout and capture evidence.
  • Row-patch members. Six members carry no TIM magic: an 8-byte preamble followed by a bare TIM-style image block [u32 bnum][u16 x][u16 y][u16 w][u16 h] + halfword data (bnum = 12 + w*h*2). All six declare (960, y, 256, 1) for y ∈ {456, 457, 458, 460, 461, 462} - single rows patched over the atlas image, VRAM-edge-clipped to the visible 64 words. Live captures hold exactly these bytes over the disc atlas at those rows, in every phase.

How we know

Provenance

The retail per-TIM uploader FUN_800198E0 (ghidra/scripts/funcs/800198e0.txt) walks the pack with the word_index * 4 + 4 arithmetic and uploads each member's CLUT as a flat strip. Member offsets, VRAM placements and the six row-patch shapes are pinned on the real disc, and the engine's boot upload matches retail VRAM captures byte for byte.

See also