The Hello World example was pretty simplistic, with only the necessary font glyphs created and all of the tile IDs hard coded to write the phrase. It can be taken a few steps further without too much work, allowing us to write arbitrary strings at any tile coordinates, in a variety of colours.
First, we need a complete font. I’ll abandon my embarrassing programmer art and instead convert a nice, tidy, opensource font to the pattern format, and keep it in a separate file to be loaded and dumped at any time using some Load/Unload routines – like how I’d expect to work with other art assets in the future. This also means I’ll need to deal with organising some locations in VDP memory to store arbitrary pieces of art, since up until now I’ve been uploading patterns to VRAM address 0x000, and this won’t work when dealing with more than one asset.
Secondly, I’ll write a text display subroutine, which accepts the font, string, X and Y coordinates and colour palette as parameters, which will be used to build the tile descriptor words before sending them to the VDP.
I’ll need a nice font. For now I’ll be converting the font into a bitmap format and using a tool to convert each glyph into an assembly snippet, but if I need to do this sort of thing often I might put my C++ skills into gear and write a tool to do it automatically. I like tools. I won’t use every known character – after all, we’ve only got 64kb of graphics memory to play with, and it’s unlikely I’ll be making use of characters other than alphanumerical, full-stop and comma, and a few others. If I happen to need any more, I can always go back and add them at a later date.
The font needs to be perfectly legible at a size of 7×7 (8×8 tiles, but leaving a one-line space). It wasn’t easy to find one that matches the specification that’s also free to use, but low and behold I found an absolute beauty – a 7×7 pixel font under the Creative Commons license (link to the font and license in references section):
It needs a bit of tidying up first – I won’t make use of the smaller alpha characters, nor will I need all of those special characters, and a few of them don’t look like they’re 7×7 pixels in size either, but it’s a great start. Here’s my trimmed and corrected version – I’ve removed unneccesary characters, resized the brackets and created a new forward-slash from scratch, and aligned each character to an 8×8 grid (taking care that the bottom and right line of each cell remained blank, except for the comma):
Next, it needs converting to pattern data. For this, I used a tool called BMP2Tile, which dumps out tile data in assembly. To use this, I exported the font as a BMP file, opened it up in BMP2Tile, pressed the * key to select the entire image, then File -> Save Tiles -> In ASM. It dumps out a file containing each tile in ASM format, but it needed a few corrections making. I removed the size metadata (I’ll be writing my own) and replaced all 0’s with 1’s, and all F’s with 0’s so that the background is transparent, and the text will use colour 1. I could also go one step further and fill in the font face with a different colour, I might backtrack and do this at a later date, but for now I don’t want to waste any more palette entries on just a font.
As mentioned earlier, I’ll need to solve the problem of fitting more than one asset into the VDP at a time – I can’t just write artwork to VRAM address 0x000, there needs to be some organisation of what will fit where. To do this, and be able to refer to the correct tile IDs when setting up plane tiles, we need to know the address of the font, the size of the font in tiles, and the index of the first tile. Instead of sitting there counting it all, we can make use of the assembler’s preprocessor:
PixelFont: ; Font start address dc.l $01111100 dc.l $11000110 dc.l $10111010 dc.l $10000010 dc.l $10111010 dc.l $10101010 dc.l $11101110 dc.l $00000000 ; Rest of font data... PixelFontEnd ; Font end address PixelFontSizeB: equ (PixelFontEnd-PixelFont) ; Font size in bytes PixelFontSizeW: equ (PixelFontSizeB/2) ; Font size in words PixelFontSizeL: equ (PixelFontSizeB/4) ; Font size in longs PixelFontSizeT: equ (PixelFontSizeB/32) ; Font size in tiles PixelFontVRAM: equ 0x0100 ; Dest address in VRAM PixelFontTileID: equ (PixelFontVRAM/32) ; ID of first tile
Now we have some defines for all of the font’s sizes and addresses in various units, and they’ll be correct wherever we include the font file in code. I’ve chosen the arbitrary VDP address 0x0100 to upload the font to, simply as a demonstration (and to make sure the addressing works correctly when I implement the code), but I’m sure when I start making use of more artwork I’ll need to sit and plan the VDP’s memory layout properly.
This shouldn’t be too difficult, I did it in the last article, but this time we need to specify arbitrary fonts of any size, from any location, to any destination. This means we need to pass some parameters to a subroutine. There’s a few ways of achieving this – move the parameters to registers, or push data to the stack. Moving params to registers is the quickest (in terms of clock cycles) method, but we only have a limited amount of registers, and when the game code starts to get complex it would be difficult to juggle all of the registers around. The latter method allows us to specify a large amount of parameters, but since the subroutine would still need to make use of some registers internally we’d need some way of backup up and restoring them when entering and exiting the subroutine. For simplicity’s sake, I’ll go with the former method – moving parameters to registers – and if it starts to cause issues at a later date I’ll backtrack and change it.
Here’s what I came up with:
LoadFont: ; a0 - Font address (l) ; d0 - VRAM address ; d1 - Num chars swap d0 ; Shift VRAM addr to upper word add.l #vdp_write_tiles, d0 ; VRAM write cmd + VRAM destination address move.l d0, vdp_control ; Send address to VDP cmd port subq.b #0x1, d1 ; Num chars - 1 @CharCopy: move.w #0x07, d2 ; 8 longwords in tile @LongCopy: move.l (a0)+, vdp_data ; Copy one line of tile to VDP data port dbra d2, @LongCopy dbra d1, @CharCopy rts
I’ve also defined the VDP control and data ports, as well as the VDP tile write command + address, since they’re likely to be used often. Using the subroutine should be pretty simple:
; Load font lea PixelFont, a0 ; Move font address to a0 move.l #PixelFontVRAM, d0 ; Move VRAM dest address to d0 move.l #PixelFontSizeT, d1 ; Move number of characters (font size in tiles) to d1 jsr LoadFont ; Jump to subroutine
As long as a palette has been uploaded too, we can use the Regen debugger to view the contents of VRAM and confirm that everything is in its right place:
Mapping ASCII characters
My aim is to be able to write arbitrary strings, defined in the ROM somewhere. The assembler encodes text characters as ASCII, which means I’ll need some method of converting each ASCII character to the font’s tile IDs. In my first attempt at this, I was only using alpha characters, and since character A in ASCII is 65 I could get away with just adding 65 to each byte in the string. Now that I’ve introduced numerical and special characters, I’ll need to come up with something else. I intend to ensure that every font I make sticks to the same characters and layout, so the simplest method would be to create a table which maps ASCII codes to tile IDs of the font. It certainly won’t be the fastest method, it means using a lookup table when drawing every character, but it’ll do for now. Perhaps a better method would be to encode the string itself to match the font tile IDs, but that would complicate development. If I need to do some optimisation, I’ll look into it.
ASCIIStart: equ 0x20 ; First ASCII code in table ASCIIMap: dc.b 0x00 ; SPACE (ASCII code 0x20) dc.b 0x28 ; ! Exclamation mark dc.b 0x2B ; " Double quotes dc.b 0x2E ; # Hash dc.b 0x00 ; UNUSED dc.b 0x00 ; UNUSED dc.b 0x00 ; UNUSED dc.b 0x2C ; ' Single quote dc.b 0x29 ; ( Open parenthesis dc.b 0x2A ; ) Close parenthesis dc.b 0x00 ; UNUSED dc.b 0x2F ; + Plus dc.b 0x26 ; , Comma dc.b 0x30 ; - Minus dc.b 0x25 ; . Full stop dc.b 0x31 ; / Slash or divide dc.b 0x1B ; 0 Zero dc.b 0x1C ; 1 One dc.b 0x1D ; 2 Two dc.b 0x1E ; 3 Three dc.b 0x1F ; 4 Four dc.b 0x20 ; 5 Five dc.b 0x21 ; 6 Six dc.b 0x22 ; 7 Seven dc.b 0x23 ; 8 Eight dc.b 0x24 ; 9 Nine dc.b 0x2D ; : Colon dc.b 0x00 ; UNUSED dc.b 0x00 ; UNUSED dc.b 0x00 ; UNUSED dc.b 0x00 ; UNUSED dc.b 0x27 ; ? Question mark dc.b 0x00 ; UNUSED dc.b 0x01 ; A dc.b 0x02 ; B dc.b 0x03 ; C dc.b 0x04 ; D dc.b 0x05 ; E dc.b 0x06 ; F dc.b 0x07 ; G dc.b 0x08 ; H dc.b 0x09 ; I dc.b 0x0A ; J dc.b 0x0B ; K dc.b 0x0C ; L dc.b 0x0D ; M dc.b 0x0E ; N dc.b 0x0F ; O dc.b 0x10 ; P dc.b 0x11 ; Q dc.b 0x12 ; R dc.b 0x13 ; S dc.b 0x14 ; T dc.b 0x15 ; U dc.b 0x16 ; V dc.b 0x17 ; W dc.b 0x18 ; X dc.b 0x19 ; Y dc.b 0x1A ; Z (ASCII code 0x5A)
There we go, ASCII characters from 0x20 to 0x5A, mapped to font tile IDs. When looking them up, I’ll need to add 0x20 to the ASCII code, so I’ve also defined this for readability.
The methods used to get the text on screen should be very similar to the previous article – set up the Plane A tile IDs. We already have the ID of the first tile in VRAM (PixelFontTileID), so we just need to offset that by the tiles in the ASCII map. For the time being, I’ll be looking up the table whilst it is still in ROM, but I have doubts about the speed of reading data from cartridge so in future I may move the table into a location in main RAM to make the lookups faster (unless, of course, I discover that there’s no major difference). The same may go for the string data itself.
The first step is to calculate the destination address in VRAM. Since I plan to support specifying the X and Y coordinates in tiles, the address needs to be offset by 64 for each horizintal line (in H40 mode), plus 1 for each vertical tile:
DrawTextPlaneA: ; a0 (l) - String address ; d0 - First tile ID of font ; d1 (bb)- XY coord (in tiles) ; d2 (b) - Palette clr.l d3 ; Clear d3 ready to work with move.b d1, d3 ; Move Y coord (lower byte of d1) to d3 mulu.w #0x0040, d3 ; Multiply Y by line width (H40 mode - 64 lines horizontally) to get Y offset ror.l #0x8, d1 ; Shift X coord from upper to lower byte of d1 add.b d1, d3 ; Add X coord to offset mulu.w #0x2, d3 ; Convert to words swap d3 ; Shift address offset to upper word add.l #vdp_write_plane_a, d3 ; Add PlaneA write cmd + address move.l d3, vdp_control ; Send to VDP control port
It’s the most complex thing I’ve written yet, but hopefully the comments should explain it well enough. There’s a new opcode here – ror (roll right) – which shifts bits to the right by a specified offset (up to 8). Here, ror.l #0x08, d1 is used to shift the X coord from the upper to the lower byte of a word in d1, since the swap opcode can only operate on a longword, swapping two words around. The least significant bit gets brought back round to the most significant, who’s place is determined by the operation size (so a byte-sized ror operation with offset of 1 on 0001 would give us 1000). There’s also a corresponding rol (roll left) opcode, which isn’t demonstrated here. The offset is converted to words (since the tile descriptors are 1 word in size) and added to the ‘write to plane A’ VDP command + address, which I’ve defined for ease of use.
Next, we need to set up the word-sized tile descriptor, which contains the palette ID, the pattern ID, and flip bits (not used here). The palette ID fits into two bits, and belongs in bits 14 and 15 of the tile descriptor word, so we’ll start with that. I can use the ror opcode again for this, but since it can only move bits a maximum of 8 places at a time, it’ll need doing twice in order to shift the ID up 13 bits:
clr.l d3 ; Clear d3 ready to work with again move.b d2, d3 ; Move palette ID (lower byte of d2) to d3 rol.l #0x8, d3 ; Shift palette ID to bits 14 and 15 of d3 rol.l #0x5, d3 ; Can only rol bits up to 8 places in one instruction
Now we need to loop round each byte in the string, adding the pattern ID of the text glyph to d2, before sending the complete tile descriptor word to the VDP. Our exit case for the loop will be a string terminator 0x0 (so we also need to make sure our strings actually end in 0x0), and along the way we need to convert the ASCII byte to a pattern ID using the ASCII table:
lea ASCIIMap, a1 ; Load address of ASCII map into a1 @CharCopy: move.b (a0)+, d2 ; Move ASCII byte to lower byte of d2 cmp.b #0x0, d2 ; Test if byte is zero (string terminator) beq.b @End ; If byte was zero, branch to end sub.b #ASCIIStart, d2 ; Subtract first ASCII code to get table entry index move.b (a1,d2.w), d3 ; Move tile ID from table (index in lower word of d2) to lower byte of d3 add.w d0, d3 ; Offset tile ID by first tile ID in font move.w d3, vdp_data ; Move palette and pattern IDs to VDP data port jmp @CharCopy ; Next character @End: rts
Hopefully it should be self-explanatory, with the exception of that move.b (a1,d2.w), d3 line. The parenthesis mean to offset the source address of the move command – so we’re moving the byte at address a1 + d2 to d3. This is how array access is done in 68k assembler. I haven’t yet tested, but I’m assuming the same can be done for the destination addresses, so offsets into the array can be written to as well.
The subroutine relies on the string being zero-terminated, else it will continue to loop until it finds one and just displays garbage. For each string, I’ll need to remember to append the zero manually, unlike in languages like C where strings inside double-quotes are automatically one byte longer than the string was defined, to hold the terminator.
String1: dc.b "ABCDEFGHIJKLM",0
Since the font includes the ” character, if we were to use it in a string constant we will need the equivalent of an ‘escape character’ in C, and that is to prefix the ” with another “. This seems to be unique to the ASM68K assembler, the C escape characters are used in other assemblers.
Here’s the finished result, showing off a few different strings, colour palettes and X/Y coordinates:
; Load font lea PixelFont, a0 ; Move font address to a0 move.l #PixelFontVRAM, d0 ; Move VRAM dest address to d0 move.l #PixelFontSizeT, d1 ; Move number of characters (font size in tiles) to d1 jsr LoadFont ; Jump to subroutine ; Draw text lea String1, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0501, d1 ; XY (5, 1) move.l #0x0, d2 ; Palette 0 jsr DrawTextPlaneA ; Call draw text subroutine lea String2, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0502, d1 ; XY (5, 2) move.l #0x1, d2 ; Palette 1 jsr DrawTextPlaneA ; Call draw text subroutine lea String3, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0503, d1 ; XY (5, 3) move.l #0x2, d2 ; Palette 2 jsr DrawTextPlaneA ; Call draw text subroutine lea String4, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0504, d1 ; XY (5, 4) move.l #0x3, d2 ; Palette 3 jsr DrawTextPlaneA ; Call draw text subroutine lea String5, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0106, d1 ; XY (1, 6) move.l #0x3, d2 ; Palette 3 jsr DrawTextPlaneA ; Call draw text subroutine lea String6, a0 ; String address move.l #PixelFontTileID, d0 ; First tile id move.w #0x0107, d1 ; XY (1, 7) move.l #0x3, d2 ; Palette 3 jsr DrawTextPlaneA ; Call draw text subroutine ; Text strings (zero terminated) String1: dc.b "ABCDEFGHIJKLM",0 String2: dc.b "NOPQRSTUVWXYZ",0 String3: dc.b "0123456789",0 String4: dc.b ",.?!()""':#+-/",0 String5: dc.b "THE QUICK BROWN FOX JUMPS",0 String6: dc.b "OVER THE LAZY DOG",0 ; Include art assets include 'fonts\pixelfont.asm'
There’s plenty of improvements which can be made in future – there’s only support for uppercase letters (although the ASCII table could map any lowercase characters to the uppercase pattern IDs just for completeness), there’s no text wrapping at the end of a line (although perhaps some higher-level UI code could handle that). It would also be quite easy to be able to specify a font’s colour in the LoadFont subroutine, which would just replace any 1’s in the patterns as it copies.
It’s also unlikely that the code will be the fastest and most optimal method to do this sort of thing, but I’m still learning.
This source contains some corrections and improvements to init.asm posted previously: