Writing direct to screen memory
By writing direct to screen memory we are bypassing the general purpose ROM routines in favour of lean output routines written with performance in mind.
Pros | Cons |
---|---|
Complete flexibility | Need to understand the complex Spectrum screen memory layout |
Can fine tune the routines for better performance – only write the code to do the bare minimum | More work to make feature rich, i.e. add colouring, control codes, etc |
Takes up a little more RAM to store the extra code |
You can find out more about how the Spectrum screen memory is laid out here. These functions are all variants from my library output.asm, simplified a little for sake of example.
As we are bypassing the ZX Spectrum ROM (effectively its OS) and are writing directly to the screen, we do not need to set the channel. This code is sufficient to write a single character (A) to the screen.
LD A, 65 ; Character to print
LD D, 1 ; Y position
LD E, 15 ; X position
CALL Print_Char ; Print the character
RET
;
; Print a single character out to a screen address
; A: Character to print
; D: Character Y position
; E: Character X position
;
Print_Char: LD HL, 0x3C00 ; Character set bitmap data in ROM
LD B,0 ; BC = character code
LD C, A
SLA C ; Multiply by 8 by shifting
RL B
SLA C
RL B
SLA C
RL B
ADD HL, BC ; And add to HL to get first byte of character
CALL Get_Char_Address ; Get screen position in DE
LD B,8 ; Loop counter - 8 bytes per character
Print_Char_L1: LD A,(HL) ; Get the byte from the ROM into A
LD (DE),A ; Stick A onto the screen
INC HL ; Goto next byte of character
INC D ; Goto next line on screen
DJNZ Print_Char_L1 ; Loop around whilst it is Not Zero (NZ)
RET
; Get screen address from a character (X,Y) coordinate
; D = Y character position (0-23)
; E = X character position (0-31)
; Returns screen address in DE
;
Get_Char_Address: LD A,D
AND %00000111
RRA
RRA
RRA
RRA
OR E
LD E,A
LD A,D
AND %00011000
OR %01000000
LD D,A
RET ; Returns screen address in DE
We can build upon the previous example to print a string by adding the following function that calls Print_Char.
LD D, 3
LD E, 2
LD HL, TEXT2
CALL Print_String
RET
;
; Text for my print routine that uses zero-terminated strings
;
TEXT2: DB AT, 12, 2, INK, 1, PAPER, 6, BRIGHT, 1, "Hello World!", 0
;
; My print routine
; HL: Address of the string
; D: Character Y position
; E: Character X position
;
Print_String: LD A, (HL) ; Get the character
CP 0 ; CP with 0
RET Z ; Ret if it is zero
INC HL ; Skip to next character in string
CP 32 ; CP with 32 (space character)
JR C, Print_String ; If < 32, then don't ouput
PUSH DE ; Save screen coordinates
PUSH HL ; And pointer to text string
CALL Print_Char ; Print the character
POP HL ; Pop pointer to text string
POP DE ; Pop screen coordinates
INC E ; Inc to the next character position on screen
JR Print_String ; Loop
Note that we are using the same text string as in the ROM call variant. However, as this is a fairly rudimentary routine, it will ignore any screen control codes and does not change the screen attributes.
This is the nub with rolling your own code; you can tailor it for speed, but usually at the expense of rich functionality.
Note that the above routines could be further optimised, for example by only calculating the screen position once when printing a string and using INC E to move to the next character position along in the screen memory map.