ZX Spectrum Assembly, Pong – 0x0C Optimisation part 2
In this ZX Spectrum Assembly chapter, we will implement further optimisations.
We have already mentioned that Spirax pointed out several optimisations. We have left for the end one that he showed for the sound routine and another that was implemented after the one he showed for the ReprintPoints routine.
Create a folder called Step12 and copy all the .asm files from the Step11 folder into it.
Translation by Felipe Monge Corbalán
Table of contents
PlaySound
The first optimisation is in the sound routine, specifically in the way we evaluate the sound to be emitted.
Open sound.asm and locate the PlaySound tag, the first lines of which are:
PlaySound:
; Preserves the value of records
push de
push hl
cp $01 ; Sound of a dot?
jr z, playSound_point ; Point sound, emits it
cp $02 ; Paddle sound?
jr z, playSound_paddle ; Paddle sound, emits it
In this routine we use CP $01 and CP $02 to check which sound should be output. Each CP instruction occupies 2 bytes and takes seven clock cycles. We replace these instructions with DEC A, which occupies 1 byte and takes 4 clock cycles, so we save 2 bytes and 6 clock cycles. DEC does change the value of the A register, but since we are concerned with the type of sound to be emitted, it does not affect us.
Let’s see how the routine starts.
PlaySound:
; Preserves the value of records
push de
push hl
; Spirax
dec a ; Decreasing A
jr z, playSound_point ; If 0, point sound
; Spirax
dec a ; Decreasing A
jr z, playSound_paddle ; If 0, paddle sound
We preserve DE, PUSH DE, HL, PUSH HL, and decrement A, DEC A. If A was one, the result of the operation is zero and jumps to play the sound, JR Z, playSound_point.
If A was not one, the checks continue. We decrement A, DEC A, and if the result of the operation is zero, it jumps to play the sound, JR Z, playSound_paddle. If it jumps, A was initially worth two, one with the first decrement and zero with this second.
If it does not jump, it remains as it was and emits the rim sound.
Compile, load into the emulator and check that it still works.
ReprintPoints
With this optimisation we save 20 bytes and one hundred and seven clock cycles. To achieve this, we are going to use the same method we used in step 10, following Spirax’s comments; the way he pointed out.
As you may recall from step 10, we added a label so that the painting of player two’s marker could be called independently; we will do the same for player one’s marker. With this modification, the markers will take a little longer to paint (they are only painted at the start of the game and when marking a point), but we will simplify the ReprintPoints routine, saving bytes and clock cycles by eliminating redundant code.
Let’s start by modifying the PrintPoints routine so that it can be called to print both players’ markers independently.
Open video.asm and locate the PrintPoints tag. Underneath it we add another tag, the one we will call to paint the marker for player two:
printPoint_1_print:
Between the PrintPoints and printPoint_1_print tags we add the calls to paint the marker for each player:
call printPoint_1_print ; Paints the marker of player 1
jr printPoint_2_print ; Paints the marker of player 2
We call to print player one’s marker, CALL printPoint_1_print, and then jump to print player two’s marker, JR printPoint_1_print.
There is only one more change we need to make in PrintPoints. We add RET just before the printPoint_2_print tag so that CALL printPoint_1_print is output correctly; remember that the rest of the jumps are output by the PrintPoint RET.
We add RET before printPoint_2_print:
ret
printPoint_2_print:
We have finished the necessary modifications to PrintPoints, but we have not saved anything, we have added code by adding bytes and clock cycles.
Let’s start saving. We delete reprintPoint_1_print and the following lines until we reach the reprintPoint_2 tag; we do not delete it.
We locate the ReprintPoints tag and nine lines down we find the JR Z, reprintPoint_1_print statement. This tag no longer exists, so we need to change this line and leave it as follows:
jr z, printPoint_1_print
We locate the reprintPoint_1 tag and make the final changes.
The code for this tag, after deleting the entire reprintPoint_1_print part, is as follows:
reprintPoint_1:
cp POINTS_X1_R ; Compare right boundary of marker 1
jr c, reprintPoint_1_print ; Carry? Pass through marker 1, paint
jr nz, reprintPoint_2 ; !=0? Passes right, jumps
We need to change the double check. As the reprintPoint_2 tag is now below the JR NZ, reprintPoint_2 line, this jump is no longer necessary, but we do need to check if it is zero, in which case we need to paint the player a marker, JR Z, printPoint_1_print, and change the jump from JR C, reprintPoint_1_print to JR C, printPoint_1_print, so the code would look like this:
reprintPoint_1:
cp POINTS_X1_R ; Compare with right boundary marker 1
jr z, printPoint_1_print
jr c, printPoint_1_print ; 0 or carry? Pass marker 1, paint
The final appearance of PrintPoints and ReprintPoints is as follows:
; -------------------------------------------------------------------
; Paint the scoreboard.
; Each number is 1 byte wide by 16 bytes high.
; Alters the value of the AF, BC, DE and HL registers.
; -------------------------------------------------------------------
PrintPoints:
call printPoint_1_print ; Paints marker player 1
jr printPoint_2_print ; Paints marker player 2
printPoint_1_print:
ld a, (p1points) ; A = points player 1
call GetPointSprite ; Sprite to be painted on marker
; 1st digit of player 1
ld e, (hl) ; E = lower part 1st digit address
inc hl ; HL = high side
ld d, (hl) ; D = upper part
push hl ; Preserves HL
ld hl, POINTS_P1 ; HL = address where to paint digit
call PrintPoint ; Paints 1st digit
pop hl ; Retrieves HL
; 2nd digit of player 1
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = lower part
inc hl ; HL = high part
ld d, (hl) ; D = upper part
; Spirax
ld hl, POINTS_P1 + 1 ; HL = address where to paint digit
call PrintPoint ; Paint2 2nd digit
ret
printPoint_2_print:
; 1st digit of player 2
ld a, (p2points) ; A = points player 2
call GetPointSprite ; Sprite to be painted on marker
ld e, (hl) ; E = low part 1st digit address
inc hl ; HL = high part
ld d, (hl) ; D = upper part
push hl ; Preserves HL
ld hl, POINTS_P2 ; HL = address where digit
call PrintPoint ; Paints 1st digit
pop hl ; Retrieves HL
; 2nd digit of player 2
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = lower part
inc hl ; HL = high part
ld d, (hl) ; D = upper part
; Spirax
ld hl, POINTS_P2 + 1 ; HL address where to paint 2nd digit
; Paints the second digit of player 2's marker.
PrintPoint:
ld b, $10 ; Each digit 1 byte by 16 (scanlines)
printPoint_printLoop:
ld a, (de) ; A = byte to be painted
ld (hl), a ; Paints the byte
inc de ; DE = next byte
call NextScan ; HL = next scanline
djnz printPoint_printLoop ; Until B = 0
ret
; -------------------------------------------------------------------
; Repaint the scoreboard.
; Each number is 1 byte wide by 16 bytes high.
; Alters the value of the AF, BC, DE and HL registers.
; -------------------------------------------------------------------
ReprintPoints:
ld hl, (ballPos) ; HL = ball position
call GetPtrY ; Third, line and scanline ball position
cp POINTS_Y_B ; Compare lower limit marker
ret nc ; No Carry? Pass underneath
ld a, l ; A = line and column ball position
and $1f ; A = column
cp POINTS_X1_L ; Compare left boundary marker 1
ret c ; Carry? Pass left
jr z, printPoint_1_print ; 0? It's in left margin, paint
cp POINTS_X2_R ; Compare right boundary marker 2
jr z, printPoint_2_print ; 0? It's in the right margin, paint
ret nc ; No Carry? Pass on the right
reprintPoint_1:
cp POINTS_X1_R ; Compare right boundary marker 1
jr z, printPoint_1_print
jr c, printPoint_1_print ; Z or Carry? Pass marker 1, paint
reprintPoint_2:
cp POINTS_X2_L ; Compare right boundary marker 2
ret c ; Carry? Pass on the left
; Spirax
jr printPoint_2_print ; Paint marker player 2
If we compare the implementation of ReprintPoints with the one we did in step 10, we can see that the routine is much simpler, being practically reduced to the built-in checks, so that the marker is repainted only when necessary.
All that’s left is to compile, load in the emulator and check that everything still works.
Or do you want to add a loading screen?
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will add a loading screen to ZX-Pong.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.