DEV Community

Adrian
Adrian

Posted on

Displaying ZX-Spectrum encoded screens in JavaScript

Hello JavaScript enthusiasts!

In this article I'm showing you how to display an ZX-Spectrum “encoded” screen. Don’t worry, it's not complicated, we just use basic array manipulation in JavaScript. All the technical details on how the screen is encoded are provided in the article!

Alt Text

A ZX-Spectrum screen is a binary structure of exactly 6912 bytes in size. In order to avoid operations with files, we just encoded the screen as inline JavaScript array as this:

var angrychicky = [ 0,60,33,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2, ... ];

Therefore all we have to do is to read the array, and put pixels on the screen in acordance with the specs below.

Note: In order to minimize the JavaScript file size holding the array, and also to give an extra flavour to this article, the inline array is a compressed one. Therefore instead of the 6912 expected elements, it will have anywhere between 1000 and 3000 elements. We have therefore to decompress the array prior to processing.

What is an encoded ZX-Spectrum screen?

Back in the late 70s and 80s, a series of innovative companies launched a series of microcomputers for home users. Perhaps you can still remember Apple II, Commodore 64 or ZX-Spectrum 48K. All these computers were 8-bit machines with very modest specs by today’s standards.

Since resources (especially memory) were at a premium on these machines, their designers found clever ways to ensure that information is encoded in the most space effective way possible.

For instance, ZX-Spectrum had a resolution of 256 x 192 pixels. A simple math shows a true-color 24bpp RGB image in this resolution will require:
256 x 192 x 3 = 147,456 bytes of memory
This is way too much for what ZX-Spectrum had!!!

Even if we encode at 8 bpp will still need:
256 x 192 = 49,152 bytes ! – which is almost the entire memory an 8 bit machine could address in a direct mode (e.g. 64K)

As you can see, ZX-Spectrum designers had to put a lot of though into reducing the video memory need to as a low as possible.

And they succeeded! A ZX-Spectrum 48K needs just 6912 bytes of video memory. The entire screen is encoded in this memory.
And this is the purpose of our challenge. To decode a ZX-Spectrum screen (e.g. memory dump) into a regular 2D matrix.

ZX-Spectrum encoding scheme

As mentioned, a ZX-Spectrum has a 256 x 192 pixels resolution. However, these pixels are encoded as a monochrome image, with just 2 colors being applied at the level of an 8x8 pixel block (one color named “ink” for set pixels, and one color named “paper” for off pixels)!

This results in big memory savings at the expense of a reduced color!
Since pixels are monochrome, we can encode a series of 8 pixels in a single byte.
Also, we only need 1 byte to represent the attributes of an 8x8 pixels block.

Let’s recalculate the memory needs:

  • (256 x 192) / 8 = 6144 bytes needed for the pixel data
  • (24 rows x 32 columns ) * 1 byte = 768 bytes needed for attributes / color data
  • Total video memory needed: 6144 + 768 = 6912 bytes!

All seems fine... and simple and you’re almost ready to start the decoding function. But wait a second: the designers of ZX-Spectrum had to do other innovations in order to optimize this machine. As a result, the screen lines are not in order. Instead the video memory has three areas. In each area the screen lines are stored in an interlaced order.

At this point I will stop the explanations and invite you to read the following diagram. It is part of the specs and contains all the data we need!

Alt Text

Decompressing the array

At the beginning of the article we mentioned that in order to minimize the file size, we decided to compress the inline array. Therefore we need to decompress it before we even attempt to decode it and display it.

The compression scheme is pretty simple: a RLE based compression. Basically if a byte appears repeated several consecutive times, it gets compressed as a sequence of 3 bytes:

[0] [byte] [counter]

The first byte in that sequence is allways 0. Therefore if 0 appears in the original byte array, it will be itself encoded as this:

[0] [0] [1]

I want also to mention that with this scheme we can compress a sequence of maximum 255 consecutive identical bytes. If the original array contains more identical bytes, they will be compressed in succesive frames of 3 bytes.

