In the SEGA Genesis 'Hello World' ROM I created in a previous post, I output some text to the screen. The text was rendered by loading font character data into video memory (VRAM) and then writing character indices into a playfield map. This post explains how this is done in detail. Please refer to my previous post for details on characters and playfields.
Rendering text to the screen illustrates clearly how to use characters and playfield maps to render graphics on the Genesis, and even though the end result may not be particularly exciting, the techniques described here can be used to render any type of graphics. Text is a good place to start, as it can be used for debugging.
Matt Phillips had a nice example of genesis character data, and based on that example, I wrote a full font. Currently, it contains all the upper-case letters and numbers, along with a few useful symbols. The first character in the font is the empty whitespace character, which has the ASCII index
$20, followed by
$21 '!', layed out sequentially all the way up to the underscore character
The character data is included directly in the code. As an example, here is the data for the character
characters: dc.l $01111100 ; Every hex digit is a palette index dc.l $11000110 ; for a pixel. dc.l $00000110 ; Palette index 0 is black, and dc.l $00011100 ; palette index 1 is white. dc.l $00000110 dc.l $11000110 dc.l $01111100 dc.l $00000000
As explained in the previous post, all characters are 8x8 pixels, and each pixel is defined by a 4-bit value, indexing into a 16-color palette.
The bitmap data must be copied to VRAM before it can be used. Character data can be located anywhere in VRAM, and is always addressed relative to VRAM offset
$0000. My earlier post about the Genesis Video Display Processor (VDP) explained VRAM write operations in detail, but in short, I'm using this bit pattern to set up the VRAM write at
Bits [BBAA AAAA AAAA AAAA 0000 0000 BBBB 00AA] B order [10.. .... .... .... .... .... 5432 ....] oper. type A order [..DC BA98 7654 3210 .... .... .... ..FE] address ------------------------------------------------------------- VRAM Write (00 0001) to addr $0000 (0000 0000 0000 0000): [01.. .... .... .... .... .... 0000 ....] oper. type [0100 0000 0000 0000 .... .... 0000 ..00] VRAM address [0100 0000 0000 0000 0000 0000 0000 0000] add zeroes Hex: 4 0 0 0 0 0 0 0
I write this 32-bit command word to the VDP Control Port to set up the write to VRAM:
vdp_control = $C00004 move.l #$40000000,vdp_control
And then write the bitmap data to the VDP Data Port:
vdp_data = $C00000 move.l #$01111100,vdp_data ; the first longword of the character '3'
To be able to write a loop that copies all the character data to VRAM, I set the auto-increment register in the VDP to 4 bytes/write operation (this was covered in the previous post about the VDP):
Putting it all together, here is the full font loading code:
load_font: move.w #$8F04,vdp_control ; VDP autoincr. 4 bytes/write move.l #$40000000,vdp_control ; VDP write to VRAM address 0 lea characters,a0 ; Load addr. of Characters to a0 move #8*62,d0 ; 8 longwords per character, ; 62 characters in the font .Loop: move.l (a0)+,vdp_data ; Move data to VDP data port, ; increment source addr. dbra d0,.Loop rts
Now that our font is loaded, we should be able to write character indices to the playfield map and see a string of letters displayed.
The concept of playfield maps was explained in the previous post. In this section, I'll show how they are defined.
A playfield map is a matrix of entries, each defining which character goes in which grid cell on the screen. Each playfield map entry is a 16-bit word with this pattern:
[PCCV HIII IIII IIII] - P is priority (set to 0 for now) - C selects color palette - V enables vertical flip (0) - H enables horizontal flip (0) - I is an 11-bit character index: [.... .A98 7654 3210]
If we don't use priority, use palette 0, and don't flip, we simply specify a character index as an 11-bit number between 0 and 2047.
After loading the font data into VRAM, and the first character at
$0000 corresponds to ASCII character
$20 ' ', we can take the ASCII value of a character and subtract
$20 to compute the character index. Say we have the register
A0 pointing to a string, and we want to compute the index of the first character, we can do something like this:
clr.w d0 ; clear upper byte of D0 move.b (a0),d0 ; set lower byte to value at string pointer A0 sub.b #$20,d0 ; font starts at $20, subtract $20 from value
D0 has the character index for the first character, ready to write to the playfield map.
I have now explained how to load font data and how playfield maps are defined. In the next section, we use this knowledge to actually show some text on the SEGA Genesis.
The goal I set out for myself was to implement a subroutine for printing text at any screen location:
print_at(text, text_length, x, y)
Where (x, y) denotes a character position on the screen, measured in playfield 8x8 grid cells. The visible part of the playfields are 40x28 characters, so we'll use character positions ranging from (0, 0) to (39, 27).
To print a string to the screen, I went through these steps:
Before I set up the VRAM write, I tell the VDP to auto-increment the VRAM address with 2 bytes - the size of a playfield map entry - for every write. This was covered in detail in a previous post:
vdp_control = $C00004 move.w #$8F02,vdp_control ; Set VDP autoincrement to 2 bytes
With that out of the way, I compute the playfield map offset. Even though the visual part of the playfields is 40 characters wide, the playfields themselves can be larger. In my example, playfields are 64 characters wide, and since each playfield map entry takes up 2 bytes, the offset corresponding to
(x, y) can be computed like this:
; d1: x ; d2: y ; d3: VRAM address ; offset = (d1 + d2 * 64 chars/line) * 2 B/entry ; = d1 * 2 + d2 * 128 B lsl.w #1,d1 ; d1 *= 2 lsl.w #7,d2 ; d2 *= 128 move.w d1,d3 ; d3 = d1+d2 add.w d2,d3
In my example, playfield map A is at VRAM address
$C000, so we add that to
add.w #$C000,d3 ; playfield map base address
With the VRAM address in
D3, I'll generate the VDP command word to write to that address. Until now, we have created VDP command words manually, as they have all had well-defined unchanging values. For the
print_at subroutine to be able to write anywhere on the screen, we need to generate the command word dynamically in code. As shown earlier, the command word for writing to VRAM address
$40000000. We start with this command word in
The VRAM address is split into two parts:
[..DC BA98 7654 3210 .... .... .... ..FE]
I start with computing this part of the address:
[.... .... .... .... .... .... .... ..FE]
clr.l d5 ; move.w d3,d5 ; d5 = offset and.w #%1100000000000000,d5 ; d5 = 2 most significant bits of lsr.w #7,d5 ; offset shifted 14 bits right lsr.w #7,d5 or.l d5,d4 ; d4 |= d5
Next, we compute the other part of the address:
[..DC BA98 7654 3210 .... .... .... ....]
clr.l d5 ; move.w d3,d5 ; d5 = offset and.w #%0011111111111111,d5 ; d5 = 14 least sign. bits of lsl.l #8,d5 ; offset shifted 16 bits left lsl.l #8,d5 ; or.l d5,d4 ; d4 |= d5
D4 contains the VDP command word for writing to the correct VRAM address, and I can send it:
We're almost there!
Given our ASCII text string in
A0 and the number of characters in the text string in
D0, we can write the character indices to the playfield map in VRAM, one at a time. As explained earlier, I subtract
$20 from the ASCII value to compute the character index:
vdp_data = $C00000 sub.w #1,d0 ; dbra branches if not -1, so we need to ; subtract 1 from the number of iterations .loop: clr.w d3 ; clear upper byte of D3 move.b (a0)+,d3 ; set lower byte to value at string pointer (A0) ; and increment A0 to point to next character sub.b #$20,d3 ; font starts with ' ', subtract $20 from value move.w d3,vdp_data ; write to VDP dbra d0,.loop ; and repeat D0 (text_length) times
Now, the implementation of
print_at is complete. Let's see how it is called.
To set up our call to
print_at, we'll assign some registers to the formal parameters:
A0: text D0: text_length D1: x D2: y
Let's say we want to print
"HELLO WORLD" at (14,12). We then set up our string:
text: dc.b "HELLO WORLD" text_end:
With the two labels, we can easily compute
text_length = text_end - text
So, the full call to
print_at looks like this:
; print_at(text, text_length, x, y) ; A0 D0 D1 D2 ; lea text,a0 ; A0 = text move.w #text_end-text,d0 ; D0 = text_length move.w #14,d1 ; D1 = x position move.w #12,d2 ; D2 = y position jsr print_at ; call subroutine
And we're done. Now we can go nuts with printing text all over the screen.