Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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
CPU
Registers
CPU: Registers
A
L
SP
PC
A - L: 1 byte
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
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
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
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
0x0
0xFFFF
Game Program
Video
General
IO
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 #
# 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 = MMU.new
Picture
Processing Unit
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
Not
Screen
Screen
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
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 = 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
@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 = 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
Interrupts
Link Cable
Timer
Sound
SDL
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