Skip to content

Latest commit

 

History

History
254 lines (207 loc) · 10.3 KB

File metadata and controls

254 lines (207 loc) · 10.3 KB

06 — The CPIC codec

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.

The payload header

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 five frame types

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.

The decoder

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.

What a handler does

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".

Sizes

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.

Why not CDXL

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.

The palette

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.