Chapter 18 - Two Displays, One Language
Two finished games stand behind you. Skyfall drops blocks down an
8x8 board of LEDs toward a sliding paddle; Lanternfly steers a white
sprite through a night garden with a wasp on its tail. Read them as
designs and they are near twins: three cards joined in the same
three-press loop, a writable timer whose period is the difficulty, a
one-shot guarding the restart, ApiRandom masked for every respawn,
and a game-over card the second game took from the first keystroke
for keystroke.
Read them as programs and they part company at a single line.
display matrix8x8 against display tms9918 decided what a render
writes to, what collision costs, how motion travels to the screen,
and how large a world each game could afford. This closing chapter
reads the two games side by side and traces every difference back to
that one line - and every sameness to the language around it.
The two loops
Build either game and open its generated file at the runtime loop.
From skyfall.main.asm:
; --- runtime loop ---
Start:
call FbClear
call HudBlankDig
MainLoop:
call ScanFrame ; show one full frame, then blank
call GlimPollBindings ; game work runs in the blank window
ld a,(CurrentCard) ; latch: card transitions land at
ld (GlimActiveCard),a ; frame start, never mid-frame
call GlimTickTimers
call GlimRunLogicEffects
call GlimMergeRaised
call GlimRunRenderEffects
call GlimEndFrame
jp MainLoop
And from lanternfly.main.asm:
; --- runtime loop ---
Start:
call VdpInit
call LoadResourcesVram
MainLoop:
call VdpWaitVBlank ; pace on the status-register flag
call GlimCommit ; flush shadows in the blank window
call GlimPollBindings
ld a,(CurrentCard) ; latch: card transitions land at
ld (GlimActiveCard),a ; frame start, never mid-frame
call GlimTickTimers
call GlimRunLogicEffects
call GlimMergeRaised
call GlimRunRenderEffects
call GlimEndFrame
jp MainLoop
From GlimPollBindings down, the two loops run the same nine
instructions: poll, latch the card, tick the timers, run the phases,
roll the frame over. That identical tail is the language - the frame you have known
since chapter 2, unchanged under either display.
Everything above it is the profile, and the two heads describe two
relationships with a screen. Skyfall’s frame produces its picture:
ScanFrame drives all eight LED rows with a fixed dwell and returns
with the matrix dark, so the whole game - polling, rules, renders -
runs inside the blank window between scans, and the scan is the
frame’s largest cost. Lanternfly’s frame waits for its picture:
the VDP paints 256x192 pixels from its own 16 KiB of VRAM over and
over without help, VdpWaitVBlank catches the rest between two
paintings, and GlimCommit spends that rest moving the previous
frame’s changes into VRAM. One display is something the CPU does;
the other is something the CPU writes to.
The board the program is
On the matrix, the scene is 32 bytes of program RAM, and Skyfall’s whole visible world - drop, paddle - lives in them. A render writes the framebuffer; the next scan shows it; and because the CPU re-presents those bytes every frame, the picture persists exactly as long as the bytes do.
With a scene that small, the cheapest render repaints all of it. Skyfall draws its board with one block:
render DrawBoard
on PadX, DropX, DropY
begin
call FbClear
ld a,(DropX)
ld b,a ; B = x
ld a,(DropY)
ld c,a ; C = y
ld a,COLOR_YELLOW
call FbPlot
ld a,(PadX)
ld b,a
ld c,7 ; the bottom row
ld hl,Shape_Paddle
call ShapeDraw
end
Whatever moved - paddle, drop, or both - the block clears the canvas
and repaints everything on it, and the cost stays trivial because
everything on it is a plot and a three-pixel shape. FbClear
handles erasing wholesale: every picture starts from darkness, so
whatever vacated a pixel is gone before the plots begin.
The same smallness shapes the rules. Positions on the matrix are
cells, so Skyfall’s entire collision question - did the paddle catch
the drop? - is one subtraction and one unsigned compare: sub b,
cp 3, carry means caught. The repository’s matrix games push the
board shape further in the same direction. Snake packs each body
segment into a single byte, y*8+x, and walks a 64-byte ring buffer
of them; Tetro keeps its settled board as occupancy and colour plane
bytes, merged into the framebuffer a row at a time. When the world
is 64 cells, boards, bodies, and collisions all become byte
arithmetic, and a game’s hardest questions take a handful of
instructions to answer.
The board also sets the budget. Every rule and render shares the blank window between scans, and the scan paces the game at its sixty-odd frames a second - which is why Skyfall’s difficulty lives in a timer period, counted in frames, rather than in how much work a frame does.
The scene the program describes
On the VDP, the scene outlives the frame that drew it. Lanternfly’s
splash card plants five reeds with five tile_at lines, once, in an
enter block; the commit carries them to VRAM; and the VDP repaints
them in every picture for the rest of the run without another
instruction spent. A matrix render repaints its whole layer whenever
a fact changes. A VDP program writes each cell once and writes again
only what differs.
Renders write that difference into shadow tables - ordinary RAM mirroring the VRAM the VDP reads - and the commit moves only the marked portions during the blank: all 128 sprite-attribute bytes if any sprite moved, and 32 bytes for each grid row whose dirty bit stands. On a frame where only the fly moved, the traffic is one table; on a still frame, none. Motion becomes cheap in exactly the way whole-scene redraws were cheap on the matrix: moving the fly is two shadow bytes, wherever it stands on a 256x192 screen.
That scale rewrites the rules. Positions are pixels now, so
Lanternfly’s collision is the distance between two facts - absolute
pixel difference per axis, each under a tolerance of 6 - and the
tolerance itself became a design decision Skyfall never faced,
choosing overlap over touch. The lantern pickup crosses the two
coordinate systems on purpose: the fly lives in pixels, the lantern
in grid cells, so Gather centres the fly (+4), divides by eight
(three shifts), and compares cells. And erasing turned explicit.
When Gather takes a lantern, it blanks the old grid cell itself,
inside the effect, because four lines later the respawn overwrites
LampCol and LampRow and no render would ever again know which
cell to clear. A persistent scene remembers what you drew, including
what you meant to remove.
The commit pacing sets this profile’s motion cost: a held key reaches the screen two frames later - defer, shadow write, commit - at full rate, sixty-odd pixels a second. Skyfall’s paddle reaches the next scan one frame after its pulse. Both games feel immediate; the difference matters the day you count frames in the debugger and find the pipeline exactly where these chapters said it would be.
One table holds the divergence:
| Skyfall, matrix | Lanternfly, VDP | |
|---|---|---|
| The scene | 32 bytes, redrawn on change | 768 cells + 32 sprites, persistent in VRAM |
| A render writes | the whole framebuffer | the shadow bytes that changed |
| Who shows it | ScanFrame, every frame |
the VDP, from VRAM, on its own |
| Positions | cells on an 8x8 board | pixels on 256x192; grid cells, 32x24 |
| Collision | one subtract, one compare | pixel distance per axis, under a tolerance |
| Erasing | FbClear opens each redraw |
an explicit blank of the old cell |
| Game budget | the blank window between scans | the vblank window between paintings |
One language
Now read what the display line left alone. Skyfall and Lanternfly
declare their games in interchangeable sentences: state bytes and
words for facts, pulses for moments, bind key ... held for
steering and bind key any rising for the restart key, a writable
oscillator as the difficulty screw - Gravity at 18 quickened to a
floor of 6, Pace at 8 quickened to a floor of 1, the same dec
and store in both - and a one-shot word timer armed at 90 frames to
gate the restart. Three cards each, entered through enter blocks
that re-raise what their renders need, left by goto or a
conditional write to CurrentCard. The whole GameOver card moved
between profiles without an edit.
The phases carried over too, with their delivery rule intact. Both
games trust that a render draws a settled world; both stage changes
through Raised0 and Next0 by the same exactly-once rule; both
print their design with glimmer --deps in the same report shape,
raisers and triggers per fact. Skyfall spends 12 of the 32
change-flag cells, Lanternfly 16, on the same budget.
The dividing line runs exactly where the loops drew it. The profile
owns the loop, so everything about showing - scan or commit,
framebuffer or shadow, FbPlot or SpriteSet - came from one
declaration and lives above the identical tail. You own the
behaviour, and the model that carries it - facts, moments, rules,
pictures, phases, cards - ran unchanged under both. Two displays,
one language: the display decides how pictures happen, and the
language decides what a game is.
So choosing a display for your next idea is a question about the world it needs. A game whose world is a board of cells that change together - pieces locking, lines clearing, a body growing - belongs on the matrix, where the whole scene is 32 bytes and cell arithmetic answers everything. A game whose world is a place - standing scenery, a few movers gliding over it, room to travel - belongs on the VDP, where persistence and size cost nothing and motion is two bytes. Either way, the declarations you write first, chapter 14’s habit, will read almost the same.
Where the road goes
The Glimmer repository’s examples/ directory holds seven built,
running programs, and every one of them is now readable with what
you know. counter, dot, slide, and trail are single-idea
warm-ups. snake.glim is the matrix under a different pressure than
Skyfall’s: a growing body in a 64-byte ring buffer, with its
body-scan and draw loops in an imported AZM engine. You read
tetro.glim in chapter 15 and sprite-chase.glim in 17; both reward a
second visit as yours to change - a new piece, a smarter fleeing
target. Bending a working game teaches what building one began.
When the engine files you import grow past helpers into modules of their own, the AZM books hold the assembler’s whole story: ops, routines, register contracts, and the module system Glimmer’s output leans on. Debug80 Book 1 covers the workshop end to end, from project setup to sending a build to a physical board.
And the board is the last stop worth naming. Every program in this book produced a HEX file beside its assembly, and that file runs on a real TEC-1G exactly as it ran in the emulator - the same bytes, the same scan or the same commit, with actual LEDs doing the glowing. If a board is within reach, Skyfall on real hardware is one transfer away.
Summary
- The two profiles differ in the loop’s head and agree in its tail:
ScanFrameproduces the matrix picture and the game runs in the blank that follows;VdpWaitVBlankandGlimCommitpace the VDP game and flush shadows while the VDP rests. Poll, latch, tick, phases, and rollover are identical. - The matrix makes whole scenes cheap: 32 bytes,
FbClearand repaint, positions as cells, collision as byte arithmetic - the shape of Skyfall, Snake, and Tetro. - The VDP makes persistence and motion cheap: scenery written once, movers as two shadow bytes, positions as pixels, collision as distance under a tolerance, erasing done explicitly - the shape of Lanternfly and Sprite Chase.
- State, pulses, bindings, timers, phases, delivery, and cards are identical under both displays; Lanternfly reuses Skyfall’s GameOver card verbatim. The profile owns the loop; you own the behaviour.
- Choose the display by the world the game needs: a board of cells that change together, or a place with movers over standing scenery.
A game is facts, moments, rules, and pictures, and you can now build one from an empty file on either display the TEC-1G offers. Every game you write from here starts the way Mover did: one fact, one picture, and a connection between them.