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

Palette and Artwork Formats

PAL: VGA DAC entries

Every one of the 37 PAL members is exactly 768 bytes: 256 consecutive red/green/blue triplets. Each component is in the inclusive range 0 through 63, matching the VGA’s six-bit DAC. There is no header. The host renderer expands a component with bit replication, eight_bit = (six_bit << 2) | (six_bit >> 4), so both endpoints remain exact.

The executable confirms this interpretation in two independent paths:

  • vga_load_palette_bios at 0xA017 invokes video BIOS function 1012h with 256 entries, start index 0, and a far pointer to the triplets.
  • vga_write_palette_range at 0xA032 waits for vertical retrace through port 03DAh, writes a starting index to 03C8h, then sends three unmodified bytes per entry to 03C9h.

There are 35 unique palette payloads. The two copies of GANTRY.PAL are identical, and RICH.PAL is identical to 1.PAL. The game also changes palette ranges at runtime for fades and color cycling, so a base PAL preview does not necessarily reproduce every animated color at a particular instant.

ART: descriptors and indexed pixels

An ART member begins with one or more fixed 12-byte frame descriptors. It has no explicit descriptor count; the first descriptor’s pixel offset is also the end of the descriptor table, so the count is first_offset / 12.

Descriptor offsetSizeTypeMeaning
0x002signed little-endianHorizontal origin or anchor offset.
0x022signed little-endianVertical origin or anchor offset.
0x042unsigned little-endianWidth in pixels.
0x062unsigned little-endianHeight in pixels.
0x084unsigned little-endianAbsolute pixel-data offset in this member.

Each descriptor is followed indirectly by exactly width * height bytes of row-major, eight-bit palette indices. Pixel blocks are contiguous in descriptor order and occupy the rest of the resource without padding.

This layout validates without exception across all 143 ART members:

PropertyValue
Total frame descriptors1,178
Total indexed pixels4,850,699
Largest frame table63 frames in MAP.ART
Full 320×200 frames at origin (0, 0)11

Signed origins matter. For example, RUN.ART frame 0 has origin (-28, -5) and size 46×61; these values position a running sprite relative to an entity, not directly on the screen. LOGO.ART demonstrates a multi-piece screen:

FrameXYWidthHeightPixel offset
08854124740x48
194801121070x2420
214791381650x52F0
320715101510xABE2
4-3-3790xC001
522138275300xC040

The object renderer treats the 8.8 scale word as a divisor. It computes frame dimensions and origins as value * 256 / scale, so 0x0200 halves artwork. For a horizontal reflection it computes anchor_x - scaled_origin_x - scaled_width; it does not reuse the normal anchor_x + scaled_origin_x position. This is directly visible in LOGO.BIN, which reflects frame 3 at X=303 to form the left half of the dome.

The descriptor itself does not specify transparency. The draw-call flags choose the copy operation. blit_rect_transparent_zero at 0xA106 tests each source byte and advances the destination without writing when the index is 0. blit_rect_opaque at 0xA136 copies every byte. The fast VGA copy at 0xA0C9 copies rows into segment A000h using a 320-byte screen stride.

Layering uses the stable render slot allocated by the scene’s mixed display list, rather than controller update order. The compositor at 0xC000 walks those slots in increasing order. This lets the direct LOGO.BIN dome pieces at display indices 7 through 9 occlude the moving actor at index 4, producing the oval entry and exit mask without a dedicated ellipse-clipping routine.

STUFF.ART: font and interface labels

The game does not obtain its dialogue font from the video BIOS. Startup calls the routine at 0xAE42, which loads STUFF.ART, retains frame 0’s pixel pointer and width, and constructs a 95-entry glyph-offset table at DS:A1E8. Frame 0 is a 257-by-14 atlas containing only source values 0, 1, and 2. The first seven rows hold bytes 21h through 60h; the offset resets to atlas row 7 for byte 61h (a) and continues through byte 7Fh.

The proportional widths live in the executable at DS:3462, indexed by character minus 21h. Renderer 0xAF36 copies exactly seven scanlines and advances by the glyph width plus one. Space and control bytes advance three pixels without copying. The modal compositor later doubles each logical pixel in both axes, yielding seven-row glyphs at 640-by-400 output with a 16-output-pixel line pitch. This explains why a fixed 5-by-7 host font drawn at one horizontal pixel per source column appeared narrow and tall.

The text-object renderer at 0xBE7E remaps the atlas values through a three-palette-index style. The ten recovered triplets are:

1:  1,  7,  3    2:  1, 37,  4
3: 16, 15, 17    4: 15, 84, 84
5:  0, 64, 70    6:  0, 32, 37
7: 15, 86, 90    8: 15, 74, 69
9: 15, 32, 36   10: 15,  1,  8

The live BOSS capture confirms style 2 for normal dialogue: its glyph pixels are framebuffer indexes 1, 37, and 4. State 1 uses style 1 for unselected choice rows and style 2 for the selected row. Character and Captain Bible messages enter state 2 and use style 2; adversary messages enter state 7 and use style 7. The Computer Bible prompt above the book also uses style 7 for an L cyber lie, style 9 for a P paraphrase, and style 10 for * victim-conversation text.

Several interface captions are complete indexed sprites in the same resource, including frame 16 GET VERSE, frame 28 SELECT, frame 29 CONTINUE, frame 30 UNLOAD, and frame 31 COMPUTER OFF. Their descriptors include the signed origin used to overlap the associated panel or row. In particular, SELECT is (-12,-3,24,7) and CONTINUE is (-17,-3,35,7). These sprites include their own letters and borders and are drawn with index zero transparent. Transparent-pixel matching against the preserved VGA dumps finds one unique placement for each: CONTINUE at logical top-left (220,81) with anchor (237,84), and SELECT at (154,69) with anchor (166,72).

