DECRUNCH/DECRUNCHER.S, hunk offset 0x0a3b0 in /cosm, entered at 0x0a3b8
from the CPIC handler at 0x979e. Roughly 1.6 KB of 68020 assembler that runs
30,707 times over the course of the game.
A CPIC payload begins with sixteen bytes:
offset size field
0x00 2 frame type: 4, 5, 6, 7 or 8
0x02 2 offset of sub-stream A, from the start of the payload
0x04 4 offset of sub-stream B
0x08 4 offset of sub-stream C
0x0c 2 width -- 0x0140 = 320 on every frame measured
0x0e 2 height -- 0x0090 = 144 on every frame measured
0x10 n the token stream
So a frame is 320 x 144 and the decoder is handed four streams: the token
stream that starts at 0x10 and three others whose offsets it pushes onto the
stack before it begins.
The CPIC handler dispatches on the type word:
move.w $14(a6),d0 ; the type word
cmp.w #4,d0 -> handler index 6
cmp.w #5,d0 -> index 5 or 0 depending on globals+$21ec
cmp.w #6,d0 -> index 8 or 1
cmp.w #7,d0 -> index 9 or 2
cmp.w #8,d0 -> index 10 or 3
otherwise -> error
globals+$21ec selects between two sets of handlers — single-buffered and
double-buffered — which is the same flag the FRAMEHANDLERS/ modules test.
Five types, five frame handlers, five copper lists, 32 to 256 colours
(08).
The handler also compares the width against 0x140 and, if it differs, calls a
mode-change routine at 0xaa14. So the container can hold frames narrower than
320; none was found in the 30,707 measured.
Entry, with a6 = the payload, d5 = a direction flag from globals+$21f6:
movem.l d6-d7/a4-a6,-(a7)
move.w $e(a6),d2 ; height
subq.w #1,d2
swap d2 ; d2 high = height-1, the row counter
move.w #$7f,d2 ; d2 low = the token mask
moveq #$28,d1 ; 40
move.w $c(a6),d0 ; width
lsr.w #3,d0 ; width/8 = bytes per bitplane row = 40
sub.w d0,d1 ; 0 for a 320-wide frame
lsr.w #1,d1
adda.w d1,a3 ; centre the picture horizontally
adda.w d1,a4
move.w #$0a,d3 ; 10
lsr.w #2,d0 ; width/32 = longwords per row = 10
sub.w d0,d3 ; 0
swap d3
move.w #$28,d3 ; d3 low = 40, the row stride
move.w $0(a6),d4 ; the type word, used as a token budget
lea $10(a6),a1 ; the token stream
push a6 + $8(a6) ; sub-stream C
push a6 + $4(a6) ; sub-stream B
push a6 + $2(a6) ; sub-stream A
tst.w d5
beq forwards
d6 = a3 + $1658 ; a5 = a4 + $1658 ; neg.w d3 ; backwards
forwards:
d6 = a3 ; a5 = a4
0x1658 is 5,720 = 143 x 40, the offset of the last row of a 144-row, 40-byte
bitplane, and d3 is negated with it. The decoder alternates direction from
frame to frame: one frame is written top-down, the next bottom-up. That is a
raster-chasing trick — with a single buffer, writing away from the beam avoids
tearing — and it is the reason the direction flag lives in the globals rather
than in the stream.
The main loop is a byte-token interpreter:
loop:
moveq #0,d0
subq.w #1,d4
bmi done
move.b (a1)+,d0 ; fetch a token
move.b d0,d1
and.w d2,d1 ; low seven bits
move.l $a444(pc,d1.w*4),d1
jmp $a444(pc,d1.l) ; dispatch
The table at hunk offset 0x0a444 is twelve longwords, and the first
handler begins immediately after it at 0x0a474, so the codec has twelve
opcodes. Handler targets: 0xa438 (token 0, the exit), then 0xa474, 0xa4d4,
0xa5c8, 0xa524, 0xa650, 0xa574, 0xa6d4, 0xa8ac, 0xa750, 0xa99c,
0xa7cc.
Token 1, in full:
00a474 movea.l $8(a7),a3 ; source = one of the pushed sub-streams
00a478 swap d3
00a47a move.w d3,d0 ; 10 - width/32 (0 for 320 wide)
00a47c swap d3
00a47e lea $a490(pc,d0.w),a4 ; entry point into the unrolled copy
00a482 swap d2
00a484 move.w d2,d0 ; height-1
00a486 swap d2
00a488 movea.l a5,a2
00a48a row: movea.l a2,a0
00a48c jmp (a4)
00a490 move.l (a3)+,(a0)+ ; ten of these
00a492 move.l (a3)+,(a0)+
... ; 0xa490 .. 0xa4a2
00a4a4 adda.w d3,a2 ; next row: +40 or -40
00a4a6 dbra d0,row
00a4aa adda.w #$1680,a5 ; next bitplane: +5760
00a4ae addi.l #$1680,d6
00a4b4 move.l a3,$8(a7) ; save the advanced source pointer
00a4b8 moveq #0,d0
00a4ba bra loop
Two things settle the whole question of how this codec reaches the screen.
0x1680 is 5,760, which is exactly 40 x 144 — one bitplane of a 320 x 144
picture. The handler finishes a plane and adds one plane's worth to the
destination pointer.
The copy is ten move.l (a3)+,(a0)+ in a row, entered at a computed offset.
Ten longwords is 40 bytes is 320 bits is one row of one bitplane. The lea $a490(pc,d0.w) with d0 = 10 - width/32 lets a narrower frame skip the first
few moves; for a 320-wide frame d0 is zero and all ten execute.
So the decoder writes planar Amiga bitplanes, one plane at a time, directly into chip RAM that the copper list is already pointing at. There is no chunky buffer at any point in the pipeline: the CD delivers a compressed planar stream, and the decoder expands it into planes. Whatever chunky-to-planar conversion the video needed happened on a workstation in 1993, once, at authoring time.
That is the mechanical answer to the AKIKO question (07), and it is a more interesting answer than a byte count: not "they did not use it" but "there was never a chunky pixel on this machine to convert".
A 256-colour frame is eight planes of 5,760 bytes: 46,080 bytes
uncompressed. The average CPIC payload is 15,254 bytes, so the codec runs at
about 2.9:1 against 8-bitplane raw — a modest ratio, which is what a
per-plane token codec with a twelve-opcode vocabulary and a 2x CD to feed it
would be expected to achieve. It is not trying to be small; it is trying to
decode inside a frame time on a 14 MHz 68EC020 while the CD keeps arriving.
Commodore shipped a streaming animation format for exactly this job, and two
other discs in this pipeline use it — Marvin for a publisher logo, Prey CD32 for
a 619-frame intro. Microcosm does not, and the reason is arithmetic rather than
taste. Reproduced by tools/cdxlcost.py; raw output in
notes/cdxl-cost.txt.
CDXL is uncompressed. A frame costs the same whatever is in it. For the same 30,707 frames at the same 320 x 144:
| B/frame | fps at 2x | total | |
|---|---|---|---|
| CDXL, 8 planes (256 colours) | 46,080 | 6.67 | 1,349 MB |
| CDXL, 7 planes (128 colours) | 40,320 | 7.62 | 1,181 MB |
| CDXL, 6 planes (64 colours) | 34,560 | 8.89 | 1,012 MB |
| CDXL, 5 planes (32 colours) | 28,800 | 10.67 | 843 MB |
| CDXL, 4 planes (16 colours) | 23,040 | 13.33 | 675 MB |
| CDXL, 3 planes (8 colours) | 17,280 | 17.78 | 506 MB |
| CDXL at Prey CD32's geometry, 240 x 96 x 7 | 20,160 | 15.24 | 590 MB |
| Microcosm, measured | 15,699 | 19.57 | 460 MB |
The question never reaches picture quality. The film in CDXL at 256 colours is 1,349 MB — more than two CDs. To make it fit beside the 9.1 MB of game you would have to come down below eight colours, at which point you are not comparing two codecs but two different products. Even Prey's geometry, which is a real CDXL stream that really shipped on this format, comes to 590 MB for this much footage and still does not fit.
The frame-rate column is the other half. Both figures are ceilings computed the
same way — payload divided by the 307,200 bytes/s that CD_CONFIG asks the
drive for — so the ratio holds even though the actual display rate is unresolved
(12). CDXL at full depth tops out at 6.67 fps
against 19.57, and 6.67 fps is a slideshow.
The structural reason is variance, and it is visible in the distribution:
p0 364 bytes p25 10,348 p75 21,280
p1 912 p50 15,404 p90 26,208
p5 2,376 p100 40,224
2,798 frames (9.1 %) cost under 5,000 bytes and 1,283 cost under 2,000 — shots where almost nothing moves, which the codec pays almost nothing for and which CDXL would charge 46,080 bytes each. A fixed-size container cannot spend its budget where the picture is; that is not a tuning difference, it is what the format is.
The honest other side of the same distribution: the worst frame costs 40,224 bytes, 87.3 % of raw, a ratio of 1.15:1. On a genuinely hard frame this codec barely beats storing the bitplanes. The 2.94:1 average is earned entirely in the low tail, which is another way of saying the encoder is exploiting inter-frame redundancy rather than compressing images.
Two things this does not establish. Whether the codec is lossy was not determined — the token handler disassembled above copies literal longwords, and 364-byte frames imply exact inter-frame differencing, but that is an inference. And the quantisation to 256 colours happened at encode time either way, so it is not a difference between the two containers.
CPAL payloads are longword entries beginning with 0x00000100 (256) and
continuing with values of the shape
00 00 05 22 00 00 07 43 00 00 09 33 01 00 00 76
01 10 0b 0e 02 11 05 52 ...
The handler at 0x96bc stores a pointer to the payload into the frame
descriptor and, when a blanking flag is set, zeroes every entry from the second
onward. It does not itself write hardware registers — the palette reaches the
screen through the copper list, which is why no LoadRGB4 or LoadRGB32 call
exists anywhere in the program (08).
The exact packing of a CPAL entry is not asserted here. The structure is
plainly a run/skip plus colour form — the leading bytes take small values and
step in a way a delta list would, and the payload length varies with how much
of the palette changed — but this study did not pin the bit allocation, and a
guess would be worth less than the open question. See
12.