Skip to content

IMAG / GAMI — bitmap images

Backgrounds, sprites, portraits, maps and UI art in all five games are IMAG resources (Amiga, big-endian) or GAMI (DOS and Apple IIGS, little-endian — the same FourCC byte-reversed). They live inside a resource fork.

The pixel encoding never changes; the header does. Every variant is Amiga interleaved bitplanes with a word-aligned row stride. What differs between games is a 28-, 27- or 6-byte header in front of it — and a minority of resources have no compression wrapper at all. Detect the header, don’t assume it.

+0x00 u32 decompressedSize native endianness
+0x04 ... compressedStream to the end of the resource

Two compression schemes are in use, and the decoder auto-detects: it tries PackBits first and falls back to LZSS when the output doesn’t match the declared size.

PackBits — Apple’s run-length scheme, as in MacPaint and TIFF:

Control byte n Meaning
0x00–0x7F Copy the next n + 1 bytes literally
0x80 No-op
0x81–0xFF Repeat the next byte 257 − n times (2–128 reps)

LZSS — byte-aligned, with 8-bit flag bytes (LSB first). A 1 flag bit means “copy the next literal byte”; a 0 means a 2-byte little-endian match token whose top 10 bits are the back-reference offset and whose bottom 6 bits plus 3 are the match length. Offset 0 is a special case that outputs nothing.

In the Excal-family games the split runs roughly 322 LZSS to 9 PackBits.

bytesPerRow = ceil(width / 16) × 2 (word-aligned)
total = bytesPerRow × height × depth

Each scanline stores all depth bitplanes consecutively:

Row 0: [plane0 row0] [plane1 row0] … [planeN row0]
Row 1: [plane0 row1] [plane1 row1] … [planeN row1]

Recovering a pixel’s colour index:

rowOffset = y × (bytesPerRow × depth)
for plane p in 0 … depth-1:
bit = pixelData[rowOffset + p × bytesPerRow + (x >> 3)] >> (7 - (x & 7)) & 1
colorIndex |= bit << p

The palette is not in the resource. Each game supplies its own — see that game’s section.

+0x00 1 format 0x02 standard, 0x05 variant
+0x01 1 reserved always 0x00
+0x02 1 priority z-order, written at runtime by the loader
+0x03 2 reserved
+0x05 1 depth bitplane count (4 = 16 colours, 5 = 32)
+0x06 14 reserved zeroed runtime fields for the display engine
+0x14 2 width u16 BE
+0x16 2 height u16 BE
+0x18 4 reserved

Size check: decompressedSize == 28 + ceil(width/16) × 2 × height × depth. Holds for all 113 standard-format resources.

One fewer reserved byte at +0x06 (13 rather than 14) shifts width and height back to the odd-aligned offsets +0x13 and +0x15, stored little-endian, with the outer container’s size prefix also LE. The IIGS port is always depth 4 — never 5.

Spirit / Vengeance / Conan / Legend — 6 bytes

Section titled “Spirit / Vengeance / Conan / Legend — 6 bytes”
+0x00 1 marker always 0x00
+0x01 1 height u8, or 0 when it must be derived
+0x02 2 width u16 BE
+0x04 1 padding always 0x00
+0x05 1 depth 4 or 5

Pixel data starts at offset 6. When height is 0 the decoder derives it: height = (payloadSize − 6) / (ceil(width/16) × 2 × depth).

Detection: check byte0 == 0x00 and byte4 == 0x00, then validate width <= 1024 and depth in 1–8. On a match, use the 6-byte layout; otherwise fall through to the 27/28-byte ones.

A minority of 6-byte-header resources have no compression wrapper at all — the header sits at byte 0 of the resource. The check is an exact match of 6 + ceil(width/16) × 2 × height × depth against the resource’s total length, confirmed byte-exact against 12 real Warriors of Legend GAMI resources.

These previously failed for a reason worth knowing: the resource-fork parser’s LE fix-up unconditionally byte-swapped the first 4 bytes of every GAMI/LMRF resource, assuming they were always a size prefix. In the uncompressed variant those 4 bytes are the header, and swapping them corrupted it before the image decoder ever saw it. Fixed by checking the exact-length invariant against the pre-swap bytes.

Game / platform Total Decoded Notes
WIME Amiga 115 115 2 needed the uncompressed-payload path and still show unexplained pixel noise
WIME Apple IIGS 84 82 The 2 failures are uninitialised placeholder data in the source files, not decoder bugs
Spirit of Excalibur 333 330 3 genuinely corrupt — implausible decompressed sizes, ~458 K–589 K
Vengeance of Excalibur 496 496 Was 476/496; 18 fixed by the uncompressed-header path, the last 2 by the too-small-guard fix below
Conan the Cimmerian 336 — Not yet run through the 6-byte path
Warriors of Legend 1,070 1,070 12 recovered by the uncompressed-header path, 1 by the too-small-guard fix

The too-small guard was measuring the wrong thing

Section titled “The too-small guard was measuring the wrong thing”

decodeIMAG() used to reject any resource shorter than 27 bytes — the IIGS header size, a threshold with nothing to do with a compressed resource’s on-disk length. A well-compressed tiny image can be under 27 bytes raw and still decompress to a valid payload. Confirmed on Vengeance’s TITLE32A.RES #110 and #112 (26 bytes raw) and Legend’s res32/object.res #7049 (22 bytes): all three decompress to their declared size exactly. The guard now requires only the 4 bytes needed to read the size prefix, and the real too-small check happens downstream on the decompressed payload.

The Excal engine doesn’t read the 6-byte header directly. _OpenImag loads the resource and calls _AddHeaderBytes(8) to prepend 8 bytes; all rendering then works against a 14-byte in-memory bitmap header whose bytesPerRow is computed as ((width − 1) >> 4 + 1) << 1 and whose height, width and depth are copied straight through from the on-disk header. The engine never recomputes them.

Resources holding multiple sub-images carry a frame table immediately after that 14-byte header: a 4-byte { u16 frameCount, u16 flags } head, then one 6-byte entry per sub-image giving a packed pixel offset, a flags/plane-count byte, a sub-image index, and the sub-image’s own width and height. _DrawPrim reads per-frame dimensions from these entries, not from the 6-byte IMAG header.

docs/formats/imag.md in the main repository (not part of this site), plus the per-game palette and catalogue notes it links: WIME’s amiga/imag-catalog.md and amiga/palettes.md, the DOS/IIGS specifics in wime/dosvga/gami-format.md, and each Excal game’s own engine.md. Undecoded header variants are tracked separately in docs/reference/eliminated/imag-frml-open-variants.md.