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!

Wednesday, April 12, 2017

Apple II - Double Hi-Res From The Ground Up
Part 3: More bank switching

To start from the beginning of this series click here:

So can we use DHR page 2 or what?


While I didn't touch on it in my earlier post, there is more than one way to access AUX memory, and therefore the DHR pages.  We have already talked about using the 80STOREON softswitch $C001 to change the function of the PAGE1/PAGE2 softswitches so they map $2000-$3FFF (hi-res page 1) between MAIN and AUX memory banks respectively.   The effect of which you can see here:


One limitation you might notice is that, for whatever reason (remember that DHR was an afterthought) these switches ONLY work for hi-res page 1. In other words even with 80STOREON and PAGE2 selected, hi-res page 2 ($4000-$5FFF) still maps to main memory.

So what do we do? Well first we will use another softtswitch 80STOREOFF $C000 this returns the PAGE1/PAGE2 function to normal. Once we do that we can use a different set of softswitches: RAMWRTON  ($C005) and RAMWRTOFF ($C004) to allow us to write to AUX memory.

These switches act in a fundamentally different way from what 80STOREON gives us. When $C005 is invoked all memory WRITE operations (e.g. STA $2222) go to AUX memory and when $C004 is invoked all memory WRITE operations go to main memory. READ operations (including fetching the next instruction in your program) always go to MAIN memory.


In this configuration we can use the PAGE1/PAGE2 softswitches to display the DHR pages 1 and 2 respectively.

To demonstrate this I've provided a program below.  It stores alternating black (bit pattern 0000) and  then white (bit pattern 1111) pixels in the first position of the first eight lines of  DHR PAGE1. Which starts at AUX memory location $2000. From there we store the opposite pattern, white and then black on DHR PAGE2. Which is at AUX memory location $4000. Finally we enter into a loop which just flips endlessly between PAGE1 and PAGE2.

First we have the assembly code:
 STA $C050 ;Turn on GRAPHICS
 STA $C057 ;Turn on Hi-res
 STA $C052 ;Turn on Full screen
 STA $C05E ;Turn on DHR
 STA $C00D ;Turn on 80 Columns
 STA $C000 ;Turn OFF 80STORE
 STA $C005 ;Turn ON RAMWRT
 LDA #$00 ;Load bitpattern 000000
 STA $2000 ;Store on DHR PAGE 1, line 1
 STA $4400 ;Store on DHR PAGE 2, line 2
 STA $2800 ;Store on DHR PAGE 1, line 3
 STA $4C00 ;Store on DHR PAGE 2, line 4
 STA $3000 ;Store on DHR PAGE 1, line 5
 STA $5400 ;Store on DHR PAGE 2, line 6
 STA $3800 ;Store on DHR PAGE 1, line 7
 STA $5C00 ;Store on DHR PAGE 2, line 8
 LDA #$0F ;Load bitpattern 001111
 STA $4000 ;Store on DHR PAGE 2, line 1
 STA $2400 ;Store on DHR PAGE 1, line 2
 STA $4800 ;Store on DHR PAGE 2, line 3
 STA $2C00 ;Store on DHR PAGE 1, line 4
 STA $5000 ;Store on DHR PAGE 2, line 5
 STA $3400 ;Store on DHR PAGE 1, line 6
 STA $5800 ;Store on DHR PAGE 2, line 7
 STA $3C00 ;Store on DHR PAGE 1, line 8
LOOP STA $C055 ;Show PAGE 2
 STA $C054 ;Show PAGE 1
 JMP LOOP
I'll assume, for now that everyone understands the insane VERTICAL structure of all the Apple II hi-res screens. If you don't then just take it for granted that each line of pixels in the first eight lines of either DHR page are $400 bytes apart. Anyone who wants a tutorial on this just mention it the comments section!

Now here's the program in binary form.  You can paste it into your favorite emulator.

call -151 6000: 8d 50 c0 8d 57 c0 8d 52 c0 8d 5e c0 8d 0d c0 8d 00 c0 8d 05 c0 a9 00 8d 00 20 8d 00 44 8d 00 28 8d 00 4c 8d 00 30 8d 00 54 8d 00 38 8d 00 5c a9 0f 8d 00 40 8d 00 24 8d 00 48 8d 00 2c 8d 00 50 8d 00 34 8d 00 58 8d 00 3c 8d 55 c0 8d 54 c0 4c 46 60 6000g

So why would we ever use 80STOREON?


Virtually every graphic mode on every computer involves trade-offs. 80STOREON is no different. When 80STOREON is active we can read and write from DHR PAGE1. When 80STOREOFF is invoked RAMWRT only lets us WRITE to DHR PAGE1 and PAGE2. If we wanted to read the screen we would be out of luck. This would be problematic if, for example you were drawing a spaceship on a complicated background.  In this case it would be common to copy the small part of the background you are going to draw the ship on top of to some area in memory.  That way, later when you need to erase the spaceship.  You can simply copy that small portion of background back.  However using RAMWRT alone, you couldn't do this.


So what happens if we want to read from both DHR pages?


My linear algebra professor had this understated way of telling the class that he didn't want to explain something because it would require too much time or effort.  He would simply say: "It's non-trivial".  It didn't take long for us to understand that meant: "Here be Dragons!"
Fact
Early on, the DHR softswitches must have been rather poorly documented because on page 89 of the manual for Beagle Graphics, a popular set of DHR graphical utilities it actually says that DHR Page 2 "doesn't really exist"

Reading from both DHR pages is...non-trivial.  It requires the use of two more softswitches RAMRDOFF ($C002)  and RAMRDON ($C003).   Using them though, is tricky. Why? Remember when I said that even when RAMWRT is on our program is still being read from MAIN memory? Well as soon as you invoke $C003 - Immediately the 6502 program counter starts reading the next program instruction from AUX memory. If you don't have a program sitting in AUX memory at that location the computer will probably just crash ( more correctly it will execute random bytes as instructions until it encounters a 00 which is a BRK instruction)

To use this feature successfully we need to employ RAMWRT to write a program into the memory location the program counter will be at the instant we invoke RAMRDON. We also need a program sitting in MAIN memory exactly where we expect our program to be when we invoke RAMRDOFF to bring us back into MAIN memory again.

If that sounds like a pain in the ass? You'd be right.

Going forward we will be using both approaches. For doing single pixel plots we will be using 80STOREON as we will need to read screen memory in order to add pixels to the screen without disturbing other bytes.  As we start looking at bitmap graphics we will be using 80STOREOFF. Since, at first we will be doing things like character generation. Which can be done by blindly writing to screen memory and later when we start writing arcade games we will do operations like collision detection without reading screen memory at all...

Apple II - Double Hi-Res From The Ground Up
Part 2: How DHR works

Check out part one here!

Bank switched memory:


One limitation that 8 bit machines hit early on was memory. If you haven't noticed by now all of the addresses we've referenced here use two bytes $2000,$4000,etc... This is because the 6502 internally stores addresses using no more than two bytes. Two bytes is sixteen bits which means 216 possible memory locations. In other words 65536 bytes or 64 Kilobytes.

To get around this, many systems used a technique called bank switching. This involves having circuitry which allows you to take some part of the memory in the computer and switch it with another piece of memory of the same size. Effectively you can take what's stored at say memory location $3000-$3FFF - flip a switch - and now those same memory locations point to a completely different block of memory. Flip the switch again and you've got your original memory block back.

With the release of the Apple //e and the Extended 80 Column Card users now owned machines with 128K of memory primarily existing as two bank switched blocks of 64K. The memory that the Apple II uses when turned on is called the MAIN bank and the other 64K is referred to as the auxiliary bank or simply AUX memory.

Double hi-res works by having the Apple //e video circuitry read the memory in both the MAIN and AUX banks at the same time. This extra memory doubles the number of pixels available in monochrome hi-res and doubles the number of bits per pixel on a colour screen.  There's no documentation that I can find on who decided to make this change but evidence suggests it was an afterthought and possibly done by someone who worked on the Apple /// or understood it's video circuitry. As this function replicates the Apple ///'s graphic modes almost exactly.  In any case once this was realized, Apple made this change to their production line resulting in the Revision B motherboard.

Under DHR, pixels are read first from AUX memory and then from MAIN memory. So the screen layout for the first eight lines of DHR PAGE1 looks something like this



Let's take a closer look at just the first four bytes. Again we will use letters to represent pixels.



As you can see, we have seven pixels spread across four bytes of memory BUT using only two addresses $2000 (8192) and $2001 (8193). Some of these pixels reside in a single byte and others are split between two bytes - one in MAIN and the other in AUX.

