Spectrum Shooting Gallery: Taking Back What We’ve Learned

07:30 PM · Aug 29, 2026

After I did the ZX Spectrum shooting gallery project last month, I got to thinking about what lessons I could bring back to any of my earlier ports. The obvious candidate was the ColecoVision—same CPU, but a graphics chip that gave us proper sprites to work with. The Spectrum graphics, despite not being written a system with actual sprite or multicolor support, still were more detailed than the Coleco version, and in principle that should be easy to fix.

Happily, it was easy to fix, and suggested some more difficult improvements that would make it look even better, but before I got to the harder stuff, I noticed something interesting in my emulator tests. This derailed the project completely as I proceeded to hunt down and fix a bug in my test emulator, then get that fix properly submitted to the upstream project.

History of the Project

The ColecoVision port showed up pretty late, and it fell out of a very complicated history. It was written as a thin wrapper over the core logic of the SG-1000 port. This was part of an experiment to see how feasible it would be to “write once, run anywhere” on the constellation of systems I referred to as the “children of the TI”. These systems were built around Z80 CPUs, at least 1KB of RAM, and a TMS9918A graphics chip with a full 16KB of VRAM. The port for them was based on the port for their notional ancestor, the TI-99/4A.

That TI port was not great. When I made it, I was not particularly interested in the TMS9918A and its capabilities as a graphics system yet. That came later. Instead, my primary focus was simply on how to organize medium-to-large-scale software development on the unexpanded TI-99/4A, so I largely borrowed the 8×8 graphics and the core logic from the PICO-8 port. While I’d intended the PICO-8 port as representative of a “modern system,” it was also to closest match to the TMS9918’s actual sprite capabilities. That port, in turn, was based on the Atari 800 port, which in turn was built to demonstrate how its ANTIC chip could automate some of the basic work of sprite display on its predecessor, the Atari 2600.

Those two Atari ports joined with the Amiga spritework series to highlight the design throughline between them all, with the ANTIC and later the Copper essentially automating the line-by-line graphics that the 2600 explicitly managed in software. The TI—and by extension, the ColecoVision and many others—instead built their sprite systems around a large number of fully self-contained graphics units. Atari graphics did have a pretty firm limit of 8-pixel wide movable graphics on a display that was 160 pixels across:

That meshed conveniently with the TI, with a 256-pixel display that could display 8×8 graphics at double scale, so the ports to the TI and its descendants just reused the Atari graphics directly.

Meanwhile, the Amiga allowed a 16-pixel wide sprite against a display 320 pixels across, and also permitted three non-transparent colors per sprite. These sprites end up taking the same amount of space on the screen, but the detail improves. This is where the cannon became the “blaster truck.”

When my ZX Spectrum sprite system ended up working mainly with 16×16 monochrome blocks, I borrowed from the Amiga’s design to bring back the blaster truck and generally increase the circularity of the archery targets:

And that, at long last, takes us back to the TMS9918A, and thus, the ColecoVision display.

Making Better Use of TMS Sprites

The TI-99 port, and thus the Coleco port after it, copied the Atari graphics directly, taking advantage of a magnification option in its graphics which doubled the size of every pixel, making the targets take up a comparable amount of screen real estate. Since I wasn’t paying much attention to graphics at the time, I decided that was Good Enough and carried on with the project.

We do not need to restrict ourselves to that, though, because the TMS9918 series offers two ways to get large sprites, which we may use independently or together; there’s the sprite magnification option, and also a 4-tile mode that makes sprites be 16×16 instead of 8×8. We built the Spectrum graphics on 16×16 monochrome sprites, and by using 4-tile unmagnified sprites, that’s exactly what we get here. We can port those over directly.

In fact, we can do better yet; we aren’t, strictly speaking, restricted to monochrome sprites. The whole point of sprites, after all, is that you can put them anywhere, and that means that we can get a multicolor sprite simply by overlapping sprites of different colors. This isn’t unlimited; the TMS9918 can only put 4 sprites on any given scanline, and that’s usually a very harsh limit. But we’ve arranged the sprite display so that the blaster truck is the only thing on any of its scanlines. This means we can reasonably replicate the Amiga’s blaster truck closely.

We are not so lucky with the targets; between the three targets and a single shot our per-line budget is already completely spoken for.

The Road Not Taken

