Fast Graphics On The OSI C1P
Charles L. Stanford
I am sure that almost without exception, OSI C1P and Superboard II owners get a major part of their computer fun from games. The relative ease of graphics programming using the game symbols in the Character Generator ROM allows even the novice BASIC programmer a lot of freedom for creating exciting and fast moving figures. However, even OSI's very fast BASIC still leaves a lot to be desired when more than a few dozen screen locations are changed, resulting in distracting image changes and in slow execution of commands.
This article will present a “cookbook” version of a machine language graphics subroutine. The BASIC user with little or no background in Machine or Assembly language programming will be able to use the material presented here to “plug in” his own graphics. The more advanced programmer will be able to adapt the concepts shown to complicated, interactive graphics games and simulations.
The use of graphics in games
While many exciting and worthwhile games are played without graphics (such as most versions of Starwars, Star Trek, Quest, etc.), most family fun games revolve around the manipulation of graphics figures or objects on the screen. Six Gun Shootout, Tank, Lunar Lander, and many others are “made” by their graphics interactions. But a game like Six Gun written solely in BASIC is slowed down so badly by the time required to POKE two gunfighters and a few cacti that it loses a lot of its Oomph! Even a near-instantaneous screen routine (instead of scrolling) only helps a little when 50 or 100 or more POKE'S are needed every time a move is made.
The method presented in this article does have a few limitations, mainly in the opportunity for typing errors when entering the many DATA statements, and in the need to carefully plan and structure each and every character before starting to enter the program. But the results are really worth it. Every program I've written since devising this method has had to be slowed down with time delay loops, which is sure better than the alternative.
The USR Function
The OSI Graphics Reference Manual and the BASIC in ROM Manual are ridiculously brief in describing the use of the USR function. On the other hand, there isn't that much knowledge needed for an elementary application such as this one. In general, you will need to be capable of converting Hex numbers to Decimal quickly and accurately; the only other requirement is to be able to write a fairly structured BASIC program.
When the USR function is called by a line such as 10 X = USR(X), the program goes to the machine language program subroutine pointed to by the data at memory locations $000A and $000B (Decimal #11 and #12). The 6502 processor needs a 16 bit address to jump to a subroutine. But each memory location only has 8 bits. So the processor reads the eight lower (right-most) bits of the address from the lower of two adjacent memory addresses, and the eight higher (left-most) bits from the higher. Thus, a machine language routine at address $0222 would be called if memory locations $000A and $000B held $22 and $02 respectively. These numbers would be inserted in these two RAM locations by a line in BASIC such as 20 POKE 11,34 : POKE 12,2. Note that the Hex numbers have to be converted to their Decimal equivalents for BASIC. As you can see, many machine programs starting at as many different memory locations could be called, merely by changing the data at $000A and $000B before calling the USR function. I'll describe how to write and insert the actual machine language subroutines a bit later in this article.
After a machine language subroutine is called, it acts very similarly to a BASIC program, in that each instruction is executed in order until a RETURN is encountered. It then returns to the next instruction after the JUMP. The RETURN in machine language is the RTS instruction, which is Hex $60 (or Decimal #96).
The advantage of all this is, of course, based in the fact that machine language is somewhere around 1000 times faster than BASIC for most equivalent functions. This feature is particularly useful for graphics programming.
Machine language programming
Writing programs in machine language, especially without the use of an Assembler, is a bit tedious. But the end result is often worth the effort. In this case, you, the reader, will use the program itself exactly as presented here, only changing a table which will hold whichever graphics figures needed for your particular game.
The action of the subroutine is quite simple. After a Screen Clear (note that the first eight lines of Listing 1 are as shown in my previous article in Compute II Number 1), a series of graphics characters and their locations relative to a fixed point are read from memory and written to the TV screen.
The routine makes use of one of the 6502 processor's more efficient addressing modes, Zero Page. Line 023A uses the instruction LDA-0,Y, which loads the accumulator with the data residing at a memory location equal to the value of the address pointed to by FE (lo byte) and FF (hi byte) plus the contents of the Y register. These two memory locations at the top of Zero page were chosen as they are easy to remember, and are not used for any other purpose by BASIC or the Monitor. Since the Y register is an eight-bit register, only screen locations between the base address and the base address plus 255 can be called.
Looking at the flow chart (Figure 1), you can see that the first data loaded is the lo byte, then the hi byte, of the screen location. This information is in the table for this particular game, and is deposited within the program to give a start location on the screen for the first graphics figure. Next, the offset of the first character is loaded into the X register, and a check is made to see if the figure is ended. I have selected $FE (254) as a signal that part of the figure is completed, and $FF (255) for the end of the routine. If this data is neither, a graphics symbol will be leaded into the accumulator. This symbol is then deposited into the video refresh memory at the location previously loaded plus the offset in the X register. This may all sound a bit complicated, but that doesn't make any difference to you, the programmer. The program will take care of itself if the proper data is presented from the symbol table in the correct sequence.
Developing the screen image
The screen image is developed by following five easy steps, starting with a copy of the C1P Screen Grid. Being a railroad fan, I chose enact the “Great Train Collision” as a demonstration program. As you can see from Figure 2a, the OSI graphics symbols allow for a very reasonable steam locomotive with tender. After you have drawn whatever figures you need for your game to your satisfaction, enter the screen offsets of all locations, starting at a fixed point at the upper left corner of each figure, as shown in Figure 2b. Finally, write in the character generator code as shown in Figure 2c. The last step is to enter the data into a table as shown in Listing 2. Note that each figure is preceded by a screen address (lo byte first), and ends in either FE or FF. The last figure in the table must be FF (255) which causes the subroutine to return to BASIC.
Converting the Machine Language to BASIC
There are several methods of entering machine or assembly language programs through the use of BASIC programs, including direct entry through the monitor. There are also several ways to save combination BASIC-machine language programs to tape (see Daniel Schwartz' article in Compute II issue 1). However, I prefer the somewhat tedious but easy to debug and modify DATA-POKE method, This is hard to enter error-free, and causes a significant delay on program initiation, but it has the advantage of being more readily understandable, and allows your routines to be changed easily.
You can see that lines 100 through 145 in Listing 3 enter the machine language subroutine into memory, while lines 148 through 199 are the table of offsets and characters for the figures. This is to allow the use of this sequence with other graphics figures of your own choosing. Note also that the subroutine is inserted into RAM at $0222 (#546) in page 2. The page 2 memory from $0222 to $02FA is not used by BASIC, and is thus a good free place for this use. However, make sure your figures don't extend above $02FA. If so, you'll have to locate the table in the top of RAM, necessitating memory protection at Cold Start. The subroutine can still reside at $0222.
Animating the Figures
Once the graphics subroutine and the figure table are in RAM, the animation routines can be written in BASIC. The addresses needed to animate the demonstration figures are as follows:
|Pointer to the starting address of the USR subroutine.|
|Pointer to the starting address of the first graphics figure to be called.|
|0222||546||Actual starting address of the USR subroutine, including screen clear.|
|0238||568||Actual starting address of the USR subroutine without screen clear.|
|Actual starting address of the first graphics figure. These locations hold the video RAM reference address, low byte first, for the figure.|
|Actual starting address of the second graphics figure. Also hold the video RAM starting address for this figure.|
It is important to remember that the last two locations will vary depending on the location in RAM chosen to store the table, and according to the length of the figures in the table. The first four sets of addresses will remain the same for all programs.
In the demonstration program, memory location #12 always holds a #2, but the data in location #11 is changed back and forth between #34 and #56; this alternately inserts and omits the screen clear routine at the beginning of the machine language subroutine. If your program only has one figure, or if you end all figures but the last with #254, location #11 would stay at #34. Either method works equally well; chose the alternative which provides easiest animation in BASIC.
Note that if each figure ends with a #254, the data at memory location #254 will not change. But ending the first or any intermediate figure with #255 terminates the subroutine (see flow chart), and the only way to reach the next or subsequent figures is to change the pointer at #254 and #255.
As previously mentioned, the first two numbers in the table at the beginning of each figure are the video RAM address of the upper left hand corner of the figure. Since the video RAM of the C1P contains four pages of 256 bytes each, starting at $D000, the first number in the table at the start of each figure can vary from 0 to 253 (remember, 254 and 255 are signals), and the second can be $D0 through $D3 (#208 through #211). By POKEing different numbers into these locations, the figures can be made to move around the screen, It's better to use the actual first location of each figure in the original DATA statements, to avoid a jerk at the start of the program. After that, any desired location can be POKE'd into these addresses.
In lines 25 and 35 of Listing 2, variables A and B are established as the low bytes of the screen locations for the two figures. Then, in the subroutine starting at line 50, they are POKE'd to 608 and 669 respectively, and then incremented and decremented and POKE'd again to move the characters across the screen toward each other. To make the demonstration program more realistic, the engines are moved across the screen on different lines, and then reappear on a collision course. This is done by line 35, which changes A and B and also changes the high byte of the right locomotive to #209 from 210, moving it higher on the screen. Variable C determines how far across the screen each figure will move.
Finally, the routines at lines 200 to 399 give a rough representation of an explosion at the point of collision. I leave to the reader the exercise of adding this to the machine language table. It's not that hard!
In order, the steps for creating your own program using fast machine language graphics are as follows:
Draw a representation of your selected characters, using the characters in the Graphics Reference Manual as elements.
In an equivalent number of spaces, enter the offsets of the screen address, starting at a point above and to the left of the figures. If the number of the offset exceeds 253, just split the figure into two or more parts and treat each as a separate entity. They can be recombined in the BASIC program.
Likewise, enter the graphics character codes in the equivalent spaces on the grid.
Create a table which starts with the screen location of the figure (low byte first, then high byte); contains, alternately, the offset and code of each character; and ends in #254 or 255. If the figure ends in 254, the subroutine will continue with the next figure in the table. A #255 terminates the subroutine and returns to BASIC.
Note the starting addresses of each figure, for later use in creating the animation in BASIC.
Convert both the subroutine and the table into DATA statements. Note that if the table goes past memory location #608 + 144 (#752), it will be necessary to move all or part of it to another location in RAM, such as top of memory.
Finally, enter the DATA statements and their POKE loops into an appropriate location in your BASIC program, in a manner similar to lines 100-199 of Listing 2. Then proceed with animation as in lines 25-99.
Debugging hints: always save to tape as you go (a program crash is more likely in machine language, and all that tough typing will be lost); insert a BREAK after DATA loops, then use the Monitor to verify the machine language program entry, by single stepping starting at address $0222; insert timing loops at lines 22, 27, 32, and 37 to slow down action if there is a Bug in BASIC.
Some additional notes
Another interesting program I've written using this method is Six Gun Shootout. It has two gunfighters (one facing the other) and three cacti. After each shot, the cacti change locations at random. The program ran very slowly when written solely with BASIC POKE's to the screen, but is as fast as you would ever want with machine language graphics. I'll cover this program in a later article, together with instructions for attaching simple up-down-shoot joysticks to save wear and tear on the keyboard.
Listing 1. Machine Language Subroutine
0222 A0 00 546 160 0 LDY-IMM $00 0224 A9 20 548 169 32 LDA-IMM $20 0226 99 00 D3 550 153 0 211 STA-Y 0229 99 00 D2 553 153 0 210 STA-Y 022C 99 00 D1 556 153 0 209 STA-Y 022F 99 00 D0 559 153 0 208 STA-Y 0232 C8 562 200 INY 0233 D0 F1 563 208 241 BNE $0226 0235 EA EA EA 565 234 234 234 NOP NOP NOP 0238 A0 00 568 160 0 LDY-IMM $00 023A B1 FE 570 177 254 LDA-0,Y LDA Lo Byte Scr Loc 023C 8D 56 02 572 141 86 2 STA-ABS 023F C8 575 200 INY 0240 B1 FE 576 177 254 LDA-0,Y LDA Hi Byte Scr Loc 0242 8D 57 02 578 141 87 2 STA-ABS 0245 C8 581 200 INY 0246 B1 FE 582 172 254 LDA-0,Y LDA w/Char Offset 0248 AA 584 170 TAX A to X Register 0249 C8 585 200 INY 024A E0 FE 586 224 254 CPX-IMM X=254? End Figure 024C F0 EC 588 240 236 BEQ Branch to 023A 024E E0 FF 590 224 255 CPX-IMM X=255? End Routine 0250 F0 08 592 240 8 BEQ Branch To 025A 0252 B1 FE 594 177 254 LDA-0,Y Load Character 0254 C8 596 200 INY 0255 9D 44 D1 597 157 68 209 STA-ABS,X Char. to Screen 0258 D0 EC 600 208 236 BNE Get another Character 025A 60 EA EA 602 96 234 234 RTS NOP NOP End Routine, Return 025D EA EA EA 605 234 234 234 NOP NOP NOP
Listing 2. Graphics Table
0260 9B D1 608 155 209 028A 46 A1 650 70 161 02B3 25 A1 691 37 161 0262 01 02 610 1 2 028C 47 A1 652 71 161 02B5 26 A1 693 38 161 0264 03 A7 612 3 167 028E 48 A1 654 72 161 02B7 27 A1 695 39 161 0266 04 9D 614 4 157 0290 60 B0 656 96 176 02B9 28 A7 697 40 167 0268 05 A1 616 5 161 0292 61 E0 658 97 224 02BB 40 A1 699 64 161 026A 08 A7 618 8 167 0294 62 E2 660 98 225 02BD 41 A1 701 65 161 026C 20 A5 620 32 165 0296 63 E2 662 99 266 02BF 42 A1 703 66 161 026E 21 A1 622 33 161 0298 66 E2 664 102 226 02C1 43 80 705 67 128 0270 22 A1 624 34 161 029A 68 E2 666 104 226 02C3 44 A1 707 68 161 0272 23 A1 626 35 161 029C FF 668 255 02C5 45 A1 709 69 161 0274 24 A1 628 36 161 029D 83 D1 669 131 209 02C7 46 A1 711 70 161 0276 25 9B 630 37 155 029F 00 A5 671 0 165 02C9 47 A1 713 71 161 0278 26 B0 632 38 176 02A1 03 A1 673 3 161 02CB 48 A8 715 72 168 027A 27 A1 634 39 161 02A3 04 9C 675 4 156 02CD 60 E2 717 96 226 027C 28 A1 636 40 161 02A5 05 A7 677 5 165 02CF 62 E2 719 98 226 027E 40 A6 638 64 166 02A7 07 02 679 7 2 02D1 65 E2 721 101 226 0280 41 A1 640 65 161 02A9 20 A1 681 32 161 02D3 66 E0 723 102 224 0282 42 A1 642 66 161 02AB 21 A1 683 33 161 02D5 67 E1 725 103 225 0284 43 A1 644 67 161 02AD 22 B2 685 34 178 02D7 68 B2 727 104 178 0286 44 A1 646 68 161 02AF 23 9B 687 35 155 02D9 FF 729 255 0288 45 80 648 69 128 02B1 24 A1 689 36 161