Chasing the raster on the ZX Spectrum in Sidewize
How did Sidewize accomplish the "raster-chasing" required to update the display 50 times per second? Read on!
[note: jump down to The Results to see the videos, otherwise let's dive in!]
Twitter user Ville Krumlinde mentioned in a Twitter comment that he had captured the screen draw update sequence for a number of Spectrum games and recorded the updates to video. He mentioned that he had hacked an existing Spectrum emulator to do this, without going into specifics. This got me thinking about how I might do that for the "raster-chasing" games I've developed, specifically Sidewize and Crosswize.
I do have my own Spectrum emulator, it runs on Windows and Pocket PC (and PalmOS, but that is another story). It has not seen the light of day for over 15 years (and my Windows-foo is rusty), so the prospect of compiling it again was a bit off-putting. Then it occurred to me that the Spectrum emulator I have been using for the ZX Spectrum Next version of Nodes of Yesod, CSpect by Mike Dailly, has an interesting plugin architecture and is written in .NET. Since the emulator runs on the Mac (thanks to Mono), I wondered if I could leverage the plugin architecture to do ... something, something ... and somehow capture screen updates, and then somehow capture those snapshots to disk, and then, string them together to make a video? I am pretty familiar with Adobe Premiere (I run a YouTube travel channel) so I figured if I could just get those image snapshot files on disk, creating the videos would be a piece of cake.
More on all of that in a minute, but first let's take a quick look at how Sidewize (and Crosswize shares this basic mechanism) does its video synchronization. The raster-chasing challenge is twofold, really.
- The code that updates the screen has to be fast so that it can execute within 20ms (at 50fps) along with all the additional code needed to update the game state, read the input, output audio, etc.
- Any updates to the display must be done while the raster is not updating the visible screen. If you should "cross the raster" you'll see flicker or tearing as the areas of the screen above and below the raster position will reflect two different game states.
As far as video synchronization goes, the standard way to sync with the video display signal is to use the so-called vertical blanking interrupt (or VBL interrupt). Many games on the Spectrum would do a simple HALT instruction somewhere in the code, under the (reasonable, in many circumstances) assumption that the VBL interrupt is the only interrupt on the system. From the Zilog Z80 CPU User Manual:
The HALT instruction suspends CPU operation until a subsequent interrupt or reset is
received. While in the HALT state, the processor executes NOPs to maintain memory
refresh logic.
The effect of issuing a HALT is to cause the system to wait until the next vertical display retrace (to all intents and purposes, this is at the top of the physical display, above the border area) which gives you a little time before the bitmap display itself begins the next redraw cycle, but not really enough to comfortably update the display without risking meeting the video beam on its way down the screen.
Another approach, which was sort of discovered independently by several UK programmers at the time, is to leverage the Spectrum's so-called "floating bus", the upshot of which is that certain input ports, when read by the CPU, return the current color attribute value that is being rendered by the display hardware. Personally, I discovered this by experimentation and came across port 0x40FF which I found to return attribute colors pretty reliably. My approach was to reserve a line of "bright black" (this looks identical to non-bright black on the Spectrum) immediately below the active playing area, and only in this one area on the screen. By entering a loop reading port 0x40FF and waiting for a value of 0x40 (bright black), we know that the CRT beam is in this area of the screen, and so can begin to update from the top of the screen knowing that we are certain not to cross the raster. I must admit, this is pretty empirical, and in fact, failed to work on later revisions of the hardware (though there are some workarounds).
Additional drawing time is gained using floating bus method vs typical method of using VBL |
The snippet of code used to wait for this bright black screen row is here. The TLDR is that if we start to redraw the active gameplay area immediately after the CRT has completed updating the display from the previous frame, we buy ourselves much more time to compose the next frame (as the image above hopefully shows). Basically, we buy ourselves the time the CRT beam takes to cover the entire status display plus the bottom border area.
So that is a little insight into how Sidewize updated the display, but what does that actually look like? Well, it turns out that the plugin architecture of CSpect gives us exactly what we need to capture this visually. The CSpect plugin interface offers us the ability to do the following:
- Register a callback that is called whenever a Spectrum memory address is either read or written to.
- Register a callback that is called whenever a Spectrum port is either read or written to.
- Register a callback that is called whenever a key, or combination of keys, is pressed (and this is PC friendly, so you can specify things like <ctr><alt>1, etc).
- Upon initialization, registered to receive a callback whenever the Spectrum screen area (6,192 bytes starting at address 16384) was written to. This callback could then capture an image of the current display state to a sequentially-numbered *.scr file in one of a couple of different ways, depending on the capture mode (see below).
- Registered a callback to listen to various keypress events, to trigger a couple of variations of display memory capture modes:
- SCREENCAPMODE_BUFFER. A mode that simply snapshots the current state of the Spectrum screen to a *.scr file
- SCREENCAPMODE_BYTES. A mode that takes the current screen byte being written and composes it to an internal byte array buffer, and then snapshots that buffer to a *.scr file.
- SCREENCAPMODE_SIDEWIZE_TRIGGER. A mode that waits for the next screen update cycle (very Sidewize specific - based on the game code reading port 0x40FF) and then triggers one of the above, and then turns it off on the next update, capturing exactly one game frame.
- Registered a callback to listen for a read of port 0x40FF and return the expected 0x40 to satisfy the game's video sync-wait loop (CSpect does not emulate my particular usage of the floating bus, which in this case proved useful because I was able to hijack the port read). This callback triggers SCREENCAPMODE_BUFFER mode if SCREENCAPMODE_SIDEWIZE_TRIGGER mode is enabled (and turns screen capture off the next time the port is read).
- Additionally, I registered a callback to activate a couple of sets of Sidewize pokes, one to make the player invulnerable, and one to bypass a bug that I encountered that was likely caused by the drastic timing changes caused by hooking port 0x40ff (it looks like interrupts corrupted registers, but I did not investigate and fixed the symptom rather than the cause).
magick mogrify -path png -format png -scale 1024x768 scr/*.scr
This assumes a folder named scr contains a set of sequentially numbered *.scr files, and an empty png folder awaiting the *.png results. Once in *.png format, it was a simple matter to import into Adobe Premiere and render into video sequences.
The Results
- The top part of the screen (and the whole screen if in "space" mode) is erased using a dirty rectangle system.
- Entities overlapping with the scrolling terrain sections are erased by the terrain redraw and so are not dirty rectangle erased.
- You'll see the starfield and "ground" layers update in their own phase.
- And then finally, once all objects are erased and the landscape is redrawn in its new location, the player and enemy entities are redrawn.
- The boss monster uses a different redraw approach and I don't quite recall what it is doing; however, it looks like it is just drawn over the starfield, and then any erased stars are just added back in? Not sure about that.
Comments