Costume Loading & Decoding
How GrogVM turns a costume id into pixels on screen. This is the engine side;
the binary format it reads is documented in cost.md,
and the animation runtime that sequences frames is in
costume-anim.md. This doc covers what sits between
them: resolving a costume by id, and the lazy decode model.
Resolving a costume by id
A costume is loaded by id, through the DCOS directory — the engine does
not guess by walking the block tree and counting COST blocks. The chain is:
DCOSlookup. The costume id indexes theDCOSdirectory parsed from the index file, yielding the costume's owning room and its byte offset within that room's bundle.- LOFF base. The owning room's absolute position in the resource file comes
from the
LOFFroom-offset table. - Absolute offset. Room base + the
DCOSoffset gives the absolute byte position of the costume'sCOSTblock, which is then located in the parsed block tree.
The lookup fails loudly, with the id in the message, on each thing that can go
wrong: id 0 or out of range, an unused DCOS slot (owning room recorded as
0), an owning room absent from LOFF, or a resolved offset that doesn't land on
a COST block.
Lazy decode
Loading a costume parses only its fixed header and keeps the raw block payload alongside it. Individual frames are not decoded up front — there can be dozens per limb and only the few matching the actor's current animation state are ever needed. The compositor decodes a frame on demand from a frame pointer into the stored payload, each tick, for whichever pose each limb is currently showing.
Scaled drawing
When an actor draws below full size, frames are resampled with
phase-tuned nearest-neighbour sampling: each destination cell reads
(p + φ) · src / dst, with per-axis phases φ chosen empirically
(PHASE_Y = 11/16, PHASE_X = 3/8 in composite.ts). The phase decides
which rows and columns drop, and one-pixel features live or die by it:
centered sampling (φ = 0.5) — the obvious-looking choice — erased Guybrush's
eyes for the entire lookout dialogue of MI1's intro (scale 241, the talking
face is an 11-px overlay limb). The shipped phases came from a 16×16 grid
search with two hard constraints (every draw of that cutscene keeps an eye;
the town-dock resting pose at fixed box scale 210 keeps its eye in both
mirror senses), ranked by misses across every scale MI1's boxes and scale
slots actually use. This is not the original interpreter's row selection —
that pattern is still unrecovered (PROGRESS.md Tier-2), and it shows: at the
dock's scale 210 the original draws a visibly fuller sprite (shirt one column
wider, socks intact) from the same 14×39 budget, because it drops different
columns. At some scales features genuinely vanish in the original too. Its
dropped rows and columns bunch in the sprite's low-content margins rather than
spreading evenly, so no single sampling phase can reproduce them — the selection
is a per-scale pattern, not a decimation rate — and whatever the pattern is, it
holds a walking actor's drawn size steady frame to frame (a count-based scheme
that lets the size wobble by a pixel makes the ego visibly strut). Scale 255 is
an exact identity for any phase: no resampling, pixel for pixel.
Transparent pixels
Costume palette index 0 is the format's transparent slot (see
cost.md §5.2). The decoder emits a fixed sentinel value
of 0xFF for those pixels, which the compositor recognises and skips. Costume
indices only ever range 0..31, so 0xFF is unambiguous in this namespace — the
compositor needs a single value to test rather than tracking a per-costume
transparent colour.