Our per-line sprite budget is spoken for, but that doesn’t mean, in principle, that we’re completely out of options. The ColecoVision’s Z80 CPU actually ran very slightly faster than the Spectrum’s, and the TMS, like the Spectrum, offers a bitmap display alongside its sprites. We should be able to get two-color targets by moving a bitmap behind the target sprites. I messed around a bit in a paint program and got this mockup:

Accessing the VRAM is a bit harder than the Spectrum’s bitmap, so I don’t think the techniques would port over directly. Happily, I think there’s enough spare tiles in the Graphics I mode that you could do everything at runtime only with nametable updates, which should be more than fast enough for anything you need.

I did not, however, get around to actually implementing that, because I noticed something interesting while I was testing out the new spritework: a bunch of things I’d previously noted as emulator bugs stopped happening.

When I made the initial ports, I noticed that many of the emulators I was using had strange sprite emulation accuracy issues. In particular, on Mesen, the targets would sort of accelerate as they left the screen. Once I switched to 4-tile unmagnified sprites, this weird acceleration effect went away. This got even more interesting when I switched on magnification alongside the 4-tile sprites. With that effect, not only would the targets accelerate while leaving the screen, they started ripping themselves in half along the way.

There is definitely some wackiness here, and for some time, that would be where the story ended. However, since the time I first wrote this code and today, Mesen’s primary developer Sour decided to start stepping away from the Mesen project. Active development is somewhat more collaborative now under the fork name “Mesen Community Edition” and MesenCE is at time of writing the current preferred version. MesenCE also now runs comfortably on Linux, which is nice for me; I prefer doing C++ work on that OS.

At this point my goal for this project changed: instead of trying to replicate the techniques I’d just done on the Spectrum, instead go and find and fix the bug in magnified ColecoVision sprites in Mesen.

Finding the Bug

The overall task of fixing the bug ended up taking maybe an hour and a half, starting from scratch. I’d never looked at the Mesen code before but this generally doesn’t stop someone with a debugger… and for this particular bug I was familiar enough with the systems surrounding it that I had a bunch of useful information going in:

  • The bug has a 100% reliable trigger. Set early clock mode and magnify sprites, then place a sprite partially off the left-hand side of the screen. It will end up too far off the screen.
  • This is strictly a rendering bug. Mesen is my preferred emulator in the first place because the debuggers it provides for the emulated systems are fantastic. Even back when I first wrote the gallery I knew this because I could trivially confirm that the sprite attributes in VRAM were correct. The targets were just not being drawn in the right place.
  • This only affects the ColecoVision and SG-1000. This is why it’s not surprising a bug would lurk here; ColecoVision support is brand-new and the SG-1000 support is essentially there as part of emulating the Master System (which offers a compatibility mode). The Master System outside of that compatibility mode does not offer sprite magnification or 4-tile sprites. (It does still have a multitile sprite mode, but it is a 2-tile sprite strongly analagous to what the NES offers.)
  • This is happening at a level beneath the actual sprite level. Assuming the issue is “sprites are being drawn in the wrong place,” the “targets rip themselves in half” issue is this issue happening independently to the left and right halves of 2-tile-wide sprites.

Tracking down the issue once I started looking at the code was unglamorous but instructive. I learned a few fun things along the way.

  • The bug hadn’t been fixed while I wasn’t looking. My copy of Mesen was quite a ways out of date. Always check the tip of development before maybe accidentally reduplicating work!
  • The ColecoVision and SG-1000 use exactly the same code as a Master System out of Mode 4. A git grep TMS9918 command only finds matches in the file Core/SMS/SmsVdp.cpp.
  • Mesen’s sprite renderers track the general hardware design pretty closely, but not at register-and-signal level emulation. There are a collection of “sprite shifter” objects that load a byte of data from VRAM at the appropriate time in each scanline, and then this data is consumed a bit at a time like it’s being serialized out of a shift register. Magnified sprites only actually do the shift after every other pixel is output. 4-tile sprites assign two shifters per sprite per scanline.

That discovery confirms the “level beneath the actual sprite” assumption: two sprite shifters for a 4-tile sprite means that the issue will hit only the shifter that’s off-screen. A little more digging led me to the ShiftSpritesSg function:

void SmsVdp::ShiftSpriteSg(uint8_t sprIndex)
{
_spriteShifters[sprIndex].SpriteX -= 32;
int16_t x = _spriteShifters[sprIndex].SpriteX;
if(x < 0) {
_spriteShifters[sprIndex].TileData[0] <<= -x;
}
}

