This is a document written using ReMarkable, a shorthand syntax for generating HTML.

{	"date"		:	202301091248,
	"updated"	:	202301091248,
	"title"		:	"The Fastest Way to Update VRAM Tiles on GameBoy?",
	"licence"	:	"cc-by",
	"tags"		:	["code-is-art", "gaming"]
}

<section>

# The Fastest Way to Update VRAM Tiles on GameBoy? #

*What is the absolute fastest method for transferring tilemap ("nametable") updates* during V-blank on the original GameBoy? In this article I introduce a method I've named "pop-tile" that is notably faster than existing examples out there.

## The Issue ## (#the-issue)

<aside>
	† The GameBoy's LCD operates much a like a traditional CRT even though it isn't cathode-ray based!<!--¬¬Whether this is a by-product of how the primitive LCD worked or a choice on Nintendo's / the part-manufacturer's behalf to make things easier for developers used to CRTs, I don't know.-->
</aside>

The original GameBoy cannot make changes to VRAM during screen display, leaving only H-blank and V-blank. Time is critical; if you don't finish writing your changes to VRAM before the 'beam'† enters the screen the changes won't happen or you'll get screen glitching, or at worst, potentially harm the hardware.

This isn't a problem for the ~GameBoy Color~ which added <DMA (https://en.wikipedia.org/wiki/Direct_memory_access)> for transferring tiles outside of CPU control. However, <GameBoy homebrew (https://hh.gbdev.io/)> tends to favour the original's "pea-soup" {A E S T H E T I C|aesthetic} which means that this remains a problem that developers have to tackle.

<aside>
	† The GameBoy uses a custom CPU based on the "SM83" core that is neither Z80 nor 8080, being a half-way house of functionality.
</aside>

