Saturday, 12 September 2026

GameBoy Pixmap Demo (Part 5)

This is the 5th in the series on Framebuffer-based graphics on the Nintendo GameBoy. As before I'll start with a recap. Then we'll continue with more development, displaying characters while the PPU is active.

Recap

The incredibly successful 8-bit Nintendo GameBoy used tile-based graphics for performance reasons and memory constraints. The 160x144 pixel screen is divided into 20x18 (=360) TileMap locations and each one can point to one of 256, 8x8 2-bpp Tiles. However, a raster trick enables another 128 tiles to be selected part-way through each refresh and I want to use that feature to provide a full frame buffer.

In previous posts I described the tile system, then the CPU, then the basic mechanism for generating a full frame buffer and finally displaying characters in all ink/paper combinations.

Dynamic Displays

gbdev.io provides a lot of extensive documentation. The PPU, which renders a screen, blocks access to the TileMap and tiles while drawing pixels. It scans the video in raster order, row 0 to 143 and implements three main phases of each scan: Sprite scanning (OAM), Drawing Pixels and The Horizontal blank phase. For the first 144 scans it looks like:

Mode 2
OAM scan
Mode 3
Drawing Pixels
Mode 0
Horizontal blank
One frame
70224 dots/2²² Hz
80 dots 172-289 dots 87-204 dots @ 59.7fps

The PPU then mimics a conventional CRT scan by adding more scanned lines, called the VBlank scans:

Mode 1
VBlank

10 Scan lines..
456 dots 4560 dots in total

This is basically copied from the PanDocs' rendering page. If drawn to scale, it'd look like this:


There's 204 clock cycles available in HBlank and another 80 cycles in OAM scan, 284 in total. That should be enough for about 35 x 8 cycle instructions (which is about the average). So, it appears like there's lots of time available in HBlank and OAM scan, but little time available in VBlank.

The real question is how to update the screen. There's a great article on gbdev.io about updating the screen during screen redraw and discusses a number of methods. All of the methods involve generating interrupts either at the beginning of Mode 2 (HBlank) or on an LYC match, which is at the beginning of Mode 1 (OAM).

One method is to prepare a whole area of RAM where the target data goes and then the interrupt routine can just copy the data to the right area of RAM. The problem here is that when displaying characters we need to read video RAM before modifying it and then writing back to it. Also, it's complicated. In a future blog post you'll discover why that isn't likely to be very effective even if I managed the complexity.

The next alternative is for the foreground task (the main program) to wait for VBlank. That kind of wait routine can be done by waiting for IF bit 0 to be set and is the most common update technique in GameBoy games. It would look like:

  ld hl,rIF
Wait:
  bit 0,[hl]
  jr z,Wait ;24cycles per loop  
  res 0,[hl] ;+16/20 cycles when done.


However, a little calculation shows this would lead to a terribly slow display routine. Each tile row takes about 144 to 160 cycles and with 4560 cycles available for VBlank, that results in only about 4 characters being displayed. Also, it precludes using a VBlank interrupt. I tried it at an early stage just to prove it could work at all, but it was slow.

The gbdev.io article mentioned earlier covers a number of other techniques that mostly revolve around waiting for the rSTAT (LCD) register to say that it's not in a busy mode. The problem with that is that if you wait for the PPU to not be busy, it might be just about to start drawing pixels (Mode 3). So, then you have to wait for the PPU to be busy, then not busy, because in theory you're at the beginning of a HBlank. And this means a scan can be wasted.

What we really want to know is if there's enough time before HBlank finishes, for us to do some screen updates.

Timer-Based Waiting

My technique is to use the timer. By running it at 262144Hz (the maximum), the timer updates with a tick every 16 CPU cycles. This is quite close to the wait loop period above, so it's a good enough resolution. Then, we set rSTAT to generate HBlank interrupts and reset the timer during the interrupt.

Since the timer will count to about 456/16=28.5 in a complete scan, then providing our routine sees rTIMA as being less than the deadline, we'll be able to complete an update and won't have to wait for the PPU to become busy before writing more data. In fact if our routine takes longer than a whole scan between writes, that's not a problem and if our routine is so quick we can perform two updates before the timeout, then that's a bonus.

And we can improve things further. During VBlank we can set rTIMA to 0 and stop the timer. Then screen updates during VBLank will always see we have time to update, so that will proceed at the maximum rate. The only remaining difficulty is that on the very last VBlank scan line, we need to enable the timer again. We can do this using the LYC compare technique used to switch tile sets.

This method is closer to how it might be done on a bare-metal, real-time, microcontroller application, by assessing if there's enough time to complete a task before a timeout.

Code Snippets

The timer-based approach requires a few interrupt handler changes. The Timer needs initialising before the STAT interrupt is:

ld a,255 ;Init to 255 to stop any display updates before
ld [rTIMA],a ;handlers are ready.

In the main init code, we need to support both the STAT interrupt, and VBlank interrupt:

  xor a
  ldh [rIF],a ;clear pending interrupts.
  ld a,IE_STAT | IE_VBLANK ;need both..
  ldh [rIE],a ;interrupts enabled.
  ei

The STAT interrupt will need to handle an interrupt every scan; a line matching interrupt for switching the tile set and a line matching interrupt for the end of VBlank to restart the timer. We could also use STAT to handle stopping the timer at the beginning of VBlank, but by using VBlank itself we can eliminate a conditional test and jump for that case:

DoVBlank:
  push af
  ld a, LCDC_ON | LCDC_BG_ON | 16 ;back to tiles at $8000.
  ldh [rLCDC], a
  xor a; stop the timer.
  ldh [rTAC],a
  ldh [rTIMA],a ;and reset it.
  ld a,STAT_LYC ;select only scan match interrupt
  ldh [rSTAT],a ;and enable it
  ld a,152
  ldh [rLYC],a ;interrupt at line 152.
  ldh a,[rIF]
  and 0xfd ;clear bit 2
  ldh [rIF],a ;clear LCD interrupt.
  pop af
  reti


