Friday 12 April 2019

Plottastic ZX80!

A guide to plotting pixels in Basic on a ZX80!


Introduction

Both the ZX80 and ZX81 support 8x8 pixel character-only displays and contain 16 graphic characters that can be used to plot fat 4x4 pixel pixels on a two-by-two grid within each character:

These are called Battenberg graphics after the cake of the same name 😀

On a ZX81 it's easy to use these characters to plot (crude) graphics on the screen, the computer provides a PLOT x,y and UNPLOT x,y for this purpose. But with half the ROM on a ZX80, it's much harder - so I wondered, how much harder is it to plot pixels? It turns out it's pretty formidable!

Challenges


  • The ZX80 doesn't have any PLOT or UNPLOT commands.
  • The screen on a ZX80 is like that on a ZX81, when you run a program, the screen first collapses to a minimum of 25 newline characters and expands as you display text. However, on a ZX80, unlike the ZX81, you can't print anywhere on the screen as there's no PRINT AT function, this means we'll have to poke onto the screen.
  • The memory map on a ZX81 has the display immediately after the program, but on a ZX80, the display comes after the variables and the editing workspace which means that it'll move around just by creating a new variable or possibly by performing input (which is a potential problem).
  • Ideally, to convert from pixel coordinates to Battenberg graphics you'd want to map the odd and even x and y coordinates to successive bit positions to index the character.


  • But, unlike the ZX81, the character set on a ZX80 doesn't have the characters in the right order to display Battenberg characters. Instead they contain gaps; the last 8 codes are generated from the first 8 but in reverse order; and some of the first 8 are taken from what ought to be the inverse characters!

The Solution

The solution really comes in a few parts. Firstly, the easiest way to be able to map an ideal pixel value to its Battenberg character is to create a table in RAM, by putting them into a REM statement (the ZX80 has no READ and DATA commands so it's hard to put a table of data into an array, but the first REM statement in a program is at a fixed location). However, even this presents a problem, because only 8 of the graphics characters can be typed. The easiest way to handle that is to write a program which converts a different representation of the characters into the correct ones.

So, on the ZX80, first type this:
After you run it, you'll get this:
Even this was tricky; I had to use character codes above 32, since symbols such as +, -, /, ", etc get translated into keyword codes for the Basic interpreter. The above program illustrates an interesting feature of ZX80 Basic that differs from ZX81 and ZX Spectrum Basic in that AND and OR operations are actually bitwise operations rather than short-circuit boolean operations. Thus P AND 15 masks in only the bottom 4 bits.

Once we have generated the symbols we can delete lines 10 to 30.

The next step is to actually write the code to generate pixels and plot them. Once we know how, it's actually a bit simpler. Firstly, we fill out the screen with spaces (or in my case, with '.'):
This gives us 20, full lines of characters. Because it's going to be difficult to plot a pixel by reading the screen, figuring out the plotted pixels then incorporating the new pixel; I cache a single character in the variable P and its location in the variable L. All my variables a single letters to save space if you try to run it on a 1Kb ZX80. The idea is that if the new print location in L changes, we reset P back to 0, otherwise we incorporate the new pixel.

Next we start the loop and calculations for plotting, in this case a parabola. We loop X=0 to 63 and calculate Y on each loop (it's a parabola that starts at the bottom of the screen):

Finally we perform the pixel plotting and loop round.

This involves saving the previous location in K so we can compare it later; then calculating the new location based on X and Y (since each character position is 2 pixels, we need to divide X and Y by 2 and since there are 32 visible characters and an invisible NEWLINE character on every screen line we must multiply Y/2 by 33). Note, a quirk of ZX80 Basic means that multiplies happen before divides, so Y/2*33 would calculate Y/66!

The pixel bit calculation in line 50 makes use of ZX80 bitwise operators, we want to generate 1 if X=0 (so 1+(X AND 1) will generate either 1 or 2) and then we need to multiply that by the effect of the Y coordinate, which is 1 on the top line, and 4 on the bottom: 1+(Y AND 1)*3 will do that. Hence this will generate the values 1, 2, 4, 8 depending on bit 0 of X, Y.

We must POKE the location L plus an offset of 1 (because the first character of the display is a NEWLINE) and also we must add the location of the display file (it turns out that these calculations don't make the display file move around). We poke it with the pixel value we want indexed into the right character code from the REM statement. Finally we loop round X.

This code generates this graph:
It looks reasonable even though it's generated with integer arithmetic. It's the first pixel plotted graph I've ever seen on a ZX80!

More Examples

My original reason for doing this was to see if I could generate sine waves using a simple method I once found for a Jupiter Ace. Here's the program and the result:
It looks fairly convincing, especially as it's all done with integer arithmetic. Because the code generates both the sine and cosine value, it's easy to turn this into a circle drawing program which produces the following:

It looks a bit odd, that's because the algorithm doesn't quite generate sine and cosine curves. What it's really doing is computing a rotation matrix, but only one of the terms is computed for sine and cosine each time. Hence, the circle looks a bit like an oval with a slight eccentricity.

My graph drawing algorithm has one very serious limitation. Because it doesn't read the screen in order to compute a new pixel, if the graph goes over the same part of the screen twice, the second pass will muck up what was there before. Doing the correct calculations is possible using some table lookups and bitwise operations, though it would slow down the graph generation. I didn't bother, because I only wanted to generate simple graphs.

Conclusion

The ZX80 and ZX81 have very similar hardware, but a number of design decisions in the ZX80's firmware made drawing circles much harder than you might expect. With a lot of effort it is possible to generate some simple graphs 😀

No comments: