ZalaXa 2 — Setting up the dev environment

I’m going to dive right in and talk about setting up my dev environment. All the code and techniques we use with Z80 and ZX Spectrum development can be made to work with any toolchains, and every established developer has their favourites.

I’m no exception, and I’m an enthustiastic user of Simon Brattel‘s Zeus. Zeus has a lineage going back to 1977 on Simon’s homebrew computers, and on the Spectrum from the beginning. Many now-legendary games developers used Zeus back in the day.

Simon was also an early pioneer of cross-development. Many of his classic games, like Halls of the Things and Dark Star, were written on his Z80 homebrew computer, Basil, with a Parasys debugger link between Basil and the Spectrum.

The Windows cross-development IDE has had a continuous pedigree since, as the main IDE for Simon’s electronics and processor design business. And, for the last decade, as a continuously-developed Spectrum-oriented assembler, IDE and emulator with, incidentally, inbuilt support for NIRVANA+.

My personal affiliation to Zeus is based on how easy it makes my development process. I suspect most of this is an affinity between the way Simon and I think—some of this being that our thinking is similar, and some being the way I’m challenged to think differently.

Whatever the philosophical underpinnings, the end result is that I’ll be doing this tutorial series on Zeus. Feel free to follow along with Zeus if you’re a relative beginner, or adapt my examples for your own favourite IDE, assembler and emulator.

Installing Zeus is easy. Download the latest version of zeus.exe here, stick it in a directory and make a shortcut to it. If you’re on Windows 7/8/10, you’ll have to do the usual unblocking the first time you run the program. Zeus works well on Linux and MacOS under Wine, although I only use Windows myself.

I’ve made a GitHub repository for this series of tutorials. Familiarize yourself with the git version control system, and the process of cloning a repository, if you don’t already know this. I use TortoiseGit on Windows, and find it very simple and intuitive.

I will establish a convention that the source for the latest version of ZalaXa will be inside the /src directory, and the source for a particular tutorial post will be inside the /tutorials directory—this posts code is found at /tutorials/part2, for example.

Once you have cloned the repository and updated it to the latest version, choose File >> Open in the Zeus menu, and open \tutorials\part2\main.asm. You will see this code—the bare minimum template to assemble a Z80 program and run it in Zeus’s emulator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; main.asm
 
zeusemulate             "48K"                           ; Tell the Zeus emulator to be a 48K Spectrum
zoLogicOperatorsHighPri = false                         ; Zeus assembler options
zoSupportStringEscapes  = false                         ;   (see Config tab
zoAllowFloatingLabels   = false                         ;   for details)
Zeus_PC                 = Start                         ; Tell the Zeus emulator where to start running code at
org                     $8000                           ; Tell the Zeus assembler where to place the code
 
 
 
Start                   proc                            ; A named PROCedure (also our start point)
                        jp Start                        ; Go into an endless loop!
pend

Click the Assemble Then Emulate button:

You should be rewarded with an assembler status message saying this:

nErrors = 0 nRedef = 0 nUndef = 0

and a completely blank white spectrum screen in the Zeus emulator:

And on that bombshell, we will continue in the next post!

ZalaXa 1 — The Idea

Talking to my Spec-Chum Andy Dansby the other day, I came up with the idea of doing a blog tutorial series on writing a multicolour ZX Spectrum game.

Maybe not for the complete beginner, as there are any number of excellent tutorials for n00bs out there already. But something where you follow along and let it carry you through into a specialised subject area.

I will talk about the history of multicolour on the Spectrum in more detail later, but for now, let’s say that one of the strengths, and quirks, of the Spectrum is its display format. Unlike other home micros of the era, the Spectrum restricted you to one foreground and one background for every block of 8 by 8 pixels—the primary motivation of the designers being economy: a relatively small amount of precious RAM was taken up by graphics data, leaving more for other program features. Being as popular as it became, this was taken as a creative challenge by 1980s programmers, and many excellent games and other programs were written, making maximum use of both the available memory and these particular visual constraints.

It’s not the only way of doing things, though. Instead of the dedicated Spectrum ULA handling the graphics output, earlier homebrew computers often used the microprocessor to draw the display. This took up much of the available processor time, and involved precise timing—the program would “chase” the TV’s raster beam as it scanned across and down the screen, outputting pixels at the precisely-synchronised time.

At some point during the Spectrum’s heyday, some talented people figured out raster chasing could be used on the Spectrum too—you could let the ULA chip draw the pixels, but you could change the colours on the fly, as you chased the raster beam. In this way you could have one foreground colour in every 8×4, 8×2 or even 8×1 pixel block. Fast forward to 2015, and the smart folks had systemised this to the extent of enabling Einar Saukas to publish the NIRVANA bicolor graphics engine, using 8×2 attributes.

It’s this engine, or rather its fullscreen variant NIRVANA+, that I’d like to base this tutorial series around.

