Skip to content
Game 1Unit 2 of 201 hr learning time

The Cobbles and the Brick

Write the bitmap under the attributes — meet the Spectrum's famous screen layout, blit an 8-byte texture into every ground cell, then let the map itself decide where the brick goes.

10% of Gloaming

The square stands, but look at it: solid blocks of colour. Paint with no canvas under it. The ground should be cobbled — worn stone catching what light is left — and the walls should read as built, course on course of brick. This unit lays both, and on the way it meets the most famous oddity on the machine: how the Spectrum’s bitmap is actually arranged.

A confession, first

When we first built this game, we skipped this unit. The attributes worked, the game grew — and weeks later, whole mechanics landed invisible, because they worked by recolouring pixels and there were no pixels to recolour. A feature’s visibility can live in a different subsystem from its logic. We built the rules before the canvas, and the canvas turned out to be load-bearing. So it comes second in the whole course, right after the square itself: the canvas precedes the paint.

Writing the canvas

Unit 1 called the bitmap and the attributes the screen’s two memories, and then only ever wiped the bitmap. Today we write it. The rules of the game don’t change: a lit pixel shows its cell’s INK, an unlit one its PAPER. The cobbles’ INK has been blue since Unit 1 — set pixels in a ground cell and they’ll come up blue on black without touching the attributes at all. The paint has been waiting for the canvas.

The famous layout

One cell’s pixels are eight bytes — but they are not eight bytes in a row. The Spectrum’s screen is wired in three thirds of eight character rows each, and inside a cell the eight pixel rows sit 256 bytes apart. Step down one pixel row by adding 256 — on the Z80, that’s a single inc h.

The bitmap on the screen — $4000-$57FF in three thirdsthe whole screen, 256×192third 0 — from $4000char rows 0-7third 1 — from $4800char rows 8-15third 2 — from $5000char rows 16-23zoom: the third's first 16 pixel lines$4000row 0 · line 0$4100row 0 · line 1$4200row 0 · line 2$4300row 0 · line 3$4400row 0 · line 4$4500row 0 · line 5$4600row 0 · line 6$4700row 0 · line 7$4020row 1 · line 0$4120row 1 · line 1$4220row 1 · line 2$4320row 1 · line 3$4420row 1 · line 4$4520row 1 · line 5$4620row 1 · line 6$4720row 1 · line 7+256inc h+32
Where the bytes fall. Each third starts $0800 on from the last — and inside a third, the zoom shows the interleave: char row 0's eight pixel lines are $4000, $4100 … $4700, stepping 256 each time, while char row 1's top line is only 32 bytes in, at $4020. Consecutive bytes run across the screen; consecutive lines of one cell sit 256 apart. (This is why loading screens fill in stripes.)

To texture any cell, we need its bitmap address from its column and row at run time. This is scr_addr_cr, and it’s pure bit-shuffling: write the row number as %000ttrrr — two bits tt naming the third, three bits rrr for the character row within it — and the pieces land straight into the two halves of the address:

H — Bitmap address, high bytebase %010third tt%000740621510420311240120010
H: the fixed %010 of the $4000 screen base, then the row's third — the tt bits, kept exactly where they sat in the row number. The value shown is row 11's: third %01, so H = $48.
L — Bitmap address, low byterow rrrcolumn7406215114160381241121011
L: the character row within the third — the rrr bits, rotated up to the top — then the column in the low five bits. The value shown is row 11, column 15: rrr %011, col %01111, so L = $6F.

Two ANDs, three rotates, two ORs — no multiplication, no lookup table. The wiring that makes the layout strange also makes the address cheap.

Milestone 1 — one texture, every ground cell

A texture is just eight defb bytes — an 8×8 stamp. The cobbles are a sparse stipple: a scatter of single pixels, black left dominant so the ground stays dark:

$82$00$08$00$21$00$10$00
cobble_tex — six lit pixels in 64, no two in the same column, every other row empty. Sparse on purpose: the dusk needs the black.

blit_tex stamps those eight bytes into one cell — scr_addr_cr finds the top row, inc h walks the other seven. fill_ground calls it for every cell in rows 1–23 (row 0 stays clean: the HUD). Note the loop order — texture first, then the attribute wash — mirrors what you already know: canvas, then paint.

Step 1: blit the cobble stipple into every ground cell
+73
1818 ; screen before us still lives there. Zero it so only our
1919 ; attribute colours show.
2020 call clear_bitmap
21+
22+ ; --- texture the ground ---
23+ ; Blit the cobble stipple into every cell's bitmap, rows 1-23.
24+ ; The attributes will colour these pixels in a moment.
25+ call fill_ground
2126
2227 ; --- wash in the cobbles ---
2328 ; Seed the first attribute cell, point DE one cell ahead, and
...
8388 ld (hl), 0
8489 ld bc, 6143
8590 ldir
91+ ret
92+
93+; ----------------------------------------------------------------------------
94+; fill_ground — the cobble stipple. Not decoration: the stipple is what
95+; makes ground-state changes visible later, when the game starts
96+; recolouring these pixels. Rows 1-23 (row 0 is the HUD).
97+; ----------------------------------------------------------------------------
98+fill_ground:
99+ ld b, 1 ; rows 1-23 (row 0 is the HUD)
100+.fgr:
101+ ld c, 0
102+.fgc:
103+ ld de, cobble_tex
104+ call blit_tex
105+ inc c
106+ ld a, c
107+ cp 32
108+ jr c, .fgc
109+ inc b
110+ ld a, b
111+ cp 24
112+ jr c, .fgr
113+ ret
114+
115+; blit_tex — write the 8-byte texture at DE into cell (C, B)'s bitmap.
116+; scr_addr_cr finds the cell's first pixel row; INC H steps down the
117+; other seven, 256 bytes apart.
118+blit_tex:
119+ push bc
120+ call scr_addr_cr
121+ ld b, 8
122+.bt:
123+ ld a, (de)
124+ ld (hl), a
125+ inc de
126+ inc h
127+ djnz .bt
128+ pop bc
129+ ret
130+
131+cobble_tex:
132+ defb %10000010
133+ defb %00000000
134+ defb %00001000
135+ defb %00000000
136+ defb %00100001
137+ defb %00000000
138+ defb %00010000
139+ defb %00000000
140+
141+; ----------------------------------------------------------------------------
142+; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row.
143+; The row's top two bits pick the third of the screen (H), its bottom
144+; three become L's top bits, and the column fills L's low five.
145+; ----------------------------------------------------------------------------
146+
147+scr_addr_cr:
148+ ld a, b
149+ and %00011000 ; the third (row bits 4-3) ...
150+ or %01000000 ; ... under the screen base $40xx
151+ ld h, a
152+ ld a, b
153+ and %00000111 ; the char row within the third ...
154+ rrca ; ... rotated into bits 7-5
155+ rrca
156+ rrca
157+ or c ; the column in bits 4-0
158+ ld l, a
86159 ret
87160
88161 end start
The walled square with a sparse pattern of blue dots across the black ground; the blue walls carry faint white dots.
The stipple, washed across the whole square. The blue INK laid down in Unit 1 finally shows. The walls have caught the stipple too — in their white INK — because fill_ground doesn't know walls exist yet.

Look closely at the walls: they’ve caught the stipple as well, in their INK — white flecks on blue. fill_ground textures every cell in its rows; it doesn’t know some of them are wall. That’s not a bug to fix by making the loop cleverer. It’s the cue for the second milestone.

Milestone 2 — the map decides where the brick goes

The walls deserve their own stamp — mortar courses, staggered verticals:

$08$08$08$FF$80$80$80$FF
brick_tex — two full mortar courses, with the vertical joints staggered half a brick between them. Tiled across a wall, it reads as bond.

But which cells get it? Here’s the move that matters: don’t keep a second list of where the walls are — ask the screen. Unit 1 said a cell’s type and its colour are the same byte. fill_walls walks all the cells, reads each attribute back with attr_addr_cr, and tests one bit: wall attributes ($0F) have PAPER blue, so bit 3 — WALL_BIT — is set; cobble attributes ($01) have it clear. Wherever the bit says wall, the brick goes down, overwriting the stray stipple from milestone 1.

Step 2: read the map back, brick wherever the wall bit is set
+62
66
77 COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
9+WALL_BIT equ 3 ; the attribute bit that says "this is wall"
910
1011 start:
1112 ; --- the border goes black — the night beyond the square ---
...
3435 ldir
3536
3637 call paint_walls
38+
39+ ; --- brick the walls ---
40+ ; Now that the wall cells are painted, fill_walls can read the
41+ ; map back and lay brick wherever the wall bit is set.
42+ call fill_walls
3743
3844 forever:
3945 jr forever
...
110116 ld a, b
111117 cp 24
112118 jr c, .fgr
119+ ret
120+
121+; fill_walls — brickwork. Driven by the wall attribute bit, so anything
122+; painted as wall — now or later in the game — gets its brick for free:
123+; the map itself decides where the brick goes.
124+fill_walls:
125+ ld b, 1
126+.fwr:
127+ ld c, 0
128+.fwc:
129+ push bc
130+ call attr_addr_cr
131+ bit WALL_BIT, (hl)
132+ pop bc
133+ jr z, .fwn
134+ ld de, brick_tex
135+ call blit_tex
136+.fwn:
137+ inc c
138+ ld a, c
139+ cp 32
140+ jr c, .fwc
141+ inc b
142+ ld a, b
143+ cp 24
144+ jr c, .fwr
113145 ret
114146
115147 ; blit_tex — write the 8-byte texture at DE into cell (C, B)'s bitmap.
...
137169 defb %00000000
138170 defb %00010000
139171 defb %00000000
172+
173+brick_tex:
174+ ; mortar courses with staggered verticals — dusk-lit stone
175+ defb %00001000
176+ defb %00001000
177+ defb %00001000
178+ defb %11111111
179+ defb %10000000
180+ defb %10000000
181+ defb %10000000
182+ defb %11111111
140183
141184 ; ----------------------------------------------------------------------------
142185 ; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row.
...
155198 rrca
156199 rrca
157200 or c ; the column in bits 4-0
201+ ld l, a
202+ ret
203+
204+; attr_addr_cr — HL = attribute address of cell (C, B):
205+; $5800 + row*32 + col, the row shifted up five times.
206+attr_addr_cr:
207+ ld a, b
158208 ld l, a
209+ ld h, 0
210+ add hl, hl
211+ add hl, hl
212+ add hl, hl
213+ add hl, hl
214+ add hl, hl
215+ ld de, $5800
216+ add hl, de
217+ ld a, c
218+ ld e, a
219+ ld d, 0
220+ add hl, de
159221 ret
160222
161223 end start
The complete program
; Gloaming — Unit 2: The Cobbles and the Brick
; Cumulative build; every step runs on its own. Narrative: the unit page.
; Textures: 8 bytes per cell — cobble stipple on the ground, brick on the walls.

            org     32768

COBBLE      equ     %00000001       ; PAPER black (0), INK blue (1) — dark ground
WALL        equ     %00001111       ; PAPER blue (1), INK white (7) — pale stone
WALL_BIT    equ     3               ; the attribute bit that says "this is wall"

start:
            ; --- the border goes black — the night beyond the square ---
            ; Port $FE bits 0-2 set the BORDER colour. A = 0 = black.
            ld      a, 0
            out     ($FE), a

            ; --- wipe the canvas ---
            ; The bitmap ($4000-$57FF) is the pixel layer; whatever was on
            ; screen before us still lives there. Zero it so only our
            ; attribute colours show.
            call    clear_bitmap

            ; --- texture the ground ---
            ; Blit the cobble stipple into every cell's bitmap, rows 1-23.
            ; The attributes will colour these pixels in a moment.
            call    fill_ground

            ; --- wash in the cobbles ---
            ; Seed the first attribute cell, point DE one cell ahead, and
            ; let LDIR cascade the byte through all 768 cells.
            ld      hl, $5800
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            call    paint_walls

            ; --- brick the walls ---
            ; Now that the wall cells are painted, fill_walls can read the
            ; map back and lay brick wherever the wall bit is set.
            call    fill_walls

forever:
            jr      forever

