Chapter 6 - The Matrix Profile

The TEC-1G’s matrix is eight rows of eight RGB LEDs, and the hardware lights one row at a time. Three ports carry a row’s red, green, and blue column data; a fourth selects the row that shows it. The CPU paints row 0, holds it lit a moment, paints row 1, and so on around the board. Sweep all eight rows quickly, over and over, and your eye fuses them into a steady picture. Stop sweeping and the matrix goes dark.

A program on this display carries a second job beside the game: the CPU is the display controller. Chapter 1 called showing the current picture the program’s own job, every frame, forever - and every program you have built since has done that job without a line of your code asking for it. This chapter opens the machinery: the scan that keeps the matrix lit, the loop shape it forces on the frame, the 32 bytes of memory your renders have been writing all along, and the library routines that write them.

The chapter’s program is Compass. Hold GO and a dot runs clockwise around the rim of the matrix, coloured by the quadrant it is crossing: red along the top, green down the right side, blue along the bottom, yellow climbing the left - north, east, south, west. Let go and it parks where it is.

Compass

program Compass

platform tec1g-mon3
display matrix8x8

state Position : byte = 0 changed

state DotX   : byte
state DotY   : byte
state Colour : byte

pulse Step

bind key KEY_GO held period 4 -> Step

effect Advance
    on Step
    updates Position
begin
    ld a,(Position)
    inc a
    cp 28
    jr c,_store     ; 27 wraps to 0
    xor a
_store:
    ld (Position),a
end

compute PlaceDot
    on Position
    updates DotX, DotY, Colour
begin
    ld a,(Position)
    cp 7
    jr c,_north
    cp 14
    jr c,_east
    cp 21
    jr c,_south
_west:              ; 21..27: left edge, climbing
    ld b,a
    ld a,28
    sub b           ; y = 28 - Position
    ld (DotY),a
    xor a
    ld (DotX),a
    ld a,COLOR_YELLOW
    jr _colour
_north:             ; 0..6: top edge, heading right
    ld (DotX),a     ; x = Position
    xor a
    ld (DotY),a
    ld a,COLOR_RED
    jr _colour
_east:              ; 7..13: right edge, heading down
    sub 7
    ld (DotY),a     ; y = Position - 7
    ld a,7
    ld (DotX),a
    ld a,COLOR_GREEN
    jr _colour
_south:             ; 14..20: bottom edge, heading left
    ld b,a
    ld a,21
    sub b           ; x = 21 - Position
    ld (DotX),a
    ld a,7
    ld (DotY),a
    ld a,COLOR_BLUE
_colour:
    ld (Colour),a
end

render DrawDot
    on DotX, DotY, Colour
begin
    call FbClear
    ld a,(DotX)
    ld b,a
    ld a,(DotY)
    ld c,a
    ld a,(Colour)
    call FbPlot
end

One fact drives the whole picture. The rim of an 8x8 matrix is 28 pixels, and Position numbers them 0 to 27, clockwise from the top-left corner. Advance is the entire movement rule: step forward, and past 27 wrap to 0. Held GO fires Step every 4 frames, so the dot orbits for as long as the key stays down.

PlaceDot carries the geometry. Position is the fact the game reasons about; the screen wants an x, a y, and a colour, and the compute derives all three in one place. A threshold ladder splits the rim into its quadrants - positions 0 to 6 lie on the top edge, 7 to 13 on the right, 14 to 20 on the bottom, 21 to 27 on the left - and each arm converts Position to coordinates. The top edge counts x upward; the bottom edge counts it back down; the sides do the same with y. Every arm leaves its quadrant’s colour in A for the shared store at _colour.

updates DotX, DotY, Colour declares the three products, and DrawDot depends on all three. The cells change together because one block writes them together, so the render always reads a settled trio: the geometry lives in the compute, the rule in the effect, the picture in the render, each block one job, connected only by the headers.

Chapter 5’s delivery rule runs through the middle of this program. Advance sits in the logic phase, and its consumer PlaceDot is a compute - an earlier phase, already finished for this frame. So the change to Position defers: the wrapper after Advance’s body stages it in Next0, and the dot you see moves on the frame after the pulse, one frame behind, every step, whatever order the source declares. A lap of the rim is 28 steps at 4 frames each, 112 frames - around two seconds at the scan’s sixty-odd frames a second. Halve the period and you halve the lap.

Build it, run it, and hold GO: the dot sets off along the top in red and changes uniform at every corner. Release anywhere and it waits, lit, in the colour of its quadrant.

The scan-shaped loop

Every program in this book has opened with the same two lines:

platform tec1g-mon3
display matrix8x8

They select the program’s profile: everything the generated file contains beyond your own declarations. The port addresses, the MON-3 key codes, the polling routine, the shape of the runtime loop, and the library at the bottom of the file all come from this one choice. platform names the board and monitor, which is where KEY_GO and the _scanKeys polling come from. display names the output device, and it is the stronger choice of the two, because the display decides the loop itself. Chapter 16 puts tms9918 on that line and gets a loop built around a video chip; the reactive core - state, flags, dispatch, rollover - stays the same.

Here is what matrix8x8 builds, from compass.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
        call    GlimRunDeriveEffects
        call    GlimMergeRaised
        call    GlimRunLogicEffects
        call    GlimMergeRaised
        call    GlimRunRenderEffects
        call    GlimEndFrame
        jp      MainLoop

At Start the profile clears its canvas and display once; then the loop, and ScanFrame leads it: one complete pass over the matrix, all eight rows, each lit for a fixed dwell, returning with the matrix dark. Every other call runs in that blank window - polling, your three phases, the rollover. Renders write memory while nothing is showing, and the next scan presents their combined result, so the player only ever sees finished pictures.

The fixed dwell is the profile’s answer to steadiness. Each row shines for the same count on every frame, so brightness stays even across rows and across laps of the loop; a frame heavy with game work differs from an idle one only in the length of the dark gap between scans, far below what the eye can catch. And the scan is by far the frame’s largest cost, which is what lets the book treat the frame as the unit of game time: the scan paces it.

The framebuffer

ScanFrame reads its picture from one place. In the state storage, directly after your facts, the profile reserves it:

; --- state storage ---
Position:         .db 0
DotX:             .db 0
DotY:             .db 0
Colour:           .db 0
Step:             .db 0
Glim_HeldKey:     .db $FF
Glim_HeldCount:   .db 0
Changed0:         .db %00000001   ; flags dispatch tests
Raised0:          .db 0   ; raises for later phases this frame
Next0:            .db 0   ; raises deferred to next frame
Framebuffer:      .ds 32           ; 8 rows x R,G,B,aux

Thirty-two bytes hold the whole picture: eight rows of four bytes - red, green, blue, and a fourth, aux, that the scanner steps over. Each of the three plane bytes carries one bit per column. A pixel is one column bit, present in up to three planes: set it in the red byte alone and the pixel glows red; set it in red and green and the pixel glows yellow. The seven visible colours from chapter 2 are the seven ways to occupy one, two, or three planes, and the profile’s constants spell that out:

COLOR_RED         .equ $01
COLOR_GREEN       .equ $02
COLOR_BLUE        .equ $04
COLOR_YELLOW      .equ COLOR_RED + COLOR_GREEN
COLOR_CYAN        .equ COLOR_GREEN + COLOR_BLUE
COLOR_MAGENTA     .equ COLOR_RED + COLOR_BLUE
COLOR_WHITE       .equ $07

A colour is a set of plane bits, and the compound colours are sums. This is the A register you have loaded before every FbPlot since chapter 1.

FbPlot turns x, y, and colour into plane-byte writes. Its head, from the profile library:

; Set one pixel. B = x (0-7), C = y (0-7), A = colour bits
; (COLOR_RED/GREEN/BLUE, OR-combined). ORs into the framebuffer.
.routine in A,B,C clobbers A,B,DE,HL,carry,zero,sign,parity,halfCarry
FbPlot:
        ld      d,a                  ; D = colour bits
        ld      a,c
        add     a,a
        add     a,a                  ; y * 4
        ld      e,a
        ld      a,b
        call    MxMask               ; A = pixel mask
        ld      b,a

Here the aux byte earns its keep: at four bytes a row, the row’s address is Framebuffer + y * 4, and multiplying by four is two add a,a instructions. The rest of the routine shifts the colour bits out of D one at a time, ORing the pixel mask into each plane byte whose bit is set. ORing means FbPlot adds light: plot red and then green at the same coordinates and that pixel shows yellow. A clean picture starts from FbClear, which zeroes the 32 bytes - the call that has opened every render in this book that redraws.

The .routine line above the label is the register interface, declared in the generated file and checked on every build: FbPlot consumes A, B, and C, and clobbers A, B, DE, and HL. C survives the call; chapter 5’s DrawBar kept its loop counter in B, a clobbered register, which is why it pushed BC around the call. When a block of yours misuses a library routine’s registers, the build fails with the contract, and these lines are where you read what the contract is.

MxMask is the small helper FbPlot leans on:

