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
pixels)
15 Hours Battery Life
CPU PPU Memory Cartridge
CPU
CPU
A F
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
0 1 2 3
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
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
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
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
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
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
64kb
Available RAM
Game Program
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
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
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