Chapter 11: Graphics Programming¶
Note
This chapter is still under construction. The display-list, plane-register, and sprite reference sections below are aligned with the current CGIA firmware; the worked examples and tutorial sections are ongoing work.
Interfacing with CGIA¶
Display List¶
The Color Graphic Interface Adaptor (CGIA) of the X65 microcomputer utilizes a display list system for defining and controlling video output. This approach is inspired by Atari’s ANTIC processor, with additional flexibility to support text, bitmap, multicolor, and advanced graphical modes.
Overview of the CGIA Display List¶
A display list is a series of instructions stored in memory that defines how the CGIA renders each scanline. Instead of the CPU handling screen rendering directly, the Video Processing Unit (VPU) interprets the display list, fetching graphics data, colors, and character definitions on a per-line basis. This method allows for a dynamic and efficient beam-chased video output without CPU intervention.
Display List Instructions¶
The CGIA display list consists of two main categories of instructions:
Control Instructions (0-7) – Used for line fills, jumps, memory loads, and register manipulation.
Mode Row Instructions (8-F) – Define how the CGIA should render specific scanlines.
For instructions requiring additional data, these follow the instruction code.
Control Instructions (0-7)¶
0 – Insert empty lines filled with the background color.
Bits 6-4: Number of empty lines.
Bit 7: Trigger a Display List Interrupt (DLI).
1 – Duplicate the last raster line multiple times.
2 – Jump to another location in the display list.
If DLI bit is set, execution waits for Vertical Blank before jumping.
3 – Load new memory pointers. Bits 4-7 are flag bits selecting which scan pointers will be updated; the matching 16-bit values follow the instruction byte in the order listed.
Bit 4: Load Memory Scan (LMS) – Points to screen data.
Bit 5: Load Foreground Scan (LFS) – Points to foreground color data.
Bit 6: Load Background Scan (LBS) – Points to background color data.
Bit 7: Load Character Generator (LCG) – Points to character shape definitions.
4 – Load an 8-bit value into a plane register. Each plane has 16 multipurpose registers, indexed 0 - 15.
Bits 7-4: Register index.
5 – Load a 16-bit value into a plane register. Plane registers can also be addressed as eight 16-bit values, indexed 0 - 7.
Bits 6-4: Register index.
6, 7 – Reserved (TBD).
Mode Row Instructions (8-F)¶
Mode row instructions define the type of graphics displayed on a scanline. The lower three bits (0-2) select the mode (MODE0..MODE7 — one of the eight display-mode slots), while bit 3 (always set in this group, mask %1000) differentiates them from control instructions. The nibble for any mode-row instruction is therefore %1000 | mode_number — so MODE0 is nibble 8, MODE1 is 9, MODE2 is A, MODE3 is B, MODE6 (HAM) is E, MODE7 (affine) is F, and the still-reserved slots 4 and 5 occupy nibbles C and D. Whenever a section below refers to “the mode-row instruction for MODE_N_”, the same %1000 | N rule applies.
8 – Palette Text/Tile Mode (MODE0) – Character-based graphics where each cell picks colors from the plane’s 8-entry palette; no attribute memory needed. See Implementing Paletted Modes below.
9 – Palette Bitmap Mode (MODE1) – Direct pixel-based graphics drawn through the plane’s 8-entry palette; no attribute memory needed. See Implementing Paletted Modes below.
A – Attribute Text/Tile Mode (MODE2) – Character-based graphics with per-cell foreground and background color attributes (VIC-II style). See Implementing Attribute Modes below.
B – Attribute Bitmap Mode (MODE3) – Direct pixel-based graphics with per-cell color attributes. See Implementing Attribute Modes below.
C, D – Reserved (TBD).
E – HAM6 (MODE6, Hold-and-Modify) – Similar to Amiga HAM, where pixel colors can be modified based on previous pixels, enabling a larger color range. See HAM Encoding Format below.
F – Affine Transform Chunky Pixel Mode (MODE7) – Inspired by SNES MODE7, allowing rotation and scaling of graphics for pseudo-3D effects.
Mode row instructions also carry two flag bits that modify how the mode is rendered:
Bit 4 (
DOUBLE_WIDTH) – Doubles the horizontal pixel size of every cell. Available on every text mode; pairs naturally with multi-color for the chunky C64-style rectangle pixels, and on its own gives the Atari 8-bit “wide character” look.Bit 5 (
MULTICOLOR) – Switches text and bitmap modes to a 4-color-per-cell representation, where each byte encodes four pixels (similar to C64 multicolor mode). On text modes this also narrows the cell to 4 pixels wide — yielding 80 columns on the standard 320-pixel screen when double-width is off.
Additionally, bit 7 in any mode row instruction triggers a Display List Interrupt (DLI), allowing for scanline-specific effects, color changes, or sprite updates.
Using the Display List¶
A typical display list sequence begins with a Load Memory Scan (LMS) instruction, setting the screen’s base address. Subsequent instructions configure row modes, colors, and additional graphics settings. The final instruction often loops back to the beginning, ensuring continuous display refresh.
By leveraging the CGIA’s display list system, developers can efficiently mix text, tiles, bitmaps, and advanced graphical effects in a single frame without CPU overhead. This design enables smooth scrolling, parallax effects, sprite multiplexing, and dynamic screen updates, making the X65’s graphics system a powerful tool for retro-style computing and game development.
Plane Registers Interpretation¶
Each CGIA plane has 16 associated registers, but their interpretation depends on the selected plane type and graphics mode. The last display list mode instruction determines how the plane registers are utilized. The register structure varies based on whether the plane is handling background graphics, HAM mode, affine transformations, or sprites.
Background Plane Registers¶
flags (8-bit) – Configuration flags (see below).
border_columns (8-bit) – Number of border columns on each side.
row_height (8-bit, unsigned) – Height of each row in raster lines.
stride (8-bit, unsigned) – Memory stride for addressing screen data.
scroll_x, scroll_y (8-bit, signed) – Fine scrolling values.
offset_x, offset_y (8-bit, signed) – Base offsets for plane positioning.
color[8] (8-bit each) – Eight plane-wide colors, used for borders, multicolor cells, and shared palette entries depending on the active mode and flags.
Background Plane Flags¶
Bit 0 – Color 0 is transparent.
Bits 1-2 – Reserved.
Bit 3 – Border is transparent.
Bit 4 – Double-width pixel mode.
Bit 5 – Multicolor pixel mode.
Bits 6-7 – Pixel bit-depth:
00= 1 bpp / 2 colors,01= 2 bpp / 4 colors,10= 3 bpp / 8 colors,11= 4 bpp / 8 colors + half-bright.
PLANE_MASK_TRANSPARENT %00000001
PLANE_MASK_BORDER_TRANSPARENT %00001000
PLANE_MASK_DOUBLE_WIDTH %00010000
PLANE_MASK_MULTICOLOR %00100000
PLANE_MASK_PIXEL_BITS %11000000
HAM Mode Registers¶
flags, border_columns, row_height (8-bit) – Same as background mode.
base_color ×8 (8-bit each) – The 8 base colors used for HAM modifications.
Affine Transform Mode Registers¶
flags, border_columns, row_height (8-bit) – Same as before.
texture_bits (8-bit) – Defines texture width and height in bits (size is power of two).
u, v (16-bit, signed) – Texture coordinates for the upper-left corner.
du, dv (16-bit, signed) – Texture step increments per pixel.
dx, dy (16-bit, signed) – Transform coefficients for scaling and rotation.
Sprite Plane Type Registers¶
When a plane is configured as a sprite plane, its 16 plane registers carry these fields (the remaining bytes are reserved):
active (8-bit) – Bitmask indicating which of the eight sprites are active.
border_columns (8-bit, unsigned) – Number of border columns.
start_y, stop_y (8-bit, unsigned) – Vertical range where sprites are visible (clipping window).
The sprite plane does not consume a display list. Instead, the plane’s offsetN register points at a sprite descriptor table in memory.
Sprite Descriptor (16 bytes)¶
Each entry in the sprite descriptor table is exactly 16 bytes, laid out as follows:
Offset |
Field |
Size |
Notes |
|---|---|---|---|
0 |
|
i16 |
Signed X position (can be negative for off-screen entry). |
2 |
|
i16 |
Signed Y position. |
4 |
|
u16 |
Sprite height in raster lines. |
6 |
|
u8 |
Width / mode bits (see below). |
7 |
reserved |
u8 |
— |
8 |
|
3×u8 |
Colors for indices 1, 2, 3 (index 0 is always transparent). |
11 |
reserved |
u8 |
— |
12 |
|
u16 |
16-bit pointer (within the sprite bank) to the sprite pixels. |
14 |
|
u16 |
Pointer to the next descriptor after |
The flags byte:
Bits 0-2 – Width minus one, in bytes (1–8 bytes = 8–64 pixels).
Bit 3 – Reserved.
Bit 4 – Double-width.
Bit 5 – Multicolor.
Bit 6 – Mirror X.
Bit 7 – Mirror Y.
SPRITE_MASK_WIDTH %00000111
SPRITE_MASK_DOUBLE_WIDTH %00010000
SPRITE_MASK_MULTICOLOR %00100000
SPRITE_MASK_MIRROR_X %01000000
SPRITE_MASK_MIRROR_Y %10000000
By utilizing dynamic plane register interpretation, the CGIA enables highly versatile graphics rendering, allowing the X65 to seamlessly mix background layers, sprites, advanced color manipulation, and affine transformations in real time.
Worked Example: Eight Multicolor Sprites¶
The following adapted snippet (from the sprites example program) sets up plane 0 as a sprite plane with all eight sprites enabled, then fills a descriptor table by stepping through it 16 bytes at a time. Each sprite is SPRITE_WIDTH bytes wide (so SPRITE_WIDTH * 8 pixels), SPRITE_HEIGHT lines tall, and uses three shared colors plus transparency on color index 0:
.define SPRITE_WIDTH 4
.define SPRITE_HEIGHT 26
reset:
sei ; disable IRQs while we reconfigure CGIA
lda #0 ; disable all planes during setup
sta CGIA::planes
lda #145 ; pick a border/background color
sta CGIA::back_color
; point plane 0 at the sprite descriptor table
lda #<sprite_descriptors
sta CGIA::offset0
lda #>sprite_descriptors
sta CGIA::offset0 + 1
; sprite-plane-specific registers
lda #%11111111 ; activate all 8 sprites
sta CGIA::plane0 + CGIA_SPRITE_REGS::active
lda #0 ; no border, sprites visible across the whole screen
sta CGIA::plane0 + CGIA_SPRITE_REGS::border_columns
sta CGIA::plane0 + CGIA_SPRITE_REGS::start_y
sta CGIA::plane0 + CGIA_SPRITE_REGS::stop_y
; ... fill 8 × 16-byte descriptors at sprite_descriptors ...
lda #%10000000 ; enable Vertical Blank NMI
sta CGIA::int_enable
lda #%00010001 ; plane0 = enabled, type = sprite
sta CGIA::planes
Per-descriptor flags can mix the modifier bits freely. For example:
; multicolor + mirror X, sprite is SPRITE_WIDTH bytes wide
lda #(SPRITE_MASK_MULTICOLOR | SPRITE_MASK_MIRROR_X | (SPRITE_WIDTH-1))
sta sprite_descriptors + 4*CGIA_SPRITE_DESC_LEN + CGIA_SPRITE::flags
; multicolor + mirror Y
lda #(SPRITE_MASK_MULTICOLOR | SPRITE_MASK_MIRROR_Y | (SPRITE_WIDTH-1))
sta sprite_descriptors + 5*CGIA_SPRITE_DESC_LEN + CGIA_SPRITE::flags
; multicolor + double-width pixels
lda #(SPRITE_MASK_MULTICOLOR | SPRITE_MASK_DOUBLE_WIDTH | (SPRITE_WIDTH-1))
sta sprite_descriptors + 3*CGIA_SPRITE_DESC_LEN + CGIA_SPRITE::flags
A VBL NMI handler can then animate sprites by writing fresh pos_x / pos_y values into individual descriptors — no per-frame display-list rebuild required.
Implementing HAM Mode Graphics¶
Hold-And-Modify (HAM) Mode¶
HAM mode is a unique graphical technique that allows for an expanded color range by modifying the color of each pixel based on the preceding pixel. This technique, inspired by the Amiga HAM mode, enables an expanded color range without requiring a full 8-bit per-pixel framebuffer.
HAM Encoding Format¶
HAM mode operates using 6-bit commands, where 4 screen pixels are packed into 3 bytes:
[CCCDDD] - C: Command bits, D: Data bits
Commands¶
000 – Load a base color from an 8-color palette (DDD specifies which color).
001 – Blend the current pixel with another color from the 8-color palette.
CCS – Modify a single color channel:
01S – Modify the Red channel.
10S – Modify the Green channel.
11S – Modify the Blue channel.
S – Sign bit (0 = positive delta, 1 = negative delta).
DDD – Delta value (000 represents +1).
Rendering HAM Mode¶
The CGIA processes HAM mode by starting with a base color and applying per-pixel modifications. This means that each pixel is calculated based on the previous pixel, allowing for smooth color transitions across the screen. However, this also means that artifacts may appear when picture colors are changing rapidly.
Optimizing HAM Graphics¶
Due to its serial dependency, HAM mode benefits from careful palette selection and strategic placement of base color indices to minimize unwanted color blending. Developers can improve visual quality by:
Careful base colors selection to minimize artifacting.
Using blending instructions to smoothly transition between colors.
Leveraging display list registry set instructions and/or interrupts (DLI) to alter base-colors mid-frame.
Applications of HAM Mode¶
HAM mode is particularly useful for high-color images, gradients, and advanced shading effects. Unlike traditional indexed-color modes, HAM can generate a broader range of colors while keeping memory usage low.
By integrating HAM Mode (MODE6) into the X65 graphics pipeline, developers can achieve highly detailed visuals with minimal additional memory overhead, making it a powerful tool for static images.
Implementing Paletted Modes (MODE0 / MODE1)¶
MODE0 and MODE1 are the CGIA’s paletted modes: the colour palette for the plane lives in the upper half of the plane’s 16 registers (color[0..7]), giving eight palette entries per plane. No separate foreground/background scan pointers are needed — the savings in both memory traffic and plane-register programming are the whole point of these modes.
Both modes pick pixel bit depth through the two high bits of the plane flags register (PLANE_MASK_PIXEL_BITS):
|
bpp |
Colours per pixel |
MODE0 glyph budget |
MODE1 bitmap layout |
|---|---|---|---|---|
|
1 |
2 |
Full 256 glyphs |
1 byte = 8 pixels |
|
2 |
4 |
128 glyphs |
1 byte = 4 pixels |
|
3 |
8 |
64 glyphs |
4 pixels packed in 3 B |
|
4 |
8 + half-bright |
32 glyphs |
2 pixels per nibble |
With the MULTICOLOR flag set, MODE0 cells narrow to 4 pixels wide and the two char-gen bits feed the low palette index — high palette-index bits (and the half-bright flag at 4 bpp) come from stolen high bits of the character code. MODE1 has no multi-color encoding and ignores the flag:
|
bpp |
Colours per pixel |
MODE0 glyph budget |
|---|---|---|---|
|
1 |
4 |
treated as 2 bpp below |
|
2 |
4 |
Full 256 glyphs |
|
3 |
4 (one of two palette halves per cell) |
128 glyphs |
|
4 |
4 + half-bright (one of two palette halves per cell) |
64 glyphs |
MODE1 — Paletted Bitmap¶
In MODE1, the bitmap bytes index the palette directly:
1 bpp: each bit picks palette entry 0 or 1. Eight pixels per byte.
2 bpp: each pair of bits picks one of palette entries 0–3. Four pixels per byte.
3 bpp: pixels are bitpacked HAM-style — four pixels in three bytes. CGIA decodes 24 bits as
4 × 6 bitsand each 6-bit group’s low three bits index the palette (the high three are reserved for mode-specific work; for pure bitmap use they default to zero).4 bpp: high bit of each nibble is the half-bright flag, bits 2:0 select palette entry 0–7.
Typical setup: pick the palette, set bit depth, point the LMS scan pointer at a packed bitmap, pick a MODE1 mode-row instruction, and let the beam do the rest.
; Palette: eight entries in plane 0's color[0..7]
lda #$10 ; dark blue
sta CGIA::plane0 + CGIA_BG_REGS::color + 0
lda #$15 ; bright blue
sta CGIA::plane0 + CGIA_BG_REGS::color + 1
lda #$22
sta CGIA::plane0 + CGIA_BG_REGS::color + 2
; ... populate color[3..7] ...
; 2 bpp paletted bitmap, solid cells
lda #PLANE_BITS_2BPP
sta CGIA::plane0 + CGIA_BG_REGS::flags
; LMS at the bitmap
lda #<pic_pixels
sta dl_lms_lo
lda #>pic_pixels
sta dl_lms_hi
The mode-row instruction itself uses nibble 9 (MODE1 = slot 9 in the display list mode-row group).
MODE0 — Paletted Text/Tile¶
MODE0 borrows MODE1’s palette-in-registers mechanism but keeps the character-generator indirection from the attribute text modes. Every screen cell is still one byte of character code plus a row-by-row fetch from the character generator, exactly as in MODE2. What is different is how colour is assigned per pixel:
At 1 bpp, the character-generator bit is the palette index. Palette entry 0 is “off”, entry 1 is “on”. Full 256-glyph fonts fit, every cell paints from
palette[0..1].At higher bpp, the character-generator bit is only the low bit of the palette index. The additional high bits of the palette index come from the high bits of the character code — the font “pays” for colour by giving up glyph-code space, and the same byte that picks a glyph also picks which slice of the palette that glyph paints with.
The character-code byte therefore splits like this (writing g… for glyph-index bits and Pn for palette-index bit n — char-gen bit always feeds palette bit 0):
1 bpp non-multi : [ g7 g6 g5 g4 g3 g2 g1 g0 ] palette[0..1] for every cell
2 bpp non-multi : [ P1 |g6 g5 g4 g3 g2 g1 g0 ] $00..$7F → palette[0..1], $80..$FF → [2..3]
3 bpp non-multi : [ P2 P1 |g5 g4 g3 g2 g1 g0 ] four palette pairs across $00..$3F / $40..$7F / $80..$BF / $C0..$FF
4 bpp non-multi : [ HB P2 P1 |g4 g3 g2 g1 g0 ] bit 7 = half-bright flag (XORs bit 2 of the looked-up CGIA color)
So at 2 bpp the top bit of the character code becomes palette bit 1 (128 glyphs $00..$7F): codes $00..$7F paint with palette[0..1], codes $80..$FF paint with palette[2..3]. At 3 bpp the top two bits become palette bits 2..1 (64 glyphs $00..$3F): four palette pairs walk across the four 64-byte char-code quarters — $00..$3F → palette[0..1], $40..$7F → [2..3], $80..$BF → [4..5], $C0..$FF → [6..7]. At 4 bpp the top three bits participate: the very top bit is the half-bright flag, the next two bits join the palette index alongside the char-gen bit, and the remaining five bits ($00..$1F) identify the glyph — 32 glyphs total.
In practice MODE0 is almost always used at 1 bpp (a crisp two-colour font over a fixed palette entry) or 2 bpp (four-colour decorative text), with higher bpps reserved for tilemap/backdrop work where glyph count is not the limiting factor.
The half-bright transform (4 bpp)¶
At 4 bpp the high bit of the 4-bit colour code does not pick between palette entries — there are only eight, and the other three bits already select among them. Instead it acts after the per-pixel shared_colors[] lookup, XORing bit 2 of the resulting 8-bit CGIA palette index:
palette_idx = (P2 P1) << 1 | char-gen-bit ; 0..7 (3-bit shared_colors index)
cgia_color = shared_colors[palette_idx] ; 0..255 (8-bit CGIA palette index)
if HB: cgia_color ^= 4 ; flip bit 2 → swap brightness half
rgb = cgia_rgb_palette[cgia_color]
The CGIA’s 256-colour palette is laid out as 32 hue rows of 8 brightness levels; bit 2 of the index is a brightness bit, so flipping it always lands on the same hue’s bright/dark twin. The flag is one bit per cell and applies to every pixel of the cell, so both colours used by a 4 bpp non-multi cell jump to their twins together. Because the swap happens in the 256-colour palette and not in shared_colors[], the effect does not depend on how shared_colors[] was loaded — any palette layout gets a free up/down “brighter / darker” control per cell, lifting the visible-colour ceiling from 8 to 16.
MODE0 multi-color text¶
MODE0 supports the same MULTICOLOR flag as the attribute text modes, and with the same effect: two character-generator bits feed the colour decision per screen pixel, so cells are 4 pixels wide instead of 8. The two char-gen bits become the low two bits of the palette index, and any remaining high bits (palette half select, half-bright) are stolen from the character code:
2 bpp multi : [ g7 g6 g5 g4 g3 g2 g1 g0 ] no stolen bits — char-gen supplies P1,P0 → palette[0..3]
3 bpp multi : [ P2 |g6 g5 g4 g3 g2 g1 g0 ] $00..$7F → palette[0..3], $80..$FF → palette[4..7]
4 bpp multi : [ HB P2 |g5 g4 g3 g2 g1 g0 ] bit 7 = half-bright (XORs bit 2 of the looked-up CGIA color)
So at 2 bpp multi no bit is stolen: the two char-gen bits are the full 2-bit palette index, all 256 character codes are usable, and every cell paints from palette[0..3]. (This is the configuration that drives the 80-column mode below.) At 3 bpp multi the top bit of the character code is stolen as palette bit 2 — 128 glyphs ($00..$7F), with codes $00..$7F painting from palette[0..3] and codes $80..$FF painting from palette[4..7]. Worked example: writing $85 to a 3 bpp multi MODE0 screen draws glyph index 5 (the low 7 bits) using the multi-color two-bit pattern that addresses palette[4..7] (because bit 7 is set).
At 4 bpp multi bit 7 is the half-bright flag and bit 6 is stolen as palette bit 2; only the low six bits identify a glyph (64 glyphs, $00..$3F). The transform follows exactly the same pseudocode as 4 bpp non-multi — palette_idx → shared_colors[] → XOR 4 if HB → cgia_rgb_palette[] — so all four colours used by the cell jump to their bright/dark twin together, raising the visible-colour ceiling per cell to 16 (8 palette × 2 brightness halves). Char-code regions split as: $00..$3F → palette[0..3], $40..$7F → palette[4..7], $80..$BF → palette[0..3] half-brighted, $C0..$FF → palette[4..7] half-brighted.
Multi-color MODE0 is meaningfully distinct only at 2, 3, and 4 bpp:
1 bpp multi-color is impossible — multi-color already takes two bits per pixel by its nature, so if set, renders identically to 2 bpp multi — multi-color.
80-column mode¶
A particularly useful side effect of 4-pixel-wide multi-color cells is the 80-column mode. On the standard 320-pixel logical screen, 320 ÷ 4 = 80 cells across. Any multi-color text mode without the double-width flag therefore yields 80 columns out of the box:
; MODE0 in multi-color, 2 bpp, no double-width
lda #(PLANE_BITS_2BPP | PLANE_MASK_MULTICOLOR)
sta CGIA::plane0 + CGIA_BG_REGS::flags
; Pick palette entries so that "ink" is a clear colour pair
lda #$00 ; background (color[0])
sta CGIA::plane0 + CGIA_BG_REGS::color + 0
lda #$00 ; unused in a two-tone 80-col font
sta CGIA::plane0 + CGIA_BG_REGS::color + 1
lda #$00 ; unused
sta CGIA::plane0 + CGIA_BG_REGS::color + 2
lda #$FE ; ink (color[3])
sta CGIA::plane0 + CGIA_BG_REGS::color + 3
; Use a 4×8 font whose glyphs only fire pixels with multi-color
; patterns `00` (background) and `11` (ink)
lda #<font_4x8
sta dl_lcg_lo
lda #>font_4x8
sta dl_lcg_hi
A 4×8 font designed for this purpose uses only the 00 and 11 multi-color codes, producing a clean, readable two-tone 80-column display — suitable for code listings, terminal UI, and long-text screens — while keeping the full one-byte-per-character storage of normal text modes. All the usual display-list mechanics (scrolling, DLI, mid-frame mode switches) still apply.
Double-width text across all modes¶
The PLANE_MASK_DOUBLE_WIDTH flag (bit 4 of the plane flag byte, also encoded as CGIA_DL_DOUBLE_WIDTH_BIT in the mode-row instruction) doubles the horizontal pixel size of any text mode. It pairs naturally with multi-color for the chunky “rectangle-pixel” look (C64 multi-color text style), and stands on its own with non-multi-color text for the Atari “wide character” modes. Because the flag lives in both the plane register and the display-list mode-row instruction, switching double-width on or off mid-screen is as easy as writing the right opcode on the target row.
Implementing Attribute Modes (MODE2 / MODE3)¶
MODE2 and MODE3 are the attribute counterparts to MODE0/1: instead of an 8-entry palette in the plane registers, every cell carries its own foreground and background colour bytes, fetched in parallel with the character or bitmap data. See MODE2 and MODE3 — Attribute Modes in Chapter 4 for the conceptual picture; this section covers programming.
Scan-pointer setup¶
The two attribute modes need three or four scan pointers loaded by a single LOAD ($03) display-list instruction (see Control Instructions):
Pointer |
LOAD bit |
MODE2 |
MODE3 |
|---|---|---|---|
|
Bit 4 (LMS) |
Character codes, one byte per cell |
Bitmap data, one byte per cell row |
|
Bit 5 (LFS) |
Per-cell foreground colour |
Per-cell foreground colour |
|
Bit 6 (LBS) |
Per-cell background colour |
Per-cell background colour |
|
Bit 7 (LCG) |
Character generator (font) data |
unused |
Both colour-scan streams are one byte per cell per row — exactly the size of one screen row in cells. Plan a colour map of columns × rows bytes for each of the foreground and background streams.
Unlike MODE0/1, the attribute modes ignore the plane’s PLANE_MASK_PIXEL_BITS field. Each cell is fixed at 8 pixels (1 bit per pixel) with MULTICOLOR cleared, or 4 pixels (2 bits per pixel) with MULTICOLOR set.
MODE3 — Attribute Bitmap¶
In MODE3 the memory_scan byte for a cell is the bitmap data directly. Each scanline of the cell uses one fresh bitmap byte — row_height + 1 bytes per cell column for one full row — while colour_scan and backgr_scan advance once per cell, giving each cell a single foreground/background pair across all of its scanlines.
Per-pixel decoding:
Source bit / code |
Non-multi ( |
Multi ( |
|---|---|---|
|
|
— |
|
|
— |
|
— |
|
|
— |
|
|
— |
|
|
— |
|
“Transparent” applies whenever PLANE_MASK_TRANSPARENT is set on the plane flags — bit 0 (non-multi) or code 00 (multi) is skipped, letting the plane below show through.
; Display list snippet exercising all four MODE3 flag combinations
.byte CGIA_DL_INS_LOAD_REG8 | (CGIA_BCKGND_REGS::row_height << 4), 7
.byte CGIA_DL_INS_LOAD_REG8 | (CGIA_BCKGND_REGS::flags << 4), $00
.byte CGIA_DL_MODE_ATTRIBUTE_BITMAP ; 8×N cells
.byte CGIA_DL_MODE_ATTRIBUTE_BITMAP | CGIA_DL_DOUBLE_WIDTH_BIT ; 16×N cells
.byte CGIA_DL_MODE_ATTRIBUTE_BITMAP | CGIA_DL_MULTICOLOR_BIT ; 4×N multi cells
.byte CGIA_DL_MODE_ATTRIBUTE_BITMAP | CGIA_DL_DOUBLE_WIDTH_BIT | CGIA_DL_MULTICOLOR_BIT ; 8×N multi cells
A typical full-screen MODE3 setup picks row_height (8 raster lines per row → row_height = 7), points the three scan pointers at the bitmap and two colour maps, then walks the rows with mode-B opcodes:
lda #7
sta CGIA::plane0 + CGIA_BCKGND_REGS::row_height
lda #$00 ; PIXEL_BITS ignored, no transparency
sta CGIA::plane0 + CGIA_BCKGND_REGS::flags
.byte CGIA_DL_INS_LOAD_MEMORY \
| CGIA_DL_INS_LM_MEMORY_SCAN \
| CGIA_DL_INS_LM_FOREGROUND_SCAN \
| CGIA_DL_INS_LM_BACKGROUND_SCAN
.word bitmap ; LMS — bitmap, columns × (row_height+1) × rows bytes
.word fg_map ; LFS — foreground colour, columns × rows bytes
.word bg_map ; LBS — background colour, columns × rows bytes
; ... mode-B rows here ...
MODE2 — Attribute Text/Tile¶
MODE2 keeps the character-generator indirection of MODE0 but replaces the palette mechanism with the attribute layout: memory_scan carries a character code per cell, the character generator produces an 8-bit bitmap row for that code on the current scanline, and that bitmap drives the same per-pixel decoding as MODE3. Both colour_scan and backgr_scan advance once per cell, exactly as in MODE3.
Because MODE2 needs the character generator, the LOAD instruction must include the CGIA_DL_INS_LM_CHARACTER_GENERATOR flag and the char_gen pointer. A typical setup mirrors MODE0 but adds the two colour-scan pointers:
.byte CGIA_DL_INS_LOAD_MEMORY \
| CGIA_DL_INS_LM_MEMORY_SCAN \
| CGIA_DL_INS_LM_FOREGROUND_SCAN \
| CGIA_DL_INS_LM_BACKGROUND_SCAN \
| CGIA_DL_INS_LM_CHARACTER_GENERATOR
.word text_offset ; LMS — character codes
.word color_offset ; LFS — foreground colour map
.word bkgnd_offset ; LBS — background colour map
.word chgen_offset ; LCG — character generator (font)
.byte CGIA_DL_INS_LOAD_REG8 | (CGIA_BCKGND_REGS::flags << 4), $00
.byte CGIA_DL_MODE_ATTRIBUTE_TEXT ; 8×N cells
.byte CGIA_DL_MODE_ATTRIBUTE_TEXT | CGIA_DL_DOUBLE_WIDTH_BIT ; 16×N cells
.byte CGIA_DL_MODE_ATTRIBUTE_TEXT | CGIA_DL_MULTICOLOR_BIT ; 4×N multi cells
.byte CGIA_DL_MODE_ATTRIBUTE_TEXT | CGIA_DL_DOUBLE_WIDTH_BIT | CGIA_DL_MULTICOLOR_BIT
Per-pixel decoding follows the same table as MODE3 — the only difference is where the source bits come from (character generator output instead of memory_scan itself).
Multicolor, double-width, and transparency¶
The three modifier flags work uniformly across MODE2 and MODE3:
PLANE_MASK_MULTICOLOR(%00100000) — switches the cell from 1-bit-per-pixel (8 pixels wide) to 2-bit-per-pixel (4 pixels wide). The two extra colour slots come from plane registerscolor[0]andcolor[1]; the per-cell colour bytes still come fromcolour_scanandbackgr_scan. Because cells narrow to 4 pixels wide, MODE2 multi-color produces an 80-column screen on the standard 320-pixel logical width — the same trick described for MODE0 above, now with per-cell foreground and background colours.PLANE_MASK_DOUBLE_WIDTH(%00010000) — doubles the horizontal pixel size, giving 16-pixel non-multi or 8-pixel multi cells. The flag lives in both the plane flag byte and the mode-row instruction (CGIA_DL_DOUBLE_WIDTH_BIT), so it can be toggled mid-screen by writing the right opcode on a row.PLANE_MASK_TRANSPARENT(%00000001) — turns the “background” code (bit0non-multi, code00multi) into transparent pixels, so the plane below shows through. Any other code still paints opaque colour from the scan pointers (orcolor[1]). Useful for layering an attribute plane on top of HAM, MODE7, or another tilemap.
The flag constants are defined alongside the plane register layout — see Background Plane Flags.
Memory layout and row_height¶
The one footgun specific to MODE3 is that memory_scan advances once per raster line within a cell, while colour_scan and backgr_scan advance once per cell row. Sizing the three buffers correctly:
bitmap (MODE3
memory_scan):columns × (row_height + 1) × rowsbytes — one byte per cell per scanline.foreground map (
colour_scan):columns × rowsbytes — one byte per cell.background map (
backgr_scan):columns × rowsbytes — one byte per cell.
In MODE2 the picture is simpler: memory_scan is one byte per cell (the character code stays the same across the cell’s scanlines), so all three of the streams above are columns × rows bytes. The character generator carries the per-scanline shape data, and char_gen + (char_code << char_shift) + line indexes the right bitmap row for the current scanline.
row_height is the same plane register documented for the paletted modes; 0 gives one raster line per row (chunky bitmap, no character-generator indirection makes sense at that height), 7 gives the C64-style 8-line cells, and intermediate values give the Atari “tall-character” or “narrow-character” variants. All three scan pointers respect the same row_height per mode row.
Creating Mixed-Mode Display Lists¶
Plane ordering¶
The CGIA supports up to four graphics planes, which can be layered to create complex visual effects. The order in which these planes are rendered is crucial for achieving the desired appearance, especially when combining different graphics modes.
It is beneficial to be able to change the order of planes without reconfiguring the entire plane settings. The CGIA allows for dynamic plane ordering through the use of a Plane Order Register.
Plane Order Register¶
With four planes, there are 24 possible permutations for their rendering order. The Plane Order Register is a register that picks the order in which the planes are drawn.
The order uses the Steinhaus–Johnson–Trotter (adjacent-swap “revolving door”) order. Each step swaps one adjacent pair, so “next” and “previous” are obvious.
Starting at 1234, the 24 permutations:
1234 1243 1423 4123 4132 1432
1342 1324 3124 3142 3412 4312
4321 3421 3241 3214 2314 2341
2431 4231 4213 2413 2143 2134
Rule to get the next permutation from the current one:
Give every element a direction (initially all point left).
Find the largest “mobile” element: one whose arrow points to a smaller neighbor.
Swap it with that neighbor in its arrow direction.
Reverse the arrows of all elements larger than the moved one.
Repeat until no mobile element exists.
To go backward, reverse the rule. This is a Gray-code analogue for permutations on the permutohedron using adjacent transpositions.