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
QkBkwith vendorBrod.Logo025.MOVhas 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 noQkBk, 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, inZBARCHIV.Z, installed next to the game), andQB.DECin the disc's root, the same component for Win16 (NE, 24 KB). -
What the codec exports: both copies export the same functions.
THNGIDENTIFYis QuickTime for Windows' component registration. The rest areQBSetLogFileName,QBSetCallBackProc,QBActivateChannel/QBDeactivateChannel,QBActivateCast/QBDeactivateCast,DrawMHBkGndToOffworldandCopyMHPortToOffworld("MH" presumably Mohawk). Both contain the stringsBckR,FrtRandXFrm, perhaps chunk tags inside frames.QB.DECsays "Copyright 1994-1995 Broderbund Software";qb32.qtccarries 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
QkBkis: 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 informats/qkbk.py(its docstring is the reference;uv run ghidra codecwrites the C it was read from tobuild/ghidra/qb32.c). The evidence, inqb32.qtc:THNGIDENTIFYregisters adcmpcomponent of subtypeQkBk, vendorBrod(at0x1000c038); its entry,0x10008568, dispatches on the selector inbxto QuickTime's image decompressor calls: 0GetCodecInfo(0x10001b40), 5PreDecompress(0x10001b70, which requires 8-bit depth), 6BandDecompress(0x10001dd0) and 7Busy(0x10002360, which does nothing). Ghidra doesn't find the handlers that take the selector that way;uv run ghidra codeccreates them.BandDecompressswaps the frame's big-endian fields in place (0x10008000swaps a u16,0x10008010a 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,FrtRandXFrm, 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 writesQkBk: 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) + 1times (skipping, not drawing, when that byte is 0) while one without copiesc + 1literal bytes. Palettes are copied to aLOGPALETTEbyte 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,f23informats/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 taggedPreFandPstF, 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):moovthenmdat; the tables' chunking isn't regular (a sound chunk and one to several frames, alternating), so the order of the chunks is recorded.mdatstarts with 788 (804 inLOGO027) bytes no chunk refers to, which look like run-length data, andLOGO027andLOGO027Beach 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 inmvhd,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 usesdirtyas the area to redraw (BandDecompresstakes 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.f4andf6have 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 verifyrebuilds all four movies fromassets/movies/and they match the disc's files byte for byte.uv run movie-checkruns the originalqb32.qtcunder emulation (Unicorn,qb32.py:PreDecompress, thenBandDecompressfor each frame over the last, with the few Windows functions it calls answered in Python) on the samples built fromassets/, and every frame of all four movies comes out pixel for pixel asformats/qkbk.pydraws 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).