As you can probably tell, to decompress you have to do the oposite operation. This is actually the code to decompress such array:

// Decompresses a series of encoded bytes.
// If byte 0 is encountered, the following 2 bytes are read and interpreted as this:
// 0, byte, counter
function decompress(bytes)
{
    var ar = [];

    for(var i = 0; i < bytes.length; i++)
    {
        var byte = bytes[i];
        var count = 1;

        if (byte == 0)
        {
            count = 0;

            if (i < bytes.length - 1)
            {
                i++;
                byte = bytes[i];
                count = 1;
            }

            if (i < bytes.length - 1)
            {
                i++;
                count = bytes[i];
            }
        }

        for(var j = 0; j < count; j++)
        {
            ar.push(byte);
        }
    }

    return ar;
}

Note: Any array containing a ZX-Spectrum screen should be exactly 6912 bytes after decompression.

Displaying the ZX-Spectrum screen

After we do the decompression, we need to pursue to decoding and displaying the screen according to the scheme above.

We present below the code to display such screen on a HTML5 canvas structure. The code is making use of Processing API in order to draw on the canvas. The entire code has been tested inside https://codeguppy.com but can be easily adapted to any Processing based environment.

Notice that in this case the function receives as argument a decompressed array (e.g. 6912 bytes in length), as well as the coordinates on the canvas where we want to display the ZX-Spectrum screen. We assume that the HTML5 canvas is bigger than the ZX-Spectrum resolution. In case of codeguppy.com the canvas is actually 800x600 pixels in size.

// Displays a ZX-Spectrum screen on the canvas at specified coordinates
function displayScreen(arScr, scrX, scrY)
{
    noStroke();

    // ZX-Spectrum screen is split into 3 areas
    // In each area there are 8 rows of 32 columns
    for(var area = 0; area < 3; area++)
    {
        // For each area, draw the rows by doing
        // first line of (1st row, 2nd row, ...)
        // then the second line of (1st row, 2nd row, ...)
        for(var line = 0; line < 8; line++)
        {
            // For each row, draw the first line, then the second, etc.
            for(var row = 0; row < 8; row++)
            {
                // There are 32 cols => 32 bytes / line
                // each byte containing 8 monochrome pixels
                for(var col = 0; col < 32; col++)
                {
                    // Determine the pixel index
                    var index = area * 2048 + (line * 8 + row) * 32 + col;
                    var byte = arScr[index];
                    var sByte = byte.toString(2).padStart(8);

                    // Determine the attribute index
                    // Attributes start after the pixel data ends (e.g. after first 6144 bytes)
                    var attrIndex = area * 256 + row * 32 + col;
                    var attr = arScr[6144 + attrIndex];
                    var oAttr = getAttr(attr);

                    for(var bit = 0; bit < 8; bit++)
                    {
                        fill( getColor(oAttr, sByte[bit] == "1") );

                        var x = col * 8 + bit;
                        var y = area * 64 + row * 8 + line;

                        square(scrX + x * 3, scrY + y * 3, 3);
                    }
                }
            }
        }
    }
}

// Decode an attribute byte into component attributes
// Encoding: FBPPPIII (Flash, Bright, Paper, Ink)
function getAttr(attr)
{
    return {
        flash : (attr & 0b10000000) == 0b10000000,
        bright : (attr & 0b01000000) == 0b01000000,
        paper : (attr & 0b00111000) >>> 3,
        ink : attr & 0b00000111
    }
}

// Returns a true RGB color using the ZX-Spectrum color number
// 0 = black, 1 = blue, 2 = red, 3 = magenta, 4 = green, 5 = cyan, 6 = yellow, 7 = white
function getColor(oAttr, bInk)
{
    var zxColor = bInk ? oAttr.ink : oAttr.paper;

    // GRB encoding
    // 000 = black, 001 = blue, 010 = red, ...
    var b = zxColor & 1;
    var r = (zxColor >>> 1) & 1;
    var g = (zxColor >>> 2) & 1;

    var value = oAttr.bright ? 255 : 216;

    return color(r * value, g * value, b * value);
}

