Chapter 16 - The TMS9918 Profile
Since chapter 6, every picture in this book has come from the matrix, and the matrix shows only what the CPU is actively pushing: the profile loop spends most of each frame driving the LED rows from the framebuffer, and your blocks work in the blank that follows. This chapter’s display paints itself. The TEC-Deck expansion card adds a TMS9918 video display processor to the TEC-1G: a second chip beside the Z80 with 16 KiB of video RAM of its own, painting a 256x192 picture from that memory over and over, whether the Z80 is busy or idle. The picture holds steady while your code takes its time, because showing it is the VDP’s job now. Yours is describing the scene.
The scene, in the VDP’s Graphics I mode, has two layers. The background is a grid, 32 columns by 24 rows, each cell showing one 8x8-pixel tile pattern; a table of one byte per cell, the name table, says which. In front of the grid stand sprites: 32 small patterns, each at a pixel position of its own, moved by rewriting two bytes. Scenery belongs in tiles; anything that glides belongs in sprites.
The Z80 reaches the VDP’s memory through two ports: control at $BF,
data at $BE. Two control-port writes set a VRAM address, and the
data port then streams bytes to consecutive addresses. The traffic
also has a best moment: after painting the last line of a picture the
VDP rests before starting the next - the vertical blank - and raises
a flag in its status register. The profile paces the whole program on
that flag, and it moves your VRAM traffic into that resting window.
Your blocks keep writing plain RAM, as they always have; the ports
belong to the generated library.
Selecting all of this takes one changed line: display tms9918, with
the platform line as before. The keypad, the LCD, and text
declarations live on the board itself and work the same here; sound
cues, shape, and the seven-segment HUD service stay with the matrix
profile.
Grove
The chapter program is Grove: a white moth over a night garden. Ferns and blooms stand still in the tile grid; the moth is a sprite, steered with 2/4/6/8, one pixel per frame while a key is held.
program Grove
platform tec1g-mon3
display tms9918
sprite Moth color white
"X..XX..X"
"XX.XX.XX"
"XXXXXXXX"
".XXXXXX."
"..XXXX.."
"...XX..."
"...XX..."
"........"
end
tile Fern color lightgreen on black
"...X...."
"X..X..X."
".X.X.X.."
"..XXX..."
"X..X..X."
".X.X.X.."
"..XXX..."
"...X...."
end
tile Bloom color lightred on black
"........"
"..X.X..."
".XXXXX.."
"..XXX..."
".XXXXX.."
"..X.X..."
"...X...."
"...X...."
end
state MothX : byte = 124 changed
state MothY : byte = 92 changed
state Init : byte = 0 changed
pulse Up
pulse Down
pulse Left
pulse Right
bind key KEY_2 held period 1 -> Up
bind key KEY_8 held period 1 -> Down
bind key KEY_4 held period 1 -> Left
bind key KEY_6 held period 1 -> Right
effect PlantScene
on Init
begin
tile_at Fern, 4, 18
tile_at Fern, 9, 6
tile_at Fern, 14, 20
tile_at Fern, 20, 9
tile_at Fern, 26, 16
tile_at Bloom, 6, 10
tile_at Bloom, 16, 4
tile_at Bloom, 23, 19
end
effect MoveUp
on Up
updates MothY
begin
ld a,(MothY)
or a
jr z,_stop ; at the top edge: stay
dec a
ld (MothY),a
_stop:
end
effect MoveDown
on Down
updates MothY
begin
ld a,(MothY)
cp 184 ; bottom clamp: 192 - sprite height
jr nc,_stop
inc a
ld (MothY),a
_stop:
end
effect MoveLeft
on Left
updates MothX
begin
ld a,(MothX)
or a
jr z,_stop ; at the left edge: stay
dec a
ld (MothX),a
_stop:
end
effect MoveRight
on Right
updates MothX
begin
ld a,(MothX)
cp 248 ; right clamp: 256 - sprite width
jr nc,_stop
inc a
ld (MothX),a
_stop:
end
render PlaceMoth
on MothX, MothY
begin
sprite_at Moth, MothX, MothY
end
The middle of the file is Rover with pixel coordinates: four pulses,
four held bindings at period 1, four clamped move effects. The clamps
say 248 and 184 because positions now name the sprite’s top-left
pixel on a 256x192 screen and the pattern is 8 pixels square.
Everything new sits at the two ends: three resource declarations at
the top, and two block bodies - PlantScene and PlaceMoth - whose
lines mention them. Take the new material in order.
Sprites and tiles are declarations
A sprite declaration is eight quoted rows of eight characters: X
for a lit pixel, . for a transparent one. Where the moth’s pattern
has dots, the garden shows through. color white picks one of the
sixteen VDP colours - transparent, black, medgreen,
lightgreen, darkblue, lightblue, darkred, cyan, medred,
lightred, darkyellow, lightyellow, darkgreen, magenta,
gray, white - and the whole sprite wears it. Inside block bodies
the same sixteen names exist as VC_* equates.
Declaration order does the numbering. The first sprite is slot 0
and pattern 0, the second slot 1, and so on, up to 31 sprites in a
program. In the generated file the pattern rows become bytes and the
name becomes its slot:
GlimSpritePats:
.db %10011001
.db %11011011
.db %11111111
.db %01111110
.db %00111100
.db %00011000
.db %00011000
.db %00000000
Moth .equ 0 ; sprite slot + pattern
A tile carries two colours, foreground on background, and its
rows read the same way. Its numbering follows the hardware: Graphics
I colours patterns in banks of eight, one colour byte covering
patterns 0..7, the next 8..15, and onward through 32 banks. Tiles
that share an (fg, bg) pair fill a bank together, and a new pair
opens a new bank. Index 0 stays the blank tile that fills every empty
cell; it lives in the first pair’s bank, so the first pair’s
background is the screen background. Grove declares black behind
both of its pairs, and the garden sits in darkness. The two pairs
also explain the generated indexes:
Fern .equ 1 ; tile index
Bloom .equ 8 ; tile index
Fern takes the seat beside the blank tile in bank 0; Bloom’s colour pair is new, so it opens bank 1 at index 8. Six more ferns and seven more blooms would cost nothing; a sixteenth distinct colour pair would open bank 16 of the 32 the colour table holds.
Where a render writes
On the matrix, renders drew into a framebuffer. Here they write two shadow tables: ordinary RAM, laid out to match the VRAM tables the VDP reads. From the generated state storage:
NameShadow: .ds 768, 0 ; 32x24 name table shadow
NameDirtyRows: .db 0, 0, 0 ; 24 dirty-row bits
SpriteShadow: .ds 128, 0 ; 32 x (y, x, pattern, colour)
SpriteDirty: .db 0
NameShadow mirrors the name table, one byte per grid cell.
SpriteShadow mirrors the sprite attribute table: four bytes per
sprite - y, x, pattern, colour - for all 32 slots. Beside each
shadow sits its own bookkeeping: three bytes of NameDirtyRows carry
one bit per grid row, and the single SpriteDirty byte covers the
whole sprite table.
Three profile routines write the shadows, and each declares its register interface in the generated file, the way chapter 11 taught you to read:
; Put tile A at column D, row E of the name-table shadow and mark
; the row dirty.
.routine in A,D,E clobbers A,BC,DE,HL,carry,zero,sign,parity,halfCarry
NamePut:
NamePut- A = tile index, D = column, E = row. Stores the shadow cell and marks the row’s dirty bit.SpriteSet- A = slot, D = x, E = y. Positions a sprite and setsSpriteDirty.SpriteInit- A = slot, D = pattern number, E = colour. Gives a slot its look; the generated startup calls it once per sprite.
For the common calls, Glimmer generates two AZM ops, so a body reads as a sentence:
op sprite_at(slot imm8, xcell imm16, ycell imm16)
ld a,(xcell)
ld d,a
ld a,(ycell)
ld e,a
ld a,slot
call SpriteSet
end
op tile_at(tile imm8, col imm8, row imm8)
ld a,tile
ld d,col
ld e,row
call NamePut
end
An op expands inline where it is written, so sprite_at Moth, MothX,
MothY in PlaceMoth becomes those six instructions: read the two
cells into D and E, load the slot, call SpriteSet. tile_at takes
immediate coordinates, which suits placing a scene; when a column or
row arrives in a register at runtime, load A, D, and E yourself and
call NamePut directly.
The commit-shaped loop
Build Grove and open grove.main.asm at the loop:
; --- 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
call GlimRunLogicEffects
call GlimMergeRaised
call GlimRunRenderEffects
call GlimEndFrame
jp MainLoop
From GlimPollBindings down, this is the frame you have known since
chapter 2. The profile’s character is the two calls above it. On the
matrix, ScanFrame produced the picture; here VdpWaitVBlank waits
for one. The routine polls the status register until the vblank flag
comes up - reading the register clears it, arming the next frame -
so the program takes exactly one trip around MainLoop per picture
the VDP paints. Glimmer still owns the loop; the VDP now owns the
clock.
GlimCommit then spends the blank window moving the previous frame’s
shadow writes into VRAM: the whole sprite table if SpriteDirty is
set, and each name-table row whose dirty bit is marked. Only after
the shadows are flushed does the frame poll keys and run your phases,
whose renders write the shadows anew.
Follow a held key 6 through one frame. The poll fires Right;
MoveRight steps MothX; render is a later phase, so the change
arrives the same frame, PlaceMoth runs, and sprite_at writes the
new x into SpriteShadow and sets SpriteDirty. The screen still
shows the old position. The next frame opens in the blank:
GlimCommit streams the sprite shadow to VRAM, and the moth stands
one pixel to the right. Every shadow write reaches the screen at the
top of the following frame, tiles and sprites alike.
A scene planted once
PlantScene runs exactly once, and the trigger is a mechanism you
have owned since chapter 3: Init is a byte declared changed, so
its flag is up before the first frame; the effect fires on frame 1,
places eight tiles, and Init never changes again. The block carries
no updates line because it changes no cell - its work lands in the
name shadow, which the profile tracks with row bits instead of change
flags.
Those eight tile_at lines touch eight different grid rows, so frame
1 ends with eight dirty bits standing, and frame 2’s commit streams
eight rows into VRAM. From then on the garden is the VDP’s to paint,
picture after picture, and the program never redraws it. A matrix
render repaints its layer whenever a fact changes; a VDP program
writes each cell once and writes again only what differs. MothX and
MothY are declared changed for the same startup reason: on frame
1, PlaceMoth puts the moth into the sprite shadow alongside the
garden, and the first picture arrives whole.
When a program has cards, an enter block is the natural home for
scene placement, and chapter 17 plants its scene that way. Grove has
one screen, so a changed cell provides the run-once trigger with the
tools already on the table.
Inside the generated file
Two generated routines carry this chapter’s machinery, and both reward a slow read. First the commit. Its sprite half is four decisions long:
; --- commit: flush dirty shadows to VRAM ---
.routine clobbers A,BC,DE,HL,carry,zero,sign,parity,halfCarry
GlimCommit:
ld a,(SpriteDirty)
or a
jr z,_names
xor a
ld (SpriteDirty),a
ld hl,VRAM_SPRITE_ATTR
call VdpSetAddrWrite
ld hl,SpriteShadow
ld bc,128
call VdpWriteBlock
One flag guards one stream: when any sprite moved, all 128 bytes go, a table small enough to send whole. The name table is 768 bytes, and that size is why the rows carry individual bits. The rest of the routine is the dirty-row loop:
_names:
ld d,0 ; D = dirty-row group 0..2
ld b,d ; B is dead here; defined only so AZM 0.3
; stale-register analysis accepts the push bc
; save/restore below (only C is live)
_group:
ld hl,NameDirtyRows
ld a,l
add a,d
ld l,a
ld a,h
adc a,0
ld h,a
ld a,(hl)
or a
jr z,_next
ld (hl),0 ; consume the group
ld c,a ; C = dirty bits, rows D*8 .. D*8+7
ld e,0 ; E = bit within the group
_bits:
srl c
jr nc,_nbit
push de
push bc
ld a,d
add a,a
add a,a
add a,a
add a,e ; row = group*8 + bit
call CommitNameRow
pop bc
pop de
_nbit:
inc e
ld a,e
cp 8
jr c,_bits
_next:
inc d
ld a,d
cp 3
jr c,_group
ret
D walks the three group bytes, eight rows to a byte. A zero group
byte costs one read and moves on; a marked one is consumed on the
spot with ld (hl),0, and srl c shifts its bits out one at a time.
Each bit that falls into carry names a row - group times eight plus
bit - and CommitNameRow turns the row number into row*32, points
the VDP at VRAM_NAME plus that offset, and streams 32 shadow bytes
through the data port. On a Grove frame where only the moth moved,
the commit reads one flag, sends 128 bytes, reads three group bytes,
and is done.
The second routine is the one-time upload that Start calls before
the loop begins:
; Upload sprite/tile patterns and the colour groups; assign each
; sprite slot's pattern and colour in the shadow. Called once from
; the loop init, after VdpInit.
.routine clobbers A,BC,DE,HL,carry,zero,sign,parity,halfCarry
LoadResourcesVram:
ld hl,VRAM_SPRITE_PAT
call VdpSetAddrWrite
ld hl,GlimSpritePats
ld bc,8
call VdpWriteBlock
ld a,0 ; Moth
ld d,0
ld e,VC_WHITE
call SpriteInit
ld hl,VRAM_PATTERN + 8 ; Fern
call VdpSetAddrWrite
ld hl,GlimTilePats + 0
ld bc,8
call VdpWriteBlock
ld hl,VRAM_PATTERN + 64 ; Bloom
call VdpSetAddrWrite
ld hl,GlimTilePats + 8
ld bc,8
call VdpWriteBlock
ld hl,VRAM_COLOR + 0
call VdpSetAddrWrite
ld a,VC_LIGHTGREEN * 16 + VC_BLACK
out (VDP_DATA),a
ld hl,VRAM_COLOR + 1
call VdpSetAddrWrite
ld a,VC_LIGHTRED * 16 + VC_BLACK
out (VDP_DATA),a
ret
Every declaration from the top of grove.glim is here as address
arithmetic. The moth’s eight pattern bytes stream to the
sprite-pattern area, and SpriteInit records slot 0’s pattern and
colour in the shadow. Each tile’s eight bytes land at VRAM_PATTERN
plus index times eight - Fern at +8 for index 1, Bloom at +64 for
index 8 - and each colour bank gets its single byte, foreground times
sixteen plus background.
Before any of this, VdpInit programs the VDP’s eight registers from
a small table, clears the pattern and name tables, and hides all 32
sprites by writing $D1 into every shadow y. That value earns its
keep twice: a sprite whose y is $D1 is off-screen, and the VDP
stops processing sprites at the first slot holding it. Declaration
order keeps your live sprites contiguous from slot 0, so the
terminator always sits right after them.
Summary
display tms9918selects the TEC-Deck VDP: a processor with 16 KiB of private VRAM, reached through the control port$BFand data port$BE, painting a 256x192 Graphics I picture on its own: a 32x24 grid of 8x8 tiles with 32 sprites in front.- The generated loop paces on the vertical blank:
VdpWaitVBlank, thenGlimCommitflushing dirty shadows to VRAM inside the blank window, then polling and the three phases. - Render blocks write shadow tables in ordinary RAM.
NamePut(A = tile, D = column, E = row) marks its row dirty;SpriteSet(A = slot, D = x, E = y) andSpriteInit(A = slot, D = pattern, E = colour) setSpriteDirty. spriteandtiledeclarations are eight rows ofXand.in one of sixteen VDP colours. Declaration order is the sprite slot and pattern number; tiles bank by (fg, bg) pair in eights, and the first pair’s background is the screen background.- The generated
sprite_atandtile_atops expand inline onto the library calls;LoadResourcesVramuploads all patterns and colour banks once at startup. - A shadow write reaches the screen at the top of the following
frame. A scene written once persists in VRAM, and a changed cell
like
Initis the cardless way to place it in a single run. - Sprite slots stay contiguous from 0: y =
$D1hides a sprite and ends sprite processing at the first unused slot.
Next, the profile carries a full game: sprite collision, scoring on the tile grid, and cards on the VDP.