At a glance

Magic
none proper - a discriminator pair byte[3] == 0x01, byte[2] < 0x10
Where on disc
certain standalone PROT.DAT entries; raw TOC entries 0 and 1 (the boot-UI region before extraction entry 0000)
Offset math
byte_offset = word_index * 4 + 4 - the +4 is what separates it from the other two pack layouts
Parser
crates/prot/src/timpack.rs (is_tim_pack, detected_ext); the boot instance via legaia_asset::system_ui_bundle
Confidence
Confirmed - retail uploader traced (how we know); disc pins + VRAM parity oracle
Used by
boot sequence, pause menu glyphs, asset viewer

Layout

A TIM-pack is a tiny index in front of a run of textures: an 8-byte header saying how many members there are, a table of where each one starts, then the members back to back. The only thing to get right is the header shape and the offset arithmetic, because the disc has three look-alike pack layouts and this one adds a constant +4 the others do not.

discriminator tim_num word_index[tim_num] member 0 member 1 +0x00 +0x04 +0x08 byte_offset = word_index * 4 + 4
Word offsets, not byte offsets - and shifted +4 relative to the pack start.
OffsetSizeFieldMeaning
+0x00u8magic_loarbitrary
+0x01u8magic_hiarbitrary
+0x02u8discdiscriminator byte, < 0x10 - NOT the count
+0x03u8marker== 0x01
+0x04u32tim_nummember count
+0x08u32 × tim_numword_index[]per-member word offsets; byte_offset = word_index * 4 + 4
  • Detection (is_tim_pack) checks the signature pair, that tim_num is positive, and that the offset table fits within the blob.
  • The constant +4 suggests offsets are relative to the end of the count word rather than the start of the pack.
  • Member typing (detected_ext): first byte 0x10 (PSX TIM magic) → "TIM", else "BIN". The boot bundle below is why the "else" branch exists: six of its members are bare image blocks with no TIM header.

The boot-resident system-UI bundle (raw TOC entries 0 and 1)

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). Together they are the system-UI bundle: the cursors, menu glyphs and sprite sheets uploaded once at boot and never evicted, resident in video memory from the title screen through every scene and mode.

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_asset::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 a UI page at image (640,0) with a 256-entry CLUT declared (0,480,256,1).

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). The engine mirrors the upload in its scene VRAM pre-pass (SceneResources::build_targeted_with_options + BuildOptions::system_ui), byte-exact under the VRAM static-mask parity oracle.

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 and raw entry 1's row-480 strip. 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

EvidenceAddress / testWhat it proves
Per-TIM uploaderFUN_800198E0; funcs/800198e0.txtWalks the pack with the word_index * 4 + 4 arithmetic and uploads each member's CLUT as a flat strip.
Disc pinscrates/asset/tests/system_ui_bundle_real.rsMember offsets, VRAM placements and the six row-patch shapes on the real disc.
VRAM paritystatic-mask parity oracleEngine's boot upload is byte-exact against retail VRAM captures.

See also