; ----------------------------------------------------------------------------
; paint_walls — the square's edge, one attribute write per cell.
; ----------------------------------------------------------------------------
paint_walls:
            ld      c, WALL         ; the byte every wall cell gets

            ; the top wall: row 1 is 32 cells in a row from $5820
            ; (row 0 is kept back — it becomes the HUD later)
            ld      hl, $5820
            ld      b, 32
.wt:
            ld      (hl), c
            inc     hl
            djnz    .wt

            ; the bottom wall: row 23, 32 cells from $5AE0
            ld      hl, $5AE0
            ld      b, 32
.wb:
            ld      (hl), c
            inc     hl
            djnz    .wb

            ; the side walls: column 0 and column 31 of rows 1-23.
            ; Write the row's first cell, hop 31 cells to its last,
            ; then step a full row (32) down — 23 times.
            ld      hl, $5820
            ld      b, 23
.ws:
            ld      (hl), c
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), c
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .ws
            ret

; ----------------------------------------------------------------------------
; clear_bitmap — zero the pixel layer, $4000-$57FF, with the same
; seed-and-cascade LDIR idiom the cobble wash uses.
; ----------------------------------------------------------------------------
clear_bitmap:
            ld      hl, $4000
            ld      de, $4001
            ld      (hl), 0
            ld      bc, 6143
            ldir
            ret

; ----------------------------------------------------------------------------
; fill_ground — the cobble stipple. Not decoration: the stipple is what
; makes ground-state changes visible later, when the game starts
; recolouring these pixels. Rows 1-23 (row 0 is the HUD).
; ----------------------------------------------------------------------------
fill_ground:
            ld      b, 1                ; rows 1-23 (row 0 is the HUD)
.fgr:
            ld      c, 0
.fgc:
            ld      de, cobble_tex
            call    blit_tex
            inc     c
            ld      a, c
            cp      32
            jr      c, .fgc
            inc     b
            ld      a, b
            cp      24
            jr      c, .fgr
            ret

; fill_walls — brickwork. Driven by the wall attribute bit, so anything
; painted as wall — now or later in the game — gets its brick for free:
; the map itself decides where the brick goes.
fill_walls:
            ld      b, 1
.fwr:
            ld      c, 0
.fwc:
            push    bc
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            pop     bc
            jr      z, .fwn
            ld      de, brick_tex
            call    blit_tex
.fwn:
            inc     c
            ld      a, c
            cp      32
            jr      c, .fwc
            inc     b
            ld      a, b
            cp      24
            jr      c, .fwr
            ret

; blit_tex — write the 8-byte texture at DE into cell (C, B)'s bitmap.
; scr_addr_cr finds the cell's first pixel row; INC H steps down the
; other seven, 256 bytes apart.
blit_tex:
            push    bc
            call    scr_addr_cr
            ld      b, 8
.bt:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .bt
            pop     bc
            ret

cobble_tex:
            defb    %10000010
            defb    %00000000
            defb    %00001000
            defb    %00000000
            defb    %00100001
            defb    %00000000
            defb    %00010000
            defb    %00000000

brick_tex:
            ; mortar courses with staggered verticals — dusk-lit stone
            defb    %00001000
            defb    %00001000
            defb    %00001000
            defb    %11111111
            defb    %10000000
            defb    %10000000
            defb    %10000000
            defb    %11111111

; ----------------------------------------------------------------------------
; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row.
; The row's top two bits pick the third of the screen (H), its bottom
; three become L's top bits, and the column fills L's low five.
; ----------------------------------------------------------------------------

scr_addr_cr:
            ld      a, b
            and     %00011000       ; the third (row bits 4-3) ...
            or      %01000000       ; ... under the screen base $40xx
            ld      h, a
            ld      a, b
            and     %00000111       ; the char row within the third ...
            rrca                    ; ... rotated into bits 7-5
            rrca
            rrca
            or      c               ; the column in bits 4-0
            ld      l, a
            ret

; attr_addr_cr — HL = attribute address of cell (C, B):
; $5800 + row*32 + col, the row shifted up five times.
attr_addr_cr:
            ld      a, b
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, $5800
            add     hl, de
            ld      a, c
            ld      e, a
            ld      d, 0
            add     hl, de
            ret

            end     start
The walled square with stippled cobbles inside and brick-patterned walls — mortar lines and staggered joints in white on blue.
The brick, laid by the map itself. fill_walls never saw a list of wall positions — it read the attribute bytes back and bricked wherever the wall bit was set.

