A lot of people in comp.sys.apple2.programmer and other places on the internet have wondered how to "print" text onto the Apple's High Resolution Graphics (HGR) screen. Here's a tutorial on "6502 Font Blitting."
**Note**: We will prefix hex numbers with `$` (or C's notation of `0x`). We will prefix binary numbers with `%`.
Fire up your favorite Apple emulator (*cough* AppleWin) or real hardware.
* [AppleWin](https://github.com/AppleWin/AppleWin) press `F2` (to reboot), `Ctrl-F2` to Ctrl-Reset, and then press `F9` until you get a Monochrome screen.
When you are at the Applesoft `]` prompt type (or paste) in the following:
(If you use AppleWin, select the lines, copy, switch back to the emulator, and press Shift-Insert to paste)
HGR
CALL-151
2000:4
2400:A
2800:11
2C00:11
3000:1F
3400:11
3800:11
Voila!
You should see an uppercase A appear in the top left of the HGR screen.
Magic? :-)
Nah, just Computer Science. :-)
The first question you probalby have is "How did I know what bytes to use?" We'll get to that in a second.
## Quirks of the Apple HGR screen
There are couple of things we need to discuss first. The preceeding example showed that the Apple's HGR screen behaves a little "funky." The Apple's, shall we say, esoteric use of hardware, is one of the reasons us fans love (or hate) it.
### Non-Linear Memory
First, we should notice that video memory is non-linear. :-( You'll want to get familiar with the HGR address for the various Y scanlines:
Don't worry if the address pattern makes no sense right now -- we'll reveal that later.
### No FONT data in ROM
Second, each glyph in the Apple font is in a 7x8 cell -- the leading line on the bottom is usually blank but we'll store that too so that we have a true "underline" and bottom descender on 'j', 'y', etc.
Unfortunately, the data for the TEXT ROM 25123 hardware chip is *not* accessible from the 6502. :-/ This means you will need to manually enter in the 8 bytes/character. :-( The good news is that I've already done this so you can copy / paste. :-)
You can find a picture of the Apple ][ ROM text font on Page 8-9, diagram 8.4 of "Understanding the Apple ]\["
We're actually going to use the Apple //e ROM text font since it has lower case and the famous "Mouse Text" glyphs.
### HGR bytes are reversed
Third, the video scanner for HGR mode scans bits in reverse. :-/ This means that we need to "flip" the bits in a byte if we want it to appear properly. Not hard, just inconvenient. We'll store the pre-flipped bits so we don't have to do this at run-time. :-)
For example, If we want these 4 scan-lines of `\`:
X___
_X__
__X_
___X_
You would _normally_ encode the pixels in binary as:
Fourth, we mentioned above that when we entered in $80 that the Apple didn't display any pixels for this byte. This is because the Apple uses the high-bit as a flag to shift that group of 7 pixels over HALF a pixel. (Yes, half a pixel.) This means the monochrome *effective* resolution is a pseudo 560x192. We can't individually access every 560 pixels, only part of them so it is not a "true" 560 resolution. :-( What this means in practice is that we can use this half-pixel shift / byte to get very smooth slopes for Y, etc. :-)
**Note**: The emulators `Virtual ][` and `Apple2js` are *broken* emulators. They do **not** emulate the half-pixel shift of real hardware at all. This is another reason we won't worry about it for now.
We're going to ignore the half-pixel shift since it is easy to touch up the font data later if we wish:
## Font Data
Alrighty then, let's get the font data!
Here is a picture of the Apple //e character set:
* [Apple //e character set](Apple2eFont7x8.png?raw=true)
If we wanted only uppercase ASCII we could get away with 64 glyphs:
64 glyphs * 8 bytes/glyph = 512 bytes.
Since the font data chews up memory anyways we'll "splurge" and use the full 128 ASCII glyphs:
Ouch 1K of our precious 64K! Now we know why all this data was in ROM.
### Raw Font Data
I've saved you the trouble of converting all the pixels to hex. You may want to mute your sound since the Apple will beep at the semi-colon "comments".
Enter in:
6000:10 08 36 7F 3F 3F 7E 36 ; ^@
6008:10 08 36 41 21 21 4A 36 ; ^A
6010:00 00 02 06 0E 1E 36 42 ; ^B
6018:7F 22 14 08 08 14 2A 7F ; ^C
6020:00 40 20 11 0A 04 04 00 ; ^D
6028:7F 3F 5F 6C 75 7B 7B 7F ; ^E
6030:70 60 7E 31 79 30 3F 02 ; ^F
6038:00 18 07 00 07 0C 08 70 ; ^G
6040:08 04 02 7F 02 04 08 00 ; ^H
6048:00 00 00 00 00 00 00 2A ; ^I
6050:08 08 08 08 49 2A 1C 08 ; ^J
6058:08 1C 2A 49 08 08 08 08 ; ^K
6060:7F 00 00 00 00 00 00 00 ; ^L
6068:40 40 40 44 46 7F 06 04 ; ^M
6070:3F 3F 3F 3F 3F 3F 3F 3F ; ^N
6078:13 18 1C 7E 1C 18 10 6F ; ^O
6080:64 0C 1C 3F 1C 0C 04 7B ; ^P
6088:40 48 08 7F 3E 1C 48 40 ; ^Q
6090:40 48 1C 3E 7E 08 48 40 ; ^R
6098:00 00 00 7F 00 00 00 00 ; ^S
60A0:01 01 01 01 01 01 01 7F ; ^T
60A8:08 10 20 7F 20 10 08 00 ; ^U
60B0:2A 55 2A 55 2A 55 2A 55 ; ^V
60B8:55 2A 55 2A 55 2A 55 2A ; ^W
60C0:00 3E 41 01 01 01 7F 00 ; ^X
60C8:00 00 3F 40 40 40 7F 00 ; ^Y
60D0:40 40 40 40 40 40 40 40 ; ^Z
60D8:08 1C 3E 7F 3E 1C 08 00 ; ^[
60E0:7F 00 00 00 00 00 00 7F ; ^\
60E8:14 14 77 00 77 14 14 00 ; ^]
60F0:7F 40 40 4C 4C 40 40 7F ; ^^
60F8:01 01 01 01 01 01 01 01 ; ^_
6100:00 00 00 00 00 00 00 00 ;
6108:08 08 08 08 08 00 08 00 ; !
6110:14 14 14 00 00 00 00 00 ; "
6118:14 14 3E 14 3E 14 14 00 ; #
6120:08 3C 0A 1C 28 1E 08 00 ; $
6128:06 26 10 08 04 32 30 00 ; %
6130:04 0A 0A 04 2A 12 2C 00 ; &
6138:08 08 08 00 00 00 00 00 ; '
6140:08 04 02 02 02 04 08 00 ; (
6148:08 10 20 20 20 10 08 00 ; )
6150:08 2A 1C 08 1C 2A 08 00 ; *
6158:00 08 08 3E 08 08 00 00 ; +
6160:00 00 00 00 08 08 04 00 ; ,
6168:00 00 00 3E 00 00 00 00 ; -
6170:00 00 00 00 00 00 08 00 ; .
6178:00 20 10 08 04 02 00 00 ; /
6180:1C 22 32 2A 26 22 1C 00 ; 0
6188:08 0C 08 08 08 08 1C 00 ; 1
6190:1C 22 20 18 04 02 3E 00 ; 2
6198:3E 20 10 18 20 22 1C 00 ; 3
61A0:10 18 14 12 3E 10 10 00 ; 4
61A8:3E 02 1E 20 20 22 1C 00 ; 5
61B0:38 04 02 1E 22 22 1C 00 ; 6
61B8:3E 20 10 08 04 04 04 00 ; 7
61C0:1C 22 22 1C 22 22 1C 00 ; 8
61C8:1C 22 22 3C 20 10 0E 00 ; 9
61D0:00 00 08 00 08 00 00 00 ; :
61D8:00 00 08 00 08 08 04 00 ; ;
61E0:10 08 04 02 04 08 10 00 ; <
61E8:00 00 3E 00 3E 00 00 00 ; =
61F0:04 08 10 20 10 08 04 00 ; >
61F8:1C 22 10 08 08 00 08 00 ; ?
6200:1C 22 2A 3A 1A 02 3C 00 ; @
6208:08 14 22 22 3E 22 22 00 ; A
6210:1E 22 22 1E 22 22 1E 00 ; B
6218:1C 22 02 02 02 22 1C 00 ; C
6220:1E 22 22 22 22 22 1E 00 ; D
6228:3E 02 02 1E 02 02 3E 00 ; E
6230:3E 02 02 1E 02 02 02 00 ; F
6238:3C 02 02 02 32 22 3C 00 ; G
6240:22 22 22 3E 22 22 22 00 ; H
6248:1C 08 08 08 08 08 1C 00 ; I
6250:20 20 20 20 20 22 1C 00 ; J
6258:22 12 0A 06 0A 12 22 00 ; K
6260:02 02 02 02 02 02 3E 00 ; L
6268:22 36 2A 2A 22 22 22 00 ; M
6270:22 22 26 2A 32 22 22 00 ; N
6278:1C 22 22 22 22 22 1C 00 ; O
6280:1E 22 22 1E 02 02 02 00 ; P
6288:1C 22 22 22 2A 12 2C 00 ; Q
6290:1E 22 22 1E 0A 12 22 00 ; R
6298:1C 22 02 1C 20 22 1C 00 ; S
62A0:3E 08 08 08 08 08 08 00 ; T
62A8:22 22 22 22 22 22 1C 00 ; U
62B0:22 22 22 22 22 14 08 00 ; V
62B8:22 22 22 2A 2A 36 22 00 ; W
62C0:22 22 14 08 14 22 22 00 ; X
62C8:22 22 14 08 08 08 08 00 ; Y
62D0:3E 20 10 08 04 02 3E 00 ; Z
62D8:3E 06 06 06 06 06 3E 00 ; [
62E0:00 02 04 08 10 20 00 00 ; \
62E8:3E 30 30 30 30 30 3E 00 ; ]
62F0:00 00 08 14 22 00 00 00 ; ^
62F8:00 00 00 00 00 00 00 7F ; _
6300:04 08 10 00 00 00 00 00 ; `
6308:00 00 1C 20 3C 22 3C 00 ; a
6310:02 02 1E 22 22 22 1E 00 ; b
6318:00 00 3C 02 02 02 3C 00 ; c
6320:20 20 3C 22 22 22 3C 00 ; d
6328:00 00 1C 22 3E 02 3C 00 ; e
6330:18 24 04 1E 04 04 04 00 ; f
6338:00 00 1C 22 22 3C 20 1C ; g
6340:02 02 1E 22 22 22 22 00 ; h
6348:08 00 0C 08 08 08 1C 00 ; i
6350:10 00 18 10 10 10 12 0C ; j
6358:02 02 22 12 0E 12 22 00 ; k
6360:0C 08 08 08 08 08 1C 00 ; l
6368:00 00 36 2A 2A 2A 22 00 ; m
6370:00 00 1E 22 22 22 22 00 ; n
6378:00 00 1C 22 22 22 1C 00 ; o
6380:00 00 1E 22 22 1E 02 02 ; p
6388:00 00 3C 22 22 3C 20 20 ; q
6390:00 00 3A 06 02 02 02 00 ; r
6398:00 00 3C 02 1C 20 1E 00 ; s
63A0:04 04 1E 04 04 24 18 00 ; t
63A8:00 00 22 22 22 32 2C 00 ; u
63B0:00 00 22 22 22 14 08 00 ; v
63B8:00 00 22 22 2A 2A 36 00 ; w
63C0:00 00 22 14 08 14 22 00 ; x
63C8:00 00 22 22 22 3C 20 1C ; y
63D0:00 00 3E 10 08 04 3E 00 ; z
63D8:38 0C 0C 06 0C 0C 38 00 ; {
63E0:08 08 08 08 08 08 08 08 ; |
63E8:0E 18 18 30 18 18 0E 00 ; }
63F0:2C 1A 00 00 00 00 00 00 ; ~
63F8:00 2A 14 2A 14 2A 00 00 ;
### Image to Font Data (Javascript)
If you were wondering how this data was generated, you see the great thing about computers is that they can automate all the tedious and boring crap, er, calculations for us. Here's a HTML + Javascript program I wrote to convert the [image to HEX](image_2_hex.html):
OK, so now that we have the font data, how do we draw a character "on screen" ?
Remember we need to transfer 8 consecutive bytes (1 byte / scanline) to 8 different scanlines.
Assuming we want to draw the `A` glyph at the top-left of the screen we would need to transfer bytes from the (source) font glyph memory locations to the (destination) screen memory locations:
($6208) -> $2000
($6209) -> $2400
($620A) -> $2800
($620B) -> $2C00
($620C) -> $3000
($620D) -> $3400
($620E) -> $3800
($620F) -> $3C00
For simplicity, we're going to "quantize" our destination Y so that we render font glyphs only on the start of every 8 rows and every 7 pixel columns. If we then had the starting address we simply could move to the next scan line by successively adding $0400 to our destination screen pointer.
How did I know $0400? One quirk of the HGR screen is that every 8 successive scan lines start this many bytes away.
## DrawChar() version 1
Before we can start a simple `DrawChar(char c)` function, we also first need to assign some zero page memory locations for our static and temporary variables:
$E5 Low byte (16-bit address) Pointer to screen destination
$E6 High byte (16-bit Address) Pointer to screen destination
$F5 Low byte (16-bit address) Working pointer to screen byte
$F6 High byte (16-bit address) Working pointer to screen byte
Here's the disassembly of our (hard-coded) DrawChar() program:
We're almost ready to run this! We just need to initialize one variable -- where to draw the glyph at:
E5:00 20
300G
And with any luck you should see the at sign `@` in the top-left.
## X Cursor Position
If we wanted to draw in columns 1 and 2 instead of column 0 then we need to set the Y register which controls which "column" we'll draw at.
Enter in:
306:1
300G
306:2
300G
This works because we are using the 6502 Indirect Zero-Page Y addressing mode to store the destination pixels with the `STA` instruction. Since the Y-register must _always_ be used in this addressing mode we get a column offset "for free." :-)
Since the Y-register controls the column we can inline this function and have the caller take care of setting the Y-Register before calling DrawChar().
LDY #column
After DrawChar() it is handy if we can advance both:
* the column of the cursor
* the pointer to the screen where the next glyph will be drawn
The glyph to draw is currently hard-coded to $40 (`@`). The pointer to the start of this glyph is located at:
source = $6000 + ($40*8) = $6000 + $200 = $6200
If we wanted to draw a different glyph, say `D` we would need to modify the source pointer of the font glyph data.
Recall that our font has this memory layout:
|Char|Index|Address|
|---:|----:|-------|
| ^@ | $00 | $6000 |
| ^A | $01 | $6008 |
| ^B | $02 | $6010 |
| ^C | $03 | $6018 |
| | : | : |
|Spc | $20 | $6100 |
| ! | $21 | $6108 |
| | : | : |
| 0 | $30 | $6180 |
| 1 | $31 | $6188 |
| 2 | $32 | $6190 |
| 3 | $33 | $6198 |
| | : | : |
| ? | $3F | $61F8 |
| @ | $40 | $6200 |
| A | $41 | $6208 |
| B | $42 | $6210 |
| C | $43 | $6218 |
| D | $44 | $6220 |
| : | : | : |
| _ | $5F | $62F8 |
The 6502 stores and loads 16-bit addresses in little-endian format so for glyph `D`, we need to store the bytes of the address `$6220` in reverse order.
Enter in:
355:20 62
And to draw the new glyph, enter in:
300G
We should see the last character of the 3 `@@@` change to `D`.
## DrawChar() version 3
Let's remove the hard-coded printing of the glyph and use the character data we really want to draw. This means we need to "fix-up" the temporary source pointer to the font glyph data. Since we have 8 bytes/glyph we need to manually calculate the array offset.
Since we are dealing with a 16-bit address offset it is simpler to break this down into a low-byte and high-byte calculation for the 6502 since it can't natively do 16-bit offsets. Every 32 characters we need to offset 256 bytes.
However we can save one instruction (and 2 cycles) if we optimize `c/32` to use the counter-intutive 6502's `ROL` instruction -- which only requires 4 instructions instead:
Let's use IncCursorCol() to automatically advance the cusor. We'll also add a space after the character but before the hex value to improve readability of the output.
Right now the line we "print" to is hard-coded since we are using a screen address of $2000 with the pointer at $E5, $E6.
We're going to digress slightly before we fix this.
The secret to getting high speed graphics rendering on the Apple is to use a look-up table. We're going to have a 16-bit address lookup table for Y=0, Y=8, Y=16, .. Y = 184
The HGR screen address is broken up a triad. Every 64 scan lines the offset change by $28.
| Y|Address|Hi |Lo |
|---:|------|---|---|
| 0| $2000 |$20|$00|
| 8| $2080 |$20|$80|
| 16| $2100 |$21|$00|
| 24| $2180 |$21|$80|
| 32| $2200 |$22|$00|
| 40| $2280 |$22|$80|
| 48| $2300 |$23|$00|
| 56| $2380 |$23|$80|
| - | ----- | - | - |
| 64| $2028 |$20|$28|
| 72| $20A8 |$20|$A8|
| 80| $2128 |$21|$28|
| 88| $21A8 |$21|$A8|
| 96| $2228 |$22|$28|
|104| $22A8 |$22|$A8|
|112| $2328 |$23|$28|
|120| $23A8 |$23|$A8|
| - | ----- | - | - |
|128| $2050 |$20|$50|
|136| $20D0 |$20|$D0|
|144| $2150 |$21|$50|
|152| $21D0 |$21|$D0|
|160| $2250 |$22|$50|
|168| $22D0 |$22|$D0|
|176| $2350 |$23|$50|
|184| $23D0 |$23|$D0|
We'll split the table of addresses into Low and High bytes for easier access. We'll also subtract off the hard-coded graphics page 1 high byte = $20 and instead use relative offsets to make it work with either graphics page 1 or 2.
This is our mini HGR Y Address look-up table. "Funny" that it has 24 entries -- the same height as our text screen. :-)
Enter these bytes:
; HgrLo EQU $6400
6400:00 80 00 80 00 80 00 80
6408:28 A8 28 A8 28 A8 28 A8
6410:50 D0 50 D0 50 D0 50 D0
; HgrHi EQU $6418
6418:00 00 01 01 02 02 03 03
6420:00 00 01 01 02 02 03 03
6428:00 00 01 01 02 02 03 03
To select which row to draw at we'll pass that in the X register to our DrawCharColRow() routine:
Unfortunately, our usage of the X and Y registers are not intuitive. This is due to the limited addressing modes of the 6502. :-/ If the 6502 had a symmetrical indirect zero-page X addressing mode:
LDA ($ZP),X
We could map the X-register to the natural column (x-axis), and the Y-register to the natural row (y-axis). Alas, we're stuck with the X=row and Y=col unless we wanted to add extra code to "swap" the two.