Spritework on the ZX Spectrum: Rendering

ibobev1 pts0 comments

Spritework on the ZX Spectrum: Rendering | Bumbershoot Software

Last week’s article saw us preparing sprite graphics for display on the ZX Spectrum’s display screen. This week we’ll actually do the rendering, and kick off the shooting gallery project properly:

It also turns out that the shooting gallery project doesn’t quite exercise all the things we’d need from a sprite renderer, so the main program this week will be a simple test program that just moves a graphic between, and off, all four corners of the screen.

Designing the Rendering Function

This routine is complex enough that we should plan it out first. We won’t have to take it all the way down to functioning C code like we did with Bresenham’s algorithm, but a proper pseudocode structure will do us some good.

We are given a pointer to our preprocessed sprite data along with the X and Y coordinates at which to draw it. The sprite data started out as a monochrome 16×16 bitmap; after our preprocessing from last week it is a data structure with the following properties:

It is now an array of 4 24×16 bitmaps.

The first element of the array is horizontally-aligned with attribute cells, with each subsequent element shifted right two pixels.

Within each 24×16 bitmap, the graphics are arranged in column-major order.

These properties are consequences of the decisions we made last week, and these have some implications for our logic here:

We may decompose our 24×16 bitmap into three horizontally-aligned 8×16 bitmaps. This will let us re-use our routines from last week.

We may only place our sprite at even pixel addresses. In most cases we’ll be better served by thinking of X coordinates as ranging from 0 to 127 instead of 0 to 255.

Since the shifted sprites are in sorted order from left to right, the 24×16 image that we want may be found by taking the last two bits of the X coordinate and using them as an index into the array.

With this, then, we’re fully equipped to handle cases where the sprite fits entirely on the screen:

Assuming X is in the 0-119 range, take the last two bits, multiply them by 48, and add them to the source pointer. (Each entry is 3 16-byte columns of graphics, so this advances us appropriately through the array.)

Compute the destination pointer from the Y coordinate and the X coordinate with the offset masked out. With our reimagining of screen coordinates, this will be very similar but not identical to the routine we wrote to plot individual pixels.

Use our unaligned-vertical-copy logic from last week to place each of the three columns of data on the screen at our computed location.

Now we need to handle cases where we’re partially off the screen. In increasing order of complexity:

The right edge can be detected at render time. If we find ourselves advancing to screen column 32, just stop the loop early.

The left edge requires some preprocessing. If we find ourselves asked to render to a negative X coordinate, we need to advance both X and the source pointer a whole number of columns while decreasing the total number of columns drawn. Happily, thanks to the way two’s-complement math works, the offset logic here for picking which shifted image to draw works exactly the same with negative numbers as it does with positive ones.

The bottom edge needs more work during the render; we can detect in advance that we should be rendering fewer than 16 rows in each column, but we will then need to advance the source pointer between columns to ensure that we start rendering each column from the right point.

The top edge is similar, but if we’re off the top edge we need to advance the source and destination pointers before the render since we’re removing the top rows.

Putting it all together gets us this pseudocode.

Input: SRC (pointer to 192-byte buffer), X (-7 - 127), Y (-15 - 191)<br>1. Bounds-check: Quit immediately if the sprite is entirely off the screen.<br>2. Horizontally align data: OFFSET = X MOD 4, SRC = SRC + 48 * OFFSET, X = INT(X / 4)<br>3. Start assuming full sprite drawn: COLUMNS = 3, ROWS = 16<br>4. Correct for being off the left edge: while X (192 - 16)...<br>6a. ... ROWS = ROWS + (192-16) - Y.<br>7. STRIDE = 16 - ROWS.<br>8. Convert final X and Y coordinates to a pointer in DEST.<br>9. Draw the sprite: for I = 1 to COLUMNS...<br>9a. Stash DEST.<br>9b. for J = 1 to ROWS...<br>9b.1. Copy a byte from SRC to DEST.<br>9b.2. Increment SRC.<br>9b.3. Alter DEST pointer to be to the next row down.<br>9c. Restore DEST and increment it to move to the next column.<br>9d. If DEST MOD 32 is now 0, quit immediately (off the right edge)<br>9e. Add STRIDE to SRC.

Implementation

Now that we have our pseudocode, we can start turning it into actual code. This turns out to be more exciting than it looks, and the excitement starts almost immediately.

First we have to nail down the API and that part is straightforward. HL will be our source pointer, D will be our Y coordinate, and E will be our X coordinate. That part isn’t exciting. The part that is exciting is that Y’s...

sprite from pointer screen last week

Related Articles