Вы находитесь на странице: 1из 74

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.
https://en.wikipedia.org/wiki/Emulator
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 F

Store values to be operated on and


B C
stores results of operations

D E Very quick to read/write


Physically stored in the CPU
H L Have general and special purposes
A - L: 1 byte
SP
SP, PC: 2 bytes

PC
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 1 2 3

0 LD A, B ADD A,B SUB D AND B

1 XOR B OR H RLA DEC BC

2 PUSH HL CALL 0x2BC6 NOP LD D,0x15

3 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 Interpret Execute


Instruction Instruction 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 4

LD A,A 4

CALL (a16) 16

AND (d8) 8

INC D 4
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
Controlledby 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 = Array.new 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 # ROM Bank 0 + n
# write to cartridge
when 0x8000...0xA000 # Video RAM
@memory[i] = v
when 0xA000...0xC000 # RAM Bank
# write to cartridge
when 0xC000..0xFFFF # RAM, Sprites, IO, Stack
@memory[i] = v
end
end
end
MMU

$mmu = MMU.new
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 = Array.new 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 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 2 2 0 0 2 2 0 0
0 0 2 2 0 0 2 2 0 0 0 2 3

0 0 2 2 2 2 2 2 0 0
0 0 2 2 0 0 2 2 0 0
0 0 2 2 0 0 2 2 0 0
0 0 0 0 0 0 0 0 0 0
0 0 3 3 3 3 3 3 0 0
0 0 0 0 0 0 0 0 0 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
SDL.InitSubSystem SDL::INIT_VIDEO
@buffer = FFI::MemoryPointer.new :uint32, SCREEN_WIDTH * SCREEN_HEIGHT
@window = SDL.CreateWindow 'waterfoul', 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SDL::SDL_WINDOW_RESIZABLE
@renderer = SDL.CreateRenderer @window, -1, 0
SDL.SetHint "SDL_HINT_RENDER_SCALE_QUALITY", "2"
SDL.RenderSetLogicalSize @renderer, WINDOW_WIDTH, WINDOW_HEIGHT
@texture = SDL.CreateTexture @renderer, SDL::PIXELFORMAT_ARGB8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT
end

def render(framebuffer)
@buffer.write_array_of_uint32 framebuffer
SDL.UpdateTexture @texture, nil, @buffer, SCREEN_WIDTH * 4
SDL.RenderClear @renderer
SDL.RenderCopy @renderer, @texture, nil, nil
SDL.RenderPresent @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


0 1 2 3 4 5 6 7

8 9 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 0x8000

Bank 0 Bank n
Cartridge: Banking
0x0000 0x4000 0x4001 0x8000

0 1 2 3
Program
Read Byte 4 5 6 7

8 9 10 11
Read
Byte
Cartridge 12 13 14 15
MMU
Controller
16 17 18 19

20 21 22 23
Cartridge: Banking
0x0000 0x4000

0 1 2 3
Program
Change
Write Byte Bank 4 5 6 7

8 9 10
0x4001 0x8000
11
Write
Byte Cartridge 12 13 14 15
MMU
Controller
16 17 18 19

20 21 22 23
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 = Array.new 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 = Array.new MEMORY_SIZE, 0
@cartridge = Cartridge.new game_program
end
end
Updating MMU

class MMU
def [](i)
case i
when 0x0000...0x8000 # ROM Bank 0 + n
@cartridge[i]
when 0x8000...0xA000 # Video RAM
@memory[i]
when 0xA000...0xC000 # RAM Bank
@cartridge[i]
when 0xC000..0xFFFF # RAM, Sprites, IO, Stack
@memory[i]
end
end
end
Bringing Everything
Together
Emulator

class Emulator
end
Emulator

class Emulator
def initialize
@cpu = CPU.new
@ppu = PPU.new
@screen = Screen.new
$mmu = MMU.new
end
end
class Emulator
def initialize(rom_path)
game_program = File.binread(rom_path).bytes
@cpu = CPU.new
@ppu = PPU.new
@screen = Screen.new
$mmu = MMU.new(game_program)
end

def run
loop do
@cpu.tick
@ppu.tick @cpu.cycles
@screen.render @ppu.framebuffer if @gpu.can_render?
end
end
end
What I Didnt
Talk About
Input Controls Memory Registers
CLI
Timer
Interrupts
SDL
Sound
Link Cable Boot ROM
colby-swandale/waterfoul
Thank You!
Sources
Gameboy Opcode Table: http://www.pastraiser.com/cpu/gameboy/
gameboy_opcodes.html

Gameboy CPU Manual: http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf

Gameboy Pandocs: http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf