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_biosat0xA017invokes video BIOS function1012hwith 256 entries, start index 0, and a far pointer to the triplets.vga_write_palette_rangeat0xA032waits for vertical retrace through port03DAh, writes a starting index to03C8h, then sends three unmodified bytes per entry to03C9h.
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 offset | Size | Type | Meaning |
|---|---|---|---|
0x00 | 2 | signed little-endian | Horizontal origin or anchor offset. |
0x02 | 2 | signed little-endian | Vertical origin or anchor offset. |
0x04 | 2 | unsigned little-endian | Width in pixels. |
0x06 | 2 | unsigned little-endian | Height in pixels. |
0x08 | 4 | unsigned little-endian | Absolute 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:
| Property | Value |
|---|---|
| Total frame descriptors | 1,178 |
| Total indexed pixels | 4,850,699 |
| Largest frame table | 63 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:
| Frame | X | Y | Width | Height | Pixel offset |
|---|---|---|---|---|---|
| 0 | 88 | 54 | 124 | 74 | 0x48 |
| 1 | 94 | 80 | 112 | 107 | 0x2420 |
| 2 | 147 | 9 | 138 | 165 | 0x52F0 |
| 3 | 207 | 15 | 101 | 51 | 0xABE2 |
| 4 | -3 | -3 | 7 | 9 | 0xC001 |
| 5 | 22 | 138 | 275 | 30 | 0xC040 |
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:
| Overlay | Differing pixels | Difference bounding box |
|---|---|---|
| Animated floppy/save icon | 333 | X 297–316, Y 1–17 |
| Mouse cursor at screen center | 19 | X 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.
Full-screen gallery
Exactly 11 frames have origin (0, 0) and dimensions 320×200. The scene
programs select the following palettes before loading them:
| Archive index | ART frame | Scene-selected palette |
|---|---|---|
| 006 | INTRO.ART frame 0 | TITLE.PAL |
| 063 | PRAY.ART frame 0 | PRAY.PAL |
| 073 | OVER.ART frame 0 | 1.PAL |
| 090 | LAW1.ART frame 0 | LAW.PAL |
| 093 | KABLAM1.ART frame 0 | KABLAM.PAL |
| 097 | SPEAKER.ART frame 0 | 1.PAL |
| 100 | HOLE.ART frame 0 | HOLE.PAL |
| 122 | DOME.ART frame 0 | DOME.PAL |
| 130 | DENY1.ART frame 0 | DENY.PAL |
| 133 | CULTA.ART frame 0 | 1.PAL |
| 165 | BOSS.ART frame 0 | BOSS.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 offset | Current name | Evidence |
|---|---|---|
0x9FF7 | vga_set_dac_entry | Writes an index to 03C8h and one RGB triplet to 03C9h. |
0xA017 | vga_load_palette_bios | Loads 256 triplets with BIOS video function 1012h. |
0xA032 | vga_write_palette_range | Writes a caller-selected range during vertical retrace. |
0xA0C9 | blit_rect_to_vga | Copies an opaque rectangle with a 320-byte destination stride. |
0xA106 | blit_rect_transparent_zero | Copies rows while skipping pixel value 0. |
0xA136 | blit_rect_opaque | Copies rows without a color key. |
0xAE42 | Font initialization | Loads STUFF.ART frame 0 and builds the glyph-offset table. |
0xAF36 | Proportional glyph renderer | Copies seven atlas rows using the executable width table. |
0xBE7E | Styled text-object renderer | Remaps atlas values 0, 1, and 2 through a palette-index triplet. |
0xB620 | update_palette_effect | Applies bounded component offsets and submits changed palette ranges. |
0xB99C | draw_art_frame_opaque | Indexes 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.