Sunday 8 May 2022

A Tale Of Two Banners: VIC-20

 I previously posted about a Banner program I wrote for the 40th anniversary of the ZX Spectrum. One of my friend's followers tweeted that he always thought my friend was a Commodore C64 owner, who could PEEK and POKE with the best of them.

This set me thinking - what would a Commodore C64 version be like? And then, because a C64 version would be too easy, what would an unexpanded VIC-20 version be like? For sure, it's more challenging than a ZX Spectrum version.

Here's a bunch of reasons why:

  • The VIC-20 has a smaller screen area, just 22 x 23 characters; so an 8x5 character banner can't be done with 8x8 pixel characters.
  • The VIC-20 has no graphics commands. It can redefine the character set and that's about it. It can't easily PRINT AT any location on the screen.
  • The VIC-20 supports an ink colour per character, but only a global paper colour. That's because it only has 4-bits per colour byte instead of 8-bits (which gives room for an ink + paper per character). Therefore, I can't use the same colour trick as the ZX Spectrum.
  • The VIC-20's INKEY$ function (GET stringVar$) doesn't return proper ASCII upper and lower case characters, but PETSCII codes (weird).
  • The VIC-20 fouls up the character set pointer when you press Shift+[C=].
Nevertheless, I was able to do it, and here I'll describe how:














Smaller Screen Area

The VIC-20 has a smaller screen area, and if I understand it correctly, the screen can't be more than 512 characters (though they can be double-height!). Normally the screen is 22x23 characters, which isn't enough to fit 8 characters across made up from Battenberg, 4x4 pixels each. You'd need 32 characters across for that. However, it's almost enough to support 8 characters across made up from 6x6 block graphic fonts from 4x4 pixel Battenberg graphics.

And... the VIC-20 screen size can be redefined. By making it 24x21 there's room for 8 characters across x 7 characters down, even more than the ZX Spectrum!

Of course, on a VIC-20 it has to be done using POKEs:

1000 A=7504:POKE 36864,10: POKE 36867,42: POKE 36866,152


So, 36867 is the number of rows, *2 in bits 1..6, Address 36866 is the number of columns in bits 0..6. The default values were 46 and 150 respectively, so I changed them to 21*2 for 21 rows and 152 for 24 columns.

Where does all the information about the POKEs come from? Well the most concise information I've found is from here, an extensive resource on the VIC-20 memory map.

The values can be directly poked in, though I'd start with changing the dimension that gets smaller, so that the screen area is always <512b.

Finally, we need to adjust the left-hand side of the screen so that it's better centred. Address 36864 does that and changing it to 10 was found by experimentation.

There Are No Graphics Commands

However, the VIC-20 can display graphics characters, and there are Battenberg block graphics characters inherited from the Commodore PET. Strangely, and unlike the ZX81 or ZX Spectrum, they don't have a very logical order. Instead, in the sequence I'd use, the codes are:

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160


Given that we have all the Block character graphics selected, all we need to do now is define the character set in terms of them. Unfortunately, that's not trivial either.  The first thing I did was to take a 6x6 bitmapped character set I'd used for a FIGnition example program:


 

















I have a java program which reads opens the image as a .png and then copies the pixels to an array where they can be subsequently transformed into a different image format.

I needed to be able to transform the character bitmaps so they could be represented in VIC-20 BASIC. I couldn't encode them as proper full bytes, because all 256 symbols can't be typed. I could have encoded them as 6-bit text, but again, the odd non-ascii use of VIC-20 characters made that more complex. So, I simply encoded them as 4-bit text using the characters A..O and then indexing each character (-65) in an array of Battenberg graphic characters. This meant the 96 printable characters would take up 864 bytes in themselves+ some overhead for the individual lines and BASIC commands, a good chunk the unexpanded VIC-20's 3.5kB memory space! Encoding as 6-bits could would have saved 33%, about 288 bytes.

Unfortunately, it wasn't likely to be feasible to just store the whole font in strings, so I figured that I could store them in DATA statements and then do RESTORE line to point to the right data statement where the character I wanted was defined.

Unfortunately, the VIC-20 only supports RESTORE to the beginning of the program. So, instead - yet again (and this is a common theme) I had to use memory PEEKing. I placed the data statements at the end, and when I'd read all the other data in the setup, I stored the system variable for where the DATA statement pointer was, and then literally PEEKed the right memory location to get the bytes.

It's possible to do a PRINT AT on a VIC-20 by printing the home and cursor control characters. Home is an inverse S, which you can display by literally typing PRINT " and then the home key, because the VIC-20 re-interprets keystrokes within quotes and similarly, you can move the print position to different locations by typing PRINT " and then the cursor keys themselves, for the same reason. This means that the VIC-20's screen editor, which is usually easy to use turns into a pain within quotes, because moving the cursor starts overwriting the rest of the text, so you have to wrestle with it to get it back into proper cursor mode (typing " usually works).

And colours work the same way, you type PRINT " and then a colour key and it will change the INK colour.

So, you can assign these to strings and then print "[HomeKey]";LEFT$(CD$,Y);LEFT$(CR$,X); to get the the right location, but it's fairly slow compared with poking directly into screen memory at 7680+22*row+column and of course, the cursor key technique doesn't work when the screen dimensions have been changed!

So, POKEing the screen is the best solution and you have to poke the colour attribute byte too, because the VIC-20 for some reason doesn't fill it in when it displays spaces. Clear screen, for example (PRINT "[Shift+Home]"; ) doesn't fill the attribute bytes with the current ink colour; it just clears the text bytes.

This is why in the real code I have to clear them explicitly:

FOR F=7680 TO 8183:POKE F,42:NEXT F

And the reason why it's code 42 and not 32 will be explained next:

Producing The Diagonal Stripes

I was pleased with how I generated the diagonal stripes on the ZX Spectrum, as it's a challenge when only 2 colours are allowed per character, and, helpfully enough, the VIC-20 does have a diagonal character!

Yet, doing the same thing on a VIC-20 is several times harder, because only 1 unique foreground colour can be defined per character and clearly we need two. Yet, it is just about possible, but only just!

The solution is that the VIC-20 supports 2-bits per pixel colours on a character-by-character basis, by setting bit 3 of every colour attribute byte. Each bit pair then selects one of four possible colours:

00: Which is the paper colour, bits 7-4 of location 36879.
01: Which is the border colour, bits 3-0 of location 36879.
10: Which is the auxiliary colour, bits 7-4 of location 36878 (the bottom 4 bits are the sound volume level).
11: Which is the ink colour of the character.

This means that one diagonal half can have a choice of 3 possible colours, while the other diagonal half (ink) can have a choice of 7 possible colours. We need to handle 5 colours: the black background (paper), Red, Yellow, Green and Cyan.





Using pairs of pixels also forces us to pair up the rows in the UDGs giving us an effective resolution of 4x4 for each character. You can see that the stripes are more blocky than an ideal 8x8 diagonal would be.

It also means we can't use the standard VIC-20 diagonal graphics character, because we actually need 5 different types of diagonal characters with bit pair combinations of xx/11 and 11/xx. This means we have to allocate space for a character set and in turn that means we can't use the built-in block graphics characters, we have to defines copies of those too. In total we need 16+5 characters (though in fact I used 16+6). In essence, then we need to first allocate space for the graphics characters:

5 POKE 52,29:POKE 51,80:POKE 56,29:POKE 55,80:PRINT CHR$(8);:CLR

Allocate the character set pointer to give us 64 graphics (thus the first character will be at code 64-6-16 = 42) and assign the Auxiliary and background colours.

1100 POKE 36878,112:POKE 36869,255:POKE 646,1:POKE 36879,11:P=7680

Copy over the block graphics from ROM (we could calculate them, but this is easier).

1010 READ P:P=P*8+32768

1015 FOR F=0 TO 7:POKE F+A,PEEK(P+F):NEXT F

1020 A=A+8:IF A<7632 THEN 1010

...

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160


Generate the stripes characters:

1030 READ N,M:FOR F=0 TO 6 STEP 2:POKE A+F,N:POKE A+F+1,N:N=(N*4+M)AND 255:NEXT F

1040 A=A+8:IF A<7680 THEN 1030

...

9020 DATA 2,2,168,0,86,2,169,1,254,2,171,3


Clear the screen the hard way:

1105 FOR F=7680 TO 8183:POKE F,42:NEXT F:PRINT “[Home]”;


Then read the character codes for the stripes and place them at the right locations.

1120 FOR X=0 TO 4:READ N,M:P=8176+X

1130 FOR F=0 TO 7-X:POKE P+30720,M:POKE P,N:P=P-23:NEXT F

1140 NEXT X

...

9030 DATA 58,10,63,10,62,13,61,13,60,8


Ascii Code Conversions & Stopping Case Swapping

You can swap between Capitals + Graphics and Capitals and Lower Case (+ a few graphics) on the VIC-20 using Shift+[C=]. However, this doesn't affect what character codes are read by GET x$. Normal lower-case characters return upper-case ASCII characters and holding down shift gives the same codes + 128.

Fortunately, that's just a simple case of mapping the characters:

111 K$=CHR$((ASC(K$)+(32 AND (K$>=“A” AND K$<=“Z”)))AND 127)

Also, fixing the case swapping issue is fairly easy, it's done by printing a control character: PRINT CHR$(8) in line 5.

Conclusion

Early 80s computers had to be creative with graphics hardware, because the relatively high memory costs limited graphics detail, and lower memory bandwidth limited the range of colours. The ZX Spectrum and VIC-20, at first sight provided a very similar style of graphics, using 1 bit per pixel + an attribute byte for colour per character, but short-cuts in the colour memory (only 4-bits per character instead of 8) added even more limitations.

Consequently, porting a program from one architecture to another often involved a lot of additional work to map or work around the respective limitations. In the case of the VIC-20, a critical aspect of the Banner program (the diagonal red, yellow, green and cyan stripes against a black background) were only made possible by the VIC chip's ability to support 2 bit per pixel multi-colour graphics, plus the ability of one of those colours to be the ink colour at the character. An ordinary 2 bit per pixel graphics mode, such as that offered by the 6847 graphics chip could not have reproduced the stripes, even though, at a minimum, 96x84 pixels graphics would need 2kB of RAM vs the 932 bytes of RAM actually used.

Finally, even the differences in the implementation of what was accepted as the standard microcomputer language: BASIC could have serious ramifications; and often hacking directly into the OS or memory map was the only solution.

The Banner program is a great, and simple way of exploring the architectural differences, and at the end of it, it's fun to type out colourful chunky characters across the whole screen!

The Listing

Finally, here's the listing! There's about 1kB free on the unexpanded VIC-20 once it's been typed in. In VICE it's possible to copy and paste a line at a time, but you need to convert the characters to lower-case first!

5 POKE 52,29:POKE 51,80:POKE 56,29:POKE 55,80:PRINT CHR$(8);:CLR

10 POKE 36869,240:GOSUB 1000

100 FOR X=0 TO 2:BG(X)=PEEK(P+48+X):POKE P+48+X,54:NEXT X

110 GET K$:IF K$=“” THEN 110

111 K$=CHR$((ASC(K$)+(32 AND (K$>=“A” AND K$<=“Z”)))AND 127)

112 F=ASC(K$):IF F<32 AND F<>13 THEN 110

113 FOR X=0 TO 2:POKE P+48+X,BG(X)::NEXT X

116 IF ASC(K$)=13 THEN P=INT((P-7680)/24)*24+7752:GOTO 160

120 C=ASC(K$)-32:C=C0+(C AND 3)*9+INT(C/4)*44

130 I=INT(RND(0)*7)+1

140 FOR Y=0 TO 2: FOR X=0 TO 2:POKE P+X,PEEK(C+X)-23:POKE P+X+30720,I:NEXT X

150 P=P+24:C=C+3:NEXT Y:P=P-69

155 P=P-7680:P=(P-INT(P/24)*24)+INT((P+48)/72)*72+7680

160 IF P>=8184 THEN P=7680

170 GOTO 100

999 POKE 36869,240:POKE 36864,12:POKE 36866,150:POKE 36867,174:STOP

1000 A=7504:POKE 36864,10: POKE 36867,42: POKE 36866,152

1005 DIM BG(3)

1010 READ P:P=P*8+32768

1015 FOR F=0 TO 7:POKE F+A,PEEK(P+F):NEXT F

1020 A=A+8:IF A<7632 THEN 1010

1030 READ N,M:FOR F=0 TO 6 STEP 2:POKE A+F,N:POKE A+F+1,N:N=(N*4+M)AND 255:NEXT F

1040 A=A+8:IF A<7680 THEN 1030

1100 POKE 36878,112:POKE 36869,255:POKE 646,1:POKE 36879,11:P=7680

1105 FOR F=7680 TO 8183:POKE F,42:NEXT F:PRINT “[Home]”;

1120 FOR X=0 TO 4:READ N,M:P=8176+X

1130 FOR F=0 TO 7-X:POKE P+30720,M:POKE P,N:P=P-23:NEXT F

1140 NEXT X

1150 C0=PEEK(65)+256*PEEK(66)+7:P=7680

1999 RETURN

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160

9020 DATA 2,2,168,0,86,2,169,1,254,2,171,3

9030 DATA 58,10,63,10,62,13,61,13,60,8

9100 DATA“AAAAAAAAAAKAACAACAFFAAAAAAANNINNIBBA"

9110 DATA"AOIBOADKAPECEGICBCJIAJMCBCCAKAAAAAAA"

9120 DATA"AJAAKAABAAGAAFAACAIIIFPACCCAKADLCACA"

9130 DATA"AAAAEAACAAAADDCAAAAAAAAAACAAECECACAA"

9140 DATA"JHIOCKBDAEKAAKABDADDIJDADDCDDIBDIDDA"

9150 DATA"EHAONIABALDCDDIDDAEDCLDIBDADDKAJAACA"

9160 DATA"JDIJDIBDAJDIBHCBCAAIAAIAAAAAAAACAECA"

9170 DATA"AJABIAABAMMIMMIAAABIAAJABAAJDIADAACA"

9180 DATA"JLIKDCBDCJDILDKCACLDILDIDDAJDCKAABDC"

9190 DATA"LGAKECDCALDCLDADDCLDCLDACAAJDCKDKBDC"

9200 DATA"KAKLDKCACBLAAKABDAAHCAFABCAFECFGABAC"

9210 DATA"FAAFAABDCOEKKCKCACOAKKGKCACJDIKAKBDA"

9220 DATA"LDILDACAAJDIKGKBDCJDILLACBCJDABDIBDA"

9230 DATA"DLCAKAACAKAKKAKBDAKAKGECACAKAKKKKBBA"

9240 DATA"GECEGACACGECAKAACADHCECADDCALAAKAADA"

9250 DATA"GAAAGAAACAHAAFAADAEGAAAAAAAAAAAAAMMM"

9260 DATA"EDAHCADDCEMAKFABDAOIAKFADCAEIAKAABCA"

9270 DATA"ENAKFABDAEIALDABDAEDAFDABAAEMAGNAEJA"

9280 DATA"OIAKFACBAAIAAIAACAACAAKAECAKAALLACBA"

9290 DATA"KAAKAABCAEIAPFACBAMIAKFACBAEIAKFABCA"

9300 DATA"JGAOJACAAJGAGNAABCAMAFAABAAAMABIADAA"

9310 DATA"FIAFAAADAIEAKFABCAIEAOCACAAIEAPPABCA"

9320 DATA"IEAFKACBAIEAGNAEJAMMAECADDAAJABKAABA"

9330 DATA"AKAAKAACABIAALABAAJJAAAAAAAJHIKHKBDA”