SEGA Genesis: Printing Text
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.
Loading Character Data
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 $5F '_'
The character data is included directly in the code. As an example, here is the data for the character $33 '3'
:
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 $0000
:
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):
move.w #$8F04,vdp_control
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.
Playfield Maps
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
Now, 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.
Printing Strings
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:
- Sending VDP command word to auto-increment VRAM access 2 bytes with every write
- Computing the byte offset in the playfield map from position x and y
- Generating VDP command word with the address of that offset
- Sending VDP command word, setting up a VRAM write to that address
- Writing character indices corresponding to letters in text string
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 D3
:
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 $0000
is $40000000
. We start with this command word in D4
:
move.l #$40000000,d4
The VRAM address is split into two parts:
[..DC BA98 7654 3210 .... .... .... ..FE]
I start with computing this part of the address:
[.... .... .... .... .... .... .... ..FE]
Like this:
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 .... .... .... ....]
Like so:
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
Now, D4
contains the VDP command word for writing to the correct VRAM address, and I can send it:
move.l d4,vdp_control
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.
How to Call our Subroutine
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_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.
References
- emudocs.org: sega2.doc: The original SEGA Genesis hardware documentation
- bigevilcorporation.co.uk: Sega Megadrive – 4: Hello, world!: Matt Phillips' Hello World example