Grayscale

Grayscale, the effect of intermediate shades of black and white, is achieved by flipping two or more pages of video memory very quickly to the screen so that (in most cases) the flipping is not noticeable and appears as gray. If a pixel on one video memory (let's call it plane0) is set and the same pixel on another video memory (plane1) is reset, then when the two pixels are flipped against each other, the screen pixel will appear gray. In this way, corresponding pixels set on both bitplanes will turn the screen pixel black.

The auto interrupt 1 is used to flip the pages of video memory used for grayscale; the flipping of pages will then be done in the background of (parallel to) your code once the interrupt handler is installed. To display something in grayscale then, you can simply write something to both planes of memory you have defined as video memories. Assuming your grayscale handler flips very quickly between planes which start at the addresses contained at (plane0) and (plane1), then to make the top left pixel of the LCD gray (bit 7 of the first byte of both video memories) you would do something like this:


 move.l (plane0),a0

 set.b #7,(a0)                   ;set the bit in one plane

 move.l (plane1),a0

 bclr.b #7,(a0)                  ;clear the bit in the other plane

So now you will have that pixel gray; if your grayscale handler uses 3 shades of gray (white, gray, black) that pixel will be a solid gray, but if your handler uses 4 shades of gray (white, light gray, dark gray, black) that pixel will be either light or dark gray.

The simplest grayscale handler will just switch the current video memory each time it is run (every time an auto int 1 is triggered) so that the resulting effect will be 3 shades of gray. In this kind of handler it doesn't matter which plane you write to for gray: if the same bit of both planes is reset the pixel will be white; if the same bit of both planes is set the pixel will be black; if the same bit in one plane is set and in the other reset that pixel will be gray. If rather than switching planes every interrupt we instead leave one of the planes in twice as long as the other, then there's suddenly a fourth shade of gray that appears! Yes, if you set a bit on the plane left in twice as long (and reset that bit on the other) you will then have a dark gray pixel. Similarly, if you set a bit only on the plane left in for only one interrupt the resulting pixel color will be light gray. For this reason, you must know what plane(s) you should write to to display what level of gray.

Following this logic, we can achieve more shades of gray by swapping between more video memories--flipping between three planes in our handler we can have up to 7 shades; flipping between four planes we can have up to 16 shades. Anymore than that would have more flicker than is fruitful. Using this grayscale would be the same as for three or four shades (two planes): write to the corresponding bits in each plane in some combination--depending on the combination you will have some particular shade--to control some level of gray in that pixel.

Implementation

So now we need to understand how a grayscale interrupt handler is implemented. You need two routines: a routine to install the interrupt handler (called to turn grayscale on) and another to remove it (called to turn grayscale off), but you may also want a routine to clear a particular video memory. Let's look at the handler itself is coded first (this is run when the interrupt is triggered, not when you install or uninstall). There are a number of ways to swap planes, the most common, though, is to cycle through a table of values, the next value in the table put out port ($600010) every time the interrupt is called. The fastest way, however, would be to use a toggle bit that can be XORed with one value to get the other. Here's an example of a 4 level grayscale handler using the table method:

int1_handler:



 movem.l d0/a0,-(a7)              ;push affected registers

 move.w phase(pc),d0              ;keep a counter with the interrupt phase

 lea table(pc),a0	

 move.w 0(a0,d0.w),($600010)      ;get the next value in the table

 add.w #2,d0                      ;point to the next value in table

 cmp.w #2*3,d0                    ;unless we've reached the table's end

 bne \no_wrap                     ;in which case we wrap back

 clr.w d0

\no_wrap:

 move.w d0,phase

 movem.l (a7)+,d0/a0              ;get back original registers

 rte                              ;leave the interrupt



phase:                            ;incremented each interrupt

 dc.w 0

table:                            ;six bytes for a table (3 word long values)

 ds.w 3

A simple bit of code; very nice.

So we now move on to look at the code that will install this interrupt handler. First an amount of memory the size of a bitplane will be needed for the second video memory (the first will be just $4c00) found using HeapAlloc; next a pointer to int1_handler will be put in the vector for auto int 1 and the interrupts enabled.


grayOn:



 move.l #$F00*1+8,-(a7)            ;allocate a heap of bitplane size + 8

 rom_call HeapAlloc                ;absolute compatability ;-)

 lea 4(a7),a7

 tst.w d0

 beq grayFail                      ;do something in case insufficient mem

 move.w d0,handle                  ;save the handle so we can uninstall it

 DEREF d0,a5

 move.l a5,d0

 and.b #%11111100,d0               ;mod 3

 add.l #8,d0

 move.w d0,plane0                  ;save the address of the light gray plane

 lsr.l #3,d0

 move.w d0,table                   ;store port readable value to table



 move.l #LCD_MEM,d0

 move.l d0,plane1                  ;$4c00 -> dark gray plane

 lsr.l #3,d0

 move.w d0,table+2                 ;this plane will get shown twice as long

 move.w d0,table+4



 move.l ($64),(oldInt1)            ;save the OS int 1 handler



 move.w #$0700,d0

 trap #1                           ;disable interrupts



 bclr.b #2,($600001)               ;allow writing to below $128

 move.l #int1,($64)                ;install new handler

 bset.b #2,($600001)               ;re-enable the lock



 trap #1                           ;enable interrupts

 rts

Because port ($600010) requires a value of the video memory address shifted three bits right rather than the actual address, we must allocate an additional 8 bytes in our handle and find the next address divisible by 8 to be our second video memory. The values in the table contain this shifted right value whereas (plane0) and (plane1) are the addresses of each pane. Here's the uninstall routine:


grayOff:



 move.w #$0700,d0

 trap #1                           ;disable interrupts

 bclr.b #2,$600001                 ;unlock access below $128

 move.l oldInt1(pc),($64)          ;restore old interrupt handler

 bset.b #2,$600001                 ;relock it

 move.w #0,d0

 trap #1                           ;enable interrupts





 move.w #LCD_MEM>>3,($600010)      ;make sure $4c00 is video mem



 move.w handle(pc),-(a7)

 rom_call HeapFree                 ;restore the allocated memory

 lea 2(a7),a7



 rts

That's fairly straightforward.

If you want to use the library routine for grayscale (if you are doing other things with auto int 1 and / or other interrupts, it's probably much more practical to use your own) you ought to take a look at the code in order to figure out what the names of the bitplanes are and which one displays what level of gray. Other than that you're zooming.

This chapter on grayscale now comes to a close ... happy coding ;-)


© 1998 - 1999 ACZ