arantor.org
NextProse (part 2)

NextProse (part 2)

OK, so I regrouped. I went away and thought a little bit more about what I was trying to do and how wildly I was trying to do it.

Did it really need to be fully in assembly, from scratch? Well, no, not as such. And did I need to dive right in up front with figuring out my own orgs and memory mapping shenanigans like I’d sort of been trying? Also, no.

It also happened that I caught up with the ZX Spectrum Next magazine from Fusion Retro where they had a programming tutorial featuring NextBuild. This is a primarily ZX BASIC-like environment, with inline assembly, and a build chain involving VS Code.

The ZX Basic part would be compiled anyway so I didn’t have to worry too hard about that part, especially not just yet. I was more concentrating on the bits I needed to understand and get right: drawing to the screen. Everything else can wait.

I still didn’t entirely understand what I didn’t understand about the screen handling at this point, but it sort of dawned on me after a fashion what I was doing wrong.

Allow me to introduce the bogeyman of the Spectrum Next: banking.

The CPU of the Next is a modified Z80 CPU. It can only ‘see’ a total of 65536 memory locations at any given time. Which is a bit of a problem given that the Next has a total of 2097152 memory locations in it. So what you do is you have ‘slots’. The CPU has 8 slots, the memory has a total of 256 slots and you say ‘this slot for the CPU corresponds to that slot of memory’.

So far so good.

But the screen memory is, comparatively, special. I was trying to directly wire up screen memory through the regular memory handling and if you can do that, I clearly wasn’t doing it right. So I stopped trying to be clever, and just followed the examples I had.

I stepped back, looked at Mike’s code from one of his demos about clearing the screen, saw that it was writing across 3 of the banks (which corresponds to Layer 2 in 256×192 mode; I’m using Layer 2 in 640×256 mode, this uses 5 banks but the principle is much the same)

Then I borrowed some of the setup code for the exact Next registers I needed from samples with NextBuild – they were setting up Layer 2 mode just fine, so I copied them. I saw where they were setting the screen to 320×256 and just changed it for 640×256, and it worked.

And something happened.

This looks like garble. It is garble. The blue stuff is the jumble of stuff in memory when the machine boots up.

But you know what that black part is? That’s memory bank 0 from it being set to black, by my code.

This is the point where my code is doing something deliberate to the screen and that I can actually see how we’re doing it.

NextReg($12,12)				' ensure L2 bank starts at 16kn bank 12 (so bank 24 in 8kb) 
NextReg($70,%00100000)			' enable 640x256 16col L2 
NextReg($7,3)				' 28mhz 
ClipLayer2(0,255,0,255)			' make all of L2 visible 
nextrega($69,%10000000)			' enables L2 

The NextReg instructions are ZX Basic helpers that do actual assembly, not me doing it directly.

Then I have my little ClearHires function that accepts the colour I’m writing as a byte.

SUB ClsHires(byval colour as ubyte)
    ASM
        ; Turn off interrupts interrupting us.
        #ifndef IM2 
            call _checkints
            di 
        #endif

        ; Preserve all registers coming in.
		push	bc
		push	de
		push	hl

        ; Get the current thing banked in 0000-3FFF, and push that onto the stack.
        ld bc, LAYER2_ACCESS_P_123B
        in a,(c)
        push af

        ; Bank in the first bank to 0000-3FFF.
        ld bc, LAYER2_ACCESS_P_123B
        ld a, 1
        out (c), a

        ; Our initial setup. D is the colour to write, E is the number of banks, A is bank offset 0.
        ld a, (IX+5)
        ld d, a
        ld e, 5
        ld a, $10

        ClearAllBanks:
            out (c), a          ; Select the bank we're using.
            push af             ; Keep track of the bank we're working on.
            ld hl, 0            ; Start at the start of the bank.

        ClearLoop:		
            ld	(hl),d          ; Write the value into the current memory area.
            inc	l               ; Next item.
            jr	nz,ClearLoop    ; If we've looped 256 bytes, that's one inner loop, carry on.
            inc	h               ; We have done one outer loop, how many do we need, $40 is 16KB bank.
            ld	a,h             ; Switch the counter into A to compare.
            cp	$40
            jr	nz,ClearLoop    ; If we haven't done the whole bank, back round.

        pop af                  ; Get our block tracker back.
        inc a                   ; Next bank please.
        dec e                   ; Do we still have banks to do
        jr nz, ClearAllBanks    ; If yes, go back and do.

        ; Reset original banked memory.
        ld bc, LAYER2_ACCESS_P_123B
        pop af
        out (c), a

        ; Reset all registers ready to go out.
        pop     hl
        pop     de
        pop     bc

        #ifndef IM2 
            ReenableInts
        #endif 
    END ASM
END SUB

Is it big and clever? No. It’s adapted fairly closely from Mike’s cls function, just inlined into how ZX Basic wants to work and pass parameters around, and clears all 5 banks.

For someone who’s perfectly content to write massive, complex platforms in PHP all day, this felt like quite an achievement to… clear the screen.

So what we’re doing here is, the screen is divided up into 5 slices, each 16KB is size, mapping 128×256 pixels, where each byte represents two pixels side by side. Byte 0 is the two top left coordinates, byte 1 is the pixels directly under that, down to byte 255 which is the two pixels in the bottom left corner.

So far so good. We get the colour of the pair we’re writing (it comes in via IX+5, we get it first into A and then move that to D because we’ll want A free for other things shortly), then we map memory so that the chunk of video memory starts at CPU position 0.

