SEGA Genesis: Printing Text

Phantasy Star IV, Sega (1993)

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

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':

  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:

    move.w #$8F04,vdp_control     ; Set VDP autoincrement to 4 bytes/write
    move.l #$40000000,vdp_control ; Set up VDP to write to VRAM address $0000
    lea    characters,a0          ; Load address of Characters into a0
    move   #8*62,d0               ; 8 longwords per character,
                                  ; 62 characters in the font
    move.l (a0)+,vdp_data ; Move data to VDP data port, increment source addr
    dbra d0,.Loop

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:

- 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

Starflight, Binary Systems (1991)

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 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 offset...
    lsr.w   #7,d5                   ;      ...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 offset...
    lsl.l   #8,d5                   ;      ...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
    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

Hello World

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:

    dc.b "HELLO WORLD"

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.

Test ROM menu with loads of text