General-purpose Speedups For Atari BASIC
D. K. Titchenell
Do you ever need to quickly move character sets around, to achieve fast vertical PIM motion, to instantly clear out players or missiles? These and many other speed-critical problems can be solved with these short, simple subroutines. You need not understand or write machine language to take advantage of its great speed. (Since BASIC itself is written in machine language (ML), you use it all the time without having to be able to explain exactly how PRINT prints.) Here are some efficient solutions to those programming problems where BASIC is just too slow. The example (Program 1) illustrates how to use these subroutines from BASIC.
BASIC is a comfortable language, very friendly, helpful and delightfully forgiving. Eventually, however, the user discovers that there are a few things that can't really be done in BASIC. Then the only solution seems to be to walk over into the rather less certain world of machine language.
There are no two ways about it — BASIC is slow — and you occasionally need to make several things happen with apparent simultaneity.
The Atari provides a very convenient channel for BASIC to communicate with machine language (ML), the USR statement. USR, allowing as it does the passage of any number of values from BASIC to ML, permits a great deal of flexibility. Not only is the number of parameters unlimited, but this number is a known quantity once in the routine and therefore it may be treated as a variable. In taking advantage of this feature, I have found that a relatively small selection of ML routines may be used to solve a large percent of these problems requiring machine language solutions.
Under certain circumstances it can be necessary to change the contents of a few addresses without the noticeable lag time between the operations that you encounter using BASIC. One instance of this is in playing music. When BASIC plays a piece with multiple voices, a sharp attack (the start of a sound) cannot be achieved because the attack of the different voices is slightly staggered due to the sluggishness of the language.
This point is brought out in De Re Atari, and a short machine language routine is presented as a remedy. Another problem of this kind occurs whenever a two-byte register must be changed in realtime — scrolling, for example. Inevitably, that perceptible interval between changing the low byte and the high byte of a register will cause embarrassment. As ANTIC goes zipping through the load memory scan 60 times a second, it can easily display several screens of material during that interval. You could, of course, write little ML routines to solve these individual problems as they arise; or better yet, if you are a little lazy or simply not overly enamored of machine language, you could write a program to solve that type of problem in general.
This was the intention behind MultiPOKE. The MultiPOKE routine acts just like several POKE statements together, performed at machine language speed. Since the number of parameters passed in the USR function is a known quantity, any number of addresses and data to be POKEd into them may be contained in the parameter list. They follow the same order as the POKE statement. The general format is:
D = USR(ADR(POK$),ADDRESS, DATA, [ADDRESS, DATA...])
A special feature of the routine was added specifically to address the high-byte, low-byte problem. If a data element passed is a one-byte quantity (less than 256), then the routine acts just like one or more POKE statements. If, however, a larger quantity is passed, the low byte is POKEd in in the normal fashion, and the high byte is POKEd into the next higher register in standard low-byte, high-byte form. This eliminates the bother of calculating the carry. Consider the following solution to the scrolling problem:
DLIST = PEEK(560) + 256*PEEK(561) : FOR I = 300 TO 20000 STEP 40 : D = USR(ADR(POK$) ,DLIST + 4,I) : NEXT I
This is a very simple way to scroll the screen RAM through most of memory; DLIST + 4 and DLIST + 5 (the LMS operand) are adjusted without BASIC.
Moving RAM With MOV$ And MOVU$
The MOV$ and MOVU$ routines solve a different type of problem: moving large, contiguous areas of RAM. When used in various ways, these utilities can perform the following functions: rapid player/missile vertical motion; initializing areas of memory to a single value or a repeating set of values; or moving around blocks of RAM, such as character sets, with no wasted time. The general form of the call to these routines is: D = USR (ADR(MOV$),FROM,TO,HOWMUCH) where FROM and TO are addresses of origin and destination and HOWMUCH is the number of bytes to move.
The routines are used in exactly the same way, but for complete versatility both are needed. Bytes are moved from the origin to the destination areas one at a time. MOV$ starts at the bottom and goes up. MOVU$ starts at the top and goes down. If the locations of origin and destination do not intersect, both perform identically; if there is overlap, though, the right routine must be chosen for the data to remain intact.
Here's why: suppose you wanted to move five bytes starting at location 500 to the five bytes starting at location 499. MOV$, whose execution proceeds up, would perform correctly – moving the byte at 500 to 499, then the byte at 501 to 500, etc., leaving the data intact. If, on the other hand, you were to use MOVU$, which proceeds down, the following would be the case: the byte at location 504 would be moved to 503, then the byte at 503 would be moved to 502 and so on, filling all five bytes with the original contents of location 504. Both effects can be very useful, but make sure to choose the right one. Let's see some examples.
Speeding Up P/M Graphics With MOV$
Vertical player/missile motion in BASIC tends to resemble an inchworm crawling up the screen. Some alternative methods I have seen have used string manipulation or dedicated machine language programs which erase the former image and position the new one rapidly. Using MOV$ is far simpler than either. You need only to put your player data into a string or other safe place with a zero or two before and after it and "MOV$" that data to the appropriate position in player/missile RAM. No erasing of the former player data is necessary because the incoming data (with help from the zeroes before and after) will obliterate it.
The simple example (Program 1) just puts a player, movable by joystick 1, on the screen, while playing a three-voice melody. It demonstrates the use of POK$ in playing multiple voice music and uses MOV$ for vertical player motion and RAM initialization. The three subroutines at lines 2000, 2100 and 2200 read the machine language code for POK$, MOV$ and MOVU$ into their respective strings.
This is, of course, a terribly inefficient use of time and space, but it is the only method possible when readable, printable characters are required. After entering these routines, you may then convert them into character strings using the following method: call the reading subroutine for POK$, for example, then enter the following line in direct mode:
FOR W = 1 TO LEN(POK$) : ? CHR$(27) ; POK$(W, W) ; : NEXT W
This will print out the character string, which then may be made into an assignment statement by putting double quotes at either end and putting "POK$ = " in front of it. Each of the three routines fits easily on a single BASIC line.
The short reading routine at line 1000 sets up the two arrays DIRH(15) and DIRV(15) with direction indicators which are selected during execution by using the value returned by the STICK function as a subscript. This is a useful and very time-efficient device.
Clearing P/M RAM With MOV$
The virtual simultaneity afforded by POK$ is not required in the P/M setup procedure at all, but it is used here in line 230 where it serves well to show the format of the routine call. It's also nice to be able to get all of that picky P/M stuff out of the way in one chunk. Line 240 then shows off one of the applications of MOV$, clearing P/M RAM in a split second.
MOV$ executes a data transfer from the bottom up and can thus be used to move blocks down in memory, leaving them intact. Here, however, it is used in the opposite direction, for a purpose. A zero is POKEd into location PMBASE + 512. Then 128 bytes are moved up a distance of one byte, starting at that point. Thus the zero value is passed up from each register to the following one, thereby clearing the entire player area.
The actual program loop is a bare skeleton. In line 310 the player data is moved into P/M RAM with MOV$ passing, as parameters, the address of PLAYERS (FROM), the P/M position (TO) and the number of bytes, ((HOWMUCH) 10 in this case) as the player is eight bytes high and a zero is added at either end.
Since this move does not involve overlapping, either MOV$ or MOVU$ could have been used with identical results; the choice was arbitrary. Lines 320 and 330 read the STICK value and adjust the X and Y coordinates accordingly. Lines 340 and 350 read the tune data and RESTORE when the end is reached. Line 360 uses POK$ to set the player X position and insert the frequency bytes into AUDF1, AUDF2 and AUDF3, the AUDC registers having been initialized by the SOUND statement in line 250. The piece chosen plays here at an appropriately frenetic pace in the absence of a delay loop. The tempo is sufficiently restrained by the snail's pace data reading speed of BASIC. Were we to retard the loop further with added processing, it would probably be advisable to read the tune data into a string first; this would more than double the tempo.
Notes On Structure
A note on the structure of the ML routines themselves: free memory locations that are safe from the meanderings of BASIC or graphics mode changes are often in high demand. In order not to consume the few safe memory areas at the programmer's disposal, each of these routines is relocatable and is placed in a character string.
Most of the space in the routines is used to handle the stack contents properly; the actual loop in each case requires very little space. Were we to POKE all of the parameters into the correct locations beforehand, the size of the routines would be considerably diminished, but the beauty and generality of the parameter list would be lost. Care must be taken in all the routines to pass the correct number of parameters.
Because the address to which execution will return upon completion of the routine is kept in the stack (just below the passed parameters), exactly the right number of bytes must be pulled off the stack or the computer will never find its way home again. In the case of POK$, the number is a variable and the routine keeps track of how many have been pulled; however, there must be an even number, pairs of [address, data]. MOV$ and MOVU$ each must receive exactly the three parameters: FROM, TO and HOWMUCH.
Whenever starting a new project I have taken to entering a listing of these routines into the program at the outset, confident that I will eventually have a need for them. In most cases I do. Possible applications for these ML BASIC helpers are certainly not limited to the ones presented here. New uses suggest themselves often.