GameBoy Emulation in JavaScript: Integration

Display mode

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.

Figure 1: jsGB implementation with graphics

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).

AddressRegisterStatus
0xFF40LCD and GPU controlRead/write
0xFF42Scroll-YRead/write
0xFF43Scroll-XRead/write
0xFF44Current scan lineRead only
0xFF47Background paletteWrite only
Table 1: Basic GPU registers

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.

BitFunctionWhen 0When 1
0Background: on/offOffOn
1Sprites: on/offOffOn
2Sprites: size (pixels)8x88x16
3Background: tile map#0#1
4Background: tile set#0#1
5Window: on/offOffOn
6Window: tile map#0#1
7Display: on/offOffOn
Table 2: GPU control register

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-page
		    case 0xF00:
		        if(addr >= 0xFF80)
			{
			    return MMU._zram[addr & 0x7F];
			}
			else
			{
			    // I/O control handling
			    switch(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-page
		    case 0xF00:
		        if(addr >= 0xFF80)
			{
			    MMU._zram[addr & 0x7F] = val;
			}
			else
			{
			    // I/O
			    switch(addr & 0x00F0)
			    {
			        // GPU
			        case 0x40: case 0x50: case 0x60: case 0x70:
				    GPU.wb(addr, val);
				    break;
			    }
			}
			break;
		}
		break;
	}
    }

GPU.js: Register handling

    rb: function(addr)
    {
        switch(addr)
	{
	    // LCD Control
	    case 0xFF40:
	        return (GPU._switchbg  ? 0x01 : 0x00) |
		       (GPU._bgmap     ? 0x08 : 0x00) |
		       (GPU._bgtile    ? 0x10 : 0x00) |
		       (GPU._switchlcd ? 0x80 : 0x00);

	    // Scroll Y
	    case 0xFF42:
	        return GPU._scy;

	    // Scroll X
	    case 0xFF43:
	        return GPU._scx;

	    // Current scanline
	    case 0xFF44:
	        return GPU._line;
	}
    },

    wb: function(addr, val)
    {
        switch(addr)
	{
	    // LCD Control
	    case 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 Y
	    case 0xFF42:
	        GPU._scy = val;
		break;

	    // Scroll X
	    case 0xFF43:
	        GPU._scx = val;
		break;

	    // Background palette
	    case 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:

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.