As a taster, here’s Einar’s NIRVANA+ demo program. Download the .TAP here.

ZalaXa — Table of Contents

  1. The Idea
  2. Setting Up the Dev Environment (code)
  3. A Simple Start Menu (code)
  4. First Glimpse of Multicolour (code)
  5. Wide Tiles (code)
  6. Preshifted Sprites (code)
  7. Moving the Ship (code)
  8. Clearing the NIRVANA+ Screen (code)
  9. Proportional Fonts With FZX (code)
  10. A Naïve Scoring Routine (code)
  11. Flashy 1UPmanship (code)
  12. Towards a Scrolling Starfield (code)
  13. You Gotta Roll With It (code)

Zeus Data Breakpoints — Part 1

One of the nice things about the Zeus Z80 cross-assembler is the debugger built into its integrated emulator.

OMOIDE, one of the ZX Spectrum games I’m working on has a huge conceit—the entire game is presented as if it’s an obscure Japanese game that never got translated into English. The text is written in English, in katakana script, as the Japanese do for foreign loanwords, and often also for videogames.

I softened slightly on the menu items, as it’s all too easy for players to accidentally select a joystick option that renders them a) unable to play the game, if not just b) confused. I added a little marquee at the top that discreetly cycles through the menu options in English.

The text for option 0 is supposed to say 0: PLAY in a tiny 3×5 pixel font (actually one of the Robotron 2084 fonts), but sadly it doesn’t. It’s something more akin to N. Qi Nw, which is no good to anybody, apart from possibly a klingon.

I checked all the obvious things, and couldn’t for the life of me figure out why this item (and only this item) gets corrupted. It happens consistently, even if you assign that text to a different option—the problem moves with the text, not with the menu slot. It’s semi-legible, like the lines got shuffled around, which makes it worse than random garbage, as there’s obviously some logic to it, albeit wrong logic.

Let’s look at the data that’s copied onto the screen. This is a multicolour NIRVANA+ menu, which means there’s only about 15,000 T-states available per frame to do everything, instead of the usual 70,000-odd. For speed of reading, writing and address calculation, I’m storing the data in the same format the screen does. Which, if you’ve ever watched a loading screen appear line by line, you’ll know is not laid out in a linear coordinate-based fashion.

I write the text in a standard .SCR file, and load it into memory at a convenient place. It turns out I don’t need the while file, only about 3/5ths of the first third of the pixels, up until the green dots. And none of the attributes – they’re just they’re to make it easier to work with in my graphics app. By checking in a hex editor, I can see I only need the first 1139 bytes—still a bit wasteful, but I have the space and I need every T-state.

This data is referenced in a table, where zxpixeladdr() is a Zeus helper function that converts pixels into addresses—zxpixeladdr(0, 0) would emit $4000, etc. Only the low byte of each address needs to be stored, because the high byte is the same for all the entries—halving the size of the table.

align 256
MenuExplanation proc Table:
 
  ;                                   Low   Index   Function
  db MenuText.Offset+zxpixeladdr(  0,  0)   ;   0   0: Play
  db MenuText.Offset+zxpixeladdr( 72,  0)   ;   1   1: Keyboard
  db MenuText.Offset+zxpixeladdr(144,  0)   ;   2   2: Kempston
  db MenuText.Offset+zxpixeladdr(  0,  8)   ;   3   2: Sinclair
  db MenuText.Offset+zxpixeladdr( 72,  8)   ;   4   2: Cursor
  db MenuText.Offset+zxpixeladdr(144,  8)   ;   5   2: Fuller
  db MenuText.Offset+zxpixeladdr(  0, 16)   ;   6   2: Kempston Mouse
  db MenuText.Offset+zxpixeladdr( 72, 16)   ;   7   2: AMX Mouse
  db MenuText.Offset+zxpixeladdr(144, 16)   ;   8   3: Help
  db MenuText.Offset+zxpixeladdr(  0, 24)   ;   9   4: High Scores
  db MenuText.Offset+zxpixeladdr( 72, 24)   ;  10   5: Credits
 
  struct
    Low         ds 1
  Size send
 
  Len           equ $-Table
  Count         equ Len/Size
  High          equ high( MenuText.Offset+zxpixeladdr(0, 0))
  Joystick      equ 2
  Items         equ 6
 
pend

The code that reads and prints the date looks like this. PageBank() is a macro I use to switch the 128K upper RAM bank. As you can see, it uses ldir to do a zigzag block copy of the data—the first, third and fifth rows from left to right, and the second and fourth rows from right to left.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PrintMenuExplanation    proc                            ; MenuExplanation.Index is passed in L
                        PageBank(0, true)
                        ld h, high(MenuExplanation)
                        ld l, (hl)
                        ld h, MenuExplanation.High
                        ld de, zxpixeladdr(80, 8)
                        ld bc, 9
                        ldir
                        inc h
                        inc d
                        dec l
                        dec e
                        ld bc, 9
                        lddr
                        inc h
                        inc d
                        inc l
                        inc e
                        ld bc, 9
                        ldir
                        inc h
                        inc d
                        dec l
                        dec e
                        ld bc, 9
                        lddr
                        inc h
                        inc d
                        inc l
                        inc e
                        ld bc, 9
                        ldir
                        ret
