Thursday, April 13, 2017

Apple II - Double Hi-Res From The Ground Up
Part 4: General Purpose Plot Routine


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.

PixelAUX LocationMAIN location
A8192Not Used
B81928192
CNot Used8192
D81938192
E8193Not Used
F81938193
GNot Used8193

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

PixelAUXMAIN
A0255
B00
C2550
D10
E1255
F11
G2551

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,1
To 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,255
This 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,255
What'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,255
In 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:

Apple II - Double Hi-Res From The Ground Up - Part 9: An API and a demo!

A better interface Perhaps you've noticed that all these drawing routines we've developed require a fair amount of memory to do an...