At a glance

Magic
none - a bare u32 count opens the pack; identity comes from the enclosing chunk's type byte (0x01 TIM_LIST or 0x02 TMD)
Where on disc
inside DATA_FIELD chunks in scene and effect entries of PROT.DAT
Parser
crates/asset/src/pack.rs
Confidence
Confirmed - the type-0x02 arm of the dispatcher FUN_8001F05C walks it and calls FUN_80026B4C per member
Used by
asset loader, asset viewer

Overview

Inside a streaming chunk the bytes say nothing about what they are - the chunk header already said "a list of TIMs" or "a pack of meshes". So the pack itself is minimal: how many members, where each starts, then the members. Offsets count 4-byte words from the start of the pack data, and there is no per-pack magic.

Implementation: crates/asset/src/pack.rs.

Layout

count word_offset[count] member 0 member 1 ... +0x00 +0x04 offset[0] * 4 offset[1] * 4 chunk end
No magic: a count, a table of 4-byte-word offsets, then the members back to back.
OffsetSizeFieldMeaning
+0x00u32countnumber of sub-assets
+0x04u32 × countword_offset[]each in 4-byte words from the start of THIS pack data
word_offset[0] * 4to chunk endsub-assetspacked back-to-back, no padding rule beyond the 4-byte word grid

Sub-asset i lives at byte range [word_offset[i] * 4 .. word_offset[i+1] * 4), with the last sub-asset ending at the chunk's end.

Example

A TIM_LIST chunk header followed by a 2-TIM pack (a TIM is the PlayStation's standard texture format):

chunk header:    6c 02 01 01    type=0x01 (TIM_LIST), size=0x01026C
pack count:      02 00 00 00    count = 2
offset[0]:       03 00 00 00    word offset 3 -> byte 12 (= start of TIM 0)
offset[1]:       8b 20 00 00    word offset 0x208B -> byte 0x822C (= start of TIM 1)
[then 2 PSX TIMs back-to-back]

Word offset 3 is byte 12 - exactly the size of the count word plus the two-entry table, so the first member starts right after the header.

Distinction from the standalone TIM-pack (and the third sibling)

Three pack formats coexist on the disc. They look alike in a hex dump and each has its own header math, so apply the one that matches the source:

asset::pack (this page)Standalone TIM-packField-pack
Livesinside a DATA_FIELD TIM_LIST / TMD chunkas a whole standalone PROT.DAT entryas a whole PROT.DAT entry (scene TIM/TMD bundle)
Identified bythe enclosing chunk's type bytebyte[3] == 0x01, byte[2] < 0x10magic 0x01059B84
Headeru32 count at +04 discriminator bytes, then u32 count at +4magic-prefixed; see its page
Offset table at+4+8see its page
Member offsetword_index * 4word_index * 4 + 4see its page
Parsercrates/asset::packcrates/prot::timpackcrates/asset field-pack detector

The two header shapes side by side:

Offsetasset::packStandalone TIM-pack
+0x00u32 countu8 magic_lo, u8 magic_hi, u8 disc (< 0x10), u8 0x01
+0x04u32 word_offset[0]u32 tim_num
+0x08u32 word_offset[1] ...u32 word_index[0] ...
member i atword_offset[i] * 4word_index[i] * 4 + 4

How we know

FunctionAddressWhat it provesDump
Chunk dispatcherFUN_8001F05C (type-0x02 arm)Walks the count + word-offset table and hands each member to the TMD handlerfuncs/8001f05c.txt
Per-member handlerFUN_80026B4CConsumes one member at word_offset[i] * 4, no +4 constantfuncs/80026b4c.txt

Source of record: docs/formats/pack.md.

See also