Dots tunnel: from the tables to a first Z80 tunnel
The first post ended on two byte tables, SHAPE and CENTER, computed in JavaScript. With them, the tunnel loop finds the position (x, y) of each dot with a few memory reads and additions. But to draw the dot, it still relies on g.rect(x, y, 1, 1), a PixiJS function.
On the CPC, the program must write each dot into video memory itself. This post goes all the way to the tunnel on the CPC screen, in three stages. First the CPC computes the tables in Locomotive BASIC (the BASIC built into the machine) and saves them to a file. Then RASM computes the same tables at assembly time, and must find this file again byte for byte. Last, a first version in Z80 loads the file and draws the tunnel, without trying to be fast, and we measure where its time goes. But the way the CPC lays out its pixels calls for one more table, and that is where it all starts.
The CPC screen memory
The CPC has no separate video memory: the screen takes 16 KB of ordinary memory, from &C000 to &FFFF (in BASIC, & marks a hexadecimal number). To light a pixel, the program writes to this area like to any other memory location.
One byte, four pixels
The tunnel runs in mode 1: 320 × 200 pixels and four inks. Choosing one ink out of four takes 2 bits, so an 8-bit byte holds 4 pixels.
And these bits are not next to each other. The leftmost pixel uses bits 7 and 3, the next one bits 6 and 2, then come 5 and 1, and finally 4 and 0:
| Bit of the byte | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Pixel | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 |
To draw pixel x, we must therefore find the byte that holds it, then the two bits of the pixel in that byte.
One line, 80 bytes
A line of 320 pixels takes 320 / 4 = 80 bytes. Pixel x is in byte x \ 4 of its line (\ is the integer division of BASIC). Its place in the byte is the remainder of this division, x AND 3: 0 for the leftmost pixel, 3 for the rightmost one.
Interleaved lines
You would expect line 1 of the screen right after line 0, 80 bytes further on, but the CPC orders its lines differently. Its screen is made of 25 character rows, of 8 lines each. Lines 0 of the 25 rows follow each other in memory, lines 1 start 2,048 bytes further on (&800), and so on up to lines 7.
For line y, the row is y \ 8 and the line within the row is y AND 7. Line y therefore starts at address &C000 + (y\8)*80 + (y AND 7)*2048.
One pixel
Put these three rules together, and pixel (x, y) is in the byte at address:
&C000 + (y\8)*80 + (y AND 7)*2048 + x\4
For example, pixel (100, 50) is in row 6, on line 2 of this row, and in byte 25 of its line. Its address is &C000 + 480 + 4096 + 25, that is &D1F9. And since 100 AND 3 is 0, it is the leftmost pixel of this byte: it uses bits 7 and 3.
Why a table for the lines
The Z80 must do this calculation for each dot, and the tunnel has 256 of them per picture (16 rings of 16 dots).
The x part costs it almost nothing. Dividing by 4 means shifting the bits two places to the right, and x AND 3 keeps the two low bits, which give the place of the pixel in the byte.
The y part is another matter. The Z80 can add and shift bits, but it has no multiply instruction: the *80 becomes a series of shifts and additions, on 2-byte numbers.
So we use the recipe of the first post again: compute once and for all, in advance, and store the results in a table. For each line y, the table gives (y\8)*80 + (y AND 7)*2048. This number goes up to 16 KB and does not fit in a byte, while a table stores only one byte per entry. So it is split in two, the low byte in YLO and the high byte in YHI.
The start of the screen, &C000, stays out of the table, and the Z80 will add it when it draws: the same table will then serve for a screen placed elsewhere in memory.
A table could also replace the x calculation. We keep it for later: without a measurement, nothing says it is worth it.
Tables aligned on 256
A CPC address takes 2 bytes, a high byte and a low byte. Address &4023 has the high byte &40 and the low byte &23.
Take YLO at address &4000. Entry y is at address &4000 + y, and since y stays below 256, this addition only touches the low byte: &4000 + &23 gives &4023. So the Z80 has nothing to add. It puts &40 in the high byte, y in the low byte, and reads the entry.
If YLO started at &4080, entry &90 would land at &4110, and the high byte would change. The Z80 would then need a real 2-byte addition, for each dot.
That is why each table starts at an address that is a multiple of 256. The rule has a price (a table indexed by y has 256 entries at most), but it does no harm here: the screen has 200 lines, and the dots of the tunnel stay between y = 10 and y = 190. We checked these bounds on every entry of SHAPE and CENTER.
CENTER and SHAPE are larger, but they too start at a multiple of 256. The four tables take 10.5 KB from address &4000:
| Table | Address | Size | Content |
|---|---|---|---|
YLO |
&4000 |
256 | the address of a line, low byte |
YHI |
&4100 |
256 | the address of a line, high byte |
CENTER |
&4200 |
2,048 | the centres of the rings |
SHAPE |
&4A00 |
8,192 | the shapes of the rings |
The tables in Locomotive BASIC
The program draws nothing. It fills memory, byte by byte, with POKE, then it saves the result.
BASIC also keeps its variables in memory. MEMORY &3FFF stops it from going past address &3FFF, and the table area stays free for the program. Lines 30 and 40 give the constants of the JavaScript and the address of each table:
10 MODE 1:MEMORY &3FFF
20 t=TIME
30 CX=160:CY=100:P=90:R=1:Z0=1:DOTS=16:AX=72:AY=50
40 YLO=&4000:YHI=&4100:CENTER=&4200:SHAPE=&4A00
TIME counts in 300ths of a second. Line 20 notes the start time, to show the duration at the end.
The names are those of the first post, and for CENTER and SHAPE, the listing reads side by side with the last playground, loop for loop. One difference: in JavaScript, SHAPE and CENTER are arrays, while here they are addresses, and POKE SHAPE+n stands for SHAPE[n].
YLO and YHI
Line 60 computes the address of line y, without the start of the screen. Line 70 splits it in two: v AND 255 keeps the low byte, v \ 256 gives the high byte. Lines 200 to 255 do not exist, and their entries stay at 0:
50 PRINT "YLO, YHI"
60 FOR y=0 TO 255:v=0:IF y<200 THEN v=(y\8)*80+(y AND 7)*2048
70 POKE YLO+y,v AND 255:POKE YHI+y,v\256:NEXT
CENTER
CENTER is the table of the first post, line for line: 16 levels q, 64 phases phase, two bytes per entry:
80 PRINT "CENTER"
90 FOR q=0 TO 15:FOR phase=0 TO 63:n=(q*64+phase)*2
100 POKE CENTER+n,CX+INT(AX*q/15*SIN(2*PI*phase/64)+0.5)
110 POKE CENTER+n+1,CY+INT(AY*q/15*SIN(4*PI*phase/64+0.7)+0.5)
120 NEXT:PRINT ".";:NEXT:PRINT
The PRINT "."; of line 120 prints a dot for each level: without it, nothing moves on the screen during the whole calculation.
Rounding
JavaScript rounds with Math.round. BASIC does have ROUND, but the two disagree when a number falls exactly halfway between two integers: Math.round(2.5) gives 3 and ROUND(2.5) gives 2, Math.round(-2.5) gives −2 and ROUND(-2.5) gives −3.
INT, for its part, keeps the integer just below: INT(2.7) gives 2, and INT(-2.3) gives −3. Adding 0.5 before INT therefore rounds like Math.round, halves included, and that is what lines 100 and 110 do.
SHAPE
SHAPE is the largest table: 64 depths d, 4 rotations rot, 16 dots k and two bytes per dot, that is 4,096 cosines and as many sines. The calculation follows the JavaScript one, except for two details that the next section explains: the radius s on line 140, and the sine on line 170.
130 PRINT "SHAPE"
140 FOR d=0 TO 63:s=P*R/(Z0+d*3/20)
150 FOR rot=0 TO 3:FOR k=0 TO DOTS-1:angle=2*PI*(4*k+rot)/64:n=((d*4+rot)*DOTS+k)*2
160 POKE SHAPE+n,INT(s*COS(angle)+0.5) AND 255
170 POKE SHAPE+n+1,INT(s*COS(angle-PI/2)+0.5) AND 255
180 NEXT:NEXT:PRINT ".";:NEXT:PRINT
Negative bytes
Each entry of SHAPE stores an offset from the centre of the ring, dx or dy, from −90 to 90. A dot left of the centre has a negative dx, a dot above it a negative dy. But POKE writes only one byte, from 0 to 255: a negative number must therefore be encoded in a byte.
The Z80 follows the same rule as the JavaScript Int8Array, two’s complement: a byte stores −n as 256 − n. −1 becomes 255, −22 becomes 234, and −90 becomes 166. The numbers from 0 to 127 keep their value, and the bytes from 128 to 255 encode the numbers from −128 to −1.
BASIC does this calculation by itself. It stores its integers on 2 bytes, with the same rule: there, −22 becomes 65,536 − 22, that is &FFEA. AND 255 keeps the low byte, &EA, that is 234, and this is what lines 160 and 170 do.
Two floating-point traps
To check the program, we compared each byte with the tables of the first post, computed in JavaScript. Copied as it stands, the SHAPE calculation gave 2 wrong bytes out of 8,192, both at depth d = 20. At this depth, the radius s is exactly 22.5, and a dot 22.5 pixels from the centre falls exactly halfway between two pixels: the slightest calculation error tips it one way or the other.
The first trap comes from the depth. The JavaScript writes it Z0 + d * DZ, with DZ = 0.15, but the CPC stores its numbers in binary, and 0.15 has no exact binary form (no more than 1/3 has an exact decimal one). For d = 20, the radius 90/(1+20*0.15) then comes out as 22.4999999925 instead of 22.5, and the rounding gives 22 instead of 23. So line 140 writes DZ as 3/20, and in d*3/20, BASIC multiplies before it divides: for d = 20, 60/20 gives exactly 3, and the radius is back to 22.5.
The second trap comes from the sine. On the CPC, SIN(PI/2) is not quite 1 (it misses it by a little more than 2 ten-billionths), while COS(0) is exactly 1. A sine is only a cosine shifted by a quarter turn, so line 170 computes COS(angle-PI/2) instead of SIN(angle). At the quarter turn, the cosine is taken at 0 and gives a true 1. SIN in degrees, after DEG, makes the same error.
In JavaScript, both calculations come out exact: 1 + 20 * 0.15 gives 4 there, and Math.sin(Math.PI / 2) gives 1, because it computes with more digits. And these errors only affect the dots that fall exactly halfway between two pixels. Elsewhere, the dot closest to a halfway point is still 6 hundred-thousandths of a pixel away from it, far more than the error of the CPC.
Saving the tables
What remains is to keep the tables. Nobody wants to wait 5 minutes each time the tunnel starts, so the program saves the 10,752 bytes to a binary file, TABLES.BIN. The B of SAVE asks for a binary file, followed by the start address and the length, &2A00, that is 256 + 256 + 2,048 + 8,192 bytes:
190 PRINT "DUREE :";ROUND((TIME-t)/300,1);"S"
200 SAVE "tables",B,&4000,&2A00
AMSDOS puts a 128-byte header in front of the bytes, with the name, the address and the length of the file. The file therefore takes 10,880 bytes on the disc. Line 190 prints the time taken first (DUREE is French for duration).
The complete program
10 MODE 1:MEMORY &3FFF
20 t=TIME
30 CX=160:CY=100:P=90:R=1:Z0=1:DOTS=16:AX=72:AY=50
40 YLO=&4000:YHI=&4100:CENTER=&4200:SHAPE=&4A00
50 PRINT "YLO, YHI"
60 FOR y=0 TO 255:v=0:IF y<200 THEN v=(y\8)*80+(y AND 7)*2048
70 POKE YLO+y,v AND 255:POKE YHI+y,v\256:NEXT
80 PRINT "CENTER"
90 FOR q=0 TO 15:FOR phase=0 TO 63:n=(q*64+phase)*2
100 POKE CENTER+n,CX+INT(AX*q/15*SIN(2*PI*phase/64)+0.5)
110 POKE CENTER+n+1,CY+INT(AY*q/15*SIN(4*PI*phase/64+0.7)+0.5)
120 NEXT:PRINT ".";:NEXT:PRINT
130 PRINT "SHAPE"
140 FOR d=0 TO 63:s=P*R/(Z0+d*3/20)
150 FOR rot=0 TO 3:FOR k=0 TO DOTS-1:angle=2*PI*(4*k+rot)/64:n=((d*4+rot)*DOTS+k)*2
160 POKE SHAPE+n,INT(s*COS(angle)+0.5) AND 255
170 POKE SHAPE+n+1,INT(s*COS(angle-PI/2)+0.5) AND 255
180 NEXT:NEXT:PRINT ".";:NEXT:PRINT
190 PRINT "DUREE :";ROUND((TIME-t)/300,1);"S"
200 SAVE "tables",B,&4000,&2A00
The disc tables.dsk holds this program. In an emulator or on a real CPC, RUN"TABLES starts it, and the CPC takes a little under 5 minutes to fill the tables (284 seconds on an emulated 6128). The disc must accept writes, or the SAVE at the end fails.
To check the bytes, this line sums the whole table area. Each byte counts with a weight that depends on its address, so that a misplaced byte also changes the result:
c=0:FOR i=&4000 TO &69FF:c=c+PEEK(i)*((i AND 15)+1):NEXT:PRINT c
It must print 11232147, the sum of the tables computed in JavaScript by the first post, plus the line table.
The same tables with RASM
So the CPC can compute its own tables, but it takes almost 5 minutes, and it would have to start again for each changed constant. RASM, on the other hand, can compute during assembly: its language has variables, loops, conditions and the usual maths functions. It can therefore write the tables itself, at assembly time, into a file. RASM is not unique here: any decent assembler offers similar means, under other names (sjasmplus, for example, has DUP to repeat a block).
The file tables.asm starts with the constants of the JavaScript, then places the tables from #4000 (RASM writes hexadecimal with #, where BASIC uses &):
CX = 160
CY = 100
P = 90
R = 1
Z0 = 1
DZ = 0.15
DOTS = 16
AX = 72
AY = 50
org #4000
; RASM's sin() and cos() take degrees: a full turn is 360, not 2*PI.
; floor(x+0.5) rounds like INT(x+0.5) in BASIC.
repeat 256,y,0 repeats the block up to rend 256 times, with y from 0 to 255, and each db there puts down one byte. The operators come from C: >> shifts right and & is a bitwise AND. So (y>>3)*80 is the (y\8)*80 of BASIC:
YLO: repeat 256,y,0
if y<200
db ((y>>3)*80+(y&7)*2048)&255
else
db 0
endif
rend
YHI: repeat 256,y,0
if y<200
db ((y>>3)*80+(y&7)*2048)>>8
else
db 0
endif
rend
CENTER and SHAPE follow the BASIC program loop for loop:
CENTER: repeat 16,q,0
repeat 64,phase,0
db CX+floor(AX*q/15*sin(360*phase/64)+0.5)
db CY+floor(AY*q/15*sin(720*phase/64+0.7*180/pi)+0.5)
rend
rend
SHAPE: repeat 64,d,0
s = P*R/(Z0+d*DZ)
repeat 4,rot,0
repeat DOTS,k,0
angle = 360*(4*k+rot)/64
db floor(s*cos(angle)+0.5)&255
db floor(s*sin(angle)+0.5)&255
rend
rend
rend
Two details change. The sin and cos functions of RASM take degrees: a full turn is 360, and the 0.7 radian of CENTER becomes 0.7*180/pi. And floor, which keeps the integer just below, stands in for INT.
The two detours of BASIC, on the other hand, are no longer needed. RASM takes Z0+d*DZ with DZ = 0.15 and sin(angle), like the JavaScript, and lands exactly on the 4 dots of d = 20, because it computes with more digits than BASIC.
What remains is to save the result. $ is the current address, and $-YLO the length of the tables, #2A00:
save "tables-rasm.bin",YLO,$-YLO
RASM assembles tables.asm and writes tables-rasm.bin in a hundredth of a second, against 284 seconds for BASIC. The BASIC file has 128 more bytes, the AMSDOS header. Without this header, the two files are identical, byte for byte.
RASM will make the tables of the versions that change the shape of the tunnel: change a constant, assemble, and the tunnel follows. BASIC keeps its role as the reference that runs on the machine itself. As long as the shape stays the same, the versions of the tunnel load its file, TABLES.BIN.
A first version in Z80
The v1 takes the loop of the last playground of the first post, in the most naive version possible. It runs without the CPC firmware, in a snapshot: a .sna file, which holds the image of the memory and registers of the machine. The emulator loads and starts it in one go, and RASM can write it by itself.
The start
buildsna asks RASM for a snapshot. The constants take the memory map of the BASIC program, the address of the screen and the names RINGS and DOTS from the JavaScript:
buildsna
bankset 0
YLO = #4000 ; the memory map of tables.bas
YHI = #4100
CENTER = #4200
SHAPE = #4A00
SCREEN = #C000 ; the screen that the snapshot shows
RINGS = 16
DOTS = 16
incbin copies the BASIC file into the memory of the snapshot, from YLO. The 128 skips the AMSDOS header. run start gives the address where the program starts:
org YLO
incbin "TABLES.BIN",128 ; skip the 128-byte AMSDOS header
org #1000
run start
start: di
ld sp,#1000
call init
The snapshot holds only this program: the firmware has set up nothing in memory. So di turns off the interrupts, which nobody would handle. The stack starts at #1000 and grows down, below the code.
Setting up the machine
init talks to the Gate Array, the chip that builds the picture, through port #7Fxx. #8D selects mode 1 and turns off both ROMs. Then each ink gets its colour, with two out: the number of the ink, then its colour. #10 selects the border:
; init: mode 1, ROMs off, palette.
init: ld bc,#7F8D
out (c),c
ld hl,palette
ld e,0
next_ink:
out (c),e ; E < #10: select ink E
ld a,(hl)
out (c),a ; #40 to #5F: set its colour
inc hl
inc e
ld a,e
cp 4
jr nz,next_ink
ld a,#10 ; select the border
out (c),a
ld a,BLACK
out (c),a
ret
The Gate Array takes each colour as #40 plus the number of the hardware colour, and the program names the ones it uses:
; Gate Array colours (#40 + hardware colour number)
BLACK = #54
BLUE = #44
SKY_BLUE = #57
BRIGHT_WHITE = #4B
RED = #4C
The palette takes the colours of the JavaScript: black for the background, then blue, sky blue and white. PENS stores, for each group of 16 depths, a byte of 4 pixels of the same ink:
palette:
db BLACK,BLUE,SKY_BLUE,BRIGHT_WHITE
PENS: db #FF,#0F,#F0,#F0 ; 4 pixels of ink 3, 2, 1, 1 for d >> 4
t: db 0
depth: db 0
ring: db 0
dot: db 0
ringX: db 0
ringY: db 0
pen: db 0
These bytes come from the bit order of mode 1. Bit 7 gives the low bit of the ink of the leftmost pixel, and bit 3 its high bit. Ink 1 therefore sets only bits 7 to 4 (#F0), ink 2 bits 3 to 0 (#0F), and ink 3 all bits (#FF). The nearest ring is white, as in the JavaScript, and the farthest ones are blue.
The variables keep the names of the JavaScript, except d, which becomes depth: in assembly, d is already the name of a register.
Waiting for the VBL
The v1 draws straight into the visible screen, at #C000, which the RASM snapshot already shows at start. To keep a steady speed, each picture starts at frame flyback (the VBL), when the beam goes back to the top of the screen. wait_vbl reads bit 0 of port B of the PPI (#F5xx), which is 1 during the VBL. It first waits for the end of a VBL already in progress, then for the start of the next one:
; wait_vbl: wait for the start of the frame flyback.
wait_vbl:
ld b,#F5 ; PPI port B, bit 0 = VBL
vbl_end:
in a,(c)
rra
jr c,vbl_end ; already in the VBL: wait for its end
vbl_start:
in a,(c)
rra
jr nc,vbl_start
ret
The loop
The main loop follows the playground. It waits for the VBL, clears the screen, moves t on, then draws the 16 rings of 16 dots. The border colour shows the work: red from the start to the end of the calculation, black while the program waits for the VBL. The border macro does the two out of the change. RASM copies its code at each call, with {colour} replaced by the given colour:
macro border,colour
ld bc,#7F10 ; Gate Array: select the border
out (c),c
ld a,{colour}
out (c),a
mend
border RED and border BLACK thus frame the work of each picture:
main: call wait_vbl
border RED ; work starts
ld hl,SCREEN ; the visible screen: the picture flickers
call clear16k
ld hl,t
inc (hl)
ld a,(hl)
neg
and 63
ld (depth),a ; ring 0: d = -t and 63
ld a,RINGS
ld (ring),a
ring_loop:
call ring_setup ; ringX, ringY, pen, and IX = SHAPE of the ring
ld a,DOTS
ld (dot),a
dot_loop:
ld a,(ringX)
add a,(ix+0) ; x = ringX + dx
ld b,a
ld a,(ringY)
add a,(ix+1) ; y = ringY + dy
ld c,a
ld a,(pen)
ld e,a
call plot
inc ix
inc ix
ld hl,dot
dec (hl)
jr nz,dot_loop
ld a,(depth) ; next ring: d + 4
add a,4
and 63
ld (depth),a
ld hl,ring
dec (hl)
jr nz,ring_loop
border BLACK ; work ends
jp main
clear16k uses an old Z80 trick. ld (hl),0 clears the first byte, then ldir copies each byte onto the next one: 16,383 copies, and the 16 KB are at 0:
clear16k: ; clear 16 KB from HL (L = 0)
ld d,h
ld e,1
ld (hl),0
ld bc,#3FFF
ldir
ret
t fits in a byte and goes back to 0 after 255. The tunnel does not mind: it only uses t and 63 and (t >> 2) and 3, which repeat every 64 pictures, and 256 is a multiple of 64.
The first ring is at depth -t and 63, like (4 * i - t) & 63 for i = 0 in the JavaScript, and each next ring is 4 depths further on.
One ring
ring_setup does for one ring what these lines of the JavaScript do:
const c = ((d >> 2) * 64 + (((t + d) >> 2) & 63)) * 2;
const ringX = CENTER[c], ringY = CENTER[c + 1];
const base = (d * 4 + rot) * DOTS * 2;
const color = COLORS[d >> 4];
The multiplications become shifts, and the alignment on 256 does the rest:
; ring_setup: read the centre, the ink and the shape address of ring d.
ring_setup:
ld a,(depth)
ld c,a
and #3C ; (d >> 2) * 4
ld l,a
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl ; (d >> 2) * 128
ld a,(t)
add a,c
and #FC
rrca ; phase * 2, phase = ((t + d) >> 2) and 63
or l
ld l,a
ld a,h
add a,CENTER>>8
ld h,a
ld a,(hl)
ld (ringX),a
inc hl
ld a,(hl)
ld (ringY),a
ld a,c ; pen = PENS(d >> 4)
rlca
rlca
rlca
rlca
and 3
ld e,a
ld d,0
ld hl,PENS
add hl,de
ld a,(hl)
ld (pen),a
ld a,(t) ; IX = SHAPE + d * 128 + rot * 32
and #0C
rlca
rlca
rlca ; rot * 32, rot = (t >> 2) and 3
ld e,a
ld a,c
srl a ; A = d >> 1, carry = d and 1
jr nc,even_depth
set 7,e ; + 128 if d is odd
even_depth:
add a,SHAPE>>8
ld d,a
push de
pop ix
ret
For CENTER, and #3C keeps (d >> 2) * 4, and five add hl,hl multiply it again by 32: (d >> 2) * 128. The phase comes from t + d. and #FC clears the two low bits, and rrca moves the bits one place to the right: ((t + d) >> 2) * 2, the phase already multiplied by 2. This number stays below 128, and (d >> 2) * 128 has only 0 or 128 in its low byte, so an or is enough to join them. All that is left is to add CENTER >> 8 to the high byte.
For SHAPE, the row of the ring is at d * 128 + rot * 32. d * 128 is d >> 1 in the high byte, and bit 0 of d in bit 7 of the low byte. srl a does both at once: bit 0 of d goes out into the carry, and set 7,e puts it back in its place.
One dot
plot gets x in B, y in C and the ink byte in E, and sets the pixel:
; plot: set pixel (B, C) with ink E (4 pixels of the same ink).
plot: ld a,b
and 3 ; place of the pixel in its byte
ld d,#88 ; bits of the left pixel
jr z,mask_ink
next_pixel:
srl d
dec a
jr nz,next_pixel
mask_ink:
ld a,d
and e
ld e,a ; bits of the pixel, in the ink
ld a,b
srl a
srl a ; byte column: x >> 2
ld h,YLO>>8
ld l,c
add a,(hl) ; + low byte of the line
ld b,a
inc h ; YHI = YLO + 256
ld a,SCREEN>>8
adc a,(hl) ; + high byte, carry and screen start
ld h,a
ld l,b
ld a,e
or (hl) ; keep the other pixels of the byte
ld (hl),a
ret
The pixel mask starts from #88, bits 7 and 3 of the leftmost pixel, and moves one place to the right for each pixel: #44, #22, #11. An and with the ink byte keeps only the bits of that ink.
The address comes from two additions. x >> 2 plus YLO gives the low byte, and the carry of this addition goes into the high byte through adc, which adds YHI and SCREEN>>8, the high byte of the screen start (#C0), at the same time. Last, or (hl) keeps the other pixels of the byte: two nearby dots can fall into the same byte.
The tunnel on the CPC
The CPCEC emulator below runs tunnel-v1.sna, assembled from tunnel-v1.asm and TABLES.BIN:
The tunnel turns, with the curve and the colours of the JavaScript. The visible screen, read from the memory of the emulator and compared byte for byte with the picture computed from the tables, shows no difference. But it moves slowly, the border stays red almost all the time, and the picture flickers: clear16k clears the visible screen for almost 5 frames, before the dots come back.
Where the time goes
A CPC frame lasts 19,968 NOPs (one NOP is 1 µs: 312 lines of 64 µs). To show 50 frames per second, all the work for one picture must fit in this budget. We measured the number of frames per picture in the amspirit-lite emulator (6128), over 1,000 frames, for the v1 and for two cut-down versions:
| Version | Frames per picture |
|---|---|
| full v1 | 7 |
| clearing only, no ring | 5 |
| 256 dots, no clearing | 2 |
The program always waits for the next VBL, so a picture costs a whole number of frames. The v1 runs at 50 / 7, a little over 7 pictures per second.
Clearing alone takes almost 5 frames. ldir copies a byte in 6 NOPs (5 for the last one), that is 16,382 × 6 + 5 = 98,297 NOPs for 16 KB. Even without any dot, the v1 could not go past 10 pictures per second. The 256 dots cost between 1 and 2 frames, more than 78 NOPs per dot: a call, variables in memory, a shift loop for the mask.
So drawing fewer dots will not be enough: with this clearing, no number of dots holds 50 pictures per second. Yet the tunnel touches 256 bytes at most per picture, out of the 16,384 of the screen.
Conclusion
The tunnel runs on the CPC, with tables that BASIC and RASM compute identically. The v1 has the faults of its aim, which was to be easy to read:
- 7 pictures per second instead of 50;
- a picture that flickers, because the program clears and redraws the visible screen;
- a 16 KB clear, which alone takes almost 5 frames, for 256 lit bytes at most;
- more than 78 NOPs per dot, between the
call, the variables in memory and the mask loop.
The next post will deal with the clearing and the flicker, and the one after it with the cost of each dot.
The files of this post:
- tables.bas and the disc tables.dsk, the BASIC program;
- TABLES.BIN, the file it saved, AMSDOS header included;
- tables.asm, the tables by RASM;
- tunnel-v1.asm and tunnel-v1.sna, the v1 of the tunnel, assembled by RASM 3.0.8.