Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Movies and the QkBk codec

Movies

The game plays one movie, Data\Logo025.MOV (the intro logo; decomp/town.cpp), and it's the only one either build names: the Windows 95 build's strings and the Win16 ZOOMBINI._EX's both name only Logo025.MOV. The disc holds four (LOGO025, LOGO025B, LOGO027, LOGO027B, 5.8 to 7.3 MB), all laid out the same way: moov (mvhd, a video trak, a sound trak, udta), then mdat.

  • Video: 640x480, sample description QkBk with vendor Brod. Logo025.MOV has 1,392 frames of 60 units each at a timescale of 600 (10 fps), 83,520 units in all (139.2 s).

  • Sound: twos, 8-bit mono at 11,025 Hz (1,503,811 samples).

  • No other decoder: ffmpeg's QuickTime tag table (libavformat/isom_tags.c) has no QkBk, so ffmpeg-based players can read the sound but not the video. The only decoder is Broderbund's QuickTime codec component: qb32.qtc (PE32 DLL, 45 KB, in ZBARCHIV.Z, installed next to the game), and QB.DEC in the disc's root, the same component for Win16 (NE, 24 KB).

  • What the codec exports: both copies export the same functions. THNGIDENTIFY is QuickTime for Windows' component registration. The rest are QBSetLogFileName, QBSetCallBackProc, QBActivateChannel/QBDeactivateChannel, QBActivateCast/QBDeactivateCast, DrawMHBkGndToOffworld and CopyMHPortToOffworld ("MH" presumably Mohawk). Both contain the strings BckR, FrtR and XFrm, perhaps chunk tags inside frames. QB.DEC says "Copyright 1994-1995 Broderbund Software"; qb32.qtc carries Apple's "Copyright 1988-1995" (from QuickTime's component library, presumably). qb32.qtc's PE linker version is 2.50, not TLINK32's 2.25, and it imports a C runtime's usual startup functions, so it wasn't built with the game's Borland toolchain.

  • What QkBk is: not a pixel codec but a scene compositor, like Macromedia Director's score: a frame says which sprites (bitmaps from a cast) sit where, in 256 colours, and the codec draws them, so a frame that changes nothing is 420 bytes. The layout is documented in formats/qkbk.py (its docstring is the reference; uv run ghidra codec writes the C it was read from to build/ghidra/qb32.c). The evidence, in qb32.qtc:

    • THNGIDENTIFY registers a dcmp component of subtype QkBk, vendor Brod (at 0x1000c038); its entry, 0x10008568, dispatches on the selector in bx to QuickTime's image decompressor calls: 0 GetCodecInfo (0x10001b40), 5 PreDecompress (0x10001b70, which requires 8-bit depth), 6 BandDecompress (0x10001dd0) and 7 Busy (0x10002360, which does nothing). Ghidra doesn't find the handlers that take the selector that way; uv run ghidra codec creates them.
    • BandDecompress swaps the frame's big-endian fields in place (0x10008000 swaps a u16, 0x10008010 a u32; bit 31 of the flags marks a frame already swapped) and walks the structure: header, sprite slots, cast items, then the blocks the flags ask for (BckR, FrtR and XFrm, which it compares as little-endian integers). Cast items are registered by ID (0x10002860); the compositor (0x100035f0, 0x10002f70, 0x10002db0) draws the slots in order. The dispatcher has no compress calls and the DLL nothing that writes QkBk: the codec only decompresses, so the encoder is inferred from the data.
    • A bitmap's rows are drawn by 0x10008030: each is {u16 length, data}, and a control byte with the top bit set repeats the next byte (c & 0x7f) + 1 times (skipping, not drawing, when that byte is 0) while one without copies c + 1 literal bytes. Palettes are copied to a LOGPALETTE byte by byte (0x100033f0): the high byte of each 16-bit colour channel.
    • Names and semantics of the header words are from their uses; some are unknown (f4, f6, f23 in formats/qkbk.py, and three words of each bitmap's header). They're kept as they are. The codec also has a performance log (QBPLAYER.prf: dropped frames, palette transitions), callbacks (blocks tagged PreF and PstF, flag 2) and a hook that draws a Mohawk background under the frame (DrawMHBkGndToOffworld, CopyMHPortToOffworld); none of the game's movies uses the callbacks, and the game doesn't load the codec itself (QuickTime does).
  • The encoder is inferred, and checked: all 3,156 bitmaps of the four movies (178,380 rows) re-encode byte for byte with one rule set: runs of at least 8 pixels of one colour are run tokens, shorter ones literals; 0 (transparent) always runs, never appears in a literal; runs longer than 128 are cut at 128 and the remainder, if under 8, joins the next literal; literals are at most 128; rows pad to an even length with a zero byte. The four movies share one structure (1,392 frames, 35 sprite slots, 34 palettes and 789 bitmaps each, cast item IDs unique and never re-sent) and differ in bitmaps and sound; sprite sizes always equal their bitmaps'.

  • The containers (formats/mov.py): moov then mdat; the tables' chunking isn't regular (a sound chunk and one to several frames, alternating), so the order of the chunks is recorded. mdat starts with 788 (804 in LOGO027) bytes no chunk refers to, which look like run-length data, and LOGO027 and LOGO027B each have a stray byte among the chunks. The sync-sample table (stss, 175 frames) isn't derivable from the frames (it isn't the frames that define cast items). The sound is signed 8-bit mono (twos) at 11,025, 22,050, 11,127 and 22,254 Hz in the four files, with an edit list that delays it 0.8 s and leaves a 2 s gap in it. The timestamps differ between files; everything else in mvhd, tkhd, mdhd, the handlers and sample descriptions is the same standard QuickTime boilerplate.

  • What a frame says, and what's derived: the two rects in a frame's header are the union of its sprites' rects (bounds) and the union of the old and new rects of the slots that differ from the previous frame's (dirty, all slots in the first): exact in all 5,568 frames, so the converter works them out and doesn't store them. The codec uses dirty as the area to redraw (BandDecompress takes it from the frame when the frame follows the last; when it doesn't, it works the same union out itself, 0x100016c0), so an edited frame needs it right. f4 and f6 have no pattern we could find (the frame's number or 0 with 1 or 0, and other values in frames that define cast items); the decoder never reads them.

  • Checked: uv run assets verify rebuilds all four movies from assets/movies/ and they match the disc's files byte for byte. uv run movie-check runs the original qb32.qtc under emulation (Unicorn, qb32.py: PreDecompress, then BandDecompress for each frame over the last, with the few Windows functions it calls answered in Python) on the samples built from assets/, and every frame of all four movies comes out pixel for pixel as formats/qkbk.py draws it (sprites in slot order on the background colour, colour 0 transparent), and the 56 times the codec sets the palette are where the frame's palette ID changes, to the palette's colours 10 to 245 (it asks the system whether its 20 static colours are kept: if so it realizes 236 colours, the palette's 10 to 245; if not, 255, the palette's 1 to 255, at the same device indices, since colour 0 stays black. The port's player and the emulator both answer for whichever the system says; it's static while the intro plays). An edited movie (sprites moved over 21 frames) decodes as predicted too. The codec's output buffer is in 64 KB segments of 102 rows with a 256-byte gap, a leftover from 16-bit code.

The movies' modern form is in assets/movies/, and the port plays it from a flat scene file (formats/scene.py, port/glue/quicktime.cpp).