Using Layer 2 and double buffering
I wanted to write a quick tutorial on how to set up Layer 2, its palette and how to be able to double buffer it to create flicker-free graphics. There are many ways to do this and I am offering just one way to do this. This way sets up memory so that the Layer 2 is always mapped to the lower 48K of CPU memory (the addresses $0000-$bfff). This means that your code and stack must sit at $c000 or above during rendering.
The general plan in this tutorial is to present a routine InitL2 that creates a default palette quickly where the index matches the form RRRGGGBB, set up the two Layer 2 buffers, and pages the back-buffer in (the Layer 2 buffer that you will draw to) while the front-buffer (the previously presented buffer) is being displayed by the hardware. Also to present a secondary routine called Present that will flip the buffers so that the current back-buffer becomes the front-buffer (that is, presents the buffer you drew to) and the front-buffer becomes the back-buffer (a new buffer to start drawing on).
The reason to use two buffers, if it wasn’t apparent from the previous paragraph, is to have one available to draw to and another to be displayed. Drawing on the buffer currently being displayed is the fast track to creating flickering graphics, so don’t do this!
Step 1 – Set up the default palette
This is a quick bit of code that sets up the Layer 2 first palette so that a given index also matches the given bit pattern of RRRGGGBB. So, for example, the brightest red (11100000 or $E0) is represented by index $e0 on the palette. The downside to this is that this palette has no greys. But it’s good to get you started when experimenting with the hardware.
There are 3 steps to setting up this palette. First, you select the palette for editing. Secondly, you set up the initial index that you wish to update. Finally, you send the 256 8-bit values representing the colours. This is the code that will do this:
; Set up a default Layer-2 palette
xor a ; Set both the initial index and colour
nextreg $43,%00010000 ; Set current edited palette as Layer 2's first
nextreg $40,a ; First index to set will be 0
loop:
nextreg $41,a ; Send the colour (8-bits only)
inc a ; Set up next colour to send
jr nz,loop ; Repeat until all 256 colours are sent
This page can give you more details on how to use palettes: https://specnext.dev/wiki/Palettes
Step 2 – Call the Present routine
The Present routine, as mentioned before, will flip the buffers but it will also do one other thing. It will page in the whole of the back-buffer (the current Layer 2 area that you wish to draw to) to the addresses $0000-$bfff. This means that any code you wish to execute and stack must sit at $c000 or above while you are rendering. If you are not rendering, you can retake the initial MMU slots for something else.
Now it turns out that because the Present routine does this paging, it is also the routine you can call to initially set up Layer 2 too. So here is the code you would execute to initialise Layer 2:
; Set up the current back-buffer in the lowest 48K of memory
call Present
; Enable Layer 2 so it's visible
ld bc,$123b ; I/O port $123b is the Layer 2 control port
ld a,2 ; Bit 1 is the enable bit
out (c),a ; Enable!
This means that the whole InitL2 routine is as follows:
InitL2:
; Set up a default Layer-2 palette
xor a ; Set both the initial index and colour
nextreg $43,%00010000 ; Set current edited palette as Layer 2's first
nextreg $40,a ; First index to set will be 0
loop:
nextreg $41,a ; Send the colour (8-bits only)
inc a ; Set up next colour to send
jr nz,loop ; Repeat until all 256 colours are sent
; Set up the current back-buffer in the lowest 48K of memory
call Present
; Reset Layer 2 scrolling and clip window
xor a
nextreg $16,a ; Set X scroll to 0
nextreg $17,a ; Set Y scroll to 0
nextreg $18,a
nextreg $18,255
nextreg $18,a
nextreg $18,191 ; Set clip window to (0, 0) - (255, 191)
; Enable Layer 2 so it's visible
ld bc,$123b ; I/O port $123b is the Layer 2 control port
ld a,2 ; Bit 1 is the enable bit
out (c),a ; Enable!
ret
Note that I’ve added some code to reset the Layer 2 hardware scrolling offsets and clip window.
That’s it! Now all we have left is the implementation of said Present routine.
Step 3 – Writing the Present Routine
The ZX Spectrum Next has a register that determines which 16K bank the Layer 2 pixels are. Layer 2 requires 256×192 pixels or 48K of data. This is stored in 3 consecutive 16K banks, or 6 consecutive 8K pages. Remember, the old Spectrums paged 16K memory banks into 4 memory slots. The ZX Spectrum Next has finer granularity and can page 8K memory pages into 8 memory slots.
We set aside two lots of 48K memory to hold our front and back buffers that are required to implement double-buffer rendering. The table below shows which pages we use for this:
Buffer | Bank numbers | Page numbers |
Buffer 1 | 8-10 | 16-21 |
Buffer 2 | 11-13 | 22-27 |
We have a variable that stores which buffer is currently the back-buffer and needs to be paged in the lowest 48K of CPU memory. That value will be the initial bank number. So, this variable will either store 8 or 11 to determine which of the buffers will be used for drawing.
So to present the back-buffer and make it the current front-buffer you need to do three things:
- Swap the value in the variable between 8 and 11.
- Tell the hardware to display the new front-buffer.
- Page in appropriate pages of the back-buffer to the first 6 MMU slots (i.e. the first 48K).
Below I give you the code to do this:
BackBuffer db 8 ; The current back-buffer bank
Present:
; Set the front-buffer (the old back-buffer) and switch values
ld a,(BackBuffer) ; A = back buffer (and new front buffer)
nextreg $12,a ; Set L2 address to new buffer
xor 3 ; Switch between 8 and 11
ld (BackBuffer),a ; Store the new back buffer bank #
add a,a ; Convert bank # to page #
; Set up the lower 48K to map to back-buffer, who's first page is in A
nextreg $50,a ; Set MMU 0
inc a
nextreg $51,a ; Set MMU 1
inc a
nextreg $52,a ; Set MMU 2
inc a
nextreg $53,a ; Set MMU 3
inc a
nextreg $54,a ; Set MMU 4
inc a
nextreg $55,a ; Set MMU 5
inc a
ret
Using the InitL2 and Present routines
To use this code, you call InitL2 once at the beginning of your code to set up the default palette and make Layer 2 visible. At this point you write data to the lower 48K of RAM to draw your pixels. Remember you can write to the address $YYXX, where XX and YY represent your screen coordinates. This is because each line is exactly 256 bytes long and so, for example, you can set H to your Y coordinate, and L to your X coordinate and the address held in HL will be the place to write to draw your pixel at (X,Y). Very convenient!
When you have done all your drawing, call Present, maybe after the VBLANK, and the Next will immediately show what you’ve just drawn. The lower 48K will be available to write to in order to draw your next frame. However, remember that this buffer will still contain the image you had 2 frames ago so it will need to be cleared first.
Good luck in your Layer 2 experiments and I hope this tutorial helps.