Dots tunnel: a 256 × 256 screen and 400 dots
The fourth post fit the tunnel into one frame, with a dot at 43 NOPs. In that dot, going from y to the address is still the most expensive step, 19 NOPs out of 43, between two table reads and an addition with carry.
That carry comes from the width of the screen: a CPC line takes 80 bytes, and that number is what forces the addition. No instruction can change that, but the shape of the screen can be set. So the v6 programs the CRTC for a 256 × 256 screen, where the addition becomes an or, with no carry. The v7 then spends the time saved on dots, until the frame is almost full.
What an 80-byte line costs
On the standard screen, line y starts 80*(y>>3) + 2048*(y and 7) bytes after the start of the screen.
The low byte of 80*(y>>3) goes from 0 to 240, depending on the row. Adding the column of the dot (x >> 2) can therefore overflow into the high byte, and the v5 has to chain add a,c and adc a,(hl). Since the high byte depends on that carry, the v5 cannot read it straight from a table.
With 64 bytes per line, line y starts at 64*(y>>3) + 2048*(y and 7). The low byte of 64*(y>>3) is always a multiple of 64: its 6 low bits are 0. A column goes from 0 to 63 and fills exactly these 6 bits. The addition becomes an or, which never overflows, and the high byte now depends on y alone.
A 64-byte line holds 256 pixels in mode 1, and x fits in a byte. With 256 lines, y fits in a byte too, and 64 × 256 = 16,384: the screen fills its 16 KB page exactly. Both screens therefore stay at #C000 and #8000.
Setting the CRTC
The CRTC, the chip that produces the video memory addresses and the monitor syncs, is set through its registers, and four of them change here:
- R1, the displayed width, goes from 40 to 32 characters, that is 64 bytes per line;
- R6, the displayed height, goes from 25 to 32 rows of 8 lines, that is 256 lines;
- R2 and R7 place the horizontal and vertical syncs. They go from 46 to 42 and from 30 to 34, so that the picture stays in the middle of the monitor.
init writes these four registers at the end, after the palette. A pair of ports is enough: #BC selects the register, #BD writes the value into it:
ld hl,crtc_256
ld e,4
next_reg:
ld b,#BC ; CRTC: select the register
ld a,(hl)
out (c),a
inc hl
inc b ; #BD: write into it
ld a,(hl)
out (c),a
inc hl
dec e
jr nz,next_reg
The table after the code gives, for each register, its number and then its value:
; R1 = 32 words (64 bytes) per line, R6 = 32 rows of 8 lines; R2 and R7
; move the syncs to keep the picture in the middle of the monitor.
crtc_256:
db 1,32, 2,42, 6,32, 7,34
show_screen does not change: R12 still selects the displayed screen, #C000 or #8000.
Tables computed by RASM
TABLES.BIN no longer fits. Its YLO and YHI tables follow 80-byte lines, and CENTER puts the tunnel at (160, 100), the centre of the standard screen. The second post showed that RASM computes the same tables as the BASIC, byte for byte. So the v6 hands all the tables over to it, except XMASK and COLX, which make_x_tables still builds at start-up. The .sna then holds everything, without the file made by the BASIC.
The tunnel keeps its parameters and moves its centre to (128, 128). Its dots then go from 38 to 218 on both axes, and all stay on the screen.
Since the high byte depends on y alone, YHI can give it whole, screen start included. Each screen then has its own pair of tables, and here is the pair of the #C000 screen:
; A line takes 64 bytes: line y starts at 64*(y>>3) + 2048*(y and 7). The low
; byte is a multiple of 64, and a byte column (0 to 63) fills its 6 low bits.
org YLO_C0
repeat 256,y,0
db ((y>>3)*64)&255
rend
repeat 256,y,0 ; YHI_C0 = YLO_C0 + 256
db SCREEN_C0+(((y>>3)*64+(y&7)*2048)>>8)
rend
The pair of the #8000 screen, at #6A00, differs only by SCREEN_80.
A dot at 39 NOPs
The v6 dot reads the low byte of its address in YLO, then the high byte in YHI, at the same position y:
repeat DOTS,n,0 ; the 16 dots, one after the other
ld a,(de) ; dx
inc e ; the 32 bytes of a ring stay in one page
add a,ixl ; x = ring centre x + dx
ld l,a
ld h,XMASK>>8
ld a,(hl) ; bits of the pixel in its byte
and b ; in the ink
ex af,af' ; kept aside for the end
inc h ; COLX = XMASK + 256
ld c,(hl) ; byte column: x >> 2
ld a,(de) ; dy
inc e
add a,ixh ; y = ring centre y + dy
ld l,a
ypatch{n}:
ld h,0 ; YLO page of the work screen, set by main
ld a,(hl)
or c ; low byte of the line + column: no carry
inc h ; YHI = YLO + 256
ld h,(hl) ; high byte, screen start included
ld l,a
ex af,af' ; the bits of the pixel again
or (hl) ; keep the other pixels of the byte
ld (hl),a
push hl ; record the address for the clearing
rend
After ld a,(hl), L still holds y. inc h moves from YLO to YHI, and ld h,(hl) puts the high byte straight into H. The v5 had to keep the low byte in C, read the screen start from IYH, then add with the carry. Here, going from y to the address costs 7 NOPs instead of 11. The or itself costs 1 NOP, as much as the add it replaces, and the 4 NOPs saved all come from the high byte, which the table gives as is.
Code that rewrites itself
We still have to give the dot the pair of tables of the work screen, which changes for every picture. The dot finds it in the operand of ld h,0, which main rewrites before drawing. This is self-modifying code: the program changes one byte of its own instructions.
RASM numbers the 16 copies of the dot: inside repeat DOTS,n,0, the label ypatch{n} becomes ypatch0, ypatch1, up to ypatch15. The label points to the opcode, and the operand sits in the next byte, hence the +1. For every picture, main switches the page along with the screen, then writes the new page into the 16 copies:
ld a,(ypage)
xor (YLO_C0>>8)^(YLO_80>>8)
ld (ypage),a ; the YLO table of the work screen
repeat DOTS,n,0
ld (ypatch{n}+1),a ; goes into the ld h of each dot
rend
These 16 writes cost 64 NOPs per picture. The v5 paid 2 NOPs per dot to read IYH, that is 512 per picture, and IYH is no longer used.
The dot takes 39 NOPs:
| Step | v5 | v6 |
|---|---|---|
| x: bits of the pixel and column | 15 | 15 |
| y | 8 | 8 |
| address | 11 | 7 |
| write and list | 9 | 9 |
| total | 43 | 39 |
The dot saves 4 NOPs, that is 1,024 per picture. The work of a picture goes from about 14,000 to about 13,000 NOPs: 1,280 for the clearing, 256 × 39 = 9,984 for the dots, 1,656 to set up the 16 rings.
The v6 draws the same tunnel as the v1. We checked it in a Z80 emulator: for all 256 values of t, the dots lit by the v6 are those of the v1, moved 32 pixels to the left and 28 down.
400 dots
The v6 leaves about 6,800 NOPs free in the frame, and the v7 spends them on dots.
Adding rings does not work, as the fourth post showed: 16 rings spaced 4 depths apart already cover all 64, and a seventeenth would land on the first. So the v7 adds dots to each ring, 25 instead of 16, that is 400 in all.
The shape of a ring
Dot k of a ring stays at angle 4k + rot, but a turn now has 4 × 25 = 100 steps instead of 64. Four rotation phases are still enough: after 4 steps, each dot has taken the place of its neighbour.
A (d, rot) row of SHAPE now holds 50 bytes. The v7 gives it 64 bytes. A row thus never crosses its 256-byte page, and inc e is enough to move on. defs pads each row with 14 zero bytes:
org SHAPE
repeat 64,d,0
s = P*R/(Z0+d*DZ)
repeat 4,rot,0
repeat DOTS,k,0
angle = 360*(4*k+rot)/(4*DOTS)
db floor(s*cos(angle)+0.5)&255
db floor(s*sin(angle)+0.5)&255
rend
defs ROW-2*DOTS,0 ; a row never crosses a page: inc e is enough
rend
rend
The four rows of a depth take 256 bytes, a whole page. The address of a row becomes SHAPE + 256 × d + 64 × rot: D gets the high byte of SHAPE plus d, and E gets rot × 64. The v6, like the v5, still tested whether d was odd. The v7 computes the address of the row without that test:
ld a,(t) ; DE = SHAPE + d * 256 + rot * 64
and #0C
rlca
rlca
rlca
rlca ; rot * 64, rot = (t >> 2) and 3
ld e,a
ld a,c
add a,SHAPE>>8
ld d,a
Setting up a ring thus drops from 103.5 to 99 NOPs: the test cost 5.5 NOPs on average, and the fourth rlca costs only one.
Memory
SHAPE grows to 16 KB, 64 depths of 256 bytes. It takes the whole area from #4000 to #7FFF, just below the #8000 screen. The other tables move down between #2000 and #2DFF, above the code, which ends around #1770. The two erase lists now keep 400 addresses, 800 bytes each, at #3000 and #3400.
For the rest, changing DOTS is enough: repeat RINGS*DOTS unrolls 400 clears, repeat DOTS,n,0 copies the dot 25 times, and main writes 25 operands instead of 16.
The budget
The count by hand gives, per picture:
| Item | Computation | NOPs |
|---|---|---|
| clearing | 400 × 5 | 2,000 |
| dots | 400 × 39 | 15,600 |
| ring set-up | 16 × 99 | 1,584 |
| total work | about 19,200 |
With the main loop and the 25 operands to rewrite, a picture takes about 19,450 NOPs, 97% of a frame. About 500 NOPs are left. A 26th dot per ring would cost 16 × 44 = 704 NOPs (39 to draw it, 5 to clear it) and would no longer fit.
The v7 draws what its tables describe: for all 256 values of t, the Z80 emulator finds exactly the dots that CENTER and SHAPE predict.
The tunnel on the CPC
The CPCEC emulator below runs tunnel-v7.sna, assembled from tunnel-v7.asm:
The screen is now square, with a border on the left and on the right.
What each version brings
We measured the three versions as in the previous posts, in frames per picture in amspirit-lite-headless (6128), over 1,000 frames:
| Version | Dots | Frames per picture | Pictures per second |
|---|---|---|---|
| v5: standard screen | 256 | 1 | 50 |
| v6: 256 × 256 screen | 256 | 1 | 50 |
| v7: 25 dots per ring | 400 | 1 | 50 |
Conclusion
The 256 × 256 screen brings the dot down to 39 NOPs, and the v7 shows 400 dots at 50 pictures per second, 144 more than the v5, with these limits:
- the frame is almost full: about 500 NOPs per picture are left;
- the screen is no longer the system one: the BASIC and the firmware cannot write to it, and the two screens take all the memory above
#8000; - the memory below the screens is almost full too, with 16 KB for the
SHAPEtable alone; - the code takes 1,900 bytes, of which 700 for the 25 copies of the dot and 800 for the unrolled clearing.
The next post changes method: instead of one routine shared by all the dots, it produces code compiled for this very tunnel.
The files of this post:
- tunnel-v6.asm and tunnel-v6.sna, the v6 on the 256 × 256 screen;
- tunnel-v7.asm and tunnel-v7.sna, the v7 with 400 dots.
Both versions assemble with RASM 3.0.9, without a table file.