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!

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