<!DOCTYPE html>
<!-- ========================================== kroc camen of camen design ============================================= -->
<title>code · The Fastest Way to Update VRAM Tiles on GameBoy?</title>
<link rel="stylesheet" type="text/css" href="/design/design.css" />
<meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=no" />
<link rel="alternate" type="application/rss+xml" href="/code/rss" title="Just code" />
<link rel="canonical" href="/code/gameboy-pop-tile" />
<!-- =================================================================================================================== -->
<header>
<h1><a href="/" rel="index">
Camen Design
</a></h1>
<nav><ul>
<li><a href="/">all</a></li>
<li><a href="/projects">projects</a></li>
<li><a href="http://forum.camendesign.com">forum</a></li>
</ul><ul>
<li><a href="/quote/">quote</a></li>
<li><a href="/writing/">writing</a></li>
<li><a href="/blog/">blog</a></li>
<li><a href="/photo/">photo</a></li>
<li><a href="/code/" rel="tag">code</a></li>
<li><a href="/art/">art</a></li>
<li><a href="/link/">link</a></li>
<li><a href="/poem/">poem</a></li>
<li><a href="/audio/">audio</a></li>
</ul><ul>
<li><a href="/web-dev/">web-dev</a></li>
<li><a href="/annoyances/">annoyances</a></li>
<li><a href="/eve/">eve</a></li>
<li><a href="/inspiration/">inspiration</a></li>
<li><a href="/code-is-art/">code-is-art</a></li>
<li><a href="/windows/">windows</a></li>
<li><a href="/gaming/">gaming</a></li>
<li><a href="/gift/">gift</a></li>
<li><a href="/mac/">mac</a></li>
<li><a href="/osnews/">osnews</a></li>
<li><a href="/c64/">c64</a></li>
<li><a href="/linux/">linux</a></li>
</ul>
<a rel="previous" href="/code/remarkable">
older article →
</a><a rel="next" href="/code/dom_templating">
← newer article
</a></nav>
</header>
<!-- =================================================================================================================== -->
<article><header>
<!-- date published or updated -->
<time pubdate datetime="2023-01-09T12:48:00+00:00">
<sup>12:48<abbr>pm</abbr> • 2023</sup>
<abbr title="January">Jan</abbr> 9
</time>
<!-- categories -->
<ul>
<li><a href="/code/gameboy-pop-tile" rel="bookmark tag">code</a></li>
<li><a href="/code-is-art/gameboy-pop-tile">code-is-art</a></li>
<li><a href="/gaming/gameboy-pop-tile">gaming</a></li>
</ul>
<!-- licence -->
<small>
<a rel="license" href="http://creativecommons.org/licenses/by/3.0/deed.en_GB">c</a>
share + remix
</small>
</header>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<section>
<h1>The Fastest Way to Update VRAM Tiles on GameBoy?</h1>
<p>
<strong>What is the absolute fastest method for transferring tilemap (“nametable”) updates</strong> 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.
</p>
<h2 id="the-issue">The Issue</h2>
<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>
<p>
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.
</p><p>
This isn’t a problem for the <cite>GameBoy Color</cite> which added
<a href="https://en.wikipedia.org/wiki/Direct_memory_access">DMA</a> for transferring tiles outside of CPU
control. However, <a href="https://hh.gbdev.io/">GameBoy homebrew</a> tends to favour the original’s
“pea-soup” <abbr title="aesthetic">A E S T H E T I C</abbr> which means that this remains a problem that
developers have to tackle.
</p>
<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>
<p>
There now exist <a href="https://www.gbstudio.dev/">entire IDEs / GUIs</a> 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.
</p>
<h2 id="the-standard-approach">The Standard Approach</h2>
<p>
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:
</p>
<pre>; 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</pre>
<p>
This example takes 72 cycles per tile. Assuming 32 tiles (about the maximum possible), this is roughly 2′304
cycles. V-blank is <a href="https://gbdev.io/pandocs/STAT.html">said</a> to be 4′560 cycles long but other work
needs to be done such as sprite updates (not covered here).
</p>
<h2 id="can-we-go-faster">Can We Go Faster?</h2>
<p>
Looking at the <a href="https://gbdev.io/pandocs/CPU_Instruction_Set.html">GameBoy CPU instruction set</a> is
there anything faster we can use?<br />
Ultimately, different approaches yield the same timings, e.g.
</p>
<pre> 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</pre>
<p>
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.
</p><p>
Except, there is an instruction that can read 16-bits from memory in one go…<br />
…the stack!
</p>
<pre> pop HL ; 12 cycles for a full 16-bit read!</pre>
<p>
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.
</p>
<h2 id="pop-tile">Pop Tile</h2>
<p>
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.
</p><p>
Under controlled conditions, like an interrupt handler, we can temporarily re-position the stack and then
<samp>pop</samp> from it to read 16-bit values at a time!
</p>
<pre> ; 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</pre>
<p>
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.
</p><p>
Interestingly, the A-register <em>is</em> 8-bits, so what happens if you <samp>pop</samp> a 16-bit value into
<samp>A</samp>? The other 8-bits go into the
<a href="https://gbdev.io/pandocs/CPU_Registers_and_Flags.html#the-flags-register-lower-8-bits-of-af-register">CPU
Flags register</a>. That’s right, we can <em>set the CPU flags using a pre-determined value we wrote to the
queue</em> rather than being set by instructions. As far as GameBoy programming goes, this is <em>wild</em>.
</p>
<pre> ; 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</pre>
<p>
That’s just 44 cycles per tile! (1′408 cycles for 32 tiles!) We use the extra byte to <strong>set</strong> (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.
</p><p>
Here’s a V-blank routine that implements the feature fully:
</p><p>
<small>(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.)</small>
</p>
<pre>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</pre>
<p>
Writing tiles out to the queue is more complicated and must be done carefully so I provide a sample routine to do so
below.
</p><p>
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
<samp>%10000000</samp> (128) to indicate the end of the queue.
</p>
<pre> ; 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</pre>
<p>
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, <a href="https://github.com/kroc/impatience">Impatience</a>:
</p>
<figure>
<figcaption></figcaption>
<a href="https://github.com/kroc/impatience">
<img src="/code/gameboy-pop-tile/impatience.png" alt="Screenshot of 'Impatience', a GameBoy Solitaire game." width="482" height="464" />
</a>
<figcaption><a href="https://github.com/kroc/impatience">Impatience</a> is a Solitaire collection for GameBoy completely hand-written in assembly.</figcaption>
</figure>
</section>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
</article>
<footer>
<nav><a href="http://forum.camendesign.com">‹ Discuss this in the Forum ›</a></nav>
<a href="mailto:kroc@camendesign.com">kroc@camendesign.com</a>
<nav>view-source:
<a href="/code/gameboy-pop-tile.rem">Rem</a> •
<a href="/code/gameboy-pop-tile.html">HTML</a> •
<a href="/design/">CSS</a> •
<a href="/.system/">PHP</a> •
<a href="/.htaccess">.htaccess</a>
</nav>
<form method="get" action="https://duckduckgo.com">
<input type="hidden" name="sites" value="camendesign.com" />
<input type="search" name="q" placeholder="search…" />
<input type="submit" value="Go" />
</form>
</footer>
<!-- =================================================================================================== code is art === -->