We want to be sure the interrupts are off – so other things don’t come in and try to do anything while we’re doing this – because there’s an interrupt that by default will try to jump to $38 in memory on a regular basis which will be in the middle of our screen handling – which will be a bad time if it jumps into the middle of screen pixels rather than actual executable code!

The rest is just for-loop stuff, count the bytes in the row (255 down to 0, in L), count the bytes we do across (0 to $40, in H) and that’s the bytes we’re writing. Write all the bytes, move onto the next bank, keep going until you’re done.

I even then started to write in a status bar. Similar to my mockup, grey bar with white pixels along the top and side.

This meant setting the palette of 16 colours. Well, that’s not complex code.

    NextReg($43, %00010000)     ' Enable writing to layer 2 palette 0.
    NextReg($40, 0)             ' Palette index 0
    NextReg($41, 0)             ' Palette value 0: Black - COLOUR_BG
    NextReg($41, %10110110)     ' Palette value 1: Half white - COLOUR_FG
    NextReg($41, $ff)           ' Palette value 2: Full white - COLOUR_FG_ACCENT
    NextReg($41, 0)             ' Palette value 3: Black - COLOUR_LOGO
    NextReg($41, %11100000)     ' Palette value 4: Red accent - COLOUR_RED_EDGE
    NextReg($41, %11111100)     ' Palette value 5: Yellow accent - COLOUR_YELLOW_EDGE
    NextReg($41, %00011100)     ' Palette value 6: Green accent - COLOUR_GREEN_EDGE
    NextReg($41, %00000011)     ' Palette value 7: Blue accent - COLOUR_BLUE_EDGE
    NextReg($41, %10000000)     ' Palette value 8: Red main - COLOUR_RED
    NextReg($41, %10010000)     ' Palette value 9: Yellow main - COLOUR_YELLOW
    NextReg($41, %00010000)     ' Palette value 10: Green main - COLOUR_GREEN
    NextReg($41, %00000010)     ' Palette value 11: Blue main - COLOUR_BLUE
    NextReg($41, 0)             ' Palette value 12: Black
    NextReg($41, 0)             ' Palette value 13: Black
    NextReg($41, 0)             ' Palette value 14: Black
    NextReg($41, 0)             ' Palette value 15: Black

I left myself constant names in the comments (though I ended up not using them yet) for reference. The Next’s palette is a bit weird, in that it doesn’t have 255 elements per each of RGB, it has 3 bits (so, 0-7 for intensity in each) and in the basic mode, it only sends 2 actual bits of B data and makes a guess about the third. But still, for now, good enough.

Drawing in the edges is actually fairly similar code though with a couple of slight tweaks because we don’t want to go from bottom to top, only from 10 lines down to top. (And we can go from bottom to top because that’s easier on the decrement logic than keeping a second counter to go up each time!)

SUB DrawTitleBar()
    ASM
        ; Turn off interrupts interrupting us.
        #ifndef IM2 
            call _checkints
            di 
        #endif

        ; Preserve all registers coming in.
        push	bc
        push	de
        push	hl

        ; Get the current thing banked in 0000-3FFF, and push that onto the stack.
        ld bc, LAYER2_ACCESS_P_123B
        in a,(c)
        push af

        ; Bank in the first bank to 0000-3FFF.
        ld bc, LAYER2_ACCESS_P_123B
        ld a, 1
        out (c), a

        ; Our initial setup. D is the colour to write, E is the number of banks, A is bank offset 0.
        ld d, $11
        ld e, 5
        ld a, $10

        AllBanks:
            out (c), a          ; Select the bank we're using.
            push af             ; Keep track of the bank we're working on.
            ld hl, $000a        ; Start at the start of the bank, on line 10.

        DrawLoop:		
            ld	(hl),d          ; Write the value into the current memory area.
            dec	l               ; Next item.
            jr	nz,DrawLoop     ; If we've done all the bytes we wanted, that's one inner loop, carry on.
            ld  (hl), $22       ; This should be the very top of the bar, so draw that in the accent colour.
            inc	h               ; We have done one outer loop, how many do we need, $40 is 16KB bank.
            ld  l, $0a          ; Reset L to the bottom of the title bar.
            ld	a,h             ; Switch the counter into A to compare.
            cp	$40
            jr	nz,DrawLoop     ; If we haven't done the whole bank, back round.

        pop af                  ; Get our block tracker back.
        inc a                   ; Next bank please.
        dec e                   ; Do we still have banks to do
        jr nz, AllBanks         ; If yes, go back and do.

        ; Now draw in the coloured flash.
        ld bc, LAYER2_ACCESS_P_123B
        ld a, $14
        out (c), a              ; Bank in the last bank.


        ; Now draw in the left edge.
        ld bc, LAYER2_ACCESS_P_123B
        ld a, $10
        out (c), a              ; Bank in the first bank.
        ld hl, $000a
        LeftEdgeLoop:
            ld (hl), $21
            dec l
            jr  nz, LeftEdgeLoop

        ; Reset original banked memory.
        ld bc, LAYER2_ACCESS_P_123B
        pop af
        out (c), a

        ; Reset all registers ready to go out.
        pop     hl
        pop     de
        pop     bc

        #ifndef IM2 
            ReenableInts
        #endif 
    END ASM
END SUB

You can just see it sneaking in there in the screenshot.

On that note I went to bed pretty pleased with myself. I’d gotten a routine going across all the banks and drawing in the things it was meant to draw in.

Surely it wouldn’t be that much harder to start drawing something more complex?

Arantor

Your Header Sidebar area is currently empty. Hurry up and add some widgets.