See also: part 1, part 2, part 3, part 4, unrelated, and part 5.
War by James Garon
Just as I thought I had reached the conclusion to my epic masterpiece about classic CoCo game startup screens, Robert Gault made this post to the Color Computer mailing list:
Robert Gault robert.gault at att.net
Sat Jul 2 09:56:21 EDT 2022
An author of games for Tandy, James Garon, put some lines in the Basic game WAR which is on colorcomputerarchive.com . The code is in lines 60000 and up which can’t be LISTed with a CoCo3 but can be read with a CoCo2. The lines in question contain PRINT commands which when listed with a CoCo2, look like the data inside the quotes have been converted into Basic commands. You can also see this if you use imgtool or wimgtool to extract the program from the .dsk image. These lines generate PMODE graphics for the title screen.
Do anyone have a clue as to how these Basic lines work?– Robert Gault, via CoColist
The game in question is available in tape or disk format from the Color Computer Archive:
And, you can even click the “Play Now” button and see it run right in your web browser. Click below and take a look at the title screen:
This title screen uses the CoCo’s lesser-known screen color, and has those iconic rotating color blocks.
And, it’s in BASIC! Well, almost. It contains an assembly language routine that rotates those colors, and it does it the same way I figured out in this article series. I am just thirty years too late with my solution.
As Robert mentioned, there’s some weirdness in the program starting at line 60000. When you LIST it, you get a rather odd output full of BASIC keywords and such:
The first bit looks okay… CLS to clear the screen, then A$ is created to be 28 spaces — CHR$(32). But that PRINT looks a bit … weird.
I’ve seen this trick before, and even mentioned it recently when talking about typing in an un-typable BAISC pogram. The idea is you can create a program that contains something in a string or PRINT statement, like:
…and then you alter the bytes between those quotes somehow, such as locating them and using POKE to change them. Let’s figure out an easy way to do this.
First, I’ll start with a print statement that has characters I can look for later, such as the asterisk. I like that because it is ASCII 42. If you don’t know why I like forty-two, you must be new here. This wikipedia page will give you the details…
100 PRINT "**********"
Now we need to alter those bytes and change them to something we couldn’t normally type, such as graphics blocks (characters 128-255). To do this, we can use some code that scans from the start of the BASIC program to the end, and tries to replace the 42s with something else.
This is dangerous, since 42 could easily appear in a program as part of a keyword token or line number or other data. But, I know that a quote is CHR$(34), so I could specifically look for a series of 42s that is between two 34s.
So many numbers.
In Color BASIC, memory locations 25 and 26 contains the start of the BASIC program in memory. Locations 27 and 28 is the end of the program. Code to scan that range of memory looking for a quote byte (34) that has an asterisks byte (42) after it might look like this:
0 ' CODEHACK.bas 10 S=PEEK(25)*256+PEEK(26) 20 E=PEEK(27)*256+PEEK(28) 30 L=S 40 V=PEEK(L) 50 IF V=34 THEN IF PEEK(L+1)=42 THEN 80 60 L=L+1:IF L<E THEN 40 70 END ...
We could then add code to change all the 42s encountered up until the next quote (34).
80 L=L+1:IF PEEK(L)=34 THEN END 90 POKE L,128:GOTO 80 100 PRINT "**********"
Line 80 moves to the next byte and will end if that byte is a quote.
In line 90, it uses POKE to change the character to a 128 — a black block. It continues to do this until it finds the closing quote.
If you load this program and LIST it, it looks like the code shown. But after you RUN, listing it reveals a garbled PRINT statement similar to the WAR line 60000. But, if you run that garbled PRINT statement, you get the output of black blocks:
As you can see, line 100 changes ten asterisks to the keyword FOR ten times. I am guessing that the numeric token for “FOR” is 128.
…and my guess was correct! According to Color BASIC Unravelled, the token for FOR is hex 80 — which is 128. Perfect. So the BASIC “LIST” routine is dump, and tries to detokenize things even if they are surrounded by quotes. Interesting.
At this point, if you were to EDIT that line, it would detokenize it to be…
100 PRINT "FORFORFORFORFORFORFORFORFORFOR"
…and if you saved your edit, you’d now have a line that would print exactly that, no longer ten black blocks for the word FOR ten times as if you’d typed them all in.
This makes these changes uneditable. BUT, once the modification code has been ran, you can delete it, then SAVE/CSAVE the modified program. When it loads back up, it will have those changes.
In the case of WAR line 60000, it’s a PRINT that shows a series of color blocks used for the top of the attract screen.
Here is the garbled output of lines 60000 on in the WAR.BAS program:
60000 CLS:A$=STRING$(28,32):PRINT"RESTORERESTOREMOTORMOTOR^^SCREENSCREENDRIVEDRIVEDSKI$DSKI$!!!RESTORERESTOREMOTORMOTOR^^SCREENSCREENDRIVEDRIVEDSKI$DSKI$!!!"; 60005 FORI=1TO8:GOSUB60028:NEXT:FORI=1TO6:GOSUB60028:NEXT 60010 PRINT"MOTORMOTORRESTORERESTORE!!!DSKI$DSKI$DRIVEDRIVESCREENSCREEN^^MOTORMOTORRESTORERESTORE!!!DSKI$DSKI$DRIVEDRIVESCREENSCREEN^"; 60020 POKE1535,175:T$="WAR!":PRINT@99,"A YOUNG PERSON'S CARD GAME";:PRINT@80-LEN(T$)/2,T$;:PRINT@175,"BY";:PRINT@202,"JAMES GARON";:PRINT@263,"COPYRIGHT (C) 1982";:PRINT@298,"DATASOFT INC.";:PRINT@389,"LICENSED TO TANDY CORP.";:SCREEN0,1 60025 GOSUB60030:I$=INKEY$:FORI=1TO300:FORJ=1TO30:NEXT:IFINKEY$=""THENEXECV:NEXT:RETURNELSERETURN 60028 PRINTSTRING$(2,127+16*(9-I))TAB(30)STRING$(2,127+16*I);:RETURN 60030 A$="RUN!SUBELSE,NEXTENDFORTHENFORDIM/!9" 60060 V=VARPTR(A$):V=PEEK(V+2)*256+PEEK(V+3):RETURN
Line 60005 does two FOR/NEXT loops and both GOSUB 600028, so we’ll look at that next.
This routine prints solid colored blocks on the left and right side of the screen, based on the value of I.
The two FOR loops are used to fill the entire screen. There are only 8 colors, so you can’t just do one loop from 1 to 14.
Look familiar? It’s just like the PRINT in line 60000, though the pattern of the blocks is different, and it is only printing 31. Since the one prints on the bottom of the screen, if it printed all the way to the bottom right position, the screen would scroll up one line.
60020 POKE1535,175:T$="WAR!":PRINT@99,"A YOUNG PERSON'S CARD GAME";:PRINT@80-LEN(T$)/2,T$;:PRINT@175,"BY";:PRINT@202,"JAMES GARON";:PRINT@263,"COPYRIGHT (C) 1982";:PRINT@298,"DATASOFT INC.";:PRINT@389,"LICENSED TO TANDY CORP.";:SCREEN0,1
More normal code… The POKE 1535,175 is what fills in the bottom right block of the screen, where PRINT did not go to. 175 is a blue block.
After this are just normal PRINT@ statements to put the text on the screen.
At the end, SCREEN 0,1 puts the CoCo in to its alternate screen color of pink/red/orange/whatever color that is.
This line is normal code, but the use of EXEC tells us some assembly language is being used.
The first thing it does is GOSUB 60030, then it gets any waiting keypress in I$. I don’t see I$ used so I’m unsure of this purpose.
Next it does two FOR/NEXT loops. The first “FOR I” appears to be the number of times to do this routine. The second “FOR J/NEXT” is just a timing delay.
After this is a direct check for any waiting key by using INKEY$ directly. If no key is pressed, “EXEC V” is done… This would execute whatever machine language routine is loaded in to memory at wherever V is set to. But what is V? That must be the GOSUB 60030, which we will discuss after this.
After the NEXT (FOR I) is “RETURN ELSE RETURN”. That way it does a return whether or not the IF INKEY$ is true. Since either way will RETURN, with nothing executed after this line, this might have also worked (extra spaces added for readability):
60025 GOSUB 60030:I$=INKEY$:FOR I=1 TO 300:FOR J=1 TO 30:NEXT:IF INKEY$=""THEN EXEC V:NEXT 6026 RETURN
But James Garon seems to know his stuff. Each line number takes up 5 bytes of space. The three keywords “RETURN ELSE RETURN” (without spaces) only takes up four (I’m guessing RETURN is a one byte token and ELSE is a two byte token.)
So indeed, the odd “RETURN ELSE RETURN” is less memory than putting one RETURN on the next line. (In my Benchmarking BASIC articles, I’ve focused on speed versus space, so perhaps I’ll have to do a series on “Making BASIC Smaller” some time…)
This leads us to the GOSUB 60030.
60030 A$="RUN!SUBELSE,NEXTENDFORTHENFORDIM/!9" 60060 V=VARPTR(A$):V=PEEK(V+2)*256+PEEK(V+3):RETURN
Line 60030 creates a string, but the string seems reminiscent of those PRINT lines seen earlier. Since we don’t see anything using this string, it’s probably not for displaying block graphics characters.
We do see the use of VARPTR(A$) on the next line. VARPTR returns the “variable pointer” for a specified variable. I’ve discussed VARPTR in earlier articles, but the Getting Started with Color BASIC manual describes it as:
VARPTR (var) Returns addresses of pointer to the specified variable.– Getting Started with Color BASIC
When using VARPTR on a numeric (floating point) variable, it returns the location of the five bytes that make up the number, somewhere in variable space.
But strings are different. Strings live in separate string memory (reserved by using the CLEAR command, with a default of 200 bytes), or they could be contained in the program code itself. See my String Theory series for more on that. With a string, the VARPTR points to five bytes that point to where the string data is contained.
In part 3 of my my DEFUSR series, I describe it as:
The first byte where the string is stored will be the size of that string:
A$=”THIS IS A STRING IN MEMORY”
X = VARPTR(A$)
PRINT “A$ IS LOCATED AT”;X
PRINT “A$ IS”;PEEK(X);”LONG”
I forget what the second byte is used for, but bytes three and four are the actual address of the string character data:– Interfacing assembly with BASIC via DEFUSR, part 3
PRINT “STRING DATA IS AT”;PEEK(X+2)*256+PEEK(X+3)
This looks familiar, since the VARPTR(A$) in this code then gets the address of the string by PEEKing bytes three and four:
This tells me A$ contains a machine language routine. The GOSUB to this routine returns the address of the bytes between the quotes of the A$=”…” then that location is EXECuted to run whatever the routine does.
To figure this one out is much simpler, since no PEEKing of BASIC code is needed. It’s in a string, so we can just print the ASC() value of each byte in the string:
60030 A$="RUN!SUBELSE,NEXTENDFORTHENFORDIM/!9" 60031 FOR I=1 TO LEN(A$):PRINT ASC(MID$(A$,I,1));:NEXT:END
By doing a RUN 60030 I then get a list of bytes that are inside of that string:
142 3 255 48 1 166 132 44 4 139 16 138 128 167 128 140 6 1 47 241 57
I recognize 57 as the op code for an RTS instruction, so this does look like it’s machine code. And while I could use some 6809 data sheet and look up each of those bytes to figure out what they are, I’d rather have something else do the work for me.
Online 6809 Simulator to the rescue!
At www.6809.uk is an incredible 6809 simulator that I recently wrote about. It lets you paste in assembly code and run it in a debugger, showing the registers, op codes, etc. To get these bytes in to the emulator, I just turned them in to a stream of DATA using the “fcb” command in the simulator’s assembler:
routine fcb 142,3,255,48,1,166,132,44 fcb 4,139,16,138,128,167,128,140,6 fcb 1,47,241,57
By pasting that in to the simulator’s “Assembly language input” box and then clicking “Assemble source code“, the code is built and then displays on the left side in the debugger, complete with op codes:
Now I can see the code is:
4000: 8E 03 FF LDX #$03FF 4003: 30 01 LEAX $01,X 4005: A6 84 LDA ,X 4007: 2C 04 BGE $400D 4009: 8B 10 ADDA #$10 400B: 8A 80 ORA #$80 400D: A7 80 STA ,X+ 400F: 8C 06 01 CMPX #$0601 4012: 2F F1 BLE $4005 4014: 39 RTS
Since I did not give any origination address for where this code should go, the simulator used hex 4000. There are two branch instructions that refer to memory locations, so I’ll change those to labels and convert this just to the source code. I’ll even include some comments:
LDX #$03FF * X points to 1023, one byte before screen memory LEAX $01,X * Increment X by 1 so it is now 1024 L4005 LDA ,X * Load A with the byte pointed to by X BGE L400D * If that byte is not a graphics char, branch to L400d ADDA #$10 * Add hex 10 (16) to the character, next color up ORA #$80 * Set the high bit (in case value rolled over) L400D STA ,X+ * Store (possibly changed) value back at X and increment X. CMPX #$0601 * Compare X to two bytes past end of screen BLE L4005 * If X is less than that, branch to L4005 RTS * Return
This code scans each byte of the 23 column screen. If the character there has the high bit set, it is a graphics character (128-255 range). It adds 16 to the value, which moves it to the next color (16 possible combinations of a 2×2 character block for 8 colors). It stores the value back to the screen (either the original, or one that has been shifted) and then increments X to the next screen position. If X is less than two bytes past the end of the screen, it goes back and does it again.
Hmm, it seems this routine would actually write to one byte past the end of the screen memory if it contained a value 128-255. Hopefully nothing important is stored there. (Am I right?)
And, for folks more used to BASIC, here is the same code with BASIC-style comments:
LDX #$03FF * X=&H3FF (1023, byte before screen) LEAX $01,X * X=X+1 L4005 LDA ,X * A=PEEK(X) BGE L400D * IF A<128 GOTO L400D ADDA #$10 * A=A+&H10:IF A>&HFF THEN A=A-&HFF ORA #$80 * A=A OR &H80 L400D STA ,X+ * POKE X,A:X=X+1 CMPX #$0601 * Compare X to &H601 (two bytes past end) BLE L4005 * IF X<=&H601 GOTO L4005 RTS * RETURN
My BASIC-style comments don’t exactly match what happens in assembly since. For example, “BGE” means “Branch If Greater Than”, but there was no compare instruction before it. In this case, it’s branching based on what was just loaded in to the A register, and would be compared to 0. That looks odd, but BGE is comparing the value based on it being signed — instead of the byte representing 0-255, it represents -127 to 128, with the high bit set to indicate the value is negative. So, if the high bit is set, it’s a negative value, and the BGE would NOT branch. Fun.
If you run this BASIC program, it will BASICally do the same thing … just much slower:
100 X=&H3FF 110 X=X+1 120 A=PEEK(X) 130 IF A<128 GOTO 160 140 A=A+&H10:IF A>&HFF THEN A=A-&HFF 150 A=A OR &H80 160 POKE A,X:X=X+1 170 'Compare X to &H601 180 IF X<=&H601 GOTO 120 190 RETURN
So that is the magic to how this attract screen works so fast. It uses POKEd PRINT statements to quickly print the top and bottom of the screen, FOR/NEXT loops to print the sides, then this assembly routine to shift the colors and make them rotate around the screen.
And, this code is very similar to the routine I came up with earlier in this series:
start ldx #1024 X points to top left of 32-col screen loop lda ,x+ load A with what X points to and inc X bpl skip if not >128, skip adda #16 add 16, changing to next color ora #$80 make sure high gfx bit is set sta -1,x save at X-1 skip cmpx #1536 compare X with last byte of screen bne loop if not there, repeat sync wait for screen sync rts done
My routine starts with X at 1024, then uses BPL instead of BGE. I also increment X after I load the character, and I only update it if it gets modified by storing it 1 before where X is then.
At first, I thought mine was more clever. But I decided to see what LWASM said.
Counting cycles for fun and profit. Or just more speed.
LWASM can be made to display a listing of the compiled program and, optionally, list how many cycles each line will take. And, optionally optionally, keep a running total of cycles between places in the code you mark. (I have another article about how to use this.)
Here is my version, with (cycles) in parens, and running totals in the column next to it.
start (3) ldx #1024 loop (5) 5 lda ,x+ (3) 8 bpl skip (2) 10 adda #16 (2) 12 ora #$80 (5) 17 sta -1,x skip (3) 20 cmpx #1536 (3) 23 bne loop (4) rts
My routine uses 23 cycles from “loop” to “bne loop”.
Here is the routine from WAR.BAS:
(3) LDX #$03FF (5) LEAX $01,X L4005 (4) 4 LDA ,X (3) 7 BGE L400D (2) 9 ADDA #$10 (2) 11 ORA #$80 L400D (5) 16 STA ,X+ (3) 19 CMPX #$0601 (3) 22 BLE L4005 (4) RTS
This one appears to use one cycle less — 22 — in it’s loop. Nice! Even though I thought it would be worse, once again, James Garon appears to know his stuff.
I think his routine may be a bit larger, and I wondered why he started X one byte before the screen memory and then incremented it. That seems wasteful.
However, William “Lost Wizard” Astle saw exactly why in a reply on the CoCo mailing list:
He uses that sequence instead of the more obvious one to avoid having a NUL byte in the code. A NUL would cause the interpreter to think it’s the end of the line and break things.– William Astle via CoCo mailing list
It took me a moment, but I think I understand now. If he had done “LDA #$4000”, the byte sequence would have been whatever byte is LDA, followed by $04 and $00. You can’t put a 0 in a string, or BASIC will think that is the end of the string. Any assembly encoded this way must avoid using a zero. This is also the reason he doesn’t compare to the byte past the end of the screen, which is $6000. Though, I expect comparing to 1535 and using “Branch if less than OR equal to” would have worked and avoided the zero.
But James Garon knows his stuff, so I had to see if my way was larger or slower:
(3) cmpx #1535 (3) ble loop (3) CMPX #$0601 (3) BLE L4005
Well, they look the same speed, and neither generates a zero byte in the machine code. I don’t know why he does it that way.
If one were to patch the Color BASIC “UNCRUNCH” routine to show things between quotes, here is what those lines would look like… (And if one did such a patch, I expect they’d be writing a future article about it…)
For some reason, James Garon chose to embed assembly code and graphics characters like this, rather than using DATA statements and building strings or POKEing assembly bytes in to memory somewhere.
It’s cool to see. But unless someone knows James Garon, I guess we don’t know why this method was done.
Other than “because it’s cool,” which is always a good reason to do something when programming.
Until next time…