Emulating a Sega Master System
in Javascript

Matt Godbolt
@mattgodbolt
Use cursor keys or space to navigate.

What's a SMS?


  • 1985 (JP); 1986 (US);1987 (EU)
  • 8-bit Z80
  • 8KB RAM
  • Custom VDP
    • 16KB RAM
    • 256 x 192, 64-colour
  • SN76489 Sound Chip
  • 32, 64, 128, 256KB ROMs

Why the Master System?

Backstory

1995

Why Javascript?

2011

Emulating a computer

  • Emulate the CPU
  • Emulate the video
  • Emulate the sound
  • ...
  • Profit?

Emulating a Z80

  • CISC chip - 900+ instruction
  • IO bus
  • 3.53MHz
    • 4 cycles (1µs) minimum
    • 7 cycles load or add

Emulating a Z80

  • 18 8-bit registers
    • A, B, C, D, E, H, L
    • A', B', C', D', E', H', L'
    • Flags, IRQs, RAM refresh
    • Pairable: AF, BC, DE, HL
  • 4 16-bit registers
    • IX, IY
    • SP, PC

Z80 - Example


21 2a 06       LD HL,0x062a  ; HL = 0x062a
87             ADD A,A       ; A = 2*A
5f             LD E,A
16 00          LD D,0x00     ; DE = (u16)A
19             ADD HL,DE     ; HL = HL + DE
7e             LD A,(HL)     ; A = *(u8*)HL;
23             INC HL        ; HL++
66             LD H,(HL)     ; A = *(u8*)HL;
6f             LD L,A        ; L = A
e9             JP HL         ; jump to HL
                

Z80 - Decoding

  • Optional prefix byte (cb, dd, ed or fd)
  • One byte opcode
  • 0, 1 or 2 operand bytes

af             XOR A, A
3e ff          LD A, 0xff

46             LD B, (HL)
dd 46 0f       LD B, (IX + 0x0f)

5b             LD E, E
ed 5b 19 d0    LD DE, (0xd019)
                

Z80 - Executing


const opcode = readbyte(z80.pc++);

switch (opcode) {
  case 0xaf: // XOR A, A
    z80.a ^= z80.a;
    break;

  case 0x3e: // LD A, constant
    z80.a = readbyte(z80.pc++);
    break;

  case 0x46: // LD B, (HL)
    z80.b = readbyte((z80.h<<8) | z80.l);
    break;

  // and so on for all the other instructions

  case 0xdd:
    return handle_prefix_dd();
  // ... etc
                
… but there's more

Z80 - Executing

z80 die
  • Flags:
    • carry, half-carry
    • zero, sign
    • overflow, parity
  • IRQs
  • Input/output

Z80 - Executing

Z80 - JSSpeccy

  • Perl -> C
  • C -> Javascript
  • Avoids very repetitive code:
    • 938 instructions
    • 1242 lines of perl
    • 5400+ lines of Javascript

ADD A, imm


const sz53_table[256], hc_add_table[8], oflo_add_table[8] = // ... computed once

function add_a_imm() {
  const c = readbyte(z80.pc++);
  const result = z80.a + c;

  const lookup = ((z80.a & 0x88) >> 3) | ((c & 0x88) >> 2) | ((result & 0x88) >> 1);
  z80.f = (result & 0x100 ? C_FLAG : 0) // Carry
        | hc_add_table[lookup & 0x07]   // 1/2 carry
        | oflo_add_table[lookup >> 4]   // overflow
        | sz53_table[z80.a];            // sign, zero, “undef” bits

  z80.a = result & 0xff;
  clock += 7;
}
                

Memory Map

fffc-ffff Paging registers
e000-fffb Mirror of 8KB RAM
c000-dfff 8KB RAM
8000-bfff 16KB ROM page 2
(or Cartridge RAM)
4000-7fff 16KB ROM page 1
0400-3fff 15KB ROM page 0
0000-03ff 1KB ROM bank 0
ffff Page 2 ROM
fffe Page 1 ROM
fffd Page 0 ROM
fffc ROM/RAM select
Emulating Memory

const romBanks = [new Uint8Array(16384), ...];  // 16KB blocks from ROM files
const pages = [0, 1, 2];                        // Paging registers
const ram = new Uint8Array(8192);               // 8KB of on-board RAM

function readByte(address) {
  const offset = address & 0x3fff;              // offset withing a 16KB page
  if (address < 0xc000) {                       // It's ROM: which page?
    const page = (address < 0x400) ? 0
                                   : pages[address >>> 14];
    return romBanks[page][offset];
  }

  // RAM, but only 8K's worth.
  return ram[offset & 0x1fff]);
}
            
Emulating Memory

function writeByte(address, byte) {
  if (address < 0xc000)
    return; // It's ROM!

  if (address === 0xfffc)
    /*handle rom/ram*/;
  else if (address >= 0xfffd)
    pages[address - 0xfffd] = byte;
  else ram[address & 0x1fff] = byte;
}
            

VDP

VDP

  • 16KB RAM
  • 8 x 8 4bpp "tiles"
  • 2 16-entry palettes, 6-bit colour
  • Background table
  • Sprite table
  • IRQ generator

Tiles

Background

  • 32 x 24 map of tiles
  • Two bytes/tile:
    • Tile index (9 bits)
    • Horizontal, vertical flip (2 bits)
    • Palette select (1 bit)
    • Sprite overwrite (1 bit)
    • 3 "user" bits

Background

Background


                    200212202020202020202020202020202020061620202020474f4c4420202020
                    2003132020202020202020202020202020200717303120202020202020203420
                    0000000000000000000000006a7a829284940000000000000000000000000000
                    0000000000000000000000006b7b839385950000000000000000000000000000
                    00006a7a82928494000000000000000000000000000000000000000000000000
                    00006b7b83938595000000000000000000000000000000000000000000000000
                    

Sprites

  • 64 sprites
  • Global choice of 8 x 8 or 8 x 16
  • 256 byte table
    • x (8 bits)
    • y (8 bits)
    • tile index (8 bits)

Sprites

Sprites

x y t
0 116 111 16
1 116 127 18
2 124 111 20
3 124 127 22
4 132 111 24
5 132 127 26
6 211 111 102
7 211 127 104
8 219 111 106
9 219 127 108
x y t
10 147 35 44
11 155 35 46
12 155 208 142
13 155 127 144
14 219 208 116
15 0 0 0
16 0 0 0
...
62 0 0 0
63 0 0 0

Sprites

  • For each line:
    • Scan sprite table
    • Look for vertical span overlaps
    • Limit of 8
  • For each pixel:
    • Scan 8-sprite list
    • Look for first opaque match
    • Else use background

Sound

Sound

  • 4 channels:
    • 3 square wave
    • 1 noise
  • One Z80 IO bus register:
    
                                    1rr1dddd  ; volume
                                    1rr0hhhh  ; freq (high)
                                    0-llllll  ; freq (low)
                            

Sound - Tone


for (var i = 0; i < length; ++i) {
  counter[chan] -= soundchipFreq / sampleRate;
  if (counter[chan] < 0) {
    counter[chan] += reg[chan];
    out[chan] ^= 1;
  }
  result[i] += out[chan] ? 1 : -1 * vol[chan];
}
                

Sound - Noise


let lfsr = 1 << 15;

function shiftLfsrWhiteNoise() {
  const bit = (lfsr & 1) ^ ((lfsr & (1<<3)) >> 3);
  lfsr = (lfsr >> 1) | (bit << 15);
  return lfsr & 1;
}
                

Putting it all together

Conclusion

  • Time away from family can be constructive
  • Reliving your childhood is fun
  • Browsers are amazingly fast

More things