; Convert x (0-7, 0 = leftmost) to the matrix bit convention.
.routine in A out A clobbers B,carry,zero,sign,parity,halfCarry
MxMask:
        or      a
        ld      b,a
        ld      a,%10000000
        ret     z
_loop:
        srl     a
        djnz    _loop
        ret

x 0 is the leftmost column and bit 7 of the plane byte, so a binary literal in your source reads left to right like the matrix itself. MxMask is callable from your own blocks too, for the day a render builds whole row masks instead of plotting pixel by pixel.

Any fact you can turn into an x, a y, and three colour bits, you can draw. The compute is yours; the plot is one call.

ScanFrame, top to bottom

The scanner itself is twenty-nine instructions, and reading it closes the loop this chapter opened. The four ports it drives are equates from the top of the generated file: PortRow at $05 selects the row, and PortRed, PortGreen, and PortBlue at $06, $F8, and $F9 take the plane bytes. The routine, whole, from the profile library:

; Scan all 8 rows with fixed dwell, then blank the matrix so
; block work never changes visible row brightness. Sound and the
; seven-segment HUD are serviced once per row (8 ticks per frame).
.routine clobbers A,BC,DE,HL,carry,zero,sign,parity,halfCarry
ScanFrame:
        ld      hl,Framebuffer
        ld      c,%00000001          ; row select mask
_row:
        xor     a
        out     (PortRow),a          ; blank before changing colour data
        ld      a,(hl)
        out     (PortRed),a
        inc     hl
        ld      a,(hl)
        out     (PortGreen),a
        inc     hl
        ld      a,(hl)
        out     (PortBlue),a
        inc     hl
        inc     hl                   ; skip aux byte
        ld      a,c
        out     (PortRow),a          ; enable row
        push    bc
        push    hl
        call    SndService
        call    HudScanDig
        pop     hl
        pop     bc
        ld      b,ScanDwellPeriod
_dwell:
        djnz    _dwell
        rlc     c
        jr      nc,_row      ; carry after 8th rotate
        xor     a
        out     (PortRow),a          ; matrix blank on return
        ret

C holds the row select as a one-hot mask, %00000001 for row 0. Each pass around _row blanks the matrix, writes the row’s three plane bytes to the colour ports, steps past the aux byte, and switches the row on. rlc c slides the select bit up one row; after the eighth rotate the bit falls into carry, the loop exits, and a final blank leaves the matrix dark for the game work to come.

The blank at the top of the pass has a reason. The colour ports feed whichever row is enabled, so the previous row goes dark before its data changes hands; skip that blank and each row would flash its neighbour’s colours for an instant, every row, every frame - a ghost of the picture smeared one row over. Then the dwell: djnz spinning B down from ScanDwellPeriod, 255, the wait that sets how long a row shines and, eight times over, how long a frame lasts.

In the middle of every pass sit two calls, SndService and HudScanDig. The scan is the program’s heartbeat, eight beats a frame, and the profile hangs its other board services on it: an active sound cue toggles the speaker here, and one seven-segment digit is strobed here per beat - the score display chapter 3 wrote through HudWriteU16 stays lit the same way the matrix does. Chapter 9 builds on both services.

Step through a pass under Debug80 whenever you like: a breakpoint on ScanFrame in compass.main.asm catches the frame at its start. The display you have been drawing to since chapter 1 is this one routine, reading those 32 bytes, sixty-odd times a second.

Summary

  • platform and display select the profile: port equates, key codes, polling, the loop’s shape, and the library are its contribution to the generated file. The reactive core is the same under every profile.
  • The matrix is scanned: ScanFrame lights all eight rows with a fixed dwell, then blanks. All game work runs in the blank window, so the player only ever sees finished pictures, and the scan’s cost paces the frame.
  • The framebuffer is 32 bytes: 8 rows of red, green, and blue plane bytes plus an aux byte, one bit per column. A colour is a set of planes; COLOR_* names the sets; the aux byte makes a row’s address y * 4.
  • FbClear zeroes the picture. FbPlot (B = x, C = y, A = colour bits) ORs one pixel into the planes its colour names - it adds light, so overlapping plots mix. MxMask converts x to the column bit, leftmost is bit 7.
  • .routine lines in the generated file are the library’s register contracts: what a routine consumes, what it clobbers, checked on every build. C survives FbPlot.
  • Drawing is deriving: turn any fact into x, y, and colour in a compute, and one FbPlot puts it on the board. Compass draws a 28-position orbit from a single byte.

Compass moves while GO is held and rests the moment it lifts; next chapter the program gets a clock of its own and moves while the player watches: Time.