Showing posts with label plot. Show all posts
Showing posts with label plot. Show all posts

Friday, April 14, 2017

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


The story so far....

In the previous post we developed a routine which could write a green pixel anywhere across the top line of the DHR screen.  This was achieved by SIX table lookups.  Two to find the MAIN and AUX bytes used by this pixel, two more to MASK out the appropriate bits using the AND operation and finally another two to OR the bit pattern of a green pixel into memory.

Colour me impressed...


Adding the ability to select our pixels colour may seem like something of a challenge since the pixel colour comes from the MAINGR and AUXGR tables.  Obviously we can create more tables.  For example here are MAINMB and AUXMB tables for drawing a medium blue pixel.  (From now on I'll refer to these as "colour tables".)
MAINMB
 LUP 20
 DB %00000000,%00000000,%00000110,%01100000
 DB %00000000,%00000001,%00011000
 --^
AUXMB
 LUP 20
 DB %00000011,%00110000,%00000000,%00000000
 DB %00001100,%01000000,%00000000
 --^
But the question remains: How do we select these tables instead of MAINGR and AUXGR?  Well if you remember there are only two places where the colour table is referenced in our code and they are

ORA MAINGR,X

and

ORA AUXGR,X

Wouldn't it be great if we could just change these instructions to point to different locations when we feel like it?  Well brace yourself because that's exactly what we are going to do!

Self-modifying code


As a machine language program is just a stream of bytes and since machine language programs are great at manipulating bytes.  It should come as no surprise that we can write programs that modify themselves. This technique is known as: self-modifying code.

The ORA instruction is the one we are interested in.  It is represented by the byte 1D which is then followed by two bytes forming an address.  The 6502 stores this address in what is known as little-endian format.  Meaning that we store the second byte in the address first.  So for example if AUXGR was located at $6E12 then the CPU would expect to read the 12 before the 6E. So when the assembler sees ORA AUXGR,X it turns it into: 1D 12 6E.

From here, it should be easy to see that we could write a routine to change the address that both our ORA instructions are pointing at. To do so we're going to have to track exactly where these instructions are in memory.  This can be done simply by giving the assembler a label for each instruction like so...
DPLOT LDY MBOFFSET,X
 BMI AUX
 STA PAGE1
 LDA $2000,Y
 AND MAINAND,X
ORMAIN ORA MAINGR,X
 STA $2000,Y
AUX LDY ABOFFSET,X
 BMI END
 STA PAGE2
 LDA $2000,Y
 AND AUXAND,X
ORAUX ORA AUXGR,X
 STA $2000,Y
END RTS
Now we have the labels ORMAIN and ORAUX pointing to where those instructions are!

Next, let's assume we have colour tables for each of the 16 DHR colours with the following names

ColourTable name(s)
BlackMAINBL/AUXBL
MagentaMAINMG/AUXMG
BrownMAINBR/AUXBR
OrangeMAINOR/AUXOR
Dark GreenMAINDG/AUXDG
Grey 1MAING1/AUXG1
GreenMAINGR/AUXGR
YellowMAINYE/AUXYE
Dark BlueMAINDB/AUXDB
VioletMAINVI/AUXVI
Grey 2MAING2/AUXG2
PinkMAINPI/AUXPI
Medium BlueMAINMB/AUXMB
Light BlueMAINLB/AUXLB
AquaMAINAQ/AUXAQ
WhiteMAINWH/AUXWHH

We will now create four tables containing the following information:

Table nameDescription
CLOMLow byte of the address of all MAIN memory colour tables
CHIMHight byte of the address of all MAIN memory colour tables
CLOALow byte of the address of all AUX memory colour tables
CHIAHight byte of the address of all AUX memory colour tables

Most assemblers have a way of accessing the high byte and low byte of any label defined in your code.  In the case of Merlin Pro the operators are < and >.  So <MAINGR and >MAINGR refer to the low and high bytes of the MAINGR table respectively.  So writing the following:
CLOM DB <MAINBL,<MAINMG,<MAINBR,<MAINOR,<MAINDG
 DB <MAING1,<MAINGR,<MAINYE,<MAINDB,<MAINVI
 DB <MAING2,<MAINPI,<MAINMB,<MAINLB,<MAINAQ,<MAINWI
CHIM DB >MAINBL,>MAINMG,>MAINBR,>MAINOR,>MAINDG,>MAING1
 DB >MAINGR,>MAINYE,>MAINDB,>MAINVI,>MAING2,>MAINPI
 DB >MAINMB,>MAINLB,>MAINAQ,>MAINWI
CLOA DB <AUXBL,<AUXMG,<AUXBR,<AUXOR,<AUXDG
 DB <AUXG1,<AUXGR,<AUXYE,<AUXDB,<AUXVI
 DB <AUXG2,<AUXPI,<AUXMB,<AUXLB,<AUXAQ,<AUXWI
CHIA DB >AUXBL,>AUXMG,>AUXBR,>AUXOR,>AUXDG,>AUXG1
 DB >AUXGR,>AUXYE,>AUXDB,>AUXVI,>AUXG2,>AUXPI
 DB >AUXMB,>AUXLB,>AUXAQ,>AUXWI
Gives us a table with all the high and low byte addresses of our colour tables.  Now all we need is a routine to set the colour table location:  Let's call this program SETDCOLOR and expect the programmer to choose the colour by passing it in the accumulator:
SETDCOLOR TAY
 LDA CLOM,Y
 STA ORMAIN+1
 LDA CHIM,Y
 STA ORMAIN+2
 LDA CLOA,Y
 STA ORAUX+1
 LDA CHIA,Y
 STA ORAUX+2
 RTS
Done.  Now each time we call SETDCOLOR it updates our DPLOT routine to point to the appropriate table.

Vertical take off:


So what's left?  Oh right! Our routine is still "imprisoned" on the first line of the hi-res screen $2000.  So how can we change this?  Well...can't we use self-modifying code like we just did with the colour table information?  Well you could....but....like any anything we code we need to ask the question: What are we assuming about our execution environment?

When we wrote SETDCOLOR we knew we had to change something about the way our program executed to get it to look at the right colour table.  No matter what we did it was going to cost us time.  Also it's not unreasonable to assume that a plotting program is going to plot a number of points in a single colour before it changes to a different colour.

Can we make the same assumption here?  Maybe not.  We will have to change these values every time we plot a point.   Each time we do we're going to do four table lookups and rewrite four bytes.  Is that going to be too much?  We can get a sense of this by adding up the time it takes to execute the main part of our SETDCOLOR program:

InstructionCycles
LDA CLOM,Y5
STA ORMAIN+14
LDA CHIM,Y5
STA ORMAIN+24
LDA CLOA,Y5
STA ORAUX+14
LDA CHIA,Y5
STA ORAUX+24
TOTAL36

So every plot it's going to cost us 36 machine cycles.  Let's compare this to a different method: indirect addressing using the zero page.

Indirect addressing:


Anyone who has written 6502 assembly should know how to use indirect addressing so this will be a quick refresher.  Examine the following code:
 LDA #$00 ;Load the low byte of the address $2000
 STA $1D ;Store it in $001D
 LDA #$20 ;Load the high byte of the addres $2000
 STA $1E ;Store it in $001E
 LDA #$FF ;Load the accumulator with 255
 LDY #$00 ;Load Y with 0
 STA ($1D),Y ;Store the contents of the accumulator at location $2000
As you can see STA($1D),Y peeks into the two adjacent memory locations $1D and $1E.  Sees they contain the bytes 00 and 20 respectively. It then puts them together to form the address $2000 and stores the contents of  the accumulator in that memory location.

But wait!  Is that really any faster?  Actually yes! First, we only have to read and write two bytes instead of four. Second, the writes to the zero page only take three cycles instead of four.  The only drawback is that STA ($1D),Y takes one cycle longer than STA $2000,Y.  However even with that we still come out ahead.

 The question remains: How do we implement this?  You know the answer! That's right! Another table!  This time representing the high and low bytes of the beginning of each screen row.  As there are 192 screen lines, which is a lot of data.  For now I'll just give  you the table for the first eight lines, which if you recall are $400 bytes apart.
HTAB_LO DB $00,$00,$00,$00,$00,$00,$00,$00
HTAB_HI DB $20,$24,$28,$2C,$30,$34,$38,$3C
I've called them HTAB_LO and HTAB_HI for the horizontal low-byte table and horizontal high-byte table respectively.

Now just as our program expects the horizontal co-ordinate in the X register we'll modify our code to expect the vertical co-ordinate in the Y register.  To accomplish that we add the following to the beginning of our program:
 LDA HTAB_LO,Y
 STA $1D
 LDA HTAB_HI,Y
 STA $1E
Then we make one final change to the main part of our program that does the plotting.  We substitute the places where we wrote:

LDA $2000,X

With

LDA ($1D),X

And believe it or not we're done!  The following is a fully commented routine to draw a pixel of any colour anywhere on the DHR page. The listing also includes our SETDCOLOR routine:
 XC ;Required for Merlin Pro to use 65C02 instructions
 ORG $6000 ;Start assembling at memory location $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
SCRN_LO EQU $1D ;Zero page location for low byte of our screen row
SCRN_HI EQU $1E ;Zero page location for high byte of our screen row
INIT STA GRAPHON ;Turn on graphics
 STA HIRESON ;Turn on hi-res mode
 STA FULLON ;Turn on fullscreen mode
 STA DHRON ;Turn on Double hi-res
 STA ON80COL ;Turn on 80 Column mode
 STA ON80STOR ;Use PAGE1/PAGE2 to switch between MAIN and AUX memory
 LDA #$0E ;Set colour to 14 = Aqua
 JSR SETDCOLOR
 LDX #00 ;Set column to 0
 LDY #00 ;Set row to 0
DPLOT LDA HTAB_LO,Y ;Find the low byte of the row address
 STA SCRN_LO 
 LDA HTAB_HI,Y ;Find the high byte of the row address
 STA SCRN_HI
 LDY MBOFFSET,X ;Find what byte if any in MAIN 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 (SCRN_LO),Y ;Load screen data
 AND MAINAND,X ;Erase pixel bits
ORMAIN ORA MAINGR,X ;Draw coloured bits
 STA (SCRN_LO),Y ;Write back to screen
AUX LDY ABOFFSET,X ;Find what byte if any in AUX we are working in
 BMI END ;If no part of the pixel is in AUX - end the program
 STA PAGE2 ;Map $2000 to AUX memory
 LDA (SCRN_LO),Y ;Load screen data
 AND AUXAND,X ;Erase pixel bits
ORAUX ORA AUXGR,X ;Draw coloured bits
 STA (SCRN_LO),Y ;Write back to screen
END RTS 
SETDCOLOR TAY ;Assume the desired colour is in the accumulator
 LDA CLOM,Y ;Lookup low byte of MAIN memory colour table
 STA ORMAIN+1 ;Update the ORA instruction
 LDA CHIM,Y ;Lookup high byte of MAIN memory colour table
 STA ORMAIN+2 ;Update the ORA instruction
 LDA CLOA,Y ;Lookup low byte of AUX memory colour table
 STA ORAUX+1 ;Update the ORA instruction
 LDA CHIA,Y ;Lookup high byte of AUX memory colour table
 STA ORAUX+2 ;Update the ORA instruction
 RTS 
MBOFFSET DB 255,0,0,0,255,1,1,255,2,2,2,255,3,3
 DB 255,4,4,4,255,5,5,255,6,6,6,255,7,7
 DB 255,8,8,8,255,9,9,255,10,10,10,255,11,11
 DB 255,12,12,12,255,13,13,255,14,14,14,255,15,15
 DB 255,16,16,16,255,17,17,255,18,18,18,255,19,19
 DB 255,20,20,20,255,21,21,255,22,22,22,255,23,23
 DB 255,24,24,24,255,25,25,255,26,26,26,255,27,27
 DB 255,28,28,28,255,29,29,255,30,30,30,255,31,31
 DB 255,32,32,32,255,33,33,255,34,34,34,255,35,35
 DB 255,36,36,36,255,37,37,255,38,38,38,255,39,39
ABOFFSET DB 0,0,255,1,1,1,255,2,2,255,3,3,3,255
 DB 4,4,255,5,5,5,255,6,6,255,7,7,7,255
 DB 8,8,255,9,9,9,255,10,10,255,11,11,11,255
 DB 12,12,255,13,13,13,255,14,14,255,15,15,15,255
 DB 16,16,255,17,17,17,255,18,18,255,19,19,19,255
 DB 20,20,255,21,21,21,255,22,22,255,23,23,23,255
 DB 24,24,255,25,25,25,255,26,26,255,27,27,27,255
 DB 28,28,255,29,29,29,255,30,30,255,31,31,31,255
 DB 32,32,255,33,33,33,255,34,34,255,35,35,35,255
 DB 36,36,244,37,37,37,255,38,38,255,39,39,39,255
MAINAND
 LUP 20
 DB %01111111,%01111110,%01100001,%00011111
 DB %01111111,%01111000,%00000111
 --^
AUXAND
 LUP 20
 DB %01110000,%00001111,%01111111,%01111100
 DB %01000011,%00111111,%01111111
 --^
MAINBL
 LUP 20
 DB %00000000,%00000000,%00000000,%00000000
 DB %00000000,%00000000,%00000000
 --^
AUXBL
 LUP 20
 DB %00000000,%00000000,%00000000,%00000000
 DB %00000000,%00000000,%00000000
 --^
MAINMG
 LUP 20
 DB %00000000,%00000001,%00010000,%00000000
 DB %00000000,%00000100,%01000000
 --^
AUXMG
 LUP 20
 DB %00001000,%00000000,%00000000,%00000010
 DB %00100000,%00000000,%00000000
 --^
MAINBR
 LUP 20
 DB %00000000,%00000000,%00001000,%00000000
 DB %00000000,%00000010,%00100000
 --^
AUXBR
 LUP 20
 DB %00000100,%01000000,%00000000,%00000001
 DB %00010000,%00000000,%00000000
 --^
MAINOR
 LUP 20
 DB %00000000,%00000001,%00011000,%00000000
 DB %00000000,%00000110,%01100000
 --^
AUXOR
 LUP 20
 DB %00001100,%01000000,%00000000,%00000011
 DB %00110000,%00000000,%00000000
 --^
MAINDG
 LUP 20
 DB %00000000,%00000000,%0000100,%01000000
 DB %00000000,%00000001,%00010000
 --^
AUXDG
 LUP 20
 DB %00000010,%00100000,%00000000,%00000000
 DB %00001000,%00000000,%00000000
 --^
MAING1
 LUP 20
 DB %00000000,%00000001,%00010100,%01000000
 DB %00000000,%00000101,%01010000
 --^
AUXG1
 LUP 20
 DB %00001010,%00100000,%00000000,%00000010
 DB %00101000,%00000000,%00000000
 --^
MAINGR
 LUP 20
 DB %00000000,%00000000,%00001100,%01000000
 DB %00000000,%00000011,%00110000
 --^
AUXGR
 LUP 20
 DB %00000110,%01100000,%00000000,%000000001
 DB %00011000,%00000000,%00000000
 --^
MAINYE
 LUP 20
 DB %00000000,%00000001,%00011100,%01000000
 DB %00000000,%00000111,%01110000
 --^
AUXYE
 LUP 20
 DB %00001110,%01100000,%00000000,%00000011
 DB %00111000,%00000000,%00000000
 --^
MAINDB
 LUP 20
 DB %00000000,%00000000,%00000010,%00100000
 DB %00000000,%00000000,%00001000
 --^
AUXDB
 LUP 20
 DB %00000001,%00010000,%00000000,%00000000
 DB %00000100,%01000000,%00000000
 --^
MAINVI
 LUP 20
 DB %00000000,%00000001,%00010010,%00100000
 DB %00000000,%00000100,%01001000
 --^
AUXVI
 LUP 20
 DB %00001001,%00010000,%00000000,%00000010
 DB %00100100,%01000000,%00000000
 --^
MAING2
 LUP 20
 DB %00000000,%00000000,%00001010,%00100000
 DB %00000000,%00000010,%00101000
 --^
AUXG2
 LUP 20
 DB %00000101,%01010000,%00000000,%00000001
 DB %00010100,%01000000,%00000000
 --^
MAINPI
 LUP 20
 DB %00000000,%00000001,%00011010,%00100000
 DB %00000000,%00000110,%01101000
 --^
AUXPI
 LUP 20
 DB %00001101,%01010000,%00000000,%00000011
 DB %00110100,%01000000,%00000000
 --^
MAINMB
 LUP 20
 DB %00000000,%00000000,%00000110,%01100000
 DB %00000000,%00000001,%00011000
 --^
AUXMB
 LUP 20
 DB %00000011,%00110000,%00000000,%00000000
 DB %00001100,%01000000,%00000000
 --^
MAINLB
 LUP 20
 DB %00000000,%00000001,%00010110,%01100000
 DB %00000000,%00000101,%01011000
 --^
AUXLB
 LUP 20
 DB %00001011,%00110000,%00000000,%00000010
 DB %00101100,%01000000,%00000000
 --^
MAINAQ
 LUP 20
 DB %00000000,%00000000,%00001110,%01100000
 DB %00000000,%00000011,%00111000
 --^
AUXAQ
 LUP 20
 DB %00000111,%01110000,%00000000,%00000001
 DB %00011100,%01000000,%00000000
 --^
MAINWI
 LUP 20
 DB %00000000,%00000001,%00011110,%01100000
 DB %00000000,%00000111,%01111000
 --^
AUXWI
 LUP 20
 DB %00001111,%01110000,%00000000,%00000011
 DB %00111100,%01000000,%00000000
 --^
CLOM DB <MAINBL,<MAINMG,<MAINBR,<MAINOR,<MAINDG
 DB <MAING1,<MAINGR,<MAINYE,<MAINDB,<MAINVI
 DB <MAING2,<MAINPI,<MAINMB,<MAINLB,<MAINAQ,<MAINWI
CHIM DB >MAINBL,>MAINMG,>MAINBR,>MAINOR,>MAINDG,>MAING1
 DB >MAINGR,>MAINYE,>MAINDB,>MAINVI,>MAING2,>MAINPI
 DB >MAINMB,>MAINLB,>MAINAQ,>MAINWI
CLOA DB <AUXBL,<AUXMG,<AUXBR,<AUXOR,<AUXDG
 DB <AUXG1,<AUXGR,<AUXYE,<AUXDB,<AUXVI
 DB <AUXG2,<AUXPI,<AUXMB,<AUXLB,<AUXAQ,<AUXWI
CHIA DB >AUXBL,>AUXMG,>AUXBR,>AUXOR,>AUXDG,>AUXG1
 DB >AUXGR,>AUXYE,>AUXDB,>AUXVI,>AUXG2,>AUXPI
 DB >AUXMB,>AUXLB,>AUXAQ,>AUXWI
HTAB_LO
 LUP 4
 DB $00,$00,$00,$00,$00,$00,$00,$00
 DB $80,$80,$80,$80,$80,$80,$80,$80
 --^
 LUP 4
 DB $28,$28,$28,$28,$28,$28,$28,$28
 DB $A8,$A8,$A8,$A8,$A8,$A8,$A8,$A8
 --^
 LUP 4
 DB $50,$50,$50,$50,$50,$50,$50,$50
 DB $D0,$D0,$D0,$D0,$D0,$D0,$D0,$D0
 --^
HTAB_HI
 LUP 3
 DB $20,$24,$28,$2C,$30,$34,$38,$3C
 DB $20,$24,$28,$2C,$30,$34,$38,$3C
 DB $21,$25,$29,$2D,$31,$35,$39,$3D
 DB $21,$25,$29,$2D,$31,$35,$39,$3D
 DB $22,$26,$2A,$2E,$32,$36,$3A,$3E
 DB $22,$26,$2A,$2E,$32,$36,$3A,$3E
 DB $23,$27,$2B,$2F,$33,$37,$3B,$3F
 DB $23,$27,$2B,$2F,$33,$37,$3B,$3F
 --^
This assembles to about 5K of code - mostly tables. It's worth pointing out that there are many ways to cut this program down in terms of size.  For example since all the colour tables and mask tables repeat in cycles of seven, we could take the value in the X register modulus 7 before doing our lookup.  This would save us a whopping 2K.  It would also increase the number of cycles we spend plotting each pixel significantly.  

There are even a couple of ways to make our code plot faster, mostly using approaches that are less easy to explain.  I may cover some of these speed/size optimizations in a later article but for now this code fits our two primary goals:

  • sufficient speed to be used in an action game
  • sufficiently clear so that the reader can go on and build out their own enhancements.

Next up, we learn about some code management and put this bad boy to work with some demo code...

Thursday, April 13, 2017

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

The pixel of a thousand masks


Last time we came up with a program that can determine which byte(s) we need to alter to draw our pixel. The main drawback with our code was that it would overwrite the whole byte not just the bits we needed to colour a pixel. Today we look at how to remedy that.

Most of you who have used computers are familiar with the three fundamental logical operations: AND, NOT and OR. You may not realize it but these are the key to drawing a pixel non-destructively. First lets look at all the different outputs of the OR function. We call this it's TRUTH TABLE!

INPUT 1INPUT 2OUTPUT
011
101
111
000

Let's also reexamine our first four bytes of the DHR screen with pixels A-G:


Now lets suppose that we want to draw a green pixel in position C. Lets also assume that that byte contains the value 01100001. A green pixel requires a pattern of 0110. If we padded this value with zeros to get it to line up with position C like so 00001100. Then it seems all we need to do is OR these two bytes together. Like so:

   01100001
OR 00001100
   --------
   01101101

But what if that byte contained 01111111? Would the OR work? Nope we would just end up with 01111111. So OR works but it only handles cases where there are already zeros in the bit positions we want to change.  What we need then, is a way to "erase" - that is set to 0 - just the few bits that we are interested in.  The solution to this is to use the AND operation  The truth table for AND is as follows:

INPUT 1INPUT 2OUTPUT
010
100
111
000

As you can see  AND  always returns 0 unless both inputs bits are 1. We can use this to 'clear' a series of bits to 0 by doing the following:

    01111111
AND 01100001
------------
    01100001

Notice that by carefully choosing the value that we are ANDing against so that there are zeros in the bits we want to clear. We guarantee that those bits will be set to zero. We also guarantee, by setting all other bits to 1 that the rest of the byte will be undisturbed. This process is called bit masking.

So to answer to our original question.  We need to use both these operations, first AND the bits we are interested in to 0 then OR our pixel pattern into the space we created.

    01111111
AND 01100001
------------
    01100001
OR  00001100
------------
    01101101

Implementation:


Looking at our first four bytes again...



...we see here that pixel A exists in the last four bits of AUX $2000 and therefore needs a mask of 01110000 to clear out those bits.  Pixel B, on the other hand needs a mask of 00001111 at AUX $2000 AND a mask of 011111110 at MAIN $2000

So what do we do? Why we use another lookup table! Two actually. One for the AUX mask and one for the MAIN mask. We can write it out like this:

PixelAUX maskMAIN mask
A0111000001111111
B0000111101111110
C0111111101100001
D0111110000011111
E0100001101111111
F0011111101111000
G0111111100000111

We can turn these into code for the assembler in this way:
MAINAND DB %01111111,%01111110,%01100001,%00011111
 DB %01111111,%01111000,%00000111
AUXAND DB %01110000,%00001111,%01111111,%01111100
 DB %01000011,%00111111,%01111111
The percent sign tells the assembler that these numbers are in binary. All our code has to do is find the byte we want to draw our pixel on, load that byte into the accumulator and then AND it against the appropriate bitmask value in the table. We can implement this by altering our code into something as follows:  (NOTE:I've only written the part that uses MAIN memory for now):
 LDY MBOFFSET,X
 BMI AUX
 STA PAGE1
 LDA $2000,Y
 AND MAINAND,X
Finally we need to DRAW the bit pattern we want into the "hole" we made using AND. This is accomplished by using an OR. If we assume, for now that the pixel we want to draw is green then the four-bit pattern we want to draw is 0110. If you look again at our diagram of the first four bytes of memory you will see that we need to draw a different pattern depending on which pixel we want to write. Yet, again the solution to this is a table. It looks like this:

PixelAUX patternMAIN pattern
A0000011000000000
B0110000000000000
C0000000000001100
D0000000101000000
E0001100000000000
F0000000000000011
G0000000000110000

Translating that into assembler we get:
MAINGR DB %00000000,%00000000,%00001100,%01000000
 DB %00000000,%00000011,%00110000
AUXGR DB %00000110,%01100000,%00000000,%000000001
 DB %00011000,%00000000,%00000000
I've called these MAINGR and AUXGR to indicate that these values are for ORing a green pixel into screen memory.  The addition of an ORA instruction into our code snippet looks something like this:
 LDY MBOFFSET,X
 BMI AUX
 STA PAGE1
 LDA $2000,Y
 AND MAINAND,X
 ORA MAINGR,X
So here's the whole program:
 ORG $6000 ;Start 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
SCRN_LO EQU $1D
SCRN_HI EQU $1E
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
DPLOT LDY MBOFFSET,X ;Find what byte if any in MAIN we are working in
 BMI AUX ;If pixel has not bits in MAIN memory - go to aux routine
 STA PAGE1 ;Map $2000 to MAIN memory
 LDA $2000,Y ;Load screen data
 AND MAINAND,X ;Erase pixel bits
 ORA MAINGR,X ;Draw green pixel
 STA $2000,Y ;Write back to screen
AUX LDY ABOFFSET,X ;Find what byte if any in MAIN we are working in
 BMI END ;If no part of the pixel is in AUX - end the program
 STA PAGE2 ;Map $2000 to AUX memory
 LDA $2000,Y ;Load screen data
 AND AUXAND,X ;Erase pixel bits
 ORA AUXGR,X ;Draw green pixel
 STA $2000,Y ;Write back to screen
END RTS
MBOFFSET DB 255,0,0,0,255,1,1
ABOFFSET DB 0,0,255,1,1,1,255
MAINGR DB %00000000,%00000000,%00001100,%01000000
 DB %00000000,%00000011,%00110000
AUXGR DB %00000110,%01100000,%00000000,%000000001
 DB %00011000,%00000000,%00000000
MAINAND DB %01111111,%01111110,%01100001,%00011111
 DB %01111111,%01111000,%00000111
AUXAND DB %01110000,%00001111,%01111111,%01111100
 DB %01000011,%00111111,%01111111
That probably seems like a lot of code for just plotting seven pixels.  Which invites the question:  What do we need to change to draw across the whole line?  The answer is easier than you think.  All three of these tables REPEAT in some way after the seventh pixel.    If we assume we're starting at the beginning of a line then the MBOFFSET table the pattern is:

Unused,X,X,X,Unused,X+1,X+1,Unused,X+2,X+2,X+2,Unused,X+3,X+3..

and the ABOFFSET table the pattern is:

X,X,Unused,X+1,X+1,X+1,Unused,X+2,X+2,Unused,X+3,X+3,X+3...

So the full tables for both of those are:
MBOFFSET DB 255,0,0,0,255,1,1,255,2,2,2,255,3,3
 DB 255,4,4,4,255,5,5,255,6,6,6,255,7,7
 DB 255,8,8,8,255,9,9,255,10,10,10,255,11,11
 DB 255,12,12,12,255,13,13,255,14,14,14,255,15,15
 DB 255,16,16,16,255,17,17,255,18,18,18,255,19,19
 DB 255,20,20,20,255,21,21,255,22,22,22,255,23,23
 DB 255,24,24,24,255,25,25,255,26,26,26,255,27,27
 DB 255,28,28,28,255,29,29,255,30,30,30,255,31,31
 DB 255,32,32,32,255,33,33,255,34,34,34,255,35,35
 DB 255,36,36,36,255,37,37,255,38,38,38,255,39,39
ABOFFSET DB 0,0,255,1,1,1,255,2,2,255,3,3,3,255
 DB 4,4,255,5,5,5,255,6,6,255,7,7,7,255
 DB 8,8,255,9,9,9,255,10,10,255,11,11,11,255
 DB 12,12,255,13,13,13,255,14,14,255,15,15,15,255
 DB 16,16,255,17,17,17,255,18,18,255,19,19,19,255
 DB 20,20,255,21,21,21,255,22,22,255,23,23,23,255
 DB 24,24,255,25,25,25,255,26,26,255,27,27,27,255
 DB 28,28,255,29,29,29,255,30,30,255,31,31,31,255
 DB 32,32,255,33,33,33,255,34,34,255,35,35,35,255
 DB 36,36,244,37,37,37,255,38,38,255,39,39,39,255
The data in MAINAND,AUXAND,MAINGR,AUXGR simply repeats without change for all 140 pixel positions. Which means the implementation is easy. We simply type the DB portions of each table twenty times!  Thankfully the Merlin Pro assembler has an instruction called LUP which makes this dull task rather trivial.  Here we've repeated all four tables twenty times each using that instruction:
MAINAND
 LUP 20
 DB %01111111,%01111110,%01100001,%00011111
 DB %01111111,%01111000,%00000111
 --^
AUXAND
 LUP 20
 DB    %01110000,%00001111,%01111111,%01111100
 DB    %01000011,%00111111,%01111111
 --^
MAINGR
 LUP   20
 DB    %00000000,%00000000,%00001100,%01000000
 DB    %00000000,%00000011,%00110000
 --^
AUXGR
 LUP   20
 DB    %00000110,%01100000,%00000000,%000000001
 DB    %00011000,%00000000,%00000000
 --^
LUP, as you can see is followed by a number telling the assembler the number of times you want to repeat a section.  The section you want to repeat is delineated by the LUP opcode and the three characters: --^ which need to be on a line by themselves.

Ok that's all we have for this post!

Tune in next time for the last piece in developing our plotting function:  Changing colour, and handling changes on the Y axis!

Cheers!

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!

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...