Testing the algorithms

In order to test the algorithms, I contacted Gary Plowman. Gary provided me a few screenshots from his ZX-Spectrum games. Gary even published recently a book on Amazon with a few amazing retro BASIC games that you can write on a modern computer.

At this point I'm gone give you a mini challenge. If you want to put together a small JavaScript program to decompress, decode and display a ZX-Spectrum screen, please try it with the following array.

However, if your time is limited and you want to see these algorithms in action, just scroll to the bottom of the article where you'll find the link to the full source code and a running example of this program.

var angrychicky = [
0,60,33,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,9,56,0,0,20,0,60,2,0,0,30,60,0,67,33,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,9,54,0,0,20,0,67,2,0,0,30,67,0,181,33,0,0,30,0,181,2,0,0,30,0,181,2,0,0,4,68,0,0,25,0,181,2,0,0,30,0,181,2,0,0,30,0,181,2,0,0,3,68,0,0,1,68,0,0,3,127,0,0,20,0,181,2,0,0,6,68,0,0,23,181,0,195,33,0,0,30,0,195,2,0,0,30,0,195,2,0,0,4,40,0,0,25,0,195,2,0,0,30,0,195,2,0,0,30,0,195,2,0,0,3,40,0,0,1,40,0,0,3,229,0,0,20,0,195,2,0,0,6,40,0,0,23,195,0,129,33,0,0,30,0,129,2,0,0,30,0,129,2,0,0,4,16,0,0,25,0,129,2,0,0,30,0,129,2,0,0,30,0,129,2,0,0,3,16,0,0,1,16,0,0,3,216,0,0,20,0,129,2,0,0,6,16,0,0,23,0,129,34,0,0,30,0,129,2,0,0,30,0,129,2,0,0,4,40,0,0,25,0,129,2,0,0,30,0,129,2,0,0,30,0,129,2,0,0,3,40,0,0,1,40,0,0,3,190,0,0,20,0,129,2,0,0,6,40,0,0,23,129,0,66,33,0,0,30,0,66,2,0,0,30,0,66,2,0,0,4,68,0,0,25,0,66,2,0,0,30,0,66,2,0,0,30,0,66,2,0,0,3,68,0,0,1,68,0,0,3,127,0,0,20,0,66,2,0,0,6,68,0,0,23,66,0,60,33,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,9,60,0,0,20,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,9,60,0,0,20,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,60,67,0,0,30,0,67,2,0,0,30,0,67,2,0,0,9,120,0,0,20,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,67,181,0,0,6,68,0,0,1,68,0,0,2,56,0,0,5,56,0,0,12,0,181,2,0,0,21,68,0,0,2,56,0,0,5,0,181,2,0,0,9,103,0,0,20,0,181,2,0,0,11,56,68,0,0,2,68,0,0,7,68,0,0,6,0,181,2,0,0,19,68,0,0,5,0,68,2,0,0,3,0,181,2,0,0,2,0,68,2,0,0,10,56,0,0,15,0,181,2,0,0,5,0,68,2,0,0,7,68,0,0,11,68,0,0,3,0,181,2,0,0,30,181,195,0,0,6,40,0,0,1,40,0,0,2,68,0,0,5,68,0,0,12,0,195,2,0,0,21,40,0,0,2,68,0,0,5,0,195,2,0,0,9,36,0,0,20,0,195,2,0,0,11,68,40,0,0,2,40,0,0,7,40,0,0,6,0,195,2,0,0,19,40,0,0,5,0,40,2,0,0,3,0,195,2,0,0,2,0,40,2,0,0,10,68,0,0,15,0,195,2,0,0,5,0,40,2,0,0,7,40,0,0,11,40,0,0,3,0,195,2,0,0,30,195,129,0,0,6,16,0,0,1,16,0,0,2,68,0,0,5,68,0,0,12,0,129,2,0,0,21,16,0,0,2,68,0,0,5,0,129,2,0,0,9,189,0,0,20,0,129,2,0,0,11,68,16,0,0,2,16,0,0,7,16,0,0,6,0,129,2,0,0,19,16,0,0,5,0,16,2,0,0,3,0,129,2,0,0,2,0,16,2,0,0,10,68,0,0,15,0,129,2,0,0,5,0,16,2,0,0,7,16,0,0,11,16,0,0,3,0,129,2,0,0,30,0,129,2,0,0,6,40,0,0,1,40,0,0,2,68,0,0,5,68,0,0,12,0,129,2,0,0,21,40,0,0,2,68,0,0,5,0,129,2,0,0,9,255,0,0,20,0,129,2,0,0,11,68,40,0,0,2,40,0,0,7,40,0,0,6,0,129,2,0,0,19,40,0,0,5,0,40,2,0,0,3,0,129,2,0,0,2,0,40,2,0,0,10,68,0,0,15,0,129,2,0,0,5,0,40,2,0,0,7,40,0,0,11,40,0,0,3,0,129,2,0,0,30,129,66,0,0,6,68,0,0,1,68,0,0,2,56,0,0,5,56,0,0,12,0,66,2,0,0,21,68,0,0,2,56,0,0,5,0,66,2,0,0,9,195,0,0,20,0,66,2,0,0,11,56,68,0,0,2,68,0,0,7,68,0,0,6,0,66,2,0,0,19,68,0,0,5,0,68,2,0,0,3,0,66,2,0,0,2,0,68,2,0,0,10,56,0,0,15,0,66,2,0,0,5,0,68,2,0,0,7,68,0,0,11,68,0,0,3,0,66,2,0,0,30,66,60,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,33,0,0,128,67,0,0,30,0,67,2,0,0,30,0,67,2,0,0,30,0,67,33,0,0,1,126,0,0,4,124,0,0,3,4,0,0,2,60,0,0,114,181,0,0,30,0,181,2,0,0,18,68,0,0,11,0,181,2,0,0,30,0,181,33,0,0,1,64,0,60,2,68,0,0,1,66,28,0,56,2,4,0,0,2,70,0,0,114,195,0,0,30,0,195,2,0,0,18,40,0,0,11,0,195,2,0,0,30,0,195,33,0,0,1,124,0,68,3,0,0,1,124,32,68,4,60,16,0,0,1,74,0,0,114,129,0,0,30,0,129,2,0,0,18,16,0,0,11,0,129,2,0,0,30,0,129,33,0,0,1,64,0,68,3,0,0,1,66,32,120,60,68,0,0,2,82,0,0,114,129,0,0,30,0,129,2,0,0,18,40,0,0,11,0,129,2,0,0,30,0,129,33,0,0,1,64,0,60,3,0,0,1,66,32,64,0,68,2,0,0,2,98,0,0,114,66,0,0,30,0,66,2,0,0,18,68,0,0,11,0,66,2,0,0,30,0,66,33,0,0,1,126,0,4,3,0,0,1,124,32,0,60,3,16,0,0,1,60,0,0,114,60,0,0,30,0,60,2,0,0,30,0,60,2,0,0,30,0,60,33,0,0,2,0,56,3,0,0,123,0,57,101,17,0,57,94,17,57,17,0,57,3,58,0,57,28,17,0,57,31,17,57,17,0,57,2,49,0,57,5,49,0,57,35,17,0,57,2,49,0,57,50,49,17,0,57,2,17,0,57,7,17,0,57,27,17,0,57,5,0,17,2,0,57,7,0,17,2,0,57,10,49,0,57,22,0,17,2,0,57,7,17,0,57,11,17,0,57,87,17,0,57,140,0,32,64];

JavaScript program

This JavaScript program hosted at https://codeguppy.com displays 8 different ZX-Spectrum screen from the games built by Gary Plowman.

Just click on the link and inspect the source code, if you want to run-in, press the "Play" button.

https://codeguppy.com/code.html?zxscreens_rle

Happy coding!

Top comments (0)