Making a Gameboy
Emulator in Ruby
Colby Swandale
0xColby
What is an
emulator?
In computing, an emulator is
hardware or software that
enables one computer system
(called the host) to behave like
another computer system (called
the guest). An emulator typically
enables the host system to run
software or use peripheral
devices designed for the guest
system.
[Link]
Nintendo Gameboy
Developed
by Nintendo Japan
Released April 1989
Sold 118.69 million units (includes GBC)
Featured Games: Tetris, Super Mario
Land, Pokemon Red & Blue
LCD Monochrome display (160x144
pixels)
15 Hours Battery Life
CPU PPU Memory Cartridge
CPU
CPU
Sharp LR35902 (made specifically for the GB)
4.19Mhz clockspeed
8 bit processor
16 bit memory bus
Similar to the Zilog z80 & Intel 8080 processor
Registers
CPU: Registers
A
L
SP
PC
Store values to be operated on and
stores results of operations
Very quick to read/write
Physically stored in the CPU
Have general and special purposes
A - L: 1 byte
SP, PC: 2 bytes
CPU
class CPU
end
CPU
class CPU
def initialize
@a, @b, @c, @d, @e, @h, @l, @f = 0x00
@pc, @sp = 0x0000
end
end
Instructions
CPU: Instructions
LD A, B
ADD A,B
SUB D
AND B
XOR B
OR H
RLA
DEC BC
PUSH HL
CALL 0x2BC6
NOP
LD D,0x15
LD 0x15,A
POP BC
EI
HALT
CPU: Opcode Table
0
LD A, B
ADD A,B
SUB D
AND B
XOR B
OR H
RLA
DEC BC
PUSH HL
CALL 0x2BC6
NOP
LD D,0x15
LD 0x15,A
POP BC
EI
HALT
CPU: Opcode Table
class CPU
OPCODE = [
:nop, :ld_bc_d16, :ld_bc_a, :inc_bc,
:inc_b, :dec_b, :ld_b_d8, :rlca, :ld_a16_sp,
:add_hl_bc, :ld_a_bc, :dec_bc, :inc_c, :dec_c,
:ld_c_d8, :rrca, :stop_0, :ld_de_d16, :ld_de_a,
:inc_de, :inc_d, :dec_d, :ld_d_d8, :rla, :jr_r8,
:add_hl_de, :ld_a_de, :dec_de, :inc_e, :dec_e,
:ld_e_d8, :rra,
...
end
CPU: LD B,C
class CPU
def ld_b_c
@b = @c
end
end
CPU: INC B
class CPU
def inc_b
result = @b + 1
@b = result & 0xFF
end
end
CPU: LD C,d8
class CPU
def ld_c_d8
@c = $mmu[@pc]
@pc += 1
end
end
Fetch and Execute
CPU: Fetch And Execute
Memory
Read Byte
Program
Counter
Fetch Next
Instruction
Interpret
Instruction
Execute
Instruction
CPU: Tick
class CPU
def tick
operation_index = $mmu[@pc]
@pc += 1
self.public_send OPCODE[operation_index]
end
end
CPU: Timing
Instruction
Cycles
NOP
LD A,A
CALL (a16)
16
AND (d8)
INC D
CPU: Timing
class CPU
def tick
operation_index = $mmu[@pc]
@pc += 1
self.public_send OPCODE[operation_index]
@cycles = OPCODE_TIMING[operation_index]
end
end
Memory
Controlled
by the Memory
Management Unit
64 KB Storage
65,535 (0xFFFF) address space
MMU: Memory Map
0x0
0xFFFF
Game Program
Video
General
IO
Memory Management Unit
class MMU
MEMORY_SIZE = 65_536 # addresses
def initialise(game_program)
@game_program = game_program
@memory = [Link] MEMORY_SIZE, 0
end
end
MMU
class MMU
def [](i)
case i
when 0x0000...0x8000 # ROM Bank 0 + n
# read from cartridge
when 0x8000...0xA000 # Video RAM
@memory[i]
when 0xA000...0xC000 # RAM Bank
# read from cartridge
when 0xC000..0xFFFF # RAM, Sprites, IO, Stack
@memory[i]
end
end
end
MMU
class MMU
def []=(i, v)
case i
when 0x0000...0x8000 #
# write to cartridge
when 0x8000...0xA000 #
@memory[i] = v
when 0xA000...0xC000 #
# write to cartridge
when 0xC000..0xFFFF #
@memory[i] = v
end
end
end
ROM Bank 0 + n
Video RAM
RAM Bank
RAM, Sprites, IO, Stack
MMU
$mmu = [Link]
Picture
Processing Unit
GPU: Reading Lines
GPU: Memory
0x0
0xFFFF
Game Program
Video
General
IO
PPU
class PPU
FRAMEBUFFER_SIZE = 23_040 # 160 x 144 (screen size)
def initialize
@framebuffer = [Link] FRAMEBUFFER_SIZE, 0
@mode = :vertical_blank
@modeclock = 0
end
end
PPU: Modes
Sprite Read
Video Read
Horizontal Blank
Vertical Blank
PPU: Modes
Sprite Read
Video Read
Horizontal Blank
Vertical Blank
PPU: Modes
Sprite Read
Video Read
Horizontal Blank
Vertical Blank
PPU: Modes
Sprite Read
Video Read
Horizontal Blank
Vertical Blank
PPU: Modes
Sprite Read
Video Read
Horizontal Blank
Vertical Blank
PPU: Modes
class PPU
def tick(cycles)
@modeclock += cycles
case @mode
when :horizontal_blank
hblank if @modeclock >= 80
when :vertical_blank
vblank if @modeclock >= 172
when :sprite_read
oam if @modeclock >= 204
when :vram_read
vram if @modeclock >= 4560
end
end
end
Tile System
PPU: Tile System
Not
enough memory for
a frame buffer
8x8 pixels = 1 title
PPU: Tile System
0
Screen
Screen
160 x 144 Display
60hz refresh rate
Monochrome display (black,
white, light grey, dark grey)
Screen
module Waterfoul
class Screen
def initialize
[Link] SDL::INIT_VIDEO
@buffer = FFI::[Link] :uint32, SCREEN_WIDTH * SCREEN_HEIGHT
@window = [Link] 'waterfoul', 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SDL::SDL_WINDOW_RESIZABLE
@renderer = [Link] @window, -1, 0
[Link] "SDL_HINT_RENDER_SCALE_QUALITY", "2"
[Link] @renderer, WINDOW_WIDTH, WINDOW_HEIGHT
@texture = [Link] @renderer, SDL::PIXELFORMAT_ARGB8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT
end
def render(framebuffer)
@buffer.write_array_of_uint32 framebuffer
[Link] @texture, nil, @buffer, SCREEN_WIDTH * 4
[Link] @renderer
[Link] @renderer, @texture, nil, nil
[Link] @renderer
end
end
end
Cartridge
Cartridge
29 different cartridge types
Up to 2Mb ROM space
Up to 32Kb of external memory
supports external hardware i.e: RTC, RAM,
Rumble
Controlled by the Memory Bank Controller
Cartridge: Memory
64kb
Available RAM
Game Program
0x0
0xFFFF
0xFFFFFF
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
16KB
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Cartridge: Memory
Game Program
Video
General
IO
Cartridge: Memory
0x0
0x4000
Bank 0
0x8000
Bank n
Cartridge: Banking
0x0000 0x4000 0x4001 0x8000
Program
Read Byte
Read
Byte
MMU
Cartridge
Controller
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Cartridge: Banking
0x0000 0x4000
Program
Write Byte
Change
Bank
Write
Byte
MMU
Cartridge
Controller
12
13
14
15
16
17
18
19
20
21
22
23
10
11
0x4001 0x8000
require 'forwardable'
class Cartridge
extend Forwardable
CARTRIDGE_TYPE_MEM_LOC = 0x147
def_delegators :@mbc, :[], :[]=
def initialize(program)
cartridge_type = program[CARTRIDGE_TYPE_MEM_LOC]
@mbc = cartrdige_controller(cartridge_type, program)
end
def cartrdige_controller type, rom
controller_const(type).new rom
end
end
class Cartridge
def controller_const(controller_byte)
case controller_byte
when 0x00, 0x8, 0x9
MBC::ROM
when 0x1, 0x2, 0x3
MBC::MBC1
when 0x5, 0x6
MBC::MBC2
when 0xF, 0x10, 0x11, 0x12, 0x13
MBC::MBC3
when 0x15, 0x16, 0x17
MBC::MBC4
when 0x19, 0x1B, 0x1C, 0x1D, 0x1E
MBC::MBC5
end
end
end
Cartridge
class MBC::MBC1
EXTERNAL_RAM_SIZE = 0x2000
def initialize(program)
@rom_bank = 1
@ram_bank = 1
@game_program = program
@ram = [Link] EXTERNAL_RAM_SIZE, 0
end
end
Cartridge
class MBC::MBC1
def [](i)
case i
when 0x0...0x4000 # ROM Bank 0
@game_program[i]
when 0x4000...0x8000 # ROM Bank n
addr = i - 0x4000
offset = @rom_bank * 0x4000
@game_program[offset + addr]
end
end
end
Cartridge
class MBC::MBC1
def []=(i,v)
case i
when 0x2000...0x4000
@rom_bank = v
when 0x4000...0x6000
@ram_bank = v
when 0xA000...0xC000
offset = @ram_bank * 0x8000
@ram[offset + addr] = v
end
end
end
Updating the
MMU
Updating MMU
class MMU
MEMORY_SIZE = 65536 # bytes
def initialise(game_program)
@memory = [Link] MEMORY_SIZE, 0
@cartridge = [Link] game_program
end
end
Updating MMU
class MMU
def [](i)
case i
when 0x0000...0x8000
@cartridge[i]
when 0x8000...0xA000
@memory[i]
when 0xA000...0xC000
@cartridge[i]
when 0xC000..0xFFFF
@memory[i]
end
end
end
# ROM Bank 0 + n
# Video RAM
# RAM Bank
# RAM, Sprites, IO, Stack
Bringing Everything
Together
Emulator
class Emulator
end
Emulator
class Emulator
def initialize
@cpu = [Link]
@ppu = [Link]
@screen = [Link]
$mmu = [Link]
end
end
class Emulator
def initialize(rom_path)
game_program = [Link](rom_path).bytes
@cpu = [Link]
@ppu = [Link]
@screen = [Link]
$mmu = [Link](game_program)
end
def run
loop do
@[Link]
@[Link] @[Link]
@[Link] @[Link] if @gpu.can_render?
end
end
end
What I Didnt
Talk About
Input Controls
Memory Registers
CLI
Interrupts
Link Cable
Timer
Sound
SDL
Boot ROM
colby-swandale/waterfoul
Thank You!
Sources
Gameboy Opcode Table: [Link]
gameboy_opcodes.html
Gameboy CPU Manual: [Link]
Gameboy Pandocs: [Link]