It turned out that splitting our C implementation of Conway’s Game of Life for the ZX Spectrum into libraries provided a useful route for replacing components with equivalent Z80 assembly. Interop between C and Z80 assembly provided by z88dk meant that the rest of the code could remain unchanged, as the Z80 slowly took over like a strangler fig. The most simplistic library grid.h was replaced, although grid.c and grid.h remained but offloaded their calls to grid.asm via z88dk interop. Getting that working was fairly painless although parameters are passed via the stack so there is some overhead in wrapping the assembly routine with a C function.

With that working, it’s time to turn our attention to another library screen.h which is slightly more complicated in terms of what it does, which is implement ZX Spectrum specific graphics via the z88dk library arch/zx.h. The current implementation is as follows.

#include <arch/zx.h>

extern uint8_t cell_sprite[];
static uint8_t *font = (uint8_t *)(15360); // point font into rom character set

static void print_chr_at(uint8_t x, uint8_t y, uint8_t *c)
{
    uint8_t *p;
    uint8_t i;
    p = zx_cxy2saddr(x, y);
    for (i = 0; i < 8; ++i)
    {
        *p = *c++;
        p += 256;
    }
}

void print_block_at(uint8_t x, uint8_t y, uint8_t ink)
{
    *zx_cxy2aaddr(x, y) = ink | (ink * 8) | BRIGHT;
}

void print_cell_at(uint8_t x, uint8_t y, uint8_t ink)
{
    *zx_cxy2aaddr(x, y) = ink | PAPER_WHITE;
    print_chr_at(x, y, cell_sprite);
}

void clear_cell_at(uint8_t x, uint8_t y)
{
    *zx_cxy2aaddr(x, y) = INK_WHITE | PAPER_WHITE;
}

uint8_t *get_glyph_from_chr(uint8_t c)
{
    return font + (c * 8);
}

void clear_screen()
{
    zx_border(INK_WHITE);
    zx_cls(PAPER_WHITE);
}

As the title suggests, my first run at this involved making use of ZX Spectrum ROM routines, namely rst $10. This is a print routine that is used by Sinclair BASIC to provide a way to print some given text, in a given colour, to a given location on the screen. This is essentially what most of our library does so it provides a nice first step into rewriting in Z80 assembly. We do print an 8x8 sprite as well, but thankfully an ASCII table implemented by the ROM leaves space for up to 21 UDGs, that is User Defined Graphics, and these can be passed in as if characters themselves.

Many early games for the ZX Spectrum implemented their graphics this way as using this ROM routine, or the equivalent Sinclair BASIC PRINT AT, could provide basic but fast graphics routines for an entire game. Many early games for the ZX Spectrum were written in Sinclair BASIC in fact so supporting it in this way was a smart move, making it possible for anyone with a ZX Spectrum to use its built-in basic to create playable, graphical games. Type-ins were also popular, included in magazines or even books you could buy or borrow from the library. Our first iteration using the PRINT AT or rst $10 ROM routine is as follows.

; CONSTANTS
UDG: equ $5c7b ; RAM address of user defined graphics
VIDEORAM: equ $4000 ; address of video RAM
VIDEORAM_L: equ $1800 ; length of video RAM
VIDEOATT: equ $5800 ; address of attribute RAM
VIDEOATT_L: equ $0300 ; length of attribute RAM
LOCATE: equ $0dd9 ; ROM address for AT routine to position the cursor

SECTION code_user

PUBLIC _load_graphics_asm
;----------
; load_graphics_asm
; alters: hl
;----------
_load_graphics_asm:
            ld hl, cell_sprite ; load cell sprite location
            ld (UDG), hl ; load as first UDG
            ret

PUBLIC _print_string_asm
;----------
; print_string_asm
; inputs: hl = first position of a null ($00) terminated string
; alters: af, hl
;----------
_print_string_asm:
            ld   a, (hl) ; a = character to be printed
            or   a ; sets z register if 0
            ret  z ; return if z register set
            rst  $10 ; prints the character
            inc  hl ; hl = next character
            jr   _print_string_asm ; loop