There now exist <entire IDEs / GUIs (https://www.gbstudio.dev/)> that abstract this away from you, but what those tools are capable of is guided by the algorithms they apply and a faster method of tilemap updates may open up additional possibilities. Even if you never write a line of Z80 assembler†, everyone can still benefit from faster tile map updates.  

## The Standard Approach ## (#the-standard-approach)

There isn't enough time in V-blank to directly compare VRAM with some in-RAM screen buffer and, whether your game uses a screen-buffer or not, the standard approach is to have some kind of "queue" of tile-updates written out as a list of address and tile-value since you can't write directly to VRAM during game-logic unless you sync your entire game-logic with V-blank! Rather you push a tile you want to change to the queue and have an interrupt routine write out these changes during V-blank:

~~~>
; tile queue consisting of 3-bytes each entry; VRAM address + tile-value
queue	= $c000

	; during V-blank...							;cycles
	ld	C,	32	; number of tiles to update			;+ 8=8
-	ld	HL,	queue	; start of queue				;+12=20
	ld	A,	[HL+]	; read addr lo-byte and move to next byte	;+ 8=28
	ld	E,	A	; make DE our VRAM address (lo-byte)		;+ 4=32
	ld	A,	[HL+]	; read addr hi-byte and move to next byte	;+ 8=40
	ld	D,	A	; make DE our VRAM address (hi-byte)		;+ 4=44
	ld	A,	[HL+]	; read the tile-value				;+ 8=52
	ld	[DE],	A	; write tile-value to VRAM address		;+ 8=60
	dec	C		; one less tile to process			;+ 4=64
	jr	nz,	-	; keep looping until all done			;+12=72
<~~~

This example takes 72 cycles per tile. Assuming 32 tiles (about the maximum possible), this is roughly 2'304 cycles. V-blank is <said (https://gbdev.io/pandocs/STAT.html)> to be 4'560 cycles long but other work needs to be done such as sprite updates (not covered here).

## Can We Go Faster? ## (#can-we-go-faster)

Looking at the <GameBoy CPU instruction set (https://gbdev.io/pandocs/CPU_Instruction_Set.html)> is there anything faster we can use? _
Ultimately, different approaches yield the same timings, e.g. 

~~~>
	ld	E,	[HL]	; read byte to register directly		;+8=8
	inc	L		; increment queue addr				;+4=12

	ld	A,	[HL+]	; read byte and increment queue addr		;+8=8
	ld	E,	A	; set the register				;+4=12
<~~~

The bottleneck is the GameBoy CPU's limited 16-bit capabilities compared to a full Z80; there's no way to read the full 16-bit VRAM address we want to write to in one go, we have to always read two bytes separately.

Except, there is an instruction that can read 16-bits from memory in one go... _
...the stack!

~~~>
	pop	HL		; 12 cycles for a full 16-bit read!
<~~~

The only problem, this is the stack! You can't just write arbitrary data to the stack without corrupting your return addresses and even if you manage that, you want to write the data during the main thread and then output it during V-blank, which could interrupt your code at absolutely any time.

## Pop Tile ## (#pop-tile)

I am almost certain that this method already exists out there, even as far back as the '90s, as it presents itself as the obvious approach for any hacker wanting to bend the hardware; however I don't know of an example I can point to.

Under controlled conditions, like an interrupt handler, we can temporarily re-position the stack and then `pop` from it to read 16-bit values at a time!

~~~>
	; read tile and write to VRAM:
 	ld	A,	32		; tile-count (could be in RAM)	;
-	pop	HL			; pop VRAM from address		;+12=12
	pop	BC			; pop tile-value (B unused!)	;+12=24
	ld	[HL],	C		; write tile to VRAM		;+ 8=32
	dec	A			; one less tile...		;+ 4=36
	jr	nz,	-		; finished?			;+12=48
<~~~

However, we can go faster than this; there's still a lot of loop management. You can unroll it, and it will be faster, but you will need to make sure to fill your queue up with dummy values to compensate.

Interestingly, the A-register _is_ 8-bits, so what happens if you `pop` a 16-bit value into `A`? The other 8-bits go into the <CPU Flags register (https://gbdev.io/pandocs/CPU_Registers_and_Flags.html#the-flags-register-lower-8-bits-of-af-register)>. That's right, we can _set the CPU flags using a pre-determined value we wrote to the queue_ rather than being set by instructions. As far as GameBoy programming goes, this is _wild_.

~~~>
        ; read tiles and write to VRAM:
-       pop     BC                      ; pop VRAM address from queue   ;+12=12
        pop     AF                      ; pop tile / CPU flags          ;+12=24
        ld      [BC],   A               ; write tile to VRAM            ;+ 8=32
        jr      z,      -               ; end of queue?                 ;+12=44
<~~~

That's just 44 cycles per tile! (1'408 cycles for 32 tiles!) We use the extra byte to *set* (i.e. =1) the zero flag on the last tile in the queue allowing us a quick and easy way to detect the end of the queue without having to do a counter comparison.

Here's a V-blank routine that implements the feature fully:

(((A byte in HRAM is used to indicate when the queue is ready so that the routine doesn't try empty the queue whilst it's still being populated by the main thread.)))

~~~>
update_tiles	= $ff80		; flag in HRAM to indicate when queue is ready
tile_queue	= $c000		; the tile queue itself, 128 bytes (4*32)

.ORGA	$40
	; various documentation says that an IRQ takes 20 cycles        ;+20=20
        ; (note the relative jump to save 4 cycles over JP)
        jr      irq_vblank                                              ;+12=34

; we place the IRQ handler as low down as possible to allow
; for the relative jump from the IRQ vector to save 4 cycles :P
.ORGA   $61

irq_vblank:                                                                ;=34
;===============================================================================
        push    AF                                                      ;+16=50
        push    BC                                                      ;+16=66
        push    HL                                                      ;+16=82

        ; any tile updates?
        ;=======================================================================
        ldh     A,      [<update_tiles]                                 ;+12=94
        and     A,      A               ; (set flags!)                  ;+ 4=98
        jr      z,      @skip                                           ;+ 8=106

        ; backup current stack pointer: we don't use HL during the
        ; tile-writing so we don't need to write the old SP to RAM!
        ld      HL,     SP+0                                            ;+12=118
        ld      SP,     tile_queue                                      ;+ 8=126

        ;-----------------------------------------------------------------------
        ; read tiles and write to VRAM:
-       pop     BC                      ; pop VRAM address from queue   ;+12=12
        pop     AF                      ; pop tile / CPU flags          ;+12=24
        ld      [BC],   A               ; write tile to VRAM            ;+ 8=32
        jr      z,      -               ; end of queue?                 ;+12=44

        ; restore original stack pointer
        ld      SP,     HL                                              ;+8
        ;-----------------------------------------------------------------------

        ; mark the queue as done (don't process the same tile-queue twice)
        xor     A                       ; (set A to zero)
        ldh     [<update_tiles],        A

@skip:	; ... any other V-blank code ...

	pop     HL
        pop     BC
        pop     AF
        reti
<~~~

Writing tiles out to the queue is more complicated and must be done carefully so I provide a sample routine to do so below.

Note that once the last tile has been added to the queue, the CPU-flags byte of the last queue entry must be set to `%10000000` (128) to indicate the end of the queue.

~~~>
	; the position with the tile queue should be saved
	; somewhere to be able to append to the queue
	ld	HL,	tile_queue
	ld	DE,	$9800		; somewhere in VRAM
	ld	B,	$00		; some tile index
	ld	C,	$00		; use $80 for the last tile in the queue!

	; in:	HL	tile-queue address
	;	DE	VRAM address
	;	B	tile-value
	;	C	last-tile flag ($80)
	;
	; out:	HL	advanced to the next queue position
	;
	; WARNING: doesn't protect from buffer overflow!
push_tile:
	ld	A,	E		; VRAM lo-byte
	ld	[HL+],	A		; write to queue and advance
	ld	A,	D		; VRAM hi-byte
	ld	[HL+],	A		; write to queue and advance
	ld	A,	C		; CPU flags (last-tile flag)
	ld	[HL+],	A		; write to queue and advance
	ld	A,	B		; tile-value
	ld	[HL+],	A		; write and advance

	; since V-blank could interrupt whilst the queue is being added to,
	; DON'T set the queue-ready flag until AFTER the tile is added
	;
	ld	A,	C		; get last-tile flag again
	ld	[<update_tiles],A	; inform V-blank of queue if last tile

	ret
<~~~

This article only explains and demonstrates the pop-tile concept; building a complete screen update system in assembly for the original GameBoy is quite difficult so I have a full, double-buffered screen update system that compares a front-buffer in RAM against a back-buffer and sends any changed tiles to the queue that you can use from my Solitaire collection for the GameBoy, <Impatience (https://github.com/kroc/impatience)>:

fig.	<"Screenshot of 'Impatience', a GameBoy Solitaire game."
	/&__HREF__;/impatience.png = https://github.com/kroc/impatience>
	: <Impatience (https://github.com/kroc/impatience)> is a Solitaire collection for GameBoy completely hand-written in assembly. 

</section>