The STAT interrupt is now fairly involved, but at any one stage it only executes one of 3 paths:

LcdStat: ;
  push af
  ld a,240 ;set up timer so it's 0 by
  ldh [rTIMA],a ;HBlank.
  ldh a,[rSTAT]
  bit B_STAT_LYCF,a ;LYCF match?
  jr nz,LcdStat10 ;so the common OAM int case..
  pop af ;can just return now.
  reti ;normally 8 instructions.
LcdStat10: ;only reached on scan 96 & 153.
  bit B_STAT_LYC,a ;
  
jr nz,LcdStatELine ;it was the scan 153 case.
  ld a, LCDC_ON | LCDC_BG_ON ;switch to tiles at $9000
  ldh [rLCDC], a
  pop af
  reti ;12 instructions on scan 96.
LcdStatELine: ;we're at the end of vblank.
  ld a,(256/20)*8
  ldh [rLYC],a ;back to the proper line
  ld a,STAT_MODE_2 ;STAT_LYC | STAT_MODE_0
  ldh [rSTAT],a
  ldh a,[rIF]
  and 0xfd ;clear bit 2
  ldh [rIF],a ;clear LCD interrupt.
  ld a,TAC_START|TAC_262KHZ ;back to normal mode for timer.
  ldh [rTAC],a
  pop af
  reti ;19 instructions on scan 153.


Finally, the wait routine is used multiple times as a Macro, once before each plane byte is written:

MACRO waitScan
.waitScan\@
  ldh a,[rTIMA]
  cp \1 ;\1 is the wait period.
  jr nc,.waitScan\@
ENDM

In the end I found that waitScan 6 was the optimum. Anything more than that and I'd see artefacts appearing in the text output, like pixels of text being the wrong colour. And this is because there must be some kind of clash between the PPU and the main code accessing VRAM.

Performance

My original estimates were that I should be able to update an entire screen with text in much less than a second. It takes about 16.7s to display 50*95=4,750 characters, so that's 4750/16.7=284.431 characters per second. Interesting, so it's not so terrible.


Conclusion

I started doodling the display code on a train while going to a fun 27K cycle ride. It's fairly easy to write down the concepts behind the code, and it really is fun to write in assembly, because it's far more like a logic puzzle than a more expressive high-level language.

Then again, it's taken me 5 reasonable-length blog posts to get to the primary objective, a real-time display of text in multiple colours. It takes a lot of tricks to do this on the Nintendo GameBoy, because it is really designed as a low-power handheld for playing platform games. I had to switch tiles and add synchronisation mechanisms to stop the PPU and my code from clashing with VRAM accesses. This isn't something that 8-bit programmers had to deal with in the early 1980s.

One interesting puzzle remains, My estimates were that I'd get far more time during HBlank and OAM to update the screen contents. I thought I'd be able to update an entire tile row. However, I'm only updating 284*8/50.97=44.575 tile rows (or 89 bytes) per frame, which seems really poor: less than 1 byte per HBlank, because VBlank is 100% available for updates. It's like it's not updating in HBlank at all, though in fact placing breakpoints after the Waits show that it does (because I can see the scan line is <144 much of the time).

Improving upon this will be for a future blog post, but in the meantime, the next post will be about some simple additional routines for clearing the screen and scrolling!


Friday, 11 September 2026

GameBoy Pixmap Demo (Part 4)

This is the 4th in the series on Framebuffer-based graphics on the Nintendo GameBoy. As before I'll start with a recap. Then we'll continue with more development, displaying characters properly at any location on the screen, and in any colour!

Recap

The Nintendo GameBoy was an incredibly successful 8-bit hand-held game console from the late 1980s. To maintain performance given significant memory and CPU-speed constraints, it used Tile-based graphics, where the screen was divided into a 20x18 TileMap grid; where each grid location was a byte that referenced an 8x8, 2 bits-per-pixel Tile. Each tile took up 16 bytes, where each row of the tile contained the least significant bit of 8 pixels, and the next byte contained the most significant bit. 

The CPU was a sort-of Z80/Intel 8080 hybrid with some instructions missing from both; some Z80 instructions (including the Bit handling instructions) included and a few instructions of its own.

You can use RGBDS-live to both edit games in assembly and emulate the GameBoy, so development is fast. With 8-bits per Tile reference in the TileMap, only 256 tiles could be supported, but there's a raster trick to switch to another tile map (and then back) based on a scan-line interrupt. This means I should be able to support a full 160x144x2bpp Frame-Buffer. Part 3 started this process, beginning with RGBDS-live's "Hello World" demo and ending with a frame-buffer display of a 4x8 pixel font.


A Proper Character Display Routine

There are four major steps in being able to display characters the way we want.

Calculating Font and Frame-Buffer Addresses

Font Addresses

In the Firmware, the font is at address CharSet; each pair of characters occupies 8 bytes and begins at ' ' (ASCII code 32). So, the address is CharSet+(((chr-32)>>1)<<3), though in fact this gets simplified to: (CharSet-(32>>1)<<3)+((char&0x7e)<<2). This eliminates an extra subtraction, because the constants get combined, and also a shift. In addition, because the maximum character value is 0x7f, we can do 1 8-bit shift, before transferring to HL, doubling and adding (CharSet-(32>>1)<<3). We also need to retain chr&1.

Tile Addresses

These are 0x8000|( (y*20+(x>>1))<<4), because (x,y) are character, not pixel coordinates. Again, because y is in the range 0..17, we can compute y*10 entirely in 8-bits before finally doubling to *20. As before, we need to retain x&1.

Register Allocation

On input, A=char, and I use global "system" variables for the print position: gChX and gChY. For speed, the goal is to put everything in registers:

Register Use
A Temp
B bit 1 =1 if Chr is odd; bit 0=1 if X is odd.
In the display loop:
bits 7:4 are the char row (X is even)
bits 3:0 are the char row (X is odd).
C Bits<3:2>=Ink, Bits<1:0>=Paper
DE Destination Addr (Tile)
HL Source Addr (Char)

Although C ends up containing the colour bits, and these get tested using bit instructions, it's still much faster than loading them from RAM.

Shifting And Masking Bitmaps

Because each 4x8 character occupies either bits <3:0> or bits <7:4> of each character row, and needs to be copied to either pixels <3:0> or pixels <7:4> of a destination tile, we need to mask (and possibly shift) the source pixels from the font and mask the destination pixels before combining them. There are 4 possible combinations:

Tile Coord Bits=>
Font Char Code (Bits)
Even (<7:4>) Odd (<3:0>)
Even (<7:4>) (FontBits&240)|(TileBits&15) ((FontBits&240)>>4)|(TileBits&240)
Odd (<3:0>) ((FontBits&15)<<4)|(TileBits&240) (FontBits&15)|(TileBits&240)

Two of the cases are easy. When bits <7:4> of a character row (an even character code) are displayed to an even x coordinate (pixels <7:4> of a tile). Or vice-versa, when bits <3:0> of a character row are displayed an odd x coordinate (pixels <3:0>). In these cases, there's no shifting, the font bytes are masked by either 240 (even) or 15 (odd) and the tile pixels are masked by the complementary values, 15 (even) or 240 (odd).

The other two cases aren't much more complex: When bits <7:4> (even) are display to an odd x coordinate (pixels <3:0>), the font's nybbles should be swapped, and then it's treated like the odd case. Likewise, when bits <3:0> are displayed to an even x coordinate, the font's nybbles get swapped and it's treated like the even case.

In practice, because there aren't really enough registers to store the masks and determine if the source character row should be swapped, there's a main row-copying loop for each of the Font and Tile, Even and Odd combinations. In addition, the loop is unrolled for both bytes of a tile's row.

Displaying Characters In Different Ink And Paper Colours

We can consider GameBoy tile colours to be two bitmapped planes, where the first plane handles bit 0 of the colour and the second plane handles bit 1 of the colour. Thus, the same colour calculations apply for both planes.

Ink

Ink by itself is easy, you just OR the character row with a tile's plane, if that plane's colour bit is set, or skip the plane.

Paper

Is more complex as we need to consider all 4 combinations for ink and paper bits on a given plane. The diagram below shows how it works for a pair of pixels, an ink pixel followed by a paper pixel. Here the xor mask below is for X is odd case). 


The upshot is that if ink<plane>==paper<plane> then we skip; for ink<plane>=1 we OR the character row's nybble and for paper<plane>==1, we xor with the character nybble's mask.

Code Fragment

Putting all these things together gives us a code fragment for a single plane (in this case, the first plane for when the character and x are odd) which looks like:

  ldi a,[hl]
  and 15 ;The Character code is odd,

  ld b,a ;
 so low nybble.
  ;First plane byte c<2>=ink on, c<0>=paper on.
  ld a,[de]
  and 240; background in upper nybble.
  bit 2,c ;ink bit 0
  jr z,PutChL2L60 ;no ink, so just upper nybble.
  bit 0,c ;paper bit 0
  jr nz,PutChL2L65 ;paper<0> == ink<0>==1, so bottom nybble =15.
  or b ;paper<0>==0, ink<0>==1, so or in bottom nybble.
  jr PutChL2L70
PutChL2L60:
  bit 0,c ;paper bit 0
  jr z,PutChL2L70 ;paper<0>==ink<0>==0, so bottom nybble=0.
  or b ;PaperNoInk paper<0>==1, ink<0>==0, so bit pattern,
PutChL2L65:
  xor 15 ;but inverted.
PutChL2L70:
  ld [de],a ;combine.
  inc e


The whole section in bold is repeated for the second plane (but checks bits 1 and 3 for ink and paper). A swap a is inserted after the ld a,[hl] when (chr&1) isn't (x&1).

Testing Process

Again the testing process started with odd character codes on an odd x coordinate with black text only, then progressed to testing even characters on an even x coordinate; then the two remaining cases (which needed the swap a). Then ink colours were added and finally paper colours. That gave me:


So, again here, the initial image is just the font's bytes copied to the tiles, but the second set are all the characters in every ink and paper combination for every odd and even character and x combination.

Conclusion

Displaying 1bpp bitmapped characters on a 2bpp tiled screen is a fairly standard process, but if we want to maintain performance given the register limitations of an SM83 it can be helpful to consider a number of optimisations for calculating addresses and masking the data.

So far, all the display code has been static: the data was copied in its entirety to the frame buffer; then the LCD was turned on and the main code entered a loop. The only dynamic aspect was switching the tile set using the Line Counter compare interrupt.

Real code will need to update text or graphics in real-time, while the display generator is running.
We will then find out that performance optimisations are important, because the Nintendo Game Boy allows even less time per frame than a ZX81 for updating. the screen. That's the subject of the next post.

Thursday, 10 September 2026

GameBoy Pixmap Demo (Part 3)

 This is the third of a series of blog posts on my Nintendo GameBoy demo. The first summarised the machine; covered part of the tiled graphics system and introduced my concept for implementing full frame buffers on the unit. The second discussed the Game Boy's Sharp SM83 CPU as it's quite intriguing, and in some ways quite different to its Z80 and Intel 8080 relatives and very different to the 6502 used in the Nintendo NES, Atari VCS 2600 and many 1970s/early 1980s 8-bit computers.

This one explores the frame buffer proof-of-concept and defines the font I'll be using for text output.

Recap

The GameBoy uses tiled graphics, where the 160x144 pixels screen is divided into a 20x18 2-D tile map of bytes that reference one of 256, 8x8 pixel tiles. This would appear to prevent a full pixel mapped image to be displayed, because there are 360 tile map locations and only 256 tiles they can reference.

However, it's possible to switch the set of tiles used by the Game Boy to provide access to another 128 tiles (0..127) with the higher numbered tiles (128..255) being the same as in the first set.  In addition, the set of tiles can be switched on the fly by generating an interrupt just at the end of the first set. This means we can use a total of 384 tiles, more than enough to uniquely cover the screen.


Goals

My goals are fairly simple. I want to be able to recreate the kinds of text and graphics primitives available on early 8-bit home computers. These are:
  • Being able to display a character at any character location. Because I want to support at least a 40x18 display, all my characters will be on a 4x8 matrix.
  • Being able to clear the screen, by clearing the frame buffer (you'll see this isn't as trivial as I thought).
  • Being able to scroll the screen by a single line and clear the bottom line.
  • Being able to plot points and draw lines anywhere on the screen (this was surprisingly easy).
I also wanted to be able to perform all these operations using the full set of 4 colours (greyscales) for both foreground and background colours. With my Sinclair background I refer to these as Ink (foreground) and Paper (background).

Development Process

Generating the Full Frame Buffer

This was fairly easy. I started with the default Hello demo you can find on the https://gbdev.io/rgbds-live/ web page. That demo simply sets up a static image on the background tile map using 69 tiles.


The code is remarkably simple, it just turns off the audio; waits for Blank; turns the LCD off; copies the tile data (first), then the data for the Tile Map; then turns the LCD on and initialises the main display register in the first (blank) frame. The whole code (but not data) fits within a single editor window.

To support a simple frame buffer; all the tiles needed to be in order and I needed a demo image. The easiest way to do this when bringing up a new system IMHO, is to fill the frame buffer with successive values: 0..255. Then you can see them all as binary patterns. I made the first set of tiles black binary patterns and the second set of 128 tiles light grey patterns. The only other change I made was to use the first set of tiles rather than the second set (in memory order), so my code to turn on the LCD looked like:

; Turn the LCD on, use 1st tile block.
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCKS
ldh [rLCDC], a

So, initially I could see that it was using the same set of tiles twice, because they were all dark. Then I'd know the interrupt worked because the remaining 360-256=104 tiles would look light. As a general principle that works for both this kind of game development and embedded programming, incremental changes are far more preferable, because errors are caught quickly and early.

SECTION "StatVec", ROM0[INT_HANDLER_STAT]
  jp LcdStat ;This needs to go near the start of the ROM

;.. then after the ds $150 - @, 0 header line..

LcdStat: ;switch between 96 and 144 and switch tile set.
  push af
  ldh a,[rLCDC] ;get the old LCD tile mode.
  xor LCDC_BLOCKS ;swap the tile set.
  ldh [rLCDC],a
  ldh a,[rLYC];get the old scan line number.
  xor ((256/20)*8)^144 ;(See explanation below)
  ldh [rLYC],a
  pop af
  reti

  ;And later to turn on interrupts..
  ld a,(256/20)*8 ;this calculates the right scan (scan 96)
  ldh [rLYC],a
  ld a,STAT_LYC
  ldh [rSTAT],a
  ld a,IE_STAT ;initially only use the STAT interrupt.
  ldh [rIE],a
  ei

Xors can be used to swap between any two values, not merely to turn a bit on and off, because of the identity: b^a^b=a. So, if a=numberX and we want to swap with numberY then xor numberX^numberY will generate numberX^numberX^numberY=numberY the first time around and numberY^numberX^numberY=numberX the second time around. This saves on an if.. else.. control flow. The GameBoy then generates the display:


You can see that where the light-green patterns begin they're the same patterns as in the top-left, but they aren't the same tiles, because the tiles contain the colours too.

Adding a 4x8 Font

This is actually a fairly small change to the firmware, but took quite a bit of work. I wanted to use a 4x8 font, so that I could display 40x18 characters on the screen instead of 20x18 (a 4x6 font could achieve 40x24, but they're harder to read and it'd involve crossing tile boundaries). The font looked like this:


(It's the Tasword 2 font, except I inverted the © sign). To save space, in the ROM I also just wanted to store them as 1bpp images and combine two characters per 8x8 pixels.

GameBoy tiles are 8x8, but 2 bits per pixel. The pixels aren't paired, instead there's 8 light-grey pixels followed by 8 dark-grey pixels (and setting the same bits on both bytes gives black).This means I would need to unpack my font bytes, but I don't need to expand 4-bits into 8-bits. So, for black on white text that's easy, I just repeat both bytes for every scan on the tile.

GenTiles10:
  ld a,[de] ;de^font.
  inc de
  ldi [hl],a ;hl^frame buffer (i.e. tiles)
  ldi [hl],a ;copy twice
  dec bc ;bc was set to the number of bytes in the font
  ld a,b
  or c
  jr nz,GenTiles10

I've ended up using font like this (or a 6x6 font) several times, and I always end up having to convert it to hex, so I thought I'd just publish it here:

CharSet:
  db 0x00, 0x02, 0x02, 0x02, 0x02, 0x00, 0x02, 0x00 ; !
  db 0x00, 0x50, 0x52, 0x07, 0x02, 0x07, 0x02, 0x00 ;"#
  db 0x00, 0x25, 0x71, 0x42, 0x72, 0x14, 0x75, 0x20 ;$%
  db 0x00, 0x21, 0x52, 0x20, 0x60, 0x50, 0x60, 0x00 ;&'
  db 0x00, 0x14, 0x22, 0x22, 0x22, 0x22, 0x14, 0x00 ;()
  db 0x00, 0x00, 0x52, 0x22, 0x77, 0x22, 0x52, 0x00 ;*+
  db 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x20, 0x40 ;,-
  db 0x00, 0x01, 0x01, 0x02, 0x02, 0x64, 0x64, 0x00 ;.
  db 0x00, 0x22, 0x56, 0x52, 0x52, 0x52, 0x27, 0x00 ;01
  db 0x00, 0x22, 0x55, 0x12, 0x21, 0x45, 0x72, 0x00 ;23
  db 0x00, 0x57, 0x54, 0x76, 0x11, 0x15, 0x12, 0x00 ;45
  db 0x00, 0x37, 0x41, 0x61, 0x52, 0x54, 0x24, 0x00 ;67
  db 0x00, 0x22, 0x55, 0x25, 0x53, 0x55, 0x22, 0x00 ;89
  db 0x00, 0x00, 0x02, 0x20, 0x02, 0x22, 0x04, 0x00 ;:;
  db 0x00, 0x00, 0x10, 0x27, 0x40, 0x27, 0x10, 0x00 ;<=
  db 0x00, 0x02, 0x45, 0x21, 0x12, 0x20, 0x42, 0x00 ;>?
  
  db 0x00, 0x62, 0x95, 0xb7, 0xb5, 0x85, 0x65, 0x00 ;@A
  db 0x00, 0x62, 0x55, 0x64, 0x54, 0x55, 0x62, 0x00 ;BC
  db 0x00, 0x67, 0x54, 0x56, 0x54, 0x54, 0x67, 0x00 ;DE
  db 0x00, 0x72, 0x45, 0x74, 0x47, 0x45, 0x42, 0x00 ;FG
  db 0x00, 0x57, 0x52, 0x72, 0x52, 0x52, 0x57, 0x00 ;HI
  db 0x00, 0x35, 0x15, 0x16, 0x15, 0x55, 0x25, 0x00 ;JK
  db 0x00, 0x45, 0x47, 0x47, 0x45, 0x45, 0x75, 0x00 ;LM
  db 0x00, 0x52, 0x55, 0x75, 0x75, 0x55, 0x52, 0x00 ;NO
  db 0x00, 0x62, 0x55, 0x55, 0x67, 0x47, 0x43, 0x00 ;PQ
  db 0x00, 0x62, 0x55, 0x52, 0x61, 0x55, 0x52, 0x00 ;RS
  db 0x00, 0x75, 0x25, 0x25, 0x25, 0x25, 0x22, 0x00 ;TU
  db 0x00, 0x55, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;VW
  db 0x00, 0x55, 0x55, 0x25, 0x52, 0x52, 0x52, 0x00 ;XY
  db 0x00, 0x77, 0x14, 0x24, 0x24, 0x44, 0x77, 0x00 ;Z[
  db 0x00, 0x47, 0x41, 0x21, 0x21, 0x11, 0x17, 0x00 ;\]
  db 0x00, 0x20, 0x50, 0x00, 0x00, 0x00, 0x07, 0x00 ;^_
  
  db 0x00, 0x20, 0x56, 0x41, 0x63, 0x45, 0x73, 0x00 ;£a
  db 0x00, 0x40, 0x42, 0x65, 0x54, 0x55, 0x62, 0x00 ;bc
  db 0x00, 0x10, 0x12, 0x35, 0x56, 0x54, 0x23, 0x00 ;de
  db 0x00, 0x20, 0x52, 0x45, 0x65, 0x43, 0x45, 0x02 ;fg
  db 0x00, 0x42, 0x40, 0x66, 0x52, 0x52, 0x57, 0x00 ;hi
  db 0x00, 0x14, 0x04, 0x35, 0x16, 0x15, 0x55, 0x20 ;jk
  db 0x00, 0x40, 0x45, 0x47, 0x47, 0x55, 0x25, 0x00 ;lm
  db 0x00, 0x00, 0x62, 0x55, 0x55, 0x55, 0x52, 0x00 ;no
  db 0x00, 0x00, 0x63, 0x55, 0x55, 0x63, 0x41, 0x41 ;pq
  db 0x00, 0x00, 0x63, 0x54, 0x42, 0x41, 0x46, 0x00 ;rs
  db 0x00, 0x40, 0x75, 0x45, 0x45, 0x55, 0x22, 0x00 ;tu
  db 0x00, 0x00, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;vw
  db 0x00, 0x00, 0x55, 0x55, 0x23, 0x51, 0x55, 0x02 ;xy
  db 0x00, 0x00, 0x71, 0x12, 0x26, 0x42, 0x71, 0x00 ;z{
  db 0x00, 0x20, 0x24, 0x22, 0x23, 0x22, 0x24, 0x00 ;|}
  db 0x00, 0x06, 0xaf, 0x59, 0x0b, 0x09, 0x0f, 0x06 ;~©

As you can tell, every 8 bytes contains a pair of characters. It's just the printable 96 characters, so they take 384 bytes in total. The display then shows...


(The rest of the screen is blank).

Conclusion

Starting with a basic Tile-mapped display demo, I added a line match interrupt which was triggered on scans 96 and 144, inverting the tile set each time. I set the TileMap to point to successive tiles (mod 256). The real GameBoy Tile Map is 32x32, so in reality I had to skip 12 bytes in the TileMap after each 20 byte row. I hand-converted a 4x8 font I had lying around: two characters will fit in each half of an 8x8 bitmap. That maps nicely onto the GameBoy Tiles (I just repeat each byte), but for real text display I'll have to properly mask off the correct half of each character and combine it with the existing background.

That's the subject for the next post in this series. 

GameBoy Pixmap Demo (Part 2)

 In the my most recent post, I started to discuss a recent interest in the Nintendo GameBoy (original version), and how to use its Tile-based graphics to implement a full frame buffer.

This post discusses some more intriguing aspects of the hardware architecture before moving on to the development process.

GameBoy CPU (SM83)

The GameBoy CPU is a fairly strange design. Most of the references to it describe it as a variant of a Z80, mostly I guess because the assembly-based development environments use Z80 mnemonics.

However, the CPU is, in my opinion, much closer to the 8080 than a Z80, because it lacks a number of features forcing clumsy solutions to programming problems and often far less efficiency.

Deficiencies

Registers

Amongst the most significant is the lack of a second bank of registers. The Z80 is register-rich for an 8-bit CPU, whereas the 8080 is somewhat meh, borrowing its register set from the original 8008. Like the 8080 there are just 7 main registers; of which 6 can be paired up as 3x 16 bit address registers.

A small register set would be fairly usable if the CPU provided indexed addressing modes, but the 8080, Z80 and SM83 only support register-pair indirection from these registers. The SM83 also lacks some instructions available on the 8080, namely being able to load HL from the contents of a 16-bit address: absolute addressing is only possible for loading and storing A.

The 8080 and Z80 also have the ability to exchange DE with HL and HL with (SP) (i.e. the top of stack). The SM83 lacks these which often forces two LD instructions to copy a 16-bit register and major limitations on using the stack for operands.

Jumps

The SM83 also lacks a number of jump conditions present on the 8080 and Z80, namely JP P (Jump positive), JP M (Jump Negative), JP PO (Jump Parity Odd), JP PE (Jump Parity Even). And this is because the SM83 lacks the parity and negative flags present on the others. However, having said that, most jumps are based on the zero and carry flags, which the SM83, 8080 and Z80 all have.

The Z80 has a very helpful looping instruction, DJNZ which isn't present on the SM83.

16-bit Operations

The SM83 lacks 16-bit operations found on the Z80, namely 16-bit SBC and ADC instructions.

Timing

All of the Z80, 8080 and SM83 have variable length timings with a minimum of 4 cycles per instruction. However, the SM83's timings are all multiples of 4 cycles. This means that all the memory operations that take 7 cycles on a Z80 and 8080 take 8 cycles on an SM83. Loads which take 10 cycles on a Z80 and 8080, take 12 cycles on an SM83. Long jumps which take 10 cycles on the other two take 16 on the SM83. Call/Returns which take 17/10 cycles on the 8080 (16/10 on the Z80) take a stunning 24/16 cycles on the SM83 (it's really not good for call/return). 16-bit INCs and DECs take 6 cycles on a Z80 (5 on the 8080), but 8 on the SM83.

Z80 Features

As per many discussions, the SM83 includes the full set of the Z80's bit manipulation instructions as well as introducing the SWAP nybble instructions.

Advantages

However, the SM83 does have a few advantages over the other CPUs:

  1. Because all the instructions are a multiple of 4 cycles, it's easier to count cycles, which is often important on gaming devices of this era.
  2.  16-bit ADD instructions are actually 27% faster on the SM83 than the Z80 (20% faster than an 8080).
  3. ADD SP,n8 allows you to allocate and deallocate stack frames quickly.
  4. Even though loading data from the stack is slow, LD HLSP+d8 can be used to point HL to locals or parameters.
  5. There's a whole set of what amounts to zero-page memory you can access with LDH A,[n8] and LDH [n8],A instructions as well as LDH A,[C] and LDH [C],A, in place of the IN/OUT instructions on the 8080 and Z80. These access the I/O registers too.
  6. LDI [HL],A and LDI A,[HL] with both post-increment and post-decrement addressing modes provide for very fast FIFO or buffer operations as they're only 8 cycles each.
  7. LD [A16],SP goes some way to making context switching easier.
  8. Finally, the 8-cycle SWAP r instructions help significantly with bit shifting. For example, SWAP A, AND 15 performs a >>4 in 16 cycles (it would take 23 on a Z80, RRCA * 4, then AND 15).

Microarchitecture

At first I thought that the SM83 must be highly microcoded, due to the multiple of 4 instruction timings, but it turns out that it's all hard-wired.

Conclusion

The SM83 is quite an interesting 8080 / Z80 hybrid-variant with architectural and instruction level features missing from both; some Z80 features and a few features not found in either.

My Demo program was written entirely in SM83 assembly via the very helpful https://gbdev.io/rgbds-live/ IDE and emulator. It's quite a strange experience, because of the different timing and instructions that aren't available.

If I'd been designing it I would have made a few different choices. Firstly, I would have removed all the conditional long jump instructions, because the short +/-128, relative jumps cover nearly all cases. I would have kept the unconditional jump.

I would have included the EX DE,HL instruction, because it's just so much more useful.

I would have added LD BC/DE,[SP+d8] and LD [SP+d8],DE/HL instructions as they would have been really useful for managing stack frames (though this probably isn't very important to games developers) and making up for the lack of alternate registers on the Z80. That's 4 more instructions. I would also have added LD HL/DE, [a16] and LD [a16],HL/DE instructions, because loading globals is important. That's another 2 instructions, a net gain of 1, but I would have removed LD [a16],SP, because LD HL,SP+0 then LD [a16],HL is equivalent (so is LD HL,0; ADD HL,SP; LD [a16],HL). INC SP and DEC SP are also redundant.

I would have replaced the redundant prefixed, RL A, RLC A, RR A, RLC A, and SLA A instructions with ADC HL, HL; ADC HL,DE; SBC HL,DE; RR DE and ADC HL,HL instructions to make other 16-bit operations quicker.




GameBoy Pixmap Demo

Apologies in advance as this is my first GameBoy coding attempt!

I've had a sudden interest in the original Nintendo GameBoy after a 68KMLA article started discussing an emulator for it, that runs on a 68K Mac, at near full speed:

https://68kmla.org/bb/threads/gb6-game-boy-emulator-for-system-6.41600/

And that's quite impressive given that the original GameBoy ran on a 4MHz Z80-type CPU and has a whole pile of quirky graphics tricks that can both slow down the unit by shocking amounts (as we'll find out), but is also the source of its capabilities when programs are tailored to them.

Of course, I don't want to use the kind of normal tiling, sprite and window tricks we'd expect on a GameBoy. Instead I want to treat the screen as a simple pixel-mapped frame buffer upon which I can write text anywhere and do the kinds of graphics operations that were normal on 8-bit computers. I am interested in whether that's at all practical.

Tile-Based Graphics

And it might not be. I'm not going to fully go into the architecture here, because PanDocs does a far better job than I could. I just want to mention the most relevant aspects for my demo.

GameBoy graphics are tile-based.  It's a half-way house between early character-based computer graphics and full-fledged frame-buffer graphics that became dominant in computers from the mid-1980s and in game systems from the end of the 1980s. The way they work is that video ram contains a number of, typically 256 x 8x8 pixel blocks, which are called 'Tiles' and then the screen is divided into a 2-d array of values, called the Tile Map, each of which references a Tile. Typically, the values are 8-bits, so each entry in the tile map can reference any one of the Tiles.



When the video generator scans the screen, it scans the Tile Map from top left to bottom right, reading a TileMap entry's value to get the Tile and then it reads a row of pixels from the tile (typically 8 pixels) and transfers those pixels to the screen. When a scan line has been completed, the video generator starts on the next scan line, which for the next 7 rows means reading the same Tile Map entries as before, leading to the same Tiles, but a new row on each Tile.


At one level, this is inefficient, because for every row of pixels sent to the screen, both a Tile Map entry and the pixels themselves need to be read, which might mean up to twice the number of memory accesses.

But on early 8-bit computers (of which the GameBoy is effectively one), it's worth it, because firstly, most of the effort for manipulating the screen can be done by changing Tile Map entries rather than the pixel data. Secondly, because these 8-bit computers often lacked sufficient memory to represent an entire pixel-mapped screen, a lot of video RAM can be saved by re-using the same Tile in multiple Tile Map entries. And because most 8-bit video games had a lot of repeated graphics, it's a massive advantage. So, in summary: Tiled graphics can be much faster to manipulate and use far less memory.

Tiled graphics are best for representing relatively static background images, because otherwise the programmer has to compose the animated parts of a screen using dynamically allocated tiles, which undermines the performance advantage. So, in many systems (like the Commodore-64, the Nintendo NES, Sega Megadrive and of course the GameBoy), tiles are augmented by Sprites which are additional tiles that can overlay the tiled image at any location. The video hardware then handles the effort of composing the background tiles and Sprite data, and typically this also means a limitation in the number of Sprites that can appear on the screen at any point, due to a limitation in hardware registers for Sprites or the sheer amount of hardware effort needed to compose them on top of the tiled background.

Thus, games oriented around tiles and sprites tend to be platform-oriented: a somewhat repetitive 2-d background that can scroll around and upon which the main game characters act.

Well, that's enough about Sprites, because I'm not interested in them for my demo. Suffice to say that the GameBoy implements plenty of them; using them takes up extra video processing and also adds a second video layer called the "Window". I'm not interested in that either.

A Proposed GameBoy Full Frame Buffer

The GameBoy TileMap is 20x18 and each entry is 8-bits, which means that there are 360 tile map locations, but only 256 tile map entries are possible. This would normally preclude being able to support a full frame buffer, which would require a unique tile for each tile map entry (i.e. 360 tiles).

However, it should be possible to achieve that. The GameBoy also has an LCD control register which allows you to select an alternate set of tiles so that the same tile map entry could point to a different tile in a different part of the screen. And it's possible to generate an interrupt on a given scan line. This means that we can use the first 256 tiles for most of the screen, then on the given scan line interrupt switch to the other set of tiles; and then at the end of a video image - in what's called the VBlank region (of 10 scan lines), reset the LCD control register back to the original set of tiles.



Cleverly, the GameBoy tile mapping works so that tile map values $80 to $ff on the first set of tiles map to the same tiles as for the the second set, because the first set are treated as unsigned tile map values (so $80 = 128, which comes after $00 = 0), but the second set are signed ( because $80= -128, which comes before $00 = 0).

Conclusion

The Nintendo GameBoy was an incredibly successful handheld games console beating even more advanced handheld games consoles, thanks to its lower price and power consumption.

To save memory and performance (given its lowly 8-bit CPU), it used Tiled Graphics techniques borrowed common 8-bit graphics techniques, particularly from the Nintendo Entertainment System. Normally (as on the NES), this would heavily restrict the versatility of possible images, because the number of unique tiles will be less (256) than the number of tile locations in the tile map (360 in the case of the GameBoy). However, the GameBoy supports a second set of tiles and a raster trick which should make it possible.

In the next blog post I'll discuss the SM83 CPU at the heart of the system.

Thursday, 6 August 2026

Asymptotic Wildfires

Introduction

Amidst the record-breaking wildfires being experienced in Europe over summer 2026, many people believe the solution is to reduce the number of causes - and in fact attach agency, such as arson, to those causes in preference to the scientific explanation, global warming.

The Climate Science argument is that the critical issue is global warming itself, because it supercharges wildfires: heatwaves get more common and intense. Then land gets drier and therefore easier to combust. Once a fire starts, conditions make fires burn harder, faster and hotter.

This article explores one major reason why this property is the critical one, over and above that of the number of fires - and it's due to some fairly simple maths. People who can understand and communicate the maths, will have an advantage over climate science deniers who will repeatedly pick the wrong solution.

The Problem

The problem occurs because there is essentially a finite amount of resources we can apply to extinguishing (denoted by 'x' here) wildfires. As the speed of a wildfire front (denoted by 'f' here) increases, it has a disproportionate impact on the rate of progress (denoted by 'p' here) at which fires can be put out.

We can understand some of this intuitively. x can be measured as a speed: the rate at which we can put a fire out given reasonable resources, in m/s. Similarly, f represents the speed of the front of a wildfire, also measured in m/s. Thus if f = 0, i.e. the fire is not moving (but is still burning), then we know that as long as x is greater than 0, we can put the fire out. And the general rule here is that f≥x means the fire can never be put out. If the fire already has an extent, then for every metre of fire we put out, the fire has advanced by over 1m. So, this establishes hard limits.

Early Stages

The early stages of a fire is a front that would tend to expand outwards in a circle from its source at a constant rate. In this model we'll ignore wind speed and wind direction, in order to simplify the maths.

One thing we can observe is this: the initial part of the fire will be slower than when the fire is more mature. And this is because when the fire starts, there's a substantial curvature at the front and this means that the heat distribution at any one point at the front has to set light to a wider span ahead of it, that is: the fire is fanning out. But because it's setting light to a wider span ahead it means that there's less heat being applied to any point ahead of the front, because it has to be distributed to the span ahead.




This means that the fire will initially travel slower and is therefore easier to extinguish. It's possibly also why in the early stages of a wildfire, people believe they can stop the fire relatively easily - because it's slower and the temperature curve ahead of the front is less intense. And this is partly true, it will be easier to stop.

That's why we ignore this in our model.

Maturity

When the fire is mature, the front is essentially a straight line. Obviously terrain will affect this, but this will be a reasonable approximation. The fire is hotter now and is travelling faster, because there is no fan-out.

So, as the front advances by f metres, the amount extinguished will be along the hypotenuse of a right-angled triangle (because the fire-fighters want to move along the fire, but also outward as it advances). The amount of progress must be p.




Thus we can see that p=√(x²-f²), by a simple application of Pythagoras' theorem. So, the key question is this: as f approaches x, what's the ratio of x:p, the amount we extinguish, vs the amount of progress? So, if we normalise it, by making x=1, we get p=√(1-f²) and so we want to know 1/√(1-f²) .

Results

The results are actually quite shocking. At low rates of f, e.g. f=0.1, x/p is also very small (in this case 1.005). But as f gets increasingly close to x, even tiny changes in f result in large changes to x/p. Thus at this stage, as global warming increases, even tiny increases in local temperatures and burn rates in a wildfire result in worse than exponentially more land burned than as a result of direct human causes such as barbecues or cigarettes (which is linear).

Conclusion

Climate Science deniers always ascribe agency to anything they disagree with: Climate Scientists just say what their bosses tell them to; floods happen because people don't dredge rivers (dredging rivers doesn't really help in fact); wildfires happen because of arsonists. Conversely, they use "common sense" to determine policy, which allows them to bypass all the science they want, but in turn means their policies are driven by human agency rather than objective criteria, by definition.

But this kind of thinking means they can't plan properly for climate extremes, because they'd never trust the models that predict the future.

Wildfire Sim

MinX: MaxX: Your browser does not support the canvas element.


Tuesday, 28 April 2026

Shorter QL GetHead

 A couple of days ago I wrote a blog post about being able to read QL executable file headers so that you can restore them from original QL applications on QL disks, onto a modern OS which lacks them when running a QL emulator.

https://oneweekwonder.blogspot.com/

It's actually fairly involved here, involving one SuperBASIC program to load a machine code hex program (and then save it on disk); then write another SuperBASIC program to load that code, which adds a couple of functions to SuperBasic. And to make it even more complex I ended up wrapping them with SuperBASIC procedures to make opening the channel so you can use the SuperBASIC extensions and then closing it at the end.

It turns out though you can make it much shorter, a single, simple SuperBASIC program:

10 mcode=RESPR(8):buffer=RESPR(64):POKE_L mcode,0+"536956483":POKE_L mcode+4,0+"1879068277"
100 DEFine PROCedure GetHead(fName$)
110 OPEN #3,fName$:CALL mcode,71,64,500,0,0,0,0,ChanId(3),buffer
120 PRINT "Len:";PEEK_L(buffer);" Dataspace:";PEEK_L(buffer+6)
130 CLOSE #3
140 END DEFine
150 DEFine FuNction ChanId(chan):LOCal a6:a6=PEEK_L(163856)+104
160 RETURN PEEK_L(PEEK_L(a6+48)+a6+chan*40):END DEFine

It does the same thing. You should run the program, and then you can type things like: GetHead "mdv1_quill" to return the size of the program and the data space.

It nicely illustrates some of the impacts of having a fancy version of Basic (for the day) and the relative complexities of Sinclair's QDOS operating system.

In 1984, when the QL appeared, not many computers had 128kB of RAM. The Macintosh had just appeared, with 128kB. Most PCs were still in an era where 128kB was fairly normal; the PC/XT came with a standard 128kB as did the Sirius One. Probably most original PCs were still living with ≤128kB. The Atari ST, Amiga, PC/AT, BBC Master 128, Commodore 128, Amstrad CPC128 were all in the future.

So, to see a new computer with 128kB was quite a luxury. And since it had a structured BASIC it was very tempting to try and write elegant, i.e. wordy, code. Hence the Hex loader, which would have been a tiny thing on a ZX81 or ZX Spectrum was a couple of dozen lines long, containing multiple procedures, just to prove the programmer understood the concepts.

Wordiness can obscure as much as it reveals though. For example, I think this version is actually simpler to understand, so let's go through it.

Firstly, we allocate just 8 bytes for machine code and poke_l it in directly. It's just 4 instructions:

2001    MOVE.L d1,d0; because d0=function code and we can't pass d0 in CALL.
4E43    TRAP #3 ;all the other parameters are supplied in the CALL.
7000    MOVEQ #0,d0 ;return with no errors regardless.
4E75    RTS ;back to BASIC.

I had to use 0+"integer" in the POKE_L statements, because SuperBASIC will convert large integers into scientific notation with just 6 significant figures. 0+"integer" will perform a VAL("integer") thanks to SuperBASIC's type coercion.

As before, we also set up a 64 byte buffer.

The GetHead procedure takes fName$ as before; we open up the channel (3) and pass that to the CALL statement along with the function code (71), the size of the buffer (64), the timeout (10s); then channel Id (ChanId(3)) and finally the buffer address. All these parameters are copied directly to 68000 registers.

The complexity lies in the function ChanId(3). QDOS doesn't use SuperBASIC channel numbers as its Channel IDs, instead they're a 32-bit number that's not trivial to derive. So, you have to indirect it via the system variables into the SuperBasic variables and then from there calculate the offset into the channel table!

My method isn't fool-proof. A proper routine should calculate all of the relative to A6, because it could function as part of a multitasking job, where A6 could end up getting moved around. In my simplistic implementation, SuperBASIC is the only job running, so A6 won't change and can be calculated statically.

But the upshot is that you can simplify GetHead. You could also use the same machine code routine to implement SetHead, since it's really just a TRAP #3.