PUBLIC _clear_screen_asm
;----------
; clear_screen_asm
; clears all pixels, sets ink to black and paper white
; alters: bc, de, hl
;----------
_clear_screen_asm:
            ; clear pixels
            ld hl, VIDEORAM ; hl = video RAM address
            ld de, VIDEORAM+1 ; de = next address
            ld bc, VIDEORAM_L-1 ; bc = length of video RAM - 1 (to loop)
            ld (hl), $00 ; clear first position
            ldir ; loop and clear the rest
            ; clear attributes
            ld hl, VIDEOATT ; hl = attribute RAM address
            ld de, VIDEOATT+1 ; de = next address
            ld bc, VIDEOATT_L-1 ; bc = length of attribute RAM - 1 (to loop)
            ld (hl), @00111000 ; paper white, ink black
            ldir ; loop and set the rest            
            ret

PUBLIC _clear_cell_at_asm
;----------
; clear_cell_at_asm
; inputs: b = y, c = x
; alters: a, bc, de
;----------
_clear_cell_at_asm:
            ; extern void clear_cell_at_asm(uint8_t x, uint8_t y) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            push hl ; ret address back on stack
            call convert_x_y_coords
            call LOCATE ; call LOCATE ROM routine
            ld a, $13 ; control code for set bright
            rst $10 ; call PRINT ROM routine
            ld a, 0 ; bright value
            rst $10
            ld a, ' ' ; clear
            rst $10
            ret

PUBLIC _print_cell_at_asm
;----------
; print_cell_at_asm
; inputs: d = y, e = x, c = ink
; alters: a, bc, de, hl
;----------
_print_cell_at_asm:
            ; extern void print_cell_at_asm(uint8_t x, uint8_t y, uint16_t ink) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            pop bc ; c = ink
            push hl ; ret address back on stack
            push bc ; store bc (ink)
            call convert_x_y_coords
            call LOCATE ; call LOCATE ROM routine
            pop bc ; retrieve bc (ink)
            ld a, $10 ; control code for set ink
            rst $10 ; call PRINT ROM routine
            ld a, c ; ink value            
            rst $10
            ld a, $13 ; control code for set bright
            rst $10
            ld a, 0 ; bright value
            rst $10
            ld a, $90 ; cell UDG stored at $90
            rst $10 ; print
            ret

PUBLIC _print_block_at_asm
;----------
; print_block_at_asm
; inputs: d = y, e = x, c = ink
; alters: a, bc, de
;----------
_print_block_at_asm:
            ; extern void print_block_at_asm(uint8_t x, uint8_t y, uint16_t ink) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            pop bc ; c = ink
            push hl ; ret address back on stack
            push bc ; store bc (ink)
            call convert_x_y_coords            
            call LOCATE ; call LOCATE ROM routine
            pop bc ; retrieve bc (ink)
            ld a, $10 ; control code for set ink
            rst $10 ; call PRINT ROM routine
            ld a, c ; ink value            
            rst $10
            ld a, $13 ; control code for set bright
            rst $10
            ld a, 1 ; bright value
            rst $10
            ld a, $8F ; print block
            rst $10
            ret

;----------
; convert_x_y_coords
; inputs: d = y, e = x (top left is 0,0)
; outputs: b = y, c = x (top left is 24,33 - as expected by ROM AT)
; alters: a, bc, de
;----------
convert_x_y_coords:
            ld a, $18
            ld b, d
convert_x_y_coords_y_loop:
            dec a
            djnz convert_x_y_coords_y_loop  
            ld d, a ; y = $18-y
            ld a, $21
            ld b, e
convert_x_y_coords_x_loop:
            dec a
            djnz convert_x_y_coords_x_loop  
            ld e, a ; x = $21-x
            ld b, d ; b = y
            ld c, e ; c = x
            ret

