ZalaXa 13 — You Gotta Roll With It

In the last tutorial I looked at creating a randomized starfield for the main menu. Since then I’ve been playing around with techniques to scroll it vertically, like the starfield on the Galaga arcade machine.

I found a bug in the code I wrote. Instead of using a pair of random(ish) numbers for the X and Y coordinates of each star, I was actually using the Y coordinate of the previous star for the X coordinate of the next one. This was causing unnecessary vertically-repeating patterns.

After fiddling around with this for a while, I decided to use a third randomish number to represent the star position with the character. This leaves me with some spare bits I can use for attribute colours without introducing more repeating patterns.

I also added a seed randomizer in my test routine—instead of advancing the ROM seed sequence by one every time you press a key, I completely randomized it to any address between $0000 and $3FFF. I used Patrik Rak’s Complementary Multiply With Carry pseudo-random number generator again for this.

Same deal as last time, except this time it scrolls vertically:

Most of the code is pretty similar to last time. The section that draws the stars is duplicated. In the first iteration, the part that creates the set n, a opcode now creates a res n, a instruction instead, which undraws the previous star.

37
38
39
40
41
42
; stars.asm
 
                        ld a, (hl)                      ; Read the byte represnting the star pixel,
                        or %10000111                    ;      turn it into a "res n, a" instruction ($80 10nnn111) by setting
                        and %10111111                   ;      and clearing bits,
                        ld (ResetBit), a                ; SMC> then write this into the pixel-drawing code.

In the second iteration, we keep the set n, a instruction as it was in part 12. But before we draw the star pixel, we manipulate the Y coordinate by including the ScrollStar macro:

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
; macros.asm
 
ScrollStar              macro()
                        ld a, d                         ; Retrieve the byte containing bits 0..2 of the Y coordinate.
                        and %00000111                   ; Calculate Y mod 8 (0..7).
                        cp %00000111                    ; If 7 then
                        jp z, CharScroll                ;   do a character scroll.
                        ld a, d                         ; Otherwise do
                        inc a                           ;   a pixel
                        ld d, a                         ;   scroll.
                        inc (hl)                        ; Save px-scrolled offset back to StarField.Table.
                        jp Continue
CharScroll:
                        ld a, e                         ; Retrieve the byte containing bits 3..4 of the Y coordinate.
                        add 32                          ; Increment those two bits by one
                        ld e, a                         ;   and save back the byte. This moves into the next char row.
                        dec l                           ; Now retrieve the byte
                        ld (hl), a                      ;   containing bits 0..2 of the Y coordinate,
                        inc l                           ;   and
                        xor a                           ;   reset it to zero, ensuring we start at the top of that char row.
                        ld d, a                         ; Save that back to the byte, and also
                        ld (hl), a                      ;   save the px-scrolled offset back to StarField.Table.
Continue:
mend

Doing this as a macro isn’t really necessary at all, but when reading the code, it does emphasise that the pixel-setting half is almost the same as the pixel-clearing half. It achieves this by abstracting the extra code into a different place—without any of the call/ret/jp overhead a separate routine would entail.

The stuff in the macro decides whether we’re scrolling within a character boundary, or scrolling between character boundaries. Then it sets the appropriate bits of the Y coordinate accordingly. The specific maths here is needed because of how the Y coordinate is distributed within the screen address:

Image from the L Break Into Program blog, with thanks to Dean Belfield.

In the Spectrum’s distinctive display file, the bits labelled Y0, Y1 and Y2 govern the position (0..7) within a character row. We set these bits in the first half of the macro.

The bits labelled Y3, Y4 and Y5 govern the position of the character row (0..7) within the current screen third. We set these bits in the second half of the macro.

The bits labelled Y6 and Y7 govern which screen third the character row appears in. Because we’re repeating the same stars in all three thirds, we set these bits in the SetupStars routine:

84
85
86
87
88
89
90
; stars.asm
 
                        ld b, 8                         ; Fast way of doing ld bc, $0800 (size of a screen third).
                        add hl, bc                      ; hl is now a pixel address in the middle screen third.
                        ld (hl), a                      ; Draw the same single pixel star here, too.
                        add hl, bc                      ; hl is now a pixel address in the bottom screen third.
                        ld (hl), a                      ; Draw the same single pixel star here, too.

Actually, adding $0800 to the screen address using the add hl, bc instruction is an inefficient way of doing this. All we really need is to add 8 to h. Or even better, flip one or two bits of h. I might optimise this part another time.

I made a list of 32 of my favourite starfields using this new version of the randomizer test routine. I particularly like how it tends towards drifts of stars that look like constellations.

This is my favourite starfield (seed address $26B1), complete with the scroll effect:

Next time, I’ll explore setting attribute colours to make the stars twinkle. Cheers!

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.