This is why fill_walls runs after paint_walls in the setup: the map has to exist before it can be read. And it’s a promise about the future — when buildings appear inside the square later in the game, they’ll be painted as wall attributes, and this same loop will brick them without changing a line.

Assemble and run

As ever, either assembler takes the pasmo-syntax source to the same snapshot:

asm198x --dialect pasmonext --sna steps/step-02.asm -o steps/step-02.sna
pasmonext --sna steps/step-02.asm steps/step-02.sna

Both fills together touch every one of the 6,144 bitmap bytes and still finish in a blink — the square is textured before the first frame is out.

When it’s wrong, see why

Texture bugs are address bugs, and each symptom names its own bit:

  • The texture is there but scrambled — slivers in wrong rows. scr_addr_cr. Check the two AND masks: the third comes from row bits 4–3 (%00011000), the char row from bits 2–0 (%00000111), rotated three times into L’s top bits. One wrong mask shreds the layout.
  • The stamp’s rows are consecutive on screen but the cells are wrong. The inc h walk started from a bad base — same routine, check or %01000000: the screen lives at $4000, and H must carry that base.
  • Stipple everywhere, but no brick. fill_walls isn’t seeing wall. Confirm the bit test reads the attribute (via attr_addr_cr, $5800+) and tests WALL_BIT (bit 3 — set in $0F, clear in $01), and that the branch skips on zero.
  • Brick everywhere, cobbles gone. The branch is inverted — jr z and jr nz swapped reads the map exactly backwards.
  • A stippled HUD row. The ground loop’s bounds. It starts at row 1 and stops before 24; a loop from 0 stipples the ledge the HUD is saving for later.
  • The texture flickers or tears at the top. It doesn’t — not in this program. Both fills finish well inside a frame. If you see garbage for an instant on a real machine, it’s the old screen contents before clear_bitmap runs; the wipe from Unit 1 must stay first.

Before and after

The square began this unit as flat colour and ends it as a place — stippled stone inside brick walls. Two ideas did all of it: a texture is eight bytes stamped through inc h, and the second fill found its targets by reading the map back instead of keeping a list. The screen isn’t just output — it’s the game’s own data structure, and that idea does a lot of work between here and the end of the module.

Try this: cut your own cobbles

Design a different ground texture — click pixels below (it starts as the shipped stipple), and the defb rows update as you draw. Copy them over cobble_tex and rebuild. Keep it sparse (eight to twelve pixels): the dusk needs the black. If your pattern tiles into obvious stripes, stagger the marks between rows — the shipped stipple never puts two pixels in the same column twice.

Output
The cobble stipple, editable. Remember it tiles: imagine this cell repeated across the whole square.

Try this: a second wall course

Make the brick finer: change the two %11111111 mortar rows to %01111111 and %11110111. The joins break up and the wall reads older, more weathered. One byte per course — texture is cheap to art-direct when it’s just data.

Try this: prove the map is in charge

In step 2’s setup, paint one interior cell as wall before fill_walls runs — after the ldir wash, write $0F somewhere mid-square (say $5800 + 12*32 + 16). Rebuild: a single bricked block appears in the middle of the cobbles. You never told fill_walls about it. That’s the promise this unit makes to the rest of the game — and buildings will collect on it.

What you’ve learnt

  • The bitmap’s thirds layout: a cell’s eight pixel rows sit 256 bytes apart, and inc h walks them.
  • scr_addr_cr — column and row to bitmap address with two masks and three rotates; attr_addr_cr — the same cell’s attribute at $5800 + row*32 + col.
  • A texture is data: eight defb bytes stamped by one small blit_tex loop.
  • Read the map, don’t keep a listfill_walls finds every wall by testing one attribute bit, so future walls get brick for free.
  • Fill order matters: canvas before paint, and the map must be painted before it can be read.

What’s next

The square is ready for someone to stand in it. In Unit 3 the lamplighter arrives — an 8×8 figure defined the same way the textures were, eight bytes of data — and the first thing you’ll learn about him is that on this machine, a character is just a texture with a name. Until, that is, he needs a colour of his own.