This is the routine called when preparing a scanline render and the sprite attribute bit specifies early clock. It moves the X position of the shifter target left 32, and then if that’s negative, pre-shifts away the requisite number of pixels. Alas, that’s not what we want. Magnified sprites shift half as fast, after all; we’re clipping twice as many pixels as we’d expect in that shifter, and that matches the “acceleration” we saw before; the speed changes from 2 px/frame to 4 px/frame just as it starts leaving. Fixing the bug should be a simple matter of dividing this shift amount in half when appropriate and then handling the case where we’re only shifting out half a pixel. If Mesen were simulating the hardware more obsessively here, I think this would all take care of itself because the pixels would be clocking themselves out one at a time during HBLANK, but the ShiftSprite functions are summarizing that action. That’s fine; we just need to figure out how it’s simulating the slower clocking to make sure we do the right thing.

First we need to figure out how to know if sprites are magnified at all. Skimming the method definitions takes me to SmsVdp::WriteRegister() and this very convenient trio of lines:

        case 1:
            _state.EnableDoubleSpriteSize = (value & 0x01) != 0;
            _state.UseLargeSprites = (value & 0x02) != 0;

Register 1, $01 bit: EnableDoubleSpriteSize is what we want here, and if we see UseLargeSprites we’ll know that’s the 4-tile mode. With those names in hand I start searching for them in the VDP source to see where the actual magnification happens. That turns out to be in SmsVdp::GetPixelColor():

uint8_t sprColor = ((_spriteShifters[i].TileData[0] >> 7) & 0x01);
if(!_state.EnableDoubleSpriteSize || ((_spriteShifters[i].SpriteX - xPos) & 0x01)) {
_spriteShifters[i].TileData[0] <<= 1;
}

We lucked out. The shifter actually shifts its pixel out if magnification is off or if the distance to the starting X coordinate is odd. That means that we don’t have to track half-pixels explicitly at all. (Going in I was afraid that it simply didn’t check this at all and instead was just counting out pixels, requiring me to add wholly new machinery.)

Actually Fixing the Bug

Well, all we have to do is halve it, right? That only means we have to change half a line.

_spriteShifters[sprIndex].TileData[0] <<= -(x >> (uint8_t)_state.EnableDoubleSpriteSize);

The idiom of shifting an amount determined by casting the boolean flag to an integer is… well, it’s not generally what I’d do but it’s the standard approach in this code base. (It’s correct to do this; I double-checked the standard to make sure. When casting true to an integer, it really is exactly 1. And this fix makes the Gallery look right!

However, that’s not good enough. The gallery only ever assigns sprites to even pixels, so the rounding behavior here isn’t being tested. I cut the speed of the targets in half, so that every possible X value is used. This shows us that we got it wrong; the targets start vibrating wildly as they leave the screen. We need to negate before the shift.

_spriteShifters[sprIndex].TileData[0] <<= (-x) >> (uint8_t)_state.EnableDoubleSpriteSize;

Second time’s the charm. But fixing the bug isn’t the end off the task.

Preparing a Report

I spent a lot of words here describing my process for finding the bug, but characterizing it is very brief: magnified sprites in early clock mode are rendered in the wrong location when they’re partially off the screen because ShiftSpriteSg trims away too many pixels. The interesting part of the report was going to be making a clear test ROM.

It’s a lot easier to write a tiny self-contained ROM on the SG-1000 than the Coleco, due to the lack of BIOS. I essentially slightly reworked the Hello World program from my Sega 8-bit Platform Guide but had it configure the sprite system instead of the pattern and name tables. The core of the design was a 4-tile sprite with this pattern:

X......XXXXXXXXX
.X....X.XXXXXXXX
..X..X..XXXXXXXX
...XX...XXXXXXXX
..X..X..XXXXXXXX
.X....X.XXXXXXXX
X......XXXXXXXXX
................
................
................
................
................
................
................
................

When magnified, the sprite takes up 32×32 pixels of space but only displays anything for a 32×16 space. That lets us stack 12 of them over the course of the screen; I set up the bottom row to be 8 pixels off-screen and then have each row above it move progressively one pixel left. When the bug is active, that will produce a display where the left half of the sprite has gone completely off-screen by the end, but advances normally for a few pixels until it actually starts leaving the screen.

Once the patch is applied, we can see the sprite leave the screen one pixel at a time, which, thanks to the magnification, ends up looking like it’s a half-pixel at a time:

That is much tidier than frame-advancing the entire shooting gallery while checking VRAM values.