Standalone TIM-pack format Confirmed
A container bundling several TIMs (the PlayStation's standard texture format) into one entry of PROT.DAT, the disc's main asset archive. "Standalone" distinguishes it from the near-identical pack format found inside streaming chunks - same idea, different header math. Its most notable instance is the pair of hidden entries at the very head of the archive that hold the game's boot-resident UI graphics.
Overview
Implementation: crates/prot/src/timpack.rs.
Header (8 bytes)
u8 magic_lo // arbitrary
u8 magic_hi // arbitrary
u8 disc // < 0x10 (discriminator byte, NOT the count)
u8 marker // == 0x01
u32 tim_num // entry count at +4; offset table follows at +8
The byte[3] == 0x01 / byte[2] < 0x10 pair is the magic discriminator; the entry count is the u32 tim_num at +4 (so the offset table begins at byte +8). The detection function is_tim_pack checks the signature pair, that tim_num is positive, and that the offset table fits within the blob.
Offset table
Each table entry is a u32 word index (counting 4-byte words, not bytes), decoded as:
byte_offset = word_index * 4 + 4
The +4 is the difference from the pack format: this format adds a constant offset, suggesting the offsets are relative to the END of the count word rather than the start of the pack.
Item type detection
detected_ext(item) returns "TIM" if the first byte is 0x10 (PSX TIM magic), else "BIN".
Boot-resident system-UI instance (raw TOC entries 0 and 1)
The two entries in PROT.DAT's TOC head - raw TOC entries 0 and 1, the region the extraction index space skips (the game's own TOC counts entries 2 higher than this project's extraction: extraction entry p = raw entry p + 2; see prot) - are both TIM-packs. Together they are the boot-resident system-UI bundle: the cursors, menu glyphs and sprite sheets that stay in video memory from the title screen onward. Raw entry 0 (sectors 3..55) declares 20 members, raw entry 1 one. Parser: legaia_asset::system_ui_bundle; disc pins in crates/asset/tests/system_ui_bundle_real.rs.
The coordinates below are framebuffer positions - locations in the PSX's 1024×512 video memory (VRAM), where both images and their palettes 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's members, in table order: the boot cursor (PROT.DAT[0x01858]), the system-UI sprite sheet (0x018E0, image (896,256) 64×192), UI elements (0x07B00, 0x07F40), four small (896, 498..=501)-CLUT TIMs, the menu-glyph / interior-page atlas (0x11218, image (960,256) 64×256, declared CLUT (0,510,16,16);
extractors legaia_asset::interior_page / menu_glyph_atlas),
the UI sprite strip (0x19438, image (960,400) 60×24), six bare row-patch members (0x1A018..0x1AA7C; below), and four cursor-part TIMs (0x1AC90..0x1AED0, images 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).
Two member shapes deviate from a plain TIM list:
- Flat-strip CLUT upload. The retail per-TIM uploader
FUN_800198E0uploads every member TIM's CLUT block as a flattened(clut_x, clut_y, w*h, 1)strip, NOT the declaredw × hrect (which for the row-510/511 banks would overflow VRAM aty >= 512). The bundle's strips populate CLUT rows 510/511 (fb_x0..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)fory ∈ {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.
The whole bundle is uploaded once at boot and never evicted - resident from the title screen through every scene/mode. That is why field env meshes can reference CBA cells (CLUT-base addresses - the primitive field that says which palette row to use) on row 510 (town01 env slots 21/26/74, rikuroa slots 50/51/63: CBA (64,510), texpage (960,256)) that no scene TIM ever uploads; 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.