The plot thickens
I wonder how many articles about plotting points use that pun? Anyway in our last episode we learned that the DHR screens are made up of bytes from $2000-$3FFF (PAGE1) and $4000-$5FFF (PAGE2) and that each of these pages is bank switched with another page in auxiliary (AUX) memory. The video circuitry alternates between reading them, first from AUX and then from MAIN.
We also found out that by invoking the 80STOREON softswitch ($C001) and subsequently the PAGE1/PAGE2 ($C054/$C055) softswitches we could swap $2000-$3FFF between the MAIN and AUX banks AND by invoking the 80STOREOFF ($C000) softswitch and the RAMWRTON ($C005), RAMWRTOFF ($C004) softswitches we could swap between writing to MAIN or AUX banks for most of Apple II memory - including both hi-res PAGE1 and PAGE2.
Finally we discovered that each coloured pixel takes up four bits. The screen pixels, from left-to-right start in the four least significant bits in each byte and that only seven bits in each byte are used. The eighth bit, which is used to switch pallets in regular hi-res is ignored.
So without further ado lets see if we can turn all of this information into a machine language program to plot an arbitrary point on the DHR screen.
Based on what we have just stated it should be clear that to plot a pixel we need to be able to answer the following questions about it:
1) Is this pixel residing in AUX memory, MAIN memory or BOTH? (Remember, some pixels overlap!)
2) Which bytes in AUX memory and/or MAIN memory do we need to modify?
3) Which individual bits do we need to turn off and on?
4) How do we turn them on and off without disturbing the other pixels)?
To start lets use an illustration similar to the one we used earlier. Here we are looking at the first four bytes of the DHR screen with AAAA representing the four bits of the leftmost pixel, BBBB the next pixel, CCCC the third and so on...
So we have four bytes representing seven pixels A-G. While there is some math you could do to figure out which memory location is being used. It would take far too long to be usable. The irony of working on an 8 bit machine is that despite computers being lightning fast calculators. When speed is really important, most math is done through lookup tables...
Lookup tables
A lookup table is simply an area of memory where we place the pre-computed results of something we want to calculate. When we want an answer instead of doing a calculation we use an indexed instruction on the 6502 to get our result. Think of it like the difference between finding the answer for 5 * 3 by adding 3 + 3 + 3 + 3 + 3 or by simply memorizing the multiplication tables by rote.
To start lets build a chart showing each of these seven pixels and the memory location they use in AUX as well as the memory location they use in MAIN.
Pixel | AUX Location | MAIN location |
---|---|---|
A | 8192 | Not Used |
B | 8192 | 8192 |
C | Not Used | 8192 |
D | 8193 | 8192 |
E | 8193 | Not Used |
F | 8193 | 8193 |
G | Not Used | 8193 |
Now lets redo this but instead of talking about the memory location, lets look at the distance from the location 8192. Also, to represent "not used" we will put in a number which will never occur normally: 255
Pixel | AUX | MAIN |
---|---|---|
A | 0 | 255 |
B | 0 | 0 |
C | 255 | 0 |
D | 1 | 0 |
E | 1 | 255 |
F | 1 | 1 |
G | 255 | 1 |
We can then convert the AUX and MAIN columns into rows of bytes:
ABOFFSET DB 0,0,255,1,1,1,255 MBOFFSET DB 255,0,0,0,255,1,1To an assembler, the directive DB just tells it that it should treat the following numbers as bytes. I tend to use the Merlin Pro Assembler and by default it will assume that all the values in these lists are decimal (base 10). I've called these two tables ABOFFSET and MBOFFSET for Auxiliary Byte OFFSET and Main Byte OFFSET respectively.
Now, if we want to find the byte in AUX or MAIN memory used by....say the fourth pixel. We can do so with the following code:
LDX #$02 ;Load X with 02 LDY ABOFFSET,X ABOFFSET DB 0,0,255,1,1,1,255This will load the Y register with the third value in our AUXTAB table which is exactly the number of bytes from $2000 that the fourth pixel represents in AUX memory. Knowing now (roughly) where to put our pixel we can do something like this:
STA $C001 ;Turn 80STOREON LDX #02 ;Load X with 02 LDY ABOFFSET,X ;Load entry from table STA $C055 ;Swap $2000 with AUX memory LDA #$00 ;Load accumulator with black pixels STA $2000,Y ;Write black pixels to $2000 + Y ABOFFSET DB 0,0,255,1,1,1,255
Here we write a byte containing nothing but 0's - which will be interpreted as black pixels - to DHR PAGE1 in AUX memory. Roughly where pixel C, from our diagram will be. If pixel C had bits belonging to it in MAIN memory we could do the same thing there as well. The whole thing would look something like this:
STA $C001 LDX #02 LDY MBOFFSET,X STA $C054 LDA #$00 STA $2000,Y LDY ABOFFSET,X STA $C055 LDA #$00 STA $2000,Y MBOFFSET DB 255,0,0,0,255,1,1 ABOFFSET DB 0,0,255,1,1,1,255What's that you say? What about those 255's in that table? We said we use them to tell us when only one bank of memory was needed. Since we will never have a result from AUXTAB or MAINTAB that's greater than 40 ( 40 MAIN bytes + 40 AUX bytes = 80 bytes * 7 usable bits per byte = 560 monochrome pixels). We can take advantage of the fact that when a register gets loaded with a value greater than 127 the negative flag is set. So we can use a branch instruction to skip over the unnecessary part of the code. Like so:
LDY AMOFFSET,X BMI --put an address here to skip past the AUX memory routine--The whole routine would look then look like:
ORG $6000 ;Start our program at $6000 GRAPHON EQU $C050 HIRESON EQU $C057 FULLON EQU $C052 DHRON EQU $C05E ON80STOR EQU $C001 ON80COL EQU $C00D PAGE1 EQU $C054 PAGE2 EQU $C055 INIT STA GRAPHON ;Turn on graphics STA HIRESON ;Turn on hi-res STA FULLON ;Turn on fullscreen STA DHRON ;Turn on DHR STA ON80COL ;Turn on 80 columns STA ON80STOR ;Use PAGE1/PAGE2 to switch between MAIN and AUX memory LDX #00 ;Plot the pixel in the top left corner of the screen DPLOT LDY MBOFFSET,X ;Find what byte in MAIN if any we are working in BMI AUX ;If pixel has no bits in MAIN memory go to AUX routine STA PAGE1 ;Map $2000 to MAIN memory LDA #$00 ;All black pixels STA $2000,Y ;Write to screen AUX LDY ABOFFSET,X ;Find what byte in AUX if any we are working in BMI END ;If pixel has no bits in AUX memory end program STA PAGE2 ;Map $2000 to AUX memory LDA #$00 ;All black pixels STA $2000,Y ;Write to screen END RTS MBOFFSET DB 255,0,0,0,255,1,1 ABOFFSET DB 0,0,255,1,1,1,255In case you're not familiar with Merlin assembler directives. ORG just says we want the assembler to create this code at memory location $6000. The EQU statements let us assign names to values. So we don't have to constantly remember that $C055 is PAGE2. If you're using an emulator like AppleWin you should just be able to paste this into the assembler by pressing <shift>+<insert>.
The Code:
Starting at the INIT label we initialize all the softswitches needed to get DHR running. We also set the X register to 00. Which is the position of pixel A but you could change this to anything. The next label, DPLOT is where we do the actual work of writing our pixel data to memory. As long as the X register has a valid pixel value in it (in this case 0-6) this routine will do it's thing. So it would be tempting to call this with a JSR. later on I'll demonstrate why, in performance oriented code we don't do that and instead use an assembler feature called "macros".
The END label has an RTS so the second branch would have somewhere to go. This will cause the code to return to whatever called it (e.g. the monitor, Applesoft BASIC)
What's Next?
Right now, our program takes a horizontal co-ordinate in the X register and overwrites the bytes used by that pixel with black pixels. What we want is to be able to write only to the bits in these bytes that our pixel occupies. For that we need a MASK which is something we will cover in my next post!
No comments:
Post a Comment