Skip to content

Adding sprite graphics and displaying it (Part 2)

Peter Helcmanovsky (Ped) edited this page Jun 2, 2020 · 2 revisions

Adding sprites

The static background is on screen, now let's just add rest of the game. Hm, ok, maybe not so fast, but let's add sprite gfx file (provided by John earlier as .spr file) and add enough init code to display some sprite on the screen.

About .spr file format

This file can be edited directly on Next with spredit dot-command included with NextZXOS distro (you can try to run .spredit SBsprite.spr to see all the SpecBong sprites in the editor). It's raw 16kiB dump of sprites pattern memory, assuming the default sprite palette is used (color == index) (there are rumours the tool may get extensions to support also custom palette and more functions in future, yay!). And it is in correct form to be directly uploaded by game code to FPGA memory storing the sprite patterns.

The file contains 64 sprite "patterns", each pattern being 16x16 pixels (in case of 8bpp graphics), i.e. 16*16*64=16384 bytes. In case you are planning to use 4bpp (four bits per pixel) type of sprites, every regular 16x16 8bpp pattern is interpreted as two 4bpp patterns (the 256 byte long pattern is split into halves, 128 bytes each, containing 16x16 4bpp pixels), where one byte of pattern contains two 4 bit pixels, the "left pixel" is in bits 7-4, and "right pixel" is in bits 3-0: for example byte $41 displayed as part of 4bit pattern with no rotation or mirror will emit pixel with palette index 4 and second pixel with palette index 1. (note: you can display any pattern in 8 bit or 4 bit mode, the decision is in the sprite attributes, not in the graphics, the graphics itself does not contain any bit indicating if there is 4 or 8 bit patterns stored)

The SpecBong is using only 8bpp sprite graphics and only in default palette.

Including the pattern data into NEX file

(here is a good spot to open the SpecBong.asm file and try to match the source code lines with this text)

(you can also check the total difference between "Part 2" and "Part 1")

Let's ignore the code changes and jump toward end of file, where the Part 1 included the background image palette data. The code to include the sprite patterns is very similar, but this time we are mapping whole 16kiB region of assembler's virtual-device $C000..$FFFF to next free 8kiB page right after background image palette data. This will skip 7424 bytes after the background palette, and use second half of "Bank 12" and first half of "Bank 13" (in 16ki banks numbering schema), but it will allow us to map the data into CPU addressing space at runtime in very simple way, so let's proceed like this for the sake of simplicity.

The assembly-time mapping is done by MMU 6 7, $$BackGroundPalette + 1 directive mapping to address region $C000..$DFFF 8ki page 25 and to region $E000..$FFFF 8ki page 26. Then the ORG $C000 will set the machine code output pointer to this newly mapped region. And finally the raw data are included into it by INCBIN directive, also marking the beginning of the region with label SpritePixelData.

If you will build the NEX file with this change, it will become larger by 16kiB (as it has to dump whole "Bank 13" to the file, due to internals of NEX file format) and those 8kiB pages 25 and 26 will contain the pixel data for all sprite patterns (they will be loaded by the NEXLOAD utility before starting your code).

Uploading the pattern data to FPGA

ZX Spectrum Next has powerful HW support for "Sprites", but any sprite being HW rendered to screen must select its pixel data from one of the 64 pattern slots of memory which is physically stored inside the FPGA chip (not in the separate SRAM memory chips). This gives the sprite engine super fast access to pixel data, but it also makes the pixel data memory out of reach for the Z80N and regular machine code instructions.

So before we can display any sprite from the provided SPR file, it is not enough just to get it into the regular memory (been done in previous step), but the code must upload it to the FPGA sprite pattern memory. For this we will use I/O port $303B "sprite status/slot select" and I/O port $5B "sprite pattern upload".

The code writes first value 0 to I/O port $303B selecting pattern slot 0 for upload (while uploading pixel data, after sending 256 bytes worth of pixels the pattern index will auto-increment to point to the next slot and after uploading final "slot 63" data the internal index will wrap around back to "slot 0").

Then we map the sprite pixel data into address range $C000..$FFFF (the same range where we included them into the file at assembly-time, so the label SpritePixelData points to the same address), by writing 8ki page values 25 and 26 into NextRegs $56 and $57 (the magic numbers 25 and 26 are avoided by using the $$<label> operator to let assembler evaluate the 8ki page number of particular label). This mapping happens at runtime, when the code is executed by Next (contrary to MMU directive used to include the data, which is affecting the assembling only).

Now with the data in visible address range of Z80N CPU we can read it and send it to the pattern-upload port $5B. We will use instruction otir to write lot of data from memory to I/O port. Note the Next HW does care only about low 8 bits of I/O port address in case of $5B port, so it doesn't matter whether we send the pixel byte to port $125B or $A55B, both ports are considered as sprite pattern upload by the Next. This makes the otir instruction viable for this task, as it writes bytes to port number stored in register BC, but it also does use the half of it as counter, the B register. With B equal to zero we will get upload of 256 values (0 decrements to 255, 255 to 254, ... until 0 is reached again) with one otir instruction (note the otir is not practical for ports where the Next is decoding target port by checking also some top bits of the port number, like for example the port $303B). And because we want to upload all 64 patterns, we are using extra counter in register A to repeat this 256-bytes upload 64 times.

This uploads all sprites pixel data from main memory to the FPGA fast memory, making the graphics available for displaying.

How can we check if it works?

The uploaded sprite graphics is not readable by Next SW any more (although if you are debugging the project with Maziac's DeZog using one of the emulators, you can view the sprite pixel data after upload thanks to the extra functionality of emulators, ZEsarUX has also built-in sprite memory viewer). So how to check if everything uploaded correctly?

Well, let's just use 64 sprites to display all 64 patterns, in a grid (8x8 grid of sprites), so we can visually check that everything is alright and also we can do our "first Next sprite displayed" check mark on our bucket list.

For this we will need I/O port $57 "sprite attribute upload", where you can send four or five bytes (depending on the type of sprite) to reconfigure one of the 128 sprites, where, how and what it should render. The sprite with index 0 was already selected together with pattern-slot 0 by writing to port $303B (the port $57 has independent index into sprites, not affected by patterns upload), and it will also auto-increment after every sprite upload (but after uploading sprite 127, the wrap to 0 is not guaranteed in the future versions of core, so don't rely on the 127→0 wrapping in this case).

The grid of sprites will start at coordinates 32,32 (H register contains X and L register contains Y), and DE registers set to other helpful constants (D has palette offset +0, no mirror/rotate and MSB of X coordinate being zero, E has top bit set to make sprite visible and type of sprite data set to four-byte (b6=0) and sprite-pattern to display is set to index 0).

Then two nested loops are executed, setting up eight sprites per eight rows, each 24 pixels apart, with sprite pattern going from 0 to 63.

And because the layer ordering (with sprites on top) and sprites visibility was already set up by the init code done in Part 1, this is enough to make the sprites visible. So the familiar infinite loop follows (to guard CPU from running into wild wild memory and executing random values found there as instructions).

This is how the final result should look (with all gfx patterns displayed in 8x8 grid of sprites):

SpecBong part 2 ran