Back to GameBoy Emulation in JavaScript
This is part 5 of an article series on emulation development in JavaScript; ten parts are currently available, and others are expected to follow.
In part 4, the GameBoy's graphics subsystem was explored in detail, and an emulation put together. Without a set of register mappings for the GPU to be dealt with in software, the graphics subsystem cannot be used by the emulator; once these registers have been made available, the emulator is essentially ready for basic use.
With the additions detailed below to add the GPU registers, and a basic interface for the control of the emulator, the result is as follows.
GPU registers
The graphics unit of the GameBoy has a series of registers which are mapped into memory, in the I/O space of the memory map. In order to get a working emulation with a background image, the following registers will be needed by the GPU (other registers are also available to the GPU, and will be explored in later parts of this series).
Address | Register | Status |
---|---|---|
0xFF40 | LCD and GPU control | Read/write |
0xFF42 | Scroll-Y | Read/write |
0xFF43 | Scroll-X | Read/write |
0xFF44 | Current scan line | Read only |
0xFF47 | Background palette | Write only |
The background palette register has previously been explored, and consists of four 2-bit palette entries. The scroll registers and scanline counter are full-byte values; this leaves the LCD control register, which is made up of 8 separate flags controlling the sections of the GPU.
Bit | Function | When 0 | When 1 |
---|---|---|---|
0 | Background: on/off | Off | On |
1 | Sprites: on/off | Off | On |
2 | Sprites: size (pixels) | 8x8 | 8x16 |
3 | Background: tile map | #0 | #1 |
4 | Background: tile set | #0 | #1 |
5 | Window: on/off | Off | On |
6 | Window: tile map | #0 | #1 |
7 | Display: on/off | Off | On |
In the above table, the additional features of the GPU appear: a "window" layer which can appear above the background, and sprite objects which can be moved against the background and window. These additional features will be covered as the need for them arises; in the meantime, the background flags are most important for basic rendering functions. In particular, it can be seen here how the background tile map and tile set can be changed, simply by flipping bits in the register 0xFF40
.
Implementation: GPU registers
Armed with the conceptual GPU register layout, an emulation can be implemented simply by adding handlers for these addresses to the MMU. This can either be done by hard-coding the GPU updates into the MMU, or defining a range of registers wherein the GPU will be called from the MMU, for more specialised handling to be done from there. In the interests of modularity, the latter approach has been taken here.
MMU.js: Zero-page I/O: GPU
rb: function(addr) { switch(addr & 0xF000) { ... case 0xF000: switch(addr & 0x0F00) { ...// Zero-pagecase 0xF00: if(addr >= 0xFF80) { return MMU._zram[addr & 0x7F]; } else {// I/O control handlingswitch(addr & 0x00F0) {// GPU (64 registers)case 0x40: case 0x50: case 0x60: case 0x70: return GPU.rb(addr); } return 0; } } } }, wb: function(addr, val) { switch(addr & 0xF000) { ... case 0xF000: switch(addr & 0x0F00) { ...// Zero-pagecase 0xF00: if(addr >= 0xFF80) { MMU._zram[addr & 0x7F] = val; } else {// I/Oswitch(addr & 0x00F0) {// GPUcase 0x40: case 0x50: case 0x60: case 0x70: GPU.wb(addr, val); break; } } break; } break; } }
GPU.js: Register handling
rb: function(addr) { switch(addr) {// LCD Controlcase 0xFF40: return (GPU._switchbg ? 0x01 : 0x00) | (GPU._bgmap ? 0x08 : 0x00) | (GPU._bgtile ? 0x10 : 0x00) | (GPU._switchlcd ? 0x80 : 0x00);// Scroll Ycase 0xFF42: return GPU._scy;// Scroll Xcase 0xFF43: return GPU._scx;// Current scanlinecase 0xFF44: return GPU._line; } }, wb: function(addr, val) { switch(addr) {// LCD Controlcase 0xFF40: GPU._switchbg = (val & 0x01) ? 1 : 0; GPU._bgmap = (val & 0x08) ? 1 : 0; GPU._bgtile = (val & 0x10) ? 1 : 0; GPU._switchlcd = (val & 0x80) ? 1 : 0; break;// Scroll Ycase 0xFF42: GPU._scy = val; break;// Scroll Xcase 0xFF43: GPU._scx = val; break;// Background palettecase 0xFF47: for(var i = 0; i < 4; i++) { switch((val >> (i * 2)) & 3) { case 0: GPU._pal[i] = [255,255,255,255]; break; case 1: GPU._pal[i] = [192,192,192,255]; break; case 2: GPU._pal[i] = [ 96, 96, 96,255]; break; case 3: GPU._pal[i] = [ 0, 0, 0,255]; break; } } break; } }
Running one frame
At present, the dispatch loop for the emulator's CPU runs forever, without pause. The most basic interface for an emulator allows for the simulation to be reset or paused; in order to allow for this, a known amount of time must be used as the base unit of the emulator interface. There are three possible units of time that can be used for this:
- Instruction: Providing the opportunity to pause after every CPU instruction. This causes a great deal of overhead, since the dispatch function must be called for each step made by the CPU; at 4.19MHz, many steps must be made for an appreciable amount to happen.
- Scanline: Pausing after the rendering of each line by the GPU. This produces less of an overhead, but the dispatcher must still be called a few thousand times a second; in addition, the emulation can be paused in a state where the canvas display doesn't correspond to the current scanline.
- Frame: Allowing for the emulation to stop after a whole frame is emulated, rendered and pushed to the canvas. This provides the best compromise of timing accuracy and optimal speed, while ensuring that the emulated canvas is consistent with the GPU state.
Since a frame is made of 144 scanlines and a 10-line vertical blank, and each scanline takes 456 clock cycles to run, the length of a frame is 70224 clocks. In conjunction with an emulator-level reset function, which initialises each subsystem at the start of the emulation, the emulator itself can be run, and a rudimentary interface provided.
index.html: Emulator interface
<canvas id="screen" width="160" height="144"></canvas> <a id="reset">Reset</a> | <a id="run">Run</a>
jsGB.js: Reset and dispatch
jsGB = { reset: function() { GPU.reset(); MMU.reset(); Z80.reset(); MMU.load('test.gb'); }, frame: function() { var fclk = Z80._clock.t + 70224; do { Z80._map[MMU.rb(Z80._r.pc++)](); Z80._r.pc &= 65535; Z80._clock.m += Z80._r.m; Z80._clock.t += Z80._r.t; GPU.step(); } while(Z80._clock.t < fclk); }, _interval: null, run: function() { if(!jsGB._interval) { jsGB._interval = setTimeout(jsGB.frame, 1); document.getElementById('run').innerHTML = 'Pause'; } else { clearInterval(jsGB._interval); jsGB._interval = null; document.getElementById('run').innerHTML = 'Run'; } } }; window.onload = function() { document.getElementById('reset').onclick = jsGB.reset; document.getElementById('run').onclick = jsGB.run; jsGB.reset(); };
Testing
Previously shown in Figure 1 is the result of bringing this code together: the emulator is capable of loading and running a graphics-based demo. In this case, the test ROM being loaded is a scrolling test written by Doug Lanford: the background displayed will scroll when one of the directional keypad buttons is pressed. In this particular case, with the keypad un-emulated, a static background is displayed.
In the next part, this piece of the jigsaw will be put in place: a keypad simulation which can provide the appropriate inputs to the emulated program. I'll also be looking at how the keypad works, and how the inputs are mapped into memory.
Imran Nazar <tf@imrannazar.com>, Sep 2010.