Standalone TIM-pack format Confirmed
A container that bundles several TIMs (the PlayStation's standard texture image) 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 holding the cursor, menu glyphs and sprite sheets you see from the title screen onward.
At a glance
- Magic
- none proper - a discriminator pair
byte[3] == 0x01,byte[2] < 0x10 - Where on disc
- certain standalone
PROT.DATentries; raw TOC entries 0 and 1 (the boot-UI region before extraction entry 0000) - Offset math
byte_offset = word_index * 4 + 4- the+4is what separates it from the other two pack layouts- Parser
crates/prot/src/timpack.rs(is_tim_pack,detected_ext); the boot instance vialegaia_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.
+4 relative to the pack start.| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | u8 | magic_lo | arbitrary |
+0x01 | u8 | magic_hi | arbitrary |
+0x02 | u8 | disc | discriminator byte, < 0x10 - NOT the count |
+0x03 | u8 | marker | == 0x01 |
+0x04 | u32 | tim_num | member count |
+0x08 | u32 × tim_num | word_index[] | per-member word offsets; byte_offset = word_index * 4 + 4 |
- Detection (
is_tim_pack) checks the signature pair, thattim_numis positive, and that the offset table fits within the blob. - The constant
+4suggests offsets are relative to the end of the count word rather than the start of the pack. - Member typing (
detected_ext): first byte0x10(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 offset | Member | VRAM placement |
|---|---|---|
0x01858 | boot cursor | - |
0x018E0 | system-UI sprite sheet | image (896,256) 64×192 |
0x07B00, 0x07F40 | UI elements | - |
| - | four small TIMs | CLUTs at (896, 498..=501) |
0x11218 | menu-glyph / interior-page atlas (extractors legaia_asset::interior_page / menu_glyph_atlas) | image (960,256) 64×256, declared CLUT (0,510,16,16) |
0x19438 | UI sprite strip | image (960,400) 60×24 |
0x1A018..0x1AA7C | six bare row-patch members (below) | rows 456..462 of the atlas |
0x1AC90..0x1AED0 | four cursor-part TIMs | 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).
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 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.
How we know
| Evidence | Address / test | What it proves |
|---|---|---|
| Per-TIM uploader | FUN_800198E0; funcs/800198e0.txt | Walks the pack with the word_index * 4 + 4 arithmetic and uploads each member's CLUT as a flat strip. |
| Disc pins | crates/asset/tests/system_ui_bundle_real.rs | Member offsets, VRAM placements and the six row-patch shapes on the real disc. |
| VRAM parity | static-mask parity oracle | Engine's boot upload is byte-exact against retail VRAM captures. |