I got stuck on my multi-part Sky-Ape-Er dissection tangent, so I thought I’d do something different for today’s VIC-20 Tuesday.
The VIC-20 uses programmable character based graphics, You can change the pixels that make up a letter “A” to be a small 8×8 icon of a spaceship, for instance. But, when you move that letter A to different spots on the screen, it jumps 8 pixels at a time making movement quite jerky (as demonstrated by all my VIC-20 programs):
Since I don’t have time to write a full article at the moment, I’ll share this small VIC-20 program and come back to discuss what it is doing, and why it is doing it, later.
30 print"{clear}{reverse on}frames:"
60 for c=0 to 7:print chr$(65+c);chr$(73+c):print:next
70 gosub 950
100 poke 36869,255
105 rem
108 rem go thru each row of the character
109 rem
110 for ln=0 to 7
115 rem
118 rem read value, multiply by 256 to make 16-bits
119 rem
120 read v:v=v*256
125 rem
128 rem go thru each frame character
129 rem
130 for ch=0 to 7
135 rem
138 rem split 16-bit value into 8-bit values
139 rem
140 b1=int(v/256)
150 b2=v-(b1*256)
155 rem
158 rem poke shifted value in each charater
159 rem
160 poke 7176+ch*8+ln,b1
170 poke 7176+ch*8+ln+64,b2
175 rem
178 rem shift 16-bit value to the right one bit
179 rem
180 v=v/2
190 next
200 next
210 gosub 950
900 poke 36869,240:poke 198,0
910 end
950 get a$:if a$="" then 950
960 return
1000 DATA 60,126,255,255,255,255,126,60
More comments from the first ELSE article… First, MiaM chimes in:
MiaM:
You could also split that to two separate statements. One handling K=17 case, and then do ON K-38 GOTO 50,x,30 where x is just the line following the ON GOTO line.
donโt know about speed but you could also try ON K-16 GOTO 40,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,50,x,x,30 (also where x is the following line)
MiaM
In my example, I was getting keypress values back that represented left (17), right (39) and jump (41). By filling the ON/GOTO/GOSUB with bogus line values where the gaps are, you can now use ON/GOTO for non-sequential values. But, if the first number expected was a 17, that would be 17 dummy values at the start. Mia’s approach is to subtract that starting value, eliminating the need for 16 dummy values. Clever!
Clever, sure. But can it be benchmarked?
So how bad is this with speed? Let’s find out.
First, for the dummy lines we will just put nothing between the commas. That will be parsed as a zero, which is bad if any of those values are hit since going to 0 would restart the program, but since we are just testing and can control the value, it will give us the fastest way to parse a long ON/GOTO/GOSUB. Using real lines numbers will only be slower.
0 REM ONMIAM.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON K-16 GOTO 100,,,,,,,,,,,,,,,,,,,,,,200,,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70
Best case for the first target gives me 590. Not bad!
Trying again with “K=41” for worst case gives us 664. Still not terrible.
How does this rank against manual IF/THENs like this?
0 REM ONMIAM2.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=17 THEN 100:GOTO 70
40 IF K=39 THEN 200:GOTO 70
50 IF K=41 THEN 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70
Best case (17) reports 504 and worst case (41) reports 1128. Can there really be that much more overhead to skip two extra IF/THENs? It seems so. In this example, the long ON/GOTO is faster in worst case. Interesting. If worst case is a button not used that often (“smart bomb”), IF/THEN may be the best option, but if all buttons are used equally, there’s probably a point where a long ON/GOTO makes more sense.
But wait … there’s more!
Rob provided a suggestion about using an array:
Yep, could also do something like Dim C(256) C(17)=1:C(39)=2:C(41)=3 โฆ ON C(K) GOSUB 20,30,40 etc. But thatโs probably a bit memory hungry.
Rob
Rob’s idea of using an array to translate the non-sequential values into sequential numbers is a fun one. It uses more memory, and trades the time it takes to do an array lookup for the time it takes to parse a long ON/GOTO/GOSUB line.
Let’s try:
0 REM ONMIAM2.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
7 DIMK(41):K(17)=1:K(39)=2:K(41)=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON K(K) GOTO 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70
Since the largest value we need to check for is 41, I did a DIM K(41). That will allow for values from 0 to 41.
Best case (17) gives us 432! Faster than the manual IF/THEN check!
Worse case (41) gives us 432 … Really? ON/GOTO is really fast with just a few choices. It would be slower if there were dozens and you wanted the one at the end.
The downside of this approach is the memory it took for an array of 42 (0-41) variables. Doing something like this:
NEW:CLEAR
PRINT MEM
DIM K(41)
PRINT MEM
…shows me 22823 and 22606. That’s 217 bytes being taken by the 42 K array entries. (There is an entry reserved for the array itself, then each array element takes 5 bytes, I believe. It’s been awhile since I wrote my String Theory articles which I think looked into how variables are stored.)
This may be the fastest approach if you have a few hundred bytes available to use for this. On a VIC-20 with 3583 bytes free on startup, if I had memory left when I was done with my normal IF/THEN version, I could retrofit it with this approach and use that extra available RAM to speed up my program a tad.
Very cool.
Thanks to MiaM and Rob for these interesting ideas.
Is it really VIC-20 Tuesday again? Okay, then. Let’s get started…
The theory so far…
When we last left off, I had just described my theory about how my prototype Sky-Ape-Er game loaded as just one file which contained a custom character set — without being contained in DATA statements or anywhere in the BASIC code.
My theory was that I modified BASIC’s “start of variables” pointer (which normally points to just past the end of the BASIC code) so it was after the memory where the custom characters were stored. When saved, the file would contain the entire range of memory including those custom characters. When the program was LOADed and ran, the first thing it had to do was set the “start of variables” pointer back to where it needed to be, just after the BASIC code.
Today I want to test that theory by trying to create a standalone BASIC program that contains custom character set data. I am going to use the excellent CBM prg Studio development environment to make a BASIC project that will have three things:
A custom character set. I will use the editor to export the characters out as DATA statements into a BASIC file.
That new file will be turned in to a program that will READ the DATA statements and POKE the values into RAM memory.
Finally, I will have a simple test program that will do the necessary POKEs to enable RAM characters and animate them.
Since I haven’t owned a VIC-20 since 1983, I am going to do all of this in the VICE VIC-20 emulator. To do it like I did it back in 1982, I am going to use a virtual cassette tape for program storage. I could probably do this easier using an emulated disk drive, but never had a disk drive on my VIC-20 and I want to keep this as virtually real as possible.
Except for the whole part of using a Mac and virtual PC for development, of course.
Step 1: Custom characters and loader program.
Using CBM prg Studio’s character set editor, I created a few custom characters:
VIC-20 custom character set test in CBM prg Studio.
I then used the “Character Set -> Export -> To Listing” option to output the DATA statements containing those characters.
I then added the following code to load the DATA statements into memory, and display them to verify they work.
0 rem custom charset
1 rem protect chars
2 rem from basic.
3 REM set string end
4 POKE51,0:POKE52,28
5 REM set memory end
6 POKE55,0:POKE56,28
7 REM clear vars
8 CLR
10 for l=7168 to 7168+12*8-1
15 read v:poke l,v
20 next
25 rem clear 'space'
30 for l=7424 to 7432
35 poke l,0:next
40 print"{clear}{reverse on}charset:{reverse off}"
45 print" a b c d"
50 print" e g i"
55 print"@f@h@j@k@"
60 poke 36869,255
65 get a$
70 if a$="" then 65
75 poke 36869,240
80 print l and 255;int(l/256)
85 end
1000 DATA 255,2,4,8,16,32,255,255
1010 DATA 0,28,46,71,142,92,56,0
1020 DATA 16,40,68,98,118,62,28,8
1030 DATA 0,28,58,113,226,116,56,0
1040 DATA 16,56,124,110,70,34,20,8
1050 DATA 96,240,96,62,185,89,28,30
1060 DATA 189,68,132,66,33,0,255,255
1070 DATA 24,60,24,126,189,189,189,60
1080 DATA 189,36,36,36,102,0,255,255
1090 DATA 6,15,6,124,157,154,56,120
1100 DATA 189,34,33,66,132,0,255,255
1110 DATA 255,102,129,66,138,150,223,255
Here is what it is doing:
Lines 4 and 6 – These POKEs are used to protect the characters in memory so BASIC will not override them. They set the highest memory location that BASIC and strings can use. I set them to 7168, the address where the custom characters load.
Line 10 to 20 – FOR/NEXT loop of READ and POKE the first 8 bytes where character RAM will be. This is where the “@” symbol is (character 0).
Line 30 to 35 – These POKEs clear out the “space” character in the custom character set. I do this so my DATA statements don’t have to contain all the characters up to space.
Line 40 to 55 – Clear screen then print reverse text (which will still show up even after we switch to RAM character mode) and the custom characters.
Line 60 – Set VIC chip to use RAM starting at 7168 for custom characters. At this point, the screen will show my custom characters, and the reverse video should appear as normal text.
Line 65 and 70 – Wait for key to be pressed.
Line 75 – Set VIC chip to use normal ROM area for characters.
Line 80 – Print the two bytes that represent the last memory location used by the character set. These will be POKEd into 45 and 46 before SAVING the demo program later.
Line 85 – End.
Line 1000 to 1110 – Each line has eight bytes that make up a custom character.
Here is what it looks like when it runs:
VIC-20 custom character set demo.
Then when you press enter, it disables the custom characters and you will see it says “CHAR:” in reverse view with letters a-i and @ where the custom characters were. It then prints two numbers, which I need to write down. Those numbers represent the address of the end of the custom characters my test program uses.
I will build this into a “.prg” file, and then load that into VICE. Next, I will “Create and attach an empty tape image” (I called mine “Custom Char Demo.tap“) and then save this loader program to that virtual tape:
SAVE "CHAR SET LOAD"
Step 2: Program to use the custom characters.
The next part will be a standalone program that will make use of these characters. I am creating a simple demo where spinning bricks fall from the sky and a player character on a sidewalk below has to dodge them. Except nothing happens if a brick hits the player because this is just a demo.
Here is my demo program:
0 rem charset demo
1 REM set vars start
2 POKE45,104:POKE46,19
3 REM set string end
4 POKE51,0:POKE52,28
5 REM set memory end
6 POKE55,0:POKE56,28
7 REM clear vars
8 CLR
9 for l=7424 to 7432
10 poke l,0:next
11 REM charset in RAM
12 POKE 36869,255
13 rem
100 print "{clear}{down*20}";
105 print "@@@@@@@@@@@@@@@@@@@@@@";
110 for l=38400 to 38911:poke l,0:next
115 rem init bricks
120 for b=0 to 3:bl(b)=7680+rnd(1)*22+88*b:bc(b)=1+b:next
125 rem init player
130 p1=8109:p2=8131:pt=7:pb=8
135 rem main loop
140 pokep1,pt:pokep2,pb
145 for b=0 to 3:poke bl(b),32
150 bl(b)=bl(b)+22:if bl(b)>8120 then bl(b)=7680+rnd(1)*22
155 bc(b)=bc(b)+1:if bc(b)>4 then bc(b)=1
160 poke bl(b),bc(b)
165 next
170 get a$
175 if a$="a" then if p2>8119 then pokep1,32:pokep2,0:p1=p1-1:p2=p2-1:pt=5:pb=6:goto 140
180 if a$="s" then if p2<8141 then pokep1,32:pokep2,0:p1=p1+1:p2=p2+1:pt=9:pb=10:goto 140
185 if a$="q" then 510
190 pt=7:pb=8
195 goto 140
500 REM charset in ROM
510 POKE 36869,240
520 END
1000 PRINT PEEK(45);PEEK(46)
Here is what it is doing… Actually, I’ll skip the demo logic and just mention a few important things:
Line 1000 – This prints the programs’ current end (start of variables). Since I need the program to restore this when it loads (after being saved with the custom characters), I can load this program and “RUN 1000” to get those values. I then change the POKEs in line 2 to match those values. Thus, when the real program is loaded, it will fix those pointers which will get messed up by the SAVE process.
Thus, I would load this program into memory (but NOT run it) and do “RUN 1000” and note those numbers. I changed the POKEs on line 2 to match those values. Then I saved this after the “CHAR SET TEST” program as:
SAVE "CHAR SET TEST"
Step 3: Save the all-in-one test and charset file.
Now I reset the virtual VIC and rewind the virtual tape. Here are the steps:
LOAD and RUN the “CHAR SET LOAD” program to get the character set in memory. I make a note of the two numbers printed out at the end.
LOAD (but DO NOT run) the “CHAR SET TEST” program.
With the TEST program in memory, I do the following POKEs to change the end of BASIC pointer: POKE 45,X:POKE 46,Y …where X is the first number the loader program printed and Y is the second number the loader program printed.
I now can SAVE the test program and it should save all of the BASIC and continue saving until it gets to the end of RAM.
SAVE "CHAR SET DEMO"
Step 4: Test!
After a reboot, and rewind of the virtual tape, I try loading the “CHAR SET DEMO” program and running it…
VIC-20 error when loading my character set demo program.
Oh no! My theory is not correct. Something is still wrong. Running this program produces parts of the custom character, but not all. It’s clear I am off somewhere.
What am I doing wrong? I guess I’m gonna need a part 6. . .
Does anyone know a plug-in or method to convert a preformatted block into a code block? Since the Gutenberg editor was introduced, there has been a new type of block specially for code. All my old articles use with preformatted or a plug-in. I want to go in and clean them all up and use the current, modern WordPress features.
That might be a nice approach if the numbers were relatively close to each other, but at some point, adding a bunch of dummy numbers to the ON/GOTO line would take more time to parse than just using separate IF/THEN statements.
Arbitrary GOTO
My example was based on some VIC-20 code I wrote back in 1983. I was reading which key was currently being held down, and would get back three different values for the keys I was reading:
17 – ‘A’ key is pressed (LEFT)
42 – ‘S’ key is pressed (RIGHT)
39 – ‘F1’ key is pressed (JUMP)
I couldn’t use ON/GOTO for values 17, 42 and 39.
But Rob’s code does just that!
20 ON -(K=41)-2*(K=17)-3*(K=39) GOTO 30,40,50
In BASIC, any comparison returns a -1 if it is TRUE, or a 0 if it is FALSE:
PRINTing the result of a comparison in Color BASIC.
…so in Rob’s example, the checks in parenthesis will be turned in to either a -1 or a 0 based on the value of K.
If K is 41, then (K=42) will be (-1) and (K=17) and (K=39) will both be (0).
If K is 17, then (K=17) will be (-1) and (K=41) and (K=3) will both be (0).
If K is 39, then (K-39) will be (-1) and (K=42) and (K=17) will both be (0).
Let’s see what that does:
20 ON -(K=41)-2*(K=17)-3*(K=39) GOTO 30,40,50
K = 41 produces:
ON -(-1) - 2*(0) - 3*(0) GOTO 30,40,50
ON 1 - 0 - 0 GOTO 30,40,50
ON 1 GOTO 30,40,50
K-17 produces:
ON -(0) - 2*(-1) - 3*(0) GOTO 30,40,50
ON 0 - -2 - 0 GOTO 30,40,50
ON 2 GOTO 30,40,50
K-39 produces:
ON -(0) - 2*(0) - 3*(-1) GOTO 30,40,50
ON 0 - 0 - -3 GOTO 30,40,50
ON 3 GOTO 30,40,50
Fantastic! Subtracting a negative makes it a positive, and multiplying by zero makes zero.
Math rules! And it actually works:
0 REM robgoto.bas
10 INPUT "41, 17 OR 39";K
20 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 30,40,50
25 GOTO 10
30 PRINT "30":RETURN
40 PRINT "40":RETURN
50 PRINT "50":RETURN
Arbitrary ON/GOTO (tip by Rob).
Fantastic! What a great tip. Thanks, Rob!
Arbitrary benchmark
So of course, I now have to see how this compares to separate IF/THEN’s speed-wise. Let’s pull out the trusty benchmark test code and do a version for best case (first choice) and worst case (last choice) for each approach (Rob’s, and IF/THENs).
Arbitrary ON/GOSUB, best case:
0 REM robgoto1.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
This produces 1368.
Arbitrary ON/GOSUB, worse case:
0 REM robgoto2.bas
5 DIM TE,TM,B,A,TT
6 K=39
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
This produces 1434
Separate IF/THEN/GOSUB, best case:
0 REM ongoto1.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=41 THEN GOSUB 100:GOTO 70
40 IF K=17 THEN GOSUB 200:GOTO 70
50 IF K=39 THEN GOSUB 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
This produces 518 – almost three times faster!
Separate IF/THEN/GOSUB, worse case:
0 REM ongoto2.bas
5 DIM TE,TM,B,A,TT
6 K=39
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=41 THEN GOSUB 100:GOTO 70
40 IF K=17 THEN GOSUB 200:GOTO 70
50 IF K=39 THEN GOSUB 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
This produces 1098, meaning even worst case is still faster.
BUT, we are doing a bunch of number parsing and math here. We can’t do anything about the math, but on Color BASIC, we can change those decimal values to HEX and speed up that part. Let’s try that:
0 REM robgoto3.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=&H29)-&H2*(K=&H11)-&H3*(K=&H27) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
By switching the five integer values in line 30 one to HEX, the speed of best case goes from 1368 to 1150! That’s faster, but it still doesn’t beat 518 using separate IF/THEN/GOSUB.
We might be able to make this a bit faster by using variables, so lets try that:
0 REM robgoto4.bas
5 DIM TE,TM,B,A,TT
6 K=41
7 L=&H29:M=&H2:N=&H11:O=&H3:P=&H27
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=L)-M*(K=N)-O*(K=P) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN
This brings the time down slightly to 1092. Still not enough to beat the separate IF/THEN/GOSUB (and that could also be sped up slightly using HEX or variables).
Conclusion
This trick is very cool. From my calculations, it looks like it save code space, which could be very important on a low-memory system like a 4K CoCo or the 5K VIC-20. That alone might make this trick worth doing.
But for speed, such as a BASIC game, it looks like brute force IF/THEN may be a better approach.
It’s really nice to have options. I can’t wait for an opportunity to use this technique in something.
I have completed my code walk-through of one of my earliest computer programs, a Donkey Kong-inspired VIC-20 game called Sky-Ape-Er. But, the version I presented was not the only version of the game I created. I have dozens of saved copies of this game in various stages of completion, but one in particular stood out. It used completely different graphics.
Sky-Ape-Er: The Prototype
VIC-20 Sky-Ape-Er version 1, death screen.
VIC-20 Sky-Ape-Er version 1, screen 1.
VIC-20 Sky-Ape-Er version 1, screen 2.
VIC-20 Sky-Ape-Er version 1, screen 3.
As a reminder, here is what the graphics were later changed to in the release version:
VIC-20 Sky-Ape Er, screen 1.
VIC-20 Sky-Ape Er, screen 2.
VIC-20 Sky-Ape-Er, screen 3.
The ape was completely different, and the platform graphics were meant to resemble the ones used in the arcade game Donkey Kong. But why were there pinwheels? I have so many questions for my junior high self.
When I first uncovered these tapes I was unaware of how many variations of my programs were on them. When I started this article on my Sky-Ape-Er game, I discovered that the earlier version with different graphics was also using some different code — most notably in how it read the keyboard input.
I thought it might be fun to look at the programming choices I originally made, and speculate on why I changed them.
Sky-Ape-Er: The Mystery
The first thing I want to discuss is a mystery I am currently trying to solve. My VIC-20 games that used custom character sets seem to come in three forms:
DATA STATEMENTS
The BASIC program reads the character set from DATA statements and POKEs it into memory. This is what the Sky-Ape-Er INSTRUCTIONS program does. I believe these character sets may have been designed by the Eight by Eight Create program I previously mentioned. If true, I don’t envy my junior high self having to manually copy down the numbers to paper and type them in to my own program later. From the documentation:
“Once you have created, designed and examined enough characters, you can copy their associated numbers on paper to be used in any programs you make.”
Eight by Eight Create instruction from January 1983 Creative Computing magazine (Volume 9, Number 1), page 270.
Yipes.
LOADABLE CHARACTER SET
A standalone binary “program” of the custom character set that can be loaded into memory, presumably at the address of where the character data goes. I have found several programs called things like CHARS and TNTCH that do not have any BASIC code in them. I suspected these were character set data (especially TNTCH which was on the tape after the main program TNT) but I had no idea how to use them. I was finally able to see what was inside by importing them into the CBM prg Studio‘s characters set editor. CBM prg Studio is a Windows integrated development environment (IDE) for making Commodore programs in BASIC or assembly. It has some great features and is worth checking out.
VIC-20 Factory TNT character set in CBM prg Studio.
This let me see that these were indeed character sets, though my first attempt to import them had all the graphics off by a few lines. I needed to use an offset (bytes to skip in the file) of 2 for the characters to load properly. That told me that whatever type of file this was had some 2 byte header at the start (perhaps memory location where to load the data?).
ALL-IN-ONE PROGRAM WITH CHARACTER SET
And this is the mystery! My early prototype version of Sky-Ape-Er was just one program, and it loaded up with the custom character set. There was no font in DATA statements. There was no pre-loader that did it. It just loaded and “just worked.” I have no idea how I created this, nor do I know why, for the release version, I change it to use two programs and DATA statements.
Using a free hex editor, I opened the file and looked for the end of the BASIC program. I could tell it ended around byte 2612 because the last line was a “SYS xxxxx” command that would reboot the VIC-20. The xxxxx numeric value was visible as plain text in the tokenized BASIC file, so it was easy to spot.
VIC-20 .prg file in a HEX editor.
After this was a bunch more data. Somewhere in there must be the character set. But where? I decided to try opening the entire program file in the character set editor and using the 2612 offset where the BASIC program ended.
VIC-20 CBM prg Studio importing a .prg to find the embedded charset data.
Doing this showed garbage between the BASIC program and character data, but scrolling down let me visibly see where the font data began.
VIC-20 CBM prg Studio trying to find where character set data is in a .prg file.
I now knew that approximately 58 characters (each character is 8×8, so 8 bytes per) into the file was the start of the font data. A little math (which was hard) and some trial and error (which was easy) and I came up with 3073 as the offset to use from the stat of the .prg to where my custom characters were. I imported using that value and got this:
VIC-20 character set data imported from a .prg file.
Tada!
If I knew what the font data was to begin wish (from DATA statements), I could have just scanned the HEX file looking for those values. But I didn’t, so I couldn’t.
Now I have a BASIC file for the game, as well as a character set file in the CBM prg Studio editor. But how did I combine them together in the first place?
Where does the data go from here?
The clue is in these POKEs found on the first line of the program:
They reminded me of similar POKEs in Color BASIC that track where the program starts in memory as well as where variables and strings go. I expected CBM BASIC would be similar, so I went searching for a VIC-20 memory map.
I want to do a deep dive into this later, but for now, here are what those POKEs are doing:
*002D-002E 45-46 Pointer: Start of Variables
*0033-0034 51-52 Pointer: String storage (moving down)
*0037-0038 55-56 Pointer: Limit of memory
The “*” notes “Useful memory locations” in the memory map. I agree. I seem to be changing where variables and strings start, as well as where the end of memory is on startup.
Why was I changing the start of variables, the end of string storage, and limiting the end of BASIC? I have a theory, which parallels something I’ve done on the CoCo.
In Color BASIC, we use the CLEAR command to allocate more string space (“CLEAR 500” for 500 bytes for strings). It looks like CBM BASIC doesn’t do that, and allows strings to use as much memory as is available (the memory between the end of the BASIC program + variable arrays, and the limit of memory).
CLEAR can also limit how much memory BASIC can use (“CLEAR 200,&H3F00”). That’s useful when you are wanting to use some of that memory for machine language and don’t want BASIC to overwrite it. I am betting POKE 51/52 is like CLEAR x,XXXX.
VIC-20 Memory Map
To better visualize this, let’s take a quick look at where the 5K of RAM in the VIC-20 is located.
Hey, look at that! The memory range used by BASIC (4096-7679) is the “3583 BYTES FREE” value shown on the startup screen:
VIC-20 startup screen showing 3583 bytes free.
Notice the 3K gap (1024-4095) which is where the 3K RAM expansion cartridge goes if you have one. I never did, though I did have the Super Expander cartridge which gave extra memory as well as enhanced graphics and sound commands.
Side Note: When memory expansion cartridges are plugged in, more memory becomes available and some things shift around. But for this discussion, we will talk only about the stock 5K VIC-20. It was only in recent years that I learned the VIC-20 was a 5K computer. I’d always thought it was 4K. That now makes the weird 3K memory expansion make more sense, since that would boost it to a nice even 8K. But I digress…
Now let’s zoom in on just the memory BASIC is using:
When a new numeric variable is added, it goes into the Variables section, which grows larger downward. When a new array is added, it goes into the Arrays area (and likely the entries there point to the Variable) and it grows larger downward. When a string is added, it gets an entry in the Variable section (“A$”) which has a pointer into the actual string content in the Strings section, which grows upwards.
So why was I changing the start of variables, the end of string storage, and limiting the end of BASIC? I believe I was making BASIC think the program was larger than it really was so it would SAVE out (and thus LOAD back later) the program PLUS some custom character data. When the program would run, it would need to reset the pointers to be at the actual end of the BASIC program, and limit memory so BASIC did not write over the character data.
In order to explain this, we need to look at how the VIC-20 custom characters worked.
How the VIC-20 custom characters worked
The VIC-20 character set was 4K of data stored in ROM starting at 0x8000:
8000-83FF 32768-33791 Upper case and graphics
8400-87FF 33792-33815 Reversed upper case and graphics
8800-8BFF 33816-35839 Upper and lower case
8C00-8FFF 35840-36863 Reversed upper and lower case
Each character was 8 pixels wide (one byte) by 8 pixels tall (8 bytes total). There is room for 512 characters in that 4K. Normal printable ASCII characters are 0-127, so it looks like the Commodore PETASCII was similar, with 128 special Commodore characters per bank (128 characters * 8 bytes per character = 1024 bytes).
On power up, the VIC’s video chip is programmed to use the first of those four 1K blocks of ROM for its character set. There is a register with four bits that can be changed to select which of those four ROM blocks it uses, or point it to four 1K RAM blocks in RAM. By loading a character set in to one of those RAM areas and setting the register, the VIC will now display the custom character set rather than the one built in to the ROM. Here are the important four bits:
9005 36869 bits 0-3 start of character memory (default = 0)
bits 4-7 is rest of video address (default= F)
BITS 3,2,1,0 CM starting address
HEX DEC
0000 ROM 8000 32768
0001 8400 33792
0010 8800 34816
0011 8C00 35840
1000 RAM 0000 0000
1100 1000 4096
1101 1400 5120
1110 1800 6144
1111 1C00 7168
Memory location 36869 can be one of these 8 values:
0xF0 / 240 / 11110000 – Use 8000-83FF (Upper case and graphics)
0xF1 / 241 / 11110001 – Use 8400-87FF (Reversed upper case and graphics)
0xF2 / 242 / 11110010 – Use 8800-8BFF (Upper and lower case)
0xF3 / 243 / 11110011 – Use 8C00-8FFF (Reversed upper and lower case)
0xfc / 252 / 11111100 – Use 1000-13FF RAM area #1
0xfd / 253 / 11111101 – Use 1400-17FF RAM area #2
0xfe / 254 / 11111110 – Use 1800-1BFF RAM area #3
0xff / 255 / 11111111 – Use 1C00-1FFFF RAM area #4
The four RAM locations all are within the 4K that is used by BASIC and screen memory:
0x1000 – 0x1dff – 3583 bytes used by BASIC programs.
0x1e00 – 0x1fff – 512 bytes used by screen memory.
We can’t use RAM area #1 for characters because that is where our BASIC program is. If we kept our BASIC program and all its variables very small (1K, 0x1000-0x13FF), we could use area #2. But, it makes more sense to use area #4 and give as much memory as possible to BASIC.
In my programs I see POKE 36869,255 and POKE 36869,240. The first POKE makes the video chip start using characters in RAM starting at 0x1c00 (bit pattern 1111). This means a BASIC program and all its variables can’t be any larger than 3072 bytes (0x1000-0x1bff). The second poke switches the characters back to using the standard ROM location for uppercase and graphics characters (bit pattern 0000).
My Sky-Ape-Er INSTRUCTIONS program would READ character data and then POKE it into memory starting at 0x1c00 (7168). It then did POKE 36869,255 to start using them. To display a normal text screen, or at the end of the program, it would POKE 36869,240 to get back to the ROM character set. (This is the part I actually mostly remembered.)
I do want to point out that the character RAM area #4 overlaps with the screen memory:
0x1c00 – 0x1fff – Character RAM area #4.
0x1e00 – 0x1fff – 512 bytes used by screen memory.
This tells me that you really only have 0x1c00 to 0x1dff (7168-7679) for custom characters. That’s 512 bytes, and at 8 bytes per letter, there is only room for 64 custom characters. Assembly language programs that did not need BASIC could move things around and use one of the other blocks in its entirety, but this last block shares memory with the screen so not all of it can be used for character data.
Character sets
The PETSCII character set starts at zero with an “@” symbol, followed by the alphabet characters “A-Z” (1-26), then various punctuation and symbols (27-47), then numbers (48-57), then more punctuation and symbols (59-63). This covers the basic characters and uppercase alphabet just like standard ASCII does for characters 32-96. This means being limited to just 64 characters is not bad at all.
And, as I was working through this, I discovered why my Factory TNT game is not displaying the score correctly! I did not realize it also contained updated number characters (48-57) that I was not properly loading during my “restoration” of the game:
VIC-20 Factory TNT character set also remaps the numbers.
I’m so glad I am writing this article and figured that out. It was driving me mad!
But I digress…
Stay on target… Stay on target…
This means that you could have a BASIC program from 0x1000 to 0x1bff followed by custom character data from 0x1c00 to 0x1dff. If you had the characters in memory for your BASIC program to use, and you SAVEd your BASIC program, those custom characters would NOT be saved with it since BASIC doesn’t know anything about them.
But … you could lie to BASIC and tell it your BASIC program actually ENDS at 0x1dff, then when you SAVE it should write out the entire range of memory (0x1c00 to 0x1dff) thinking it’s just one large BASIC program…
Then, when you loaded it back it, BASIC would start loading it into memory at 0x1c00 and keep going until it got to the end of the program. You now have loaded memory that is part BASIC, and part character set!
But, if you tried to RUN it, you wouldn’t get very far because BASIC would think there is no memory left for variables. You would need to un-lie to BASIC and tell it where the program really ends, and do so without using varaibles.
2020-04-30 – Added missing semicolon in code and updated example.
NOTE: This article was originally written a few years ago, so some references may be out of date.
I have been enjoying working on the SirSound project the past month, as well as some fun challenges at my day job. Every now and then I run into something I’d like to do that is not doable in C, or doable but not proper to do in C, or … maybe doable. It’s sometimes difficult for me to find answers when I do not know how to ask the question.
With that said, I have a C question for anyone who might be able to answer it.
Using my multi-track music sequencer as an example, consider representing data like this:
It’s easy to create a sequence like this in C. Here’s some pseudo code:
track1 = { C, C, C, C };
track2 = { E, E, E, E };
sequence1 = { track1, track 2};
I thought there might be a clever way to do all of this with one initializer. If I treat the data like nested XML (like the first example), I thought it might be possible to do something like this:
…though the address of “light” is quite different than the address of a structure which contains a pointer that should point to “light”.
I was going to make each one a pointer to an array of words, so I could have a tree of words like the earlier example (with kitchen/light and kitchen/fan):
I have been dealing with some WordPress problems lately so I have been trying to restore everything to working condition. You may encounter site downtime while I am working on this. Don’t panic. I’ll get it going again, shortly…
Making BASIC run faster is hard to automate, but there have been some attempts to do this over the years.
Carl England CRUNCH
No, it’s not that cereal you remember from Saturday morning TV ads. It’s one of the coolest utilities ever created for the CoCo. Carl England wrote quite a few of those, actually.
Carl England was the creator of my all-time favorite Disk Extended BASIC program – Super Boot. It was a superb “type DOS and auto run your program” utility that added many neat features. I have it on many of my RS-DOS disks.
Carl also created THE DEFEATER, a copy utility that could duplicate any copy protected disk. It did not crack the software – it just cloned it, making a duplicate copy protected disk.
Carl also showed off a scanner attachment at the 1990 Atlanta CoCoFest that turned a Tandy DMP printer into scanner! But that’s a story for another time…
Make BASIC small again
Today I want do discuss one of Carl’s programs called CRUNCH. It is a machine language program that will pack a BASIC program to be as small as possible. It does this by removing REMs and unnecessary spaces. It also removes other unnecessary things like “IF X THEN GOTO 100” which could be written just as “IF X THEN 100”. It will even remove trailing quotes at the end of a line (which looked weird, but saved a byte).
But the most important thing it does is pack (er, crunch?) a BASIC program into as few lines as possible. It will take a program like this:
0 REM
1 REM GUESS.BAS
2 REM
3 REM BY ALLEN HUFFMAN
4 REM
5 REM GENERATE RANDOM NUMBER
10 N=RND(100)
15 REM DISPLAY INSTRUCTIONS
20 PRINT "I AM THINKING OF A NUMBER"
30 PRINT "BETWEEN 1 AND 100."
35 REM ASK FOR A GUESS
40 PRINT "WHAT IS YOUR GUESS";
50 INPUT G
55 REM IS GUESS TOO HIGH?
60 IF G > N THEN 100
65 REM IS GUESS TOO LOW?
70 IF G < N THEN 200
75 REM IS GUESS CORRECT?
80 IF G = N THEN 300
85 REM REPEAT
90 GOTO 35
100 REM TOO HIGH
110 PRINT "TOO HIGH!"
120 GOTO 35
200 REM TOO LOW
210 PRINT "TOO LOW!"
220 GOTO 35
300 REM CORRECT
310 PRINT "CORRECT!"
320 END
…and turn it into something like this:
10 N=RND(100):PRINT"I AM THINKING OF A NUMBER":PRINT "BETWEEN 1 AND 100.
40 PRINT"WHAT IS YOUR GUESS";:INPUTG:IFG>N THEN100
70 IFG<NTHEN200
80 IFG=NTHEN300:GOTO 40
110 PRINT"TOO HIGH!":GOTO35
210 PRINT"TOO LOW!":GOTO35
310 PRINT"CORRECT!":END
…except you generally can not list the program afterwards because CRUNCH will combine lines up to the maximum allowed (250 bytes or so?) and create lines too long to be EDITed or LISTed manually.
And, look at line 70 and 80. If you type them, you have to have a space after the variable in “IFG<N THEN…” because the tokenizer needs to know where the variable ends and the next keyword starts. CRUNCH can remove that space, which is impossible to do when typing that in by hand.
Here is what it looks like in operation on the GUESS.BAS program above:
Carl England’s CRUCCH program, before.
You can select a specific operation to do, or choose 7 and do them all:
Carl England’s CRUCCH program, after.
In this example, it changed a 519 byte BASIC program into 214. And, if you renumber the results by 1s starting at line zero (RENUM 0,0,1), that will save an addition 9 bytes because “GOTO30” (two digit line numbers) takes up more bytes than “GOTO9” (single digit line numbers).
CRUNCH is pretty amazing. And, you can download it today for free! It is one of the extra utilities in Carl’s DEFEATER disk copy utility package:
While that archive says CoCo 3, CRUNCH itself will work on a CoCo 1/2. At least, it seems to in my testing using the Xroar emulator.
Try CRUNCH out on some of your programs and share the results in the comments. Just keep in mind that it will destroy your ability to edit the program! Save your crunched program under a different name! You can then distribute that crunched copy, or be nice and give folks both the original (hopefully easier to read) and crunched (smaller and faster to run).
GEnie was an text-based online server owned by General Electric. I recently found the nondisclosure agreement I signed to become an assistant at the Tandy RoundTable. I was COCO-SYSOP there until the service shut down in 1999. Apparently GEnie (which became Genie by then) was not Y2K compliant… Enjoy!
Non-Disclosure Agreement
Between:
Allen C. Huffman Effective Date: March 15th, 1991
xxxxxxxxxxxxxx
Lufkin, Texas 75901
(hereinafter referred to as "Assistant SysOp" or "Assistant"),
And: General Electric Company, a New York corporation, acting through its GE
Information Services Division, (hereinafter referred to as "GE"), 401 N.
Washington Street, Rockville, Maryland 20850.
WHEREAS, GE operates remote access computer systems through which it offers
various network-based information services, both directly and through
distributors;
WHEREAS, one of the services which GE offers by such means is a consumer
information service known as GEnie Service (hereinafter referred to as "the
Service");
NOW, THEREFORE, GE and Assistant SysOp hereby agree as follows:
A: Either GE or Assistant SysOp may disclose to the other certain
information which the disclosing party deems to be confidential and
proprietary ("Information"). Such Information shall be clearly and
conspicuously marked as confidential and proprietary at the time of first
disclosure to the receiving party. Such Information would include, but is
not limited to, documentation related to the Product and the Service and
business information of GE or Assistant SysOp which is not generally
available to the public.
B: The receiving party shall exercise reasonable care to prevent
disclosure of or use for any purpose unrelated to use on the Service or to
the evaluation of the Product for suitability on the Service, at any time
prior to the expiration of three (3) years following the termination of this
Non-Disclosure Agreement, of any Information which it receives from the
other party pursuant to and in accordance with the terms of this
Non-Disclosure Agreement. The receiving party shall also require its
employees and agents to similarly restrict use and disclosure of such
Information. The receiving party, however, shall not be required to keep
confidential any Information which is or may become publicly available
without fault on its part; is already in the receiving party's possession
prior to receipt from the disclosing party; is independently developed by
the receiving party; is disclosed by the disclosing party to third parties
without similar restrictions; or is rightfully obtained by the receiving
party from third parties without restriction.
GENERAL ELECTRIC COMPANY
By: __________________________ By: __________________________
Robert Chiappone Allen C. Huffman
Title: Manager, Product Marketing Title: Tandy RT SysOp
GEnie
Date: __________________________ Date: __________________________
GEnie User Number: _____________
RT, Game or Product: ___________