Perhaps you've noticed that the palette bit is still there. Does that mean we can use it to get even more colours? Nope! When DHR is active the palette bit simply gets ignored (in the vast majority of cases).

So how do we activate this magical mode? You guessed it! Softswitches! You need FIVE - in no particular order. Here they are:

$C050 (49232) - Graphics $C057 (49239) - Hires $C052 (49234) - Fullscreen $C05E (49246) - DHR $C00D (49165) - 80 Column mode

POKE something into each one of those locations and you will be on the DHR screen. At this point  you usually see a kind of stripe pattern. This is due to the fact that the main hi-res page is, on power up filled with garbage and the AUX page gets cleared.

Drawing on the DHR screen


Ok let's put together everything we've learned and get some pixels up on the DHR screen. If you recall our diagram...



...and keeping in mind that currently we don't yet know how to access the AUX memory. The first memory location we have access to is 8192 in MAIN memory. As you can see, it has one bit from pixel B, all four from pixel C and two from pixel D. So the only pixel we can fully draw by changing this byte alone is C! In order to know what to put in this byte we need to decide what colour pixel we are going to draw. Here is full list of DHR colours and their associated bit patterns:



Lets say we want to colour pixel C yellow. The bit pattern for yellow is 1110 and putting that into the spot where pixel C is in our diagram we get PDD1110B.  For now lets assume that all the other pixels around this are black so we can set all the other bits in this byte to zero and the result is 00011100 or 28! There's probably a lot of junk on the screen so let's write a simple program to flicker the pixel between yellow and black which as you can see from the chart above the bit pattern is 0000.
10 POKE 8192,0 20 POKE 8192,28 30 GOTO 10

Accessing AUX memory


The last thing we need in order to have complete control of every pixel on the DHR screen is to figure out how to write to AUX memory. To help with that the Apple II provides a special softswitch called 80STOREON $C001 (49153). When invoked it changes the behavior of the PAGE1 and PAGE2 softswitches. Instead of switching which page is displayed. The PAGE2 softswitch now swaps the MAIN hi-res page 1 ($2000-$3FFF) with the hi-res page in in AUX memory and the PAGE1 softswitch swaps them back.

Looking back at our diagram again to the first byte of PAGE1 in aux memory. We see: PBBBAAAA and we recall the bit pattern for yellow is 1110. To set pixel A to yellow we again insert it into our diagram PBBB1110 and zero the other bits 00001110 which gives us 14. So starting from the beginning our program for writing looks like this:
10 POKE 49232,100: REM TURN ON GRAPHICS 20 POKE 49239,100: REM TURN ON HIRES 30 POKE 49234,100: REM TURN ON FULLSCREEN 40 POKE 49246,100: REM TURN ON DHR 50 POKE 49165,100: REM TURN ON 80COLUMNS 60 POKE 49153,100:POKE 49237,100:POKE 8192,14: REM TURN ON 80STORE, TURN ON PAGE2, WRITE YELLOW PIXEL RUN
As this is probably the last bit of BASIC code in this series. Here is that same program in assembly code:
 STA $C050 ;TURN ON GRAPHICS
 STA $C057 ;TURN ON HIRES
 STA FULLSCREEN ;TURN ON FULLSCREEN
 STA $C05E ;TURN ON DHR
 STA $C00D ;TURN ON 80 COLUMNS
 STA $C001 ;TURN ON 80STORE
 STA $C055 ;TURN ON PAGE2
 LDA #$0E  ;LOAD 14
 STA $2000 ;Put in first location on HIRES PAGE 1 in AUX RAM.
...and here's a binary output that you can paste into any Apple II emulator.

CALL -151 6000:8d 50 c0 8d 57 c0 8d 52 c0 8d 5e c0 8d 0d c0 8d 01 c0 8d 55 c0 a9 0e 8d 00 20 60 6000g

Now you have everything you need to activate the DHR screen and turn on any DHR pixel in any DHR colour. Next up we'll talk more about bank switched memory and how to access DHR PAGE2 and after that we will begin a two part post on writing a fast machine language program to plot a pixel of any colour anywhere on the DHR screen.

Monday, April 10, 2017

Apple II - Double Hi-Res From The Ground Up
Part 1: Hi-res Fundementals

Introduction:


In this series I'm going to explain how to code for the most difficult, colourful, and probably most poorly understood graphics mode on the Apple II: Double Hi-Res.  Make no mistake, the programs we will be developing are not toys.  They will be fast, flexible and useful for creating many types of games.  In addition they, along with the article text will give you a clear understanding of how they work.  Allowing you to develop programs of your own.  Since our goals include high performing code we will only have a few short examples in BASIC and by my next post we will have transitioned entirely to 6502 assembly.  

So what is it?:


Double hi-res is a special graphics mode available only on an Apple //e with an extended 80 column card and revision B motherboard (this includes the Apple //e card for the Macintosh), an Apple //c, or an Apple IIgs. It provides a resolution of 560 x 192 pixels on a monochrome display and 140 x 192 pixels in 16 colours on an RGB, composite monitor or TV set. To put this into perspective I've taken some emulator screenshots from two reference systems: The Tandy 1000 and the Macintosh which were both introduced around the same time the Apple //e was.


The image on the left is from an original Macintosh running System 5.  The right-hand image is a Macintosh screenshot, converted it to DHR and then displayed on the Apple II.  As you can see this Apple II graphics mode is perfectly capable of producing a Macintosh style UI.  The main thing it's lacking is vertical resolution. The black bar at the bottom shows how much the Apple II is missing (and the white bar on the left shows how much more horizontal resolution the Apple II has over the Mac!)

On a colour display, DHR provides 140 x 192 pixels in 16 colours.  Which suffers somewhat in comparison to the Tandy 1000 graphics whose 16 colour mode topped out at 300 x 200 pixels.  Below is a screenshot of the original Kings Quest, one of the first games to take advantage of both the Tandy video hardware and the Apple //e's DHR mode.


Fact
While the Tandy 1000 did have a 320x200 mode. The AGI, the engine behind Kings Quest only understood one resolution 160x200. This was due to it being designed by Sierra under contract for IBM to promote the PCjr. A machine who's minimal 16 colour mode was 160x200.
The clearly superior image on the left is the Tandy 1000 but, as you can see the Apple //e is pretty close.

It's worth noting that the Tandy 1000 and the Apple Macintosh were released in 1984, a full year after the Apple //e and given that DHR is actually a rather simple extension to the original Apple II hi-res graphics system it probably could have been added to the Apple II line much earlier. In fact, an identical mode was in the ill-fated Apple /// which was released in 1980!

Because of how deeply DHR is tied to many of the compromises Steve Wozniak made in his design of the Apple II. We're going to start our look into DHR by first getting an understanding of the original Apple II hi-res screen.

Hi-res Overview:


On all Apple II machines, with sufficient memory there are two hi-res screens they are called PAGE1 and PAGE2 respectively.  These exist as two 8KB areas of memory starting at $2000 (8192) for PAGE1 and $4000 (16384) for PAGE2. You can tell the hardware to display either page and since these are simply big blocks of memory you can update their contents simply by writing to them.

A common method of making smoother animation was to write to PAGE1 while telling the Apple to display PAGE2  Once your program is finished drawing it tells the Apple to display PAGE1 and begins drawing to PAGE2.

One other feature I'll mention is the ability to choose between having  four lines of text at the bottom of the hi-res screen - which we call MIXED mode - or the whole screen as hi-res graphics - which we call FULLSCREEN mode.

You can see all of these elements in action by doing the following.  From BASIC type the following:

HGR2 HGR POKE 8192,63 POKE 16384,21

You should now see a white bar at the top left corner of the screen.

How it works:


  • HGR2 - Clears hi-res PAGE2 and sets it up as the visible page in FULLSCREEN mode.  At which point you will no longer be able to see what you are typing.
  • HGR  - Clears PAGE1, sets it up as the visible page and sets the mode to be MIXED. You still might not see your cursor because these four lines of text are just a window into the bottom four lines of the text screen.  Hitting return a few times will bring the cursor down far enough for you to see it,  
  • POKE 8192,63 -  Puts the value 63 into memory location 8192.  Which if you remember is the beginning of the first hi-res page.  So you should have seen the white bar appear as soon as you hit the return key.
  • POKE 16384,21 - Puts 21 into location 16384 which is the top left corner of PAGE2.

But we can't see that page right now because the "HGR" command set the visible page to PAGE1!