Frames 7 through 10 are the pointer-hover navigation arrows Up, Down, Left, and Right. Their descriptors are respectively (-5,-1,11,10), (-5,-9,11,10), (0,-5,10,11), and (-9,-5,10,11). The selector-overlay routine draws one of these transparent frames at the active target’s authored logical coordinate.

The top status row is another fixed subset of STUFF.ART. Frame 4 is the Computer Bible cross at descriptor origin (4,1), frame 32 is the Map control at (23,1), frames 22 through 26 are descending Faith-meter states at (44,3), and frames 17 through 21 are Sword, Shield, No Trap, Candle, and Flight. Frame 11 is the disk indicator at (297,1). The positive origins are already logical screen positions, so the UI draws these frames at anchor zero and uses their nontransparent bounds as pointer targets.

Zero-based frame 27 is a dormant transient-status overlay. Unused opcode 8B starts its timer; the status renderer anchors the frame at logical (10,10) and derives its X/Y flip flags from the shared random source. See Runtime random source for the controller cadence.

QEMU framebuffer validation

INTRO.ART consists of one descriptor (0, 0, 320, 200, 12). Compared its 64,000 pixel bytes directly with physical VGA memory at 0xA0000 in the preserved startup dump. Of those bytes, 63,648 are identical. Every one of the 352 differences falls in one of two known live overlays visible in the QEMU screenshot:

OverlayDiffering pixelsDifference bounding box
Animated floppy/save icon333X 297–316, Y 1–17
Mouse cursor at screen center19X 154–166, Y 94–106

After excluding those rectangles, the extracted resource and live VGA memory are byte-for-byte identical. This establishes the descriptor dimensions, row-major order, one-byte pixels, and mode-13h screen placement independently of visual interpretation.

The runtime screenshot also maps each used pixel index to a consistent RGB value. Most static entries match TITLE.PAL; entries 243 through 254 differ because the opening text cycles that palette range while the story is shown.

Rendering tool

tools/render_art.py validates the complete descriptor and pixel layout and requires a separately extracted PAL for rendering. List descriptors with:

tools/render_art.py build/dd1/all/003_LOGO.ART --list

Composite all frames at their signed origins on a 320×200 canvas:

tools/render_art.py \
  build/dd1/all/003_LOGO.ART \
  --palette build/dd1/all/002_LOGO.PAL \
  --canvas --scale 2 \
  --output build/graphics/logo.png

Render one animation frame or all frames separately:

tools/render_art.py \
  build/dd1/all/082_RUN.ART \
  --palette build/dd1/all/025_TITLE.PAL \
  --frame 0 --scale 2 \
  --output build/graphics/run-0.png

tools/render_art.py \
  build/dd1/all/082_RUN.ART \
  --palette build/dd1/all/025_TITLE.PAL \
  --all-frames build/graphics/run-frames \
  --scale 2

Individual frames use index 0 as PNG transparency by default, matching the color-keyed game path. --opaque-zero preserves it as a visible palette color and selects opaque composition. --width and --height change canvas size; --scale applies nearest-neighbor integer scaling.

Exactly 11 frames have origin (0, 0) and dimensions 320×200. The scene programs select the following palettes before loading them:

Archive indexART frameScene-selected palette
006INTRO.ART frame 0TITLE.PAL
063PRAY.ART frame 0PRAY.PAL
073OVER.ART frame 01.PAL
090LAW1.ART frame 0LAW.PAL
093KABLAM1.ART frame 0KABLAM.PAL
097SPEAKER.ART frame 01.PAL
100HOLE.ART frame 0HOLE.PAL
122DOME.ART frame 0DOME.PAL
130DENY1.ART frame 0DENY.PAL
133CULTA.ART frame 01.PAL
165BOSS.ART frame 0BOSS.PAL

tools/render_fullscreen_gallery.py discovers those frames directly from the archive, infers each ART-to-PAL association from the resource-loading commands in the BIN scene programs, and labels every image with its archive index, resource name, frame number, and palette. Generate the native-pixel contact sheet with:

tools/render_fullscreen_gallery.py \
  CB/DD1.DAT \
  --output build/graphics/full-screen-gallery.png

Pass --scale 2 for a nearest-neighbor enlarged sheet. KABLAM1.ART appears almost black under KABLAM.PAL, but is not an empty image: it contains 16 distinct pixel indices and acts as a base for later overlay frames.

Executable routines

Load offsetCurrent nameEvidence
0x9FF7vga_set_dac_entryWrites an index to 03C8h and one RGB triplet to 03C9h.
0xA017vga_load_palette_biosLoads 256 triplets with BIOS video function 1012h.
0xA032vga_write_palette_rangeWrites a caller-selected range during vertical retrace.
0xA0C9blit_rect_to_vgaCopies an opaque rectangle with a 320-byte destination stride.
0xA106blit_rect_transparent_zeroCopies rows while skipping pixel value 0.
0xA136blit_rect_opaqueCopies rows without a color key.
0xAE42Font initializationLoads STUFF.ART frame 0 and builds the glyph-offset table.
0xAF36Proportional glyph rendererCopies seven atlas rows using the executable width table.
0xBE7EStyled text-object rendererRemaps atlas values 0, 1, and 2 through a palette-index triplet.
0xB620update_palette_effectApplies bounded component offsets and submits changed palette ranges.
0xB99Cdraw_art_frame_opaqueIndexes a far ART pointer by frame * 12 and uses width, height, and pixel offset.

Offsets use the unpacked load-module convention documented elsewhere in this book.