Grayscale is what makes the good graphical games. Think of ZBlitz, Vertigo, Arkanoid, Boulder Dash, Bomberman, Diamonds, and the many other games that use grayscale to enhance their sprites. There are many different forms of grayscale out there. Grayscale has the basic idea of using two or more different video screens and switching them back and forth really fast. Each will stay visible for a different amount of time.
A graphical example might help. Screen 1 is visible a third (1/3) of the time while Screen 2 is visible the remaining two-thirds (2/3) of the time. The resultant shows the shade of gray viewed.
Screen 1 (1/3) | Screen 2 (2/3) | Result |
If you still don't get this and you are really bored, get a black piece of paper and a white piece. Put one in each hand and move them back and forth really fast. You will get a blurred image that will seem sort of gray. That's the general idea of grayscale!
I've pasted Dux Gregis' Blitzgry Routine here. You can download this along with many other grayscale routines that I've come across in the Download Section. Each line in the routine is represented by a number which corresponds to an explanation at the end of the routine. Follow each line and try to picture in your head what it is doing. The easiest way to do this is to print out the Blitzgray Code and then refer to the explanations.
- ↓int_addr equ $8f8f
- ↓
- ↓;===============================
- ↓; Install IM 2 Grayscale Routine
- ↓;===============================
- ↓
- ↓OpenGray:
- ↓ ld hl,$ce00
- ↓ ld de, SixBytes
- ↓ ld bc, 6
- ↓ ldir
- ↓
- ↓ ld hl,$8e00
- ↓ ld de,$8e01
- ↓ ld (hl),$8f
- ↓ ld bc,256
- ↓ ldir
- ↓
- ↓ ld hl,int_copy
- ↓ ld de,int_addr
- ↓ ld bc,int_end-int_start
- ↓ ldir
- ↓
- ↓ call _runindicoff
- ↓
- ↓;================================================
- ↓; Set up parameters to pass the interrupt handler
- ↓; via the alternate register set
- ↓;================================================
- ↓ di
- ↓ exx
- ↓ ld b,$3c
- ↓ ld c,0
- ↓ ld d,5
- ↓ ld e,%110110
- ↓ ld hl,UserCounter
- ↓ exx
- ↓ xor a
- ↓ ld (UserCounter),a
- ↓
- ↓ ld a,$8e
- ↓ ld i,a
- ↓
- ↓
- ↓ im 2
- ↓ ei
- ↓ ret
- ↓
- ↓;==================================
- ↓; Close Grayscale Handler
- ↓;==================================
- ↓
- ↓CloseGray:
- ↓ im 1
- ↓ ld a,$3c
- ↓ out (0),a
- ↓
- ↓ call _clrLCD
- ↓
- ↓ ld hl,_plotSScreen
- ↓ ld de,_plotSScreen+1
- ↓ ld (hl), 0
- ↓ ld bc,1024
- ↓ ldir
- ↓
- ↓ ld hl, SixBytes
- ↓ ld de, $ce00
- ↓ ld bc, 6
- ↓ ldir
- ↓
- ↓ ret
- ↓
- ↓.org $8f8f
- ↓int_copy:
- ↓
- ↓int_start:
- ↓ ex af,af'
- ↓ exx
- ↓
- ↓ in a,(3)
- ↓ bit 1,a
- ↓ jr z,leave_int
- ↓
- ↓ inc (hl)
- ↓ out (c),b
- ↓
- ↓ dec d
- ↓ call z,reset_int_counter
- ↓ ld a,d
- ↓ cp 2
- ↓ call z,change_pages
- ↓
- ↓leave_int:
- ↓ in a,(3)
- ↓ rra
- ↓ ld a,c
- ↓ adc a,9
- ↓ out (3),a
- ↓ ld a,$0b
- ↓ out (3),a
- ↓
- ↓ ex af,af'
- ↓ exx
- ↓ ei
- ↓ reti
- ↓
- ↓reset_int_counter:
- ↓ ld d,5
- ↓change_pages:
- ↓ ld a,e
- ↓ xor b
- ↓
- ↓
- ↓ ld b,a
- ↓ ret
- ↓int_end:
- ↓
- ↓UserCounter: .db 5
- ↓SixBytes: .db 0,0,0,0,0,0
- ↓
- ↓.end
Line(s) | Explanation | Bytes |
↑1 | Our interrupt is going to need to be put at $8f8f. That's where all interrupts are put. This runs on an interrupt so that every 200th of a second it will be run (200 Mhz). That's fast to us; it's hard to imagine how fast the processor is running. That all depends on battery power too. Remember, this routine is only to set up grayscale, it's not to put sprites and stuff on the screen. If you put text, it will be light colored because it's only on one plane (one of the two or more video memories). | none |
↑7 | Now we are going to switch to grayscale mode. We need to prep everything and copy the grayscale interrupt routine to where it needs to be. | none |
↑8-11 | We are going to use the video memory ($fc00 to $ffff) for one plane and the graph screen (_plotScreen to $cdf9 1024 bytes) for the other plane. The graph screen needs the first 6 bytes to keep for system use. We are going to have to save those six bytes and put them back later. Our routine will use the entire area, including the six bytes we saved away. We are going to save them using a block copy to a part of our code. | 11 |
↑13-17 | The interrupt system uses a 256 byte long vector table for it's interrupt routines to know which addresses to call. We need to put $8f8f into all the slots in the vector table. We use another block copy to put $8f repeatedly which makes $8f8f8f8f8f8f8f8f which is the address we want over and over and over again. | 13 |
↑19-22 | Now we want to actually put
our routine at $8f8f to be called every
interrupt (approximately 200 times a second). Remember in
line 1 where we made an alias to
int_addr =$8f8f so we won't have to
type $8f8f anymore. It's easier to remember int_addr being where
the interrupt address is than the $8f8f stuff. Between lines
73 and 115 is our
interrupt to be copied so we just use a block copy.
| 11 |
↑24 | We don't want to see that little busy indicator at the top that TI doesn't do away with, so we call a routine that turns it off. The TI86 automatically turns it back on when the asm program is done. I just wish it would turn it off for us. That would be 3 bytes saved for every program made. | 3 |
↑30 | We are going to use the shadow registers so we need to disable interrupts for a second while we put some data in the shadow registers for use during the grayscale routine. The grayscale routine will exchange out the shadow registers with the regular ones and so it will have the contents of the shadow registers to use which we have prepared. | 1 |
↑31 | Change out bc, de, and hl with their shadow
registers. We won't be needing to change out af with
its shadow register because a changes so much and we don't
need the flags.
| 1 |
↑32 | $3c is used in conjunction with port 0 (which we will learn about later) to tell the TI86 which part of memory is to be used as the video memory. $3c would tell the processor it needs to use $fc00 as the video memory. $0a would mean $ca00 as the start of the video memory. You don't really have to understand this yet. | 2 |
↑33 | We are going to need to save which port (port 0) we are using so we can reference it faster during the routine. | 2 |
↑34 | We are going to use 5 as a counter. | 2 |
↑35 | When you xor %110110 with $3c you get $0a which is the other value we need for port 0. | 2 |
↑36 | This is the counter which we are going to need to time how long different planes are visible. | 3 |
↑37 | Put back all the shadow registers in their place for use during the grayscale routine. | 1 |
↑38-39 | We need to zero the UserCounter. Xor a is a one byte method
of ld a,0 which is two bytes. It's faster and saves a byte. Like
most counting, we start from zero and work our way up.
| 4 |
↑41-42 | Remember that i used with r together tell where on
the vector table the interrupt handler is supposed to be. We need
to put the address of the vector table ($8e) into i to make
up the most significant byte of the address.
| 4 |
↑45-46 | Start up Interrupt Mode 2 on line 45 and then Enable Interrupts on line 46. | 3 |
↑47 | No need to explain. | 1 |
↑53 | Now that we've got the routine to turn on grayscale mode, we need a routine to turn it off. | none |
↑54 | Start up interrupt Mode 1 again. | 2 |
↑55-56 | Remember port 0 is the port for the video memory. We are going to send $3c out it which tells the TI86's screen to show from $fc00 to $ffff as the video memory. | 4 |
↑58 | Clear the screen at $fc00. | 3 |
↑60-64 | Since _clrLCD only clears $fc00 to
$ffff , we need to make our
own routine that will clear the graph memory (our second plane). We
are going to clear the first byte of the graph screen, then copy it
to the second byte. Then we are going to copy the second byte to the
third byte. Now each byte gets cleared by the previous loop of
ldir . This is really handy to know. There are 1024 bytes to
clear. Think about how this works because you can use it in a lot of
things.
| 13 |
↑66-69 | Remember in lines 8 through 11 when we saved those precious first six bytes of the graph screen? Well, we need to put them back now. | 11 |
↑71 | Done with it all! | 1 |
↑73 | This is an assembler directive which I will teach you about later. Don't pay attention to it now. | none |
↑75 | Now we're going to start the actual routine to continuously switch between each of the two video planes. | none |
↑76-77 | This is sometimes called while another code is being run in normal mode. We break for a micro-second and run this. Because of this we need to save the registers that the other code was using so it will have them when it is resumed. We need to save them before we do our routine and return them like they were after we're done switching video planes. Remember that we saved some data in the shadow registers before we started, we're going to exchange the registers so that the regular registers will be filled with that data. When we're done we'll put that data back in and return our parameters to the shadow registers. | 2 |
↑79-81 | Bit 1 of port 3 is to tell us if the video screen is in the middle of refreshing itself or not. If bit 1 is reset, the screen is refreshing and we shouldn't flip panels in the middle of a refresh. These refreshes take mini-micro-seconds. | 6 |
↑83 | Remember that hl was loaded with the address of
the UserCounter which we had saved in our
parameters. We want to increment that because it's our
counter.
| 1 |
↑84 | We saved c with 0, so we can save a byte by not
putting in out (0),a . That one byte difference makes
the routine faster. Our sending $3c or $0a through port
0 will change part of memory to be shown on the screen, which
changes between $fc00 and $ca00.
| 2 |
↑86-87 | To give 4 shades of gray (white, light gray, dark gray,
black), we have to hold one of the planes
up for a little longer than the other. Remember that we loaded
d with 5. We use that as a counter to let one show 3
times when the other only shows 1. If the counter falls to
zero after we decrease it, we call the routine to reset the counter
and change planes.
| 4 |
↑88-90 | We now need to check to see if we need to flip planes. We
started at d being 5 and decreased it until it was 2. While
we are decreasing it, one of the planes is on the screen. When it
hits 2, we change to the other plane for the remaining two
decreases. That's 3 decreases on one plane and 2 on the other.
| 6 |
↑92 | We are going to have to leave now. We need to put the remains of the parameters back into the shadow registers and clean up to return from the interrupt. | none |
↑93-94 | We need to check the status of the [ON] button. We get a byte from port 3 and rotate (We'll discuss that later) it once to the right. The bit rotated out is put in the carry. That means that the carry is either set or reset. | 3 |
↑95-97 | Remember that c was zero because we were saving that
for referencing port 0 when changing video pages. We need
to zero a , so we can either xor a , sub a ,
or, ld a,c (because c is zero at this time). Each of
those are one byte long and so are faster than
the two byte ld a,0 . We use adc to
add 9 to whatever is in
a and then add the carry flag (either of value 1 or 0)
to a on top of the 9 we are already loading. A
will either be 9 or 10 ($09 or $0a; %00001001 or %00001010). That
will turn the screen back on. We refreshed it when it was off I
think. We now turn it on.
| 5 |
↑98-99 | Now we need to send %00001011 through port 3 to do something. I don't know this junk. | 4 |
↑101-102 | We need to put back the parameters in the shadow registers and return the regular registers to what they were before we started our grayscale interrupt routine. | 2 |
↑103-104 | We need to enable interrupts (line 104)
and return from the
interrupts (reti ). This not only returns from our routine but
from the whole interrupt thing. Instead of just the regular
return, this also checks the state of the interrupts for the
system.
| 3 |
↑107-108 | This is part of a call that is just added onto the front of the routine that changes pages. This part resets the counter and then leads right into the page changing routine. | 2 |
↑109-114 | Remember that e has a value that when xor ed with
our byte to be sent to change the video planes, will yield
what we need to use next time for switching video planes. We
load our mask into a and xor it with
b and put the result back into b. Then we're
done with this call.
| 4 |
↑116 | This is an easy way for us to determine in line 21 how much we need to copy and paste. We let the assembler figure out the length of the interrupt code by subtracting the beginning address from the end. This gives us the difference in the addresses which is the length of the code we need. | none |
↑118-119 | This is where we store the data to use in our routine. This
isn't copied cause we don't need to have it with the routine. It
can be stored with the rest of this
blitzgry.h code in your
program.
| 7 |
↑121 | We're done, and we need to tell the assembler that.
.end does just that.
| none |