pend

As I always do when I hit a brick wall, I reached for the Zeus debugger and its zeusdatabreakpoint feature, inserting them into the code like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PrintMenuExplanation    proc                            ; MenuExplanation.Index is passed in L
                        PageBank(0, true)
 
                        ld a, l                         ; Save L to print in the breakpoints
                                                        ; (not needed in the final code)
                        ld h, high(MenuExplanation)
                        ld l, (hl)
                        ld h, MenuExplanation.High
                        ld de, zxpixeladdr(80, 8)
                        ld bc, 9
                        zeusdatabreakpoint 2, "zeusprinthex(1, a, hl, de, bc)", $
                        ldir
                        inc h
                        inc d
                        dec l
                        dec e
                        ld bc, 9
                        zeusdatabreakpoint 2, $
                        lddr
                        inc h
                        inc d
                        inc l
                        inc e
                        ld bc, 9
                        zeusdatabreakpoint 2, $
                        ldir
                        inc h
                        inc d
                        dec l
                        dec e
                        ld bc, 9
                        zeusdatabreakpoint 2, $
                        lddr
                        inc h
                        inc d
                        inc l
                        inc e
                        ld bc, 9
                        zeusdatabreakpoint 2, $
                        ldir
                        ret
pend

There are nine general purpose slots that you can write data-driven breakpoint expressions in—plus slots to break on expressions involving data reads, writes, IO port reads and writes, and RAM page changes. Expressions can be set in the UI or in code—the latter allowing them to persist across multiple debugging sessions.

The expressions can be extremely complicated, and can read and change memory if you need to. Here I’m keeping it simple. zeusprinthex(1, a, hl, de, bc) means always (1) print these expressions (the values of a, hl, de and be) to the debug output window, whenever the emulator’s PC is pointing at these addresses (the five values of $, which equates to the five lines following each expression). I’m using the same slot (2) for all of them, as the expression is the same.

Running through the first three menu items, it looks like this:

I put a general non-data breakpoint in at the start of the routine too, purely because it separates out the debug output nicely.

Immediately you can see the pattern is wrong. For the second and third menu items, hl (the source address) increases by $100 for each of the five lines. But for the first item it doesn’t! It’s an edge-case—the lines that go wrong start on a 256-byte boundary (i.e. has an $NN00 address).

0000 D100 402A 0009
0000 D208 4132 0009
0000 D200 422A 0009
0000 D308 4332 0009
0000 D300 442A 0009
 
0001 D109 402A 0009
0001 D211 4132 0009
0001 D309 422A 0009
0001 D411 4332 0009
0001 D509 442A 0009
 
0002 D112 402A 0009
0002 D21A 4132 0009
0002 D312 422A 0009
0002 D41A 4332 0009
0002 D512 442A 0009

I could fix this in the code with special handling, but it’s much easier to shift everything along one byte—which equates to 8 pixels to the right—in the .SCR file! After all, I have the space 🙂

  ;                                   Low   Index   Function
  db MenuText.Offset+zxpixeladdr(  8,  0)   ;   0   0: Play
  db MenuText.Offset+zxpixeladdr( 80,  0)   ;   1   1: Keyboard
  db MenuText.Offset+zxpixeladdr(152,  0)   ;   2   2: Kempston
  db MenuText.Offset+zxpixeladdr(  8,  8)   ;   3   2: Sinclair
  db MenuText.Offset+zxpixeladdr( 80,  8)   ;   4   2: Cursor
  db MenuText.Offset+zxpixeladdr(152,  8)   ;   5   2: Fuller
  db MenuText.Offset+zxpixeladdr(  8, 16)   ;   6   2: Kempston Mouse
  db MenuText.Offset+zxpixeladdr( 80, 16)   ;   7   2: AMX Mouse
  db MenuText.Offset+zxpixeladdr(152, 16)   ;   8   3: Help
  db MenuText.Offset+zxpixeladdr(  8, 24)   ;   9   4: High Scores
  db MenuText.Offset+zxpixeladdr( 80, 24)   ;  10   5: Credits

Bingo, bug fixed! This took about a minute and half from starting to write the databreakpoint expressions to testing the fix—waaay shorter than it took to write up for the blog! Sure, I could have stepped through the code in any emulator, and figured out the same thing, but this approach scales up very well when the problem is more complicated, particularly when it is spread out over multiple routines across a longer timeframe.