Emulating ZX Spectrum graphics in JavaScript
TL;DR: I'm using EcmaScript 6 Proxy objects to keep track of dirty blocks in emulated ZX Spectrum video RAM. Try it out here: atornblad.github.io/zx-spectrum-bitmap.
Emulating 1982 video RAM in JavaScript
It's no secret that I have a sweet spot for the Sinclair ZX Spectrum. One of the things I was amazed by as an eight-year-old was the (then) incredible 256×192 pixel colour graphics. Using only 6.75 kilobytes of video RAM, the custom Ferranti ULA chip pieced together the video signal 50 (or 60) times per second.
Software emulation of the Ferranti ULA has been done a lot of times, but reinventing the wheel is a great way of learning new (or old) things, so I decided to make an attempt of my own.
JavaScript, CANVAS and Proxies, oh my!
First of all, I'm using a CANVAS
element and the CanvasRenderingContext2D
object to draw graphics in the browser's window. I'm also using a Uint8ClampedArray
to store the 6912 bytes of raw video RAM. For a more detailed description of the memory layout, scroll down a little. Each byte of the array corresponds exactly to one byte of ZX Spectrum RAM, so changing the contents of a single byte should trigger a redrawing of at least a part of the canvas.
I decided to redraw the canvas in blocks of 8×8 pixels, because this is close to how the ZX Spectrum ULA worked. Changing any one of the 8 bitmap bytes inside a block, or its attribute byte, should mark that block as "dirty" and when the next animation frame comes along, all dirty blocks should be rerendered. Because of this, there is also a Uint8Array
of length 728 (32×24) keeping track of dirty blocks, so that I don't have to redraw all blocks every frame.
Using a Proxy
object, I'm able to use the array normally, while correctly marking dirty blocks as needed. Without a Proxy
, I would have to expose a setter method for changing the RAM contents.
// Without a Proxy object:
data.set(address, newValue);
// With a Proxy object:
data[address] = newValue;
The Uint8ClampedArray
and Proxy
construction looks like this:
var data = new Uint8ClampedArray(6912);
var dataProxy = new Proxy(data, {
"set" : function(target, property, value, receiver) {
if (property >= 0 && property < 6912) {
data[property] = value;
var dirtBlockIndex;
if (property >= 6144) {
// The index is inside the attribute section
dirtBlockIndex = property - 6144;
} else {
// The index is inside the bitmap section
dirtBlockIndex = blockIndexFromOffset(property);
}
dirtyBlocks[dirtBlockIndex] = 1;
return true;
}
// Not a numeric index inside the boundaries
return false;
}
});
This creates a Proxy
that, when written to, sets the value of the hidden array, calculates what block is changed, and marks that block as dirty, so that the next call to the renderer only redraws the dirty blocks. This speeds up the rendering process a lot.
The ZX.Spectrum.Bitmap
object exposes the following public functions:
poke(address, value)
: Changes one byte of video RAM (valid adresses are within the 16384..23295 range)peek(address)
: Reads one byte of video RAMink(value)
: Sets the current INK colour (0..7)paper(value)
: Sets the current PAPER colour (0..7)bright(value)
: Sets the current BRIGHT value (0..1)flash(value)
: Sets the current FLASH colour (0..1)cls()
: Clears the screen using the current settingsplot(x, y)
: Sets one pixel, affecting the colour blockunplot(x, y)
: Clears one pixel, affecting the colour blockline(x1, y1, x2, y2)
: Draws a one pixel line, affecting colour blocks
Back to the 1980s
The ZX Spectrum was an amazing computer for its time. An advanced BASIC interpreter fit snugly into 16 kilobytes of ROM, and the 48 kilobytes of RAM included 6.75 kilobytes of graphics memory. Using BASIC commands like PLOT
, INK
and CIRCLE
, you could write algorithms to draw things of beauty on the screen, but you had to look out for attribute clash.
The video RAM consisted of monochrome bitmap data containing one bit per pixel for a total of 256×192=49152 bits, fitting into 49152/8=6144 bytes, starting at address 16384. The order of pixel rows inside this memory area is a little strange, as rows are not placed linearly (each line of 256 pixels is not exactly 256 bits after the one above it). To calculate the screen address of the first pixel of a Y coordinate, you encode the address as follows:
Byte 0 | Byte 1 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0 | 1 | 0 | Y7 | Y6 | Y2 | Y1 | Y0 | Y5 | Y4 | Y3 | 0 | 0 | 0 | 0 | 0 |
This effectively divided the screen vertically into three blocks of 256×64 pixels, within which it is easy to get to the next line of characters, and also easy to get to the next line within a character block by simply adding one to the high byte of the address, but calculating the screen position from a pixel coordinate is really convoluted.
Directly after that monochrome bitmap, at address 22528, was one attribute byte per 8×8 block, containing the colour values for the "ones" and "zeros" of the bitmap data. Each attribute byte is encoded like this:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
F | B | P2 | P1 | P0 | I2 | I1 | I0 |
F
holds a one for FLASH mode, where INK and PAPER alternates every 32 framesB
holds a one for BRIGHT mode, where both INK and PAPER are a little brighterP0..P2
holds a value between 0 and 7 for the PAPER colour, which is used for zeroes in the bitmapI0..I2
holds a value between 0 and 7 for the INK colour, which is used for ones in the bitmap
Avoiding the "attribute clash" was tricky, and you had to really plan your artwork or your graphics algorithm to make sure that you only ever needed two distinct colours inside each 8×8 block of pixels. Artist Mark Schofield wrote an article in 2011, describing his process of planning and creating a piece of ZX Spectrum artwork.
If you are looking for more ZX Spectrum art, here are a couple of sites you might have a look at:
You can try this solution at atornblad.github.io/zx-spectrum-bitmap. The latest version of the code is always available in the GitHub repository.