So how do we see PAGE2 without erasing it?  All these modes and pages are controlled by a series of "softswitches'.  These are memory locations that don't contain real memory, instead they let you talk to the video hardware. $C054 (49236) tells the hardware to display PAGE1 and $C055 (49237) tells the hardware to display PAGE2.

So if we type:
POKE 49237,100

We will turn on PAGE2.  You will notice that the text at the bottom of the screen looks crazy.  This is because the PAGE2 softswitch also turns on text page 2 as well as hires page 2.  Text page 2 is generally never used, even by the built-in BASIC firmware so all you see is whatever garbage was in that portion of memory.  You should also notice that there's a magenta coloured bar at the top left corner of the screen (or if you're using a monochrome monitor it will appear as a dotted line).

By the way, you don't need to put the exact value 100 into 49237 to activate PAGE2 any number will do.  You don't even need to store a value there.  You can just read that memory location by doing:

PRINT PEEK(49237)

Anything that "touches" that location flips the switch.  If you want to get back to PAGE1 just use:

POKE 49236,100


Apple II's crazy colour system:


The colour we see on the screen is, unsurprisingly related to the value we poked into memory.  To get a better understanding let's look at the binary representation of each number:

63 = 00111111
21 = 00010101

The relationship between bits and pixels on the screen is just a little odd.  On a  monochrome monitor the Apple II hi-res page appears to be made of pixels that are 1 bit wide.  So poking 21 into 8192 would produce a dotted line with three white pixels.  Each 1 in the binary representation of 27 turning on a single white pixel.   On an TV or composite monitor the Apple II video appears to be made of pixels 2 bits wide.  So that same set of bits looks like a solid magenta line.  It's worth pointing out that there are no colour/monochrome "modes" here.  It just depends what screen you're plugged into.

Each line on the display maps to a row of 40 bytes in memory.  That is from $2000 (8192)  - $2039 (8231) is the first line on PAGE1 and from $4000 (16384) - $4039 (16423) is the first line of PAGE2

Given that we would think that the Apple hi-res screen should be 8 bits * 40 bytes = 320 monochrome pixels OR 160 colour pixels.  Right?  Well not exactly....

You'll notice that he first bit in every byte (which we call the HIGH BIT) of our example POKE's is zero.  That's because the first bit doesn't represent a pixel it actually selects between one of two colour sets - which we call PALLETS.  You can see this by adding 128 (the value of the leftmost bit) to the value 21.  21+128=149

POKE 16384,149

By changing the value from 00010101 to 10010101. The line will turn from magenta to blue. So if we have two bits per pixel and two different pallets we must have eight different colours to choose from.  Right? Well not really...

Here are the colours when the high bit is zero:


Here are the colours when the high bit is one.


So there are TWO whites and TWO blacks leaving us with only six different hires colours.

Wonder what happens on a monochrome monitor?  Turning the high bit on shifts everything half a pixel to the right.  You can try this like so:
5 HGR 10 POKE 8192,21 20 POKE 8192,149 30 GOTO 10 RUN
On a colour screen you would see a three pixel wide line shift between magenta and blue and on a monochrome screen you would see a dotted line do a little half-pixel dance from left to right.

Another counter-intuitive thing about the hires display is the order of mapping bits to pixels.  Poking 21 into 8192 turns the first, second and third pixels magenta.  If you remember the binary representation of this is 00010101.  If you check the chart above you will see that magenta is represented by the bit pattern 01.  You should be able to see that the first, second and third pixels on the screen occupy the LOWEST six bits in that byte. We also know that the highest bit selects the pallet.  We can illustrate this by replacing 01 with MM to represent a magenta pixel and P to represent the pallet bit like so:


Notice something?  There's an extra zero right after the pallet bit? So what's this leftover bit do?  Well actually that's HALF of the fourth pixel's information. Which should make you wonder where the other half is. The answer of course is In the next byte!  If I draw another picture but this time use AA to represent the first pixel, BB to represent the second pixel, and so on... the first two bytes of hi-res page one would look like this:


Which means we have 7 pixels across 16 bits.  If you recall there are 40 bytes in a single line on the Apple II hires screen.  This means the horizontal resolution is 40*3.5 = 140 coloured pixels in six colours or 280 monochrome pixels.

Ok that's the short short version of the Apple II hires system.  Next post I'll get into the basics of bank switched memory and how to activate the DHR screen...

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