PUBLIC _get_glyph_from_chr_asm
;----------
; get_glyph_from_chr_asm
; inputs: l = character
; outputs: hl = pointer to glyph
; alters: hl
;----------
_get_glyph_from_chr_asm:
            ld h, $00 ; make sure h is 00
            add hl, hl
            add hl, hl
            add hl, hl ; h *= 8
            add hl, $3C00 ; add start of character fonts
            ret

SECTION rodata_user

cell_sprite:
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101

cell_sprite is loaded as a UDG via load_graphics_asm which is called from main.c, but then the rest of the C code is largely unchanged apart from screen.h and screen.c becoming wrappers for screen.asm, as we did for grid.asm. The full commit is available here (it does include some other light refactoring). The only real issue was that rst $10 treats x and y as starting from the bottom right, instead of the top left, and so I had to write an extra routine convert_x_y_coords.

I did however assume that this rst $10 routine was the fastest way of printing to the screen, with it being built into the ZX Spectrum ROM. In fact it’s not that optimal according to various forums, it was really just created to support and optimise Sinclair BASIC. Various examples and links were given to more optimal Z80 assembly that worked by returning the screen address of an x, y position that was an 8x8 square that could be updated, or an attribute address which is 8 flags representing the ink and paper colour (foreground and background).

I adapted these two routines get_char_address and get_attr_address that were attributed to Dean Belfield and Jonathan Cauldwell respectively, so shout out to them, and made them part the next iteration as follows.

VIDEORAM: equ $4000 ; address of video RAM
VIDEORAM_L: equ $1800 ; length of video RAM
VIDEOATT: equ $5800 ; address of attribute RAM
VIDEOATT_L: equ $0300 ; length of attribute RAM

SECTION code_user

PUBLIC _print_string_asm
;----------
; print_string_asm
; inputs: hl = first position of a null ($00) terminated string
; alters: af, hl
;----------
_print_string_asm:
            ld   a, (hl) ; a = character to be printed
            or   a ; sets z register if 0
            ret  z ; return if z register set
            rst  $10 ; prints the character
            inc  hl ; hl = next character
            jr   _print_string_asm ; loop

PUBLIC _clear_screen_asm
;----------
; clear_screen_asm
; clears all pixels, sets ink to black and paper white
; alters: bc, de, hl
;----------
_clear_screen_asm:
            ; clear pixels
            ld hl, VIDEORAM ; hl = video RAM address
            ld de, VIDEORAM+1 ; de = next address
            ld bc, VIDEORAM_L-1 ; bc = length of video RAM - 1 (to loop)
            ld (hl), $00 ; clear first position
            ldir ; loop and clear the rest
            ; clear attributes
            ld hl, VIDEOATT ; hl = attribute RAM address
            ld de, VIDEOATT+1 ; de = next address
            ld bc, VIDEOATT_L-1 ; bc = length of attribute RAM - 1 (to loop)
            ld (hl), @00111000 ; paper white, ink black
            ldir ; loop and set the rest            
            ret

PUBLIC _clear_cell_at_asm
;----------
; clear_cell_at_asm
; inputs: b = y, c = x
; alters: a, bc, de
;----------
_clear_cell_at_asm:
            ; extern void clear_cell_at_asm(uint8_t x, uint8_t y) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            push hl ; ret address back on stack

            call get_attr_address
            ld (hl), @00111000 ; paper white

            ex de, hl ; h = y, l = x
            call get_char_address
            ld de, clear_sprite ; h = y, l = x, de = address of glyph
            call print_char_at
            ret

PUBLIC _print_cell_at_asm
;----------
; print_cell_at_asm
; inputs: d = y, e = x, c = ink
; alters: a, bc, de, hl
;----------
_print_cell_at_asm:
            ; extern void print_cell_at_asm(uint8_t x, uint8_t y, uint16_t ink) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            pop bc ; c = ink
            push hl ; ret address back on stack

            call get_attr_address
            ld a, c ; a = ink
            or @00111000 ; paper white
            ld (hl), a ; set attribute value

            ex de, hl ; h = y, l = x
            call get_char_address
            ld de, cell_sprite ; h = y, l = x, de = address of glyph
            call print_char_at
            ret

;----------
; print_char_at
; inputs: h = y, l = x, de = location of char
; alters: a, bc, de, hl
;----------
print_char_at:
            ld b, $08 ; loop counter
print_char_at_loop:
            ld a, (de) ; get the byte
            ld (hl), a ; print to screen
            inc de ; goto next byte of character
            inc h ; goto next line of screen
            djnz print_char_at_loop ; loop 8 times
            ret

PUBLIC _print_block_at_asm
;----------
; print_block_at_asm
; inputs: d = y, e = x, c = ink
; alters: a, bc, de
;----------
_print_block_at_asm:
            ; extern void print_block_at_asm(uint8_t x, uint8_t y, uint16_t ink) __z88dk_callee;
            pop hl ; hl = ret address
            pop de ; d = y, e = x
            pop bc ; c = ink
            push hl ; ret address back on stack
            
            call get_attr_address
            ld a, c ; a = ink
            or @01111000 ; paper white, bright
            ld (hl), a ; set attribute value
            
            ex de, hl ; h = y, l = x
            call get_char_address
            ld de, block_sprite ; h = y, l = x, de = address of glyph
            call print_char_at
            ret

PUBLIC _get_glyph_from_chr_asm
;----------
; get_glyph_from_chr_asm
; inputs: l = character
; outputs: hl = pointer to glyph
; alters: hl
;----------
_get_glyph_from_chr_asm:
            ld h, $00 ; make sure h is 00
            add hl, hl
            add hl, hl
            add hl, hl ; h *= 8
            add hl, $3C00 ; add start of character fonts
            ret

;----------
; get_char_address - adapted from a routine by Dean Belfield
; inputs: h = y, l = x
; outputs: hl = location of screen address
; alters: hl
;----------
get_char_address:
            ld a,h
			and $07
			rra
			rra
			rra
			rra
			or l
			ld l,a
			ld a,h
			and $18
			or $40
			ld h,a
			ret	

;----------
; get_attr_address - adapted from a routine by Jonathan Cauldwell
; inputs: d = y, e = x
; outputs: hl = location of attribute address
; alters: hl
;----------
get_attr_address:
            ld a,d
            rrca
            rrca
            rrca
            ld l,a
            and $03
            add a, $58
            ld h,a
            ld a,l
            and $e0
            ld l,a
            ld a,e
            add a,l
            ld l,a
            ret

SECTION rodata_user

cell_sprite:
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101
            defb @10101010
            defb @01010101

block_sprite:
            defb @11111111
            defb @11111111
            defb @11111111
            defb @11111111
            defb @11111111
            defb @11111111
            defb @11111111
            defb @11111111

clear_sprite:
            defb @00000000
            defb @00000000
            defb @00000000
            defb @00000000
            defb @00000000
            defb @00000000
            defb @00000000
            defb @00000000

This next iteration doesn’t use rst $10 or UDGs for graphics so it’s necessary to create block_sprite and clear_sprite to pass into print_char_at from our main routines, which keep the same signature as is our approach. The _print_string_asm routine that was created as part of the first commit continues to use rst $10 as it’s used outside of the game loop so there’s not much point in changing that. Plus it’s handy to keep around as an example for future reference. This subsequent commit is available here.

I’m using this rewrite as a way of learning Z80 assembly so I don’t see the first commit as wasted effort. In fact it probably would have been sufficient had I not found some examples of more optimal code that were worth exploring. I’m starting to build up some knowledge and examples of various techniques and it’s tempting when you only know a few commands to use them for everything, as in “if all you have is a hammer, everything looks like a nail”. Hopefully as I continue I will pick up some more techniques and perhaps even revisit some of this code, but for now I’m reasonably happy with it.

To ROM Version Demo

Not To ROM Version Demo