This is the 6th in the series on Framebuffer-based graphics on the Nintendo GameBoy. As before I'll start with a recap. Then we'll continue with more development, classic 8-bit display routines for clearing and scrolling the screen. This one will, hopefully, be much shorter than some of the others.
Recap
The incredibly successful 8-bit Nintendo GameBoy used tile-based graphics for performance reasons and memory constraints. The 160x144 pixel screen is divided into 20x18 (=360) TileMap locations and each one can point to one of 256, 8x8 2-bpp Tiles. However, a raster trick enables another 128 tiles to be selected part-way through each refresh and I want to use that feature to provide a full frame buffer.
In previous posts I described the tile system, then the CPU, then the basic mechanism for generating a full frame buffer and displaying characters in all ink/paper combinations. The most recent post added dynamic updates which meant synchronising PPU and main software access, resulting in a fairly slow text update routine.
Clearing The Screen
This should be easy. I just need to take the paper colour; and set a pair of 8-bit registers (one for each plane) to 0 (or -1 if that plane's paper colour bit is set). Then I need to fill in all the tile rows from 0x8000 to 0x967f with the contents of those registers. So, the central loop will look like:
ClsLp:
waitScan m
REPT n
ld [hl],d
inc l
ld [hl],e
inc l
ENDR
ld [hl],d
inc l
ld [hl],e
inc hl
dec bc
ld a,b
or c
jr nz,ClsLp
waitScan m
REPT n
ld [hl],d
inc l
ld [hl],e
inc l
ENDR
ld [hl],d
inc l
ld [hl],e
inc hl
dec bc
ld a,b
or c
jr nz,ClsLp
The colour data is in 2 regs (de), and we need the a register later, so we don't use the faster ldi [hl],a instruction. Each write to a tile row takes 24 cycles and there are 204 to 284 cycles available per row, so how many REPTs would you think can be managed? There ought to be room for 7 to 10. In fact there's only room for n=1. This means that just 4 bytes can be written per whole line scan, a mere 52 cycles and m needs to be set to 5 as well.
This is unexpected. Merely changing m to 6 leads to this kind of Cls:
The lack of available HBlank cycles is why this routine is interesting. In reality, my inner loop doesn't do dec bc etc; it does a dec c: jr nz loop; then has a dec b: jr nz,outerLoop and b and c are picked to maximise c and exactly fill a screen. The a register is still needed though inside the waitScan macro.
Scrolling
Scroll is a form of copy, where the source byte (in hl) starts on the second row: 0x8000+20*16=0x8140 and the destination (in de) is on the first row: 0x8000. Then all but one rows are copied until the bottom of the screen, when we clear the bottom line in the current paper colour.
Here the inner loop copies 256 bytes at a time, incrementing e each time and exits when e=0. So it looks like:
Scroll10:
waitScan 5 ;Again, it's the shorter value (PutCh works with 6)
ldi a,[hl] ;src
ld [de],a
inc e
ldi a,[hl]
ld [de],a
inc e ;2 bytes, 10M, 40 cycles.
jr nz,Scroll10 ;a whole 256b page.
inc d
dec b ;starts at 21, so only 64b left
jr nz,Scroll10 ;after the outer loop is done.
waitScan 5 ;Again, it's the shorter value (PutCh works with 6)
ldi a,[hl] ;src
ld [de],a
inc e
ldi a,[hl]
ld [de],a
inc e ;2 bytes, 10M, 40 cycles.
jr nz,Scroll10 ;a whole 256b page.
inc d
dec b ;starts at 21, so only 64b left
jr nz,Scroll10 ;after the outer loop is done.
Results
As usual, performance would appear to be disappointing, because I ought to have about 280 cycles available per scan, but I only seem to be able to use about 40.
When timed however, clear screen appears to run at about 6.8s for 50 screen clears, which amounts to 0.136s/screen clear (8 frames/clear, which means the write rate is about 41.4kB/s, which is very respectable!
Scrolling seems to run at about 12.5s for 50 scrolls, which amounts to 0.25s/scroll (15 frames/scroll, which means that clearing the bottom line involved 50/18=2.778 equivalent full screen clears, or 0.136*2.778=0.378s. And the 50 scrolls needed 20*17*16*50*2=544,000bytes read/written, meaning an overall bandwidth of 544000/(12.5-0.378)=43.8kB/s, which seems about right, because the inner loop that transfers 4 bytes takes 52 cycles vs 68 for CLS (including the loop jump itself).
Conclusion
8-bit computers had some additional standard commands to handle the screen, clearing and scrolling being important ones. PPU scanning still gets in the way far too much. Consider a ZX Spectrum, it can clear the screen using a couple of LDIR instructions (which the SM83 doesn't have). That's 21 cycles/byte for 6kB, or 163kB/s or nearly 3x faster than the GameBoy routine here. Still, we can see the speed is tolerable.
Of course, the intention with the GameBoy design is to scroll the screen by setting the frame of the Background graphics tiles, which would be nearly instantaneous, and multiple scrolls would only need the tile maps to be updated. In theory, scrolling (but not clearing) could be done for the frame-buffer approach, though accessing the frame-buffer would then have to be done via the TileMap instead of using absolute addresses, which will be a bit slower.
In the next (and likely last) post in the series I'll explore pixel plotting and line drawing, the most basic graphics commands implemented on 8-bit computers (except for most Commodore computers, which lacked even that functionality).
No comments:
Post a Comment