Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Making A Gameboy Emulator in Ruby

Colby Swandale
February 09, 2017

Making A Gameboy Emulator in Ruby

Released in 1989 the Gameboy was the first handheld console of the Gameboy series to be released by Nintendo. It featured games such as Pokemon Red & Blue, Tetris, Super Mario Land and went on to sell 118 million units worldwide.

My talk discusses what components make up a Gameboy, such as the CPU, RAM, Graphics and Game Cartridge. How each component works as an individual unit and works as part of a higher machine. Then how to imitate the behaviour of the Gameboy in ruby to make an emulator.

Colby Swandale

February 09, 2017
Tweet

More Decks by Colby Swandale

Other Decks in Technology

Transcript

  1. • Developed by Nintendo Japan • Released April 1989 •

    Sold 118 million units (including GBC) • 1049 Games including Tetris, Super Mario Land, Pokemon Red & Blue • 15 Hours Battery Life Nintendo Gameboy
  2. 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
  3. CPU

  4. • Sharp LR35902 • 4.19Mhz clockspeed • 8 bit operations

    • 16 bit memory bus • Similar to the Zilog z80 & Intel 8080 processor CPU
  5. A F B C D E H L SP PC

    CPU: Registers • Stores data • Very quick relative to higher memory • Physically sits in the CPU • General and special purposes • A - L: 1 byte • SP, PC: 2 bytes
  6. class CPU def initialize @a, @b, @c, @d, @e, @h,

    @l, @f = 0x00 @pc, @sp = 0x0000 end end CPU
  7. 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: Instructions
  8. Opcode (hex) 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
  9. Opcode (hex) 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
  10. 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: Opcode Table
  11. class CPU def inc_b result = @b + 1 @b

    = result & 0xFF end end CPU: INC B
  12. Fetch Next Instruction Read Byte Program Counter CPU: Fetch And

    Execute Interpret Instruction Execute Instruction Memory
  13. class CPU def tick operation = $mmu[@pc] @pc += 1

    self.public_send OPCODE[operation] end end CPU: Tick
  14. Instruction Cycles NOP 4 LD A,A 4 CALL (a16) 16

    AND (d8) 8 INC D 4 CPU: Timing
  15. class CPU def tick @cycles = OPCODE_TIMING[operation] end end CPU:

    Timing operation = $mmu[@pc] @pc += 1 self.public_send OPCODE[operation]
  16. • Controlled by the Memory Management Unit • 64 KB

    Storage • 65,535 (0xFFFF) address space • Spread Across multiple chips
  17. class MMU MEMORY_SIZE = 65_536 # addresses def initialise @memory

    = Array.new MEMORY_SIZE, 0 end end Memory Management Unit
  18. 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 # User RAM, Sprites, IO, Stack @memory[i] end end end MMU
  19. 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 # User RAM, Sprites, IO, Stack @memory[i] = v end end end MMU
  20. 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
  21. • 160 x 144 pixels (renders 255x255) • 4 color

    pallete • Emulates CRT • 60hz refresh rate • 8Kb VRAM Picture Processing Unit
  22. PPU: Background, Windows and Sprites Background World, occupies entire screen

    Windows Menus, Fixed positions on screen Sprites Characters, items, objects moving indepently of the background
  23. PPU: Modes 1 Frame 80 172 204 4560 OAM Read

    VRAM Read HBlank VBlank 1 Line NO VRAM NO VRAM VRAM VRAM
  24. 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 PPU: Modes
  25. PPU: Tile 8x8 pixels 20x16 tiles on screen 32x32 tiles

    rendered by PPU 16 Bytes 2 bits per pixel encoding 4 Colors White, Light Grey, Dark Grey, Black
  26. PPU: Tile 8x8 pixels 20x16 tiles on screen 32x32 rendered

    by PPU 16 Bytes 2 bits per pixel encoding 4 Colors White, Light Grey, Dark Grey, Black 0 0 1 1 1 1 1 1 1 0 1 0 1 0 0 0 1 1 0 0 0 0 1 1 1 0 1 0 1 0 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 1 0 1 1 0 0 1 1 0 1 0 1 1 0 1 0 1 0 1 1 0 0 1 1 0 0 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0
  27. Map

  28. • Holds pointer to tile in memory • 32x32 tiles

    • Used for Backgrounds and Windows PPU: Map
  29. 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 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 0 2 3 PPU: Map
  30. OAM

  31. • 29 different cartridge types • Up to 2Mb ROM

    • Up to 32Kb of external memory • supports external hardware i.e: RTC, RAM • Controlled by the Memory Bank Controller Cartridge
  32. 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
  33. 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 .. 255
  34. 0x0000 … 0x4000 0x4001 … 0x8000 0 1 2 3

    4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 MMU Cartridge Controller Cartridge: Banking Read Byte Program Write Byte
  35. 0 1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 MMU Cartridge Controller 0x0000 … 0x4000 0x4001 … 0x8000 Write Byte Cartridge: Banking Program Write Byte Change Bank
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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 Cartridge
  41. class MMU MEMORY_SIZE = 65_536 # bytes def initialise(game_program) @memory

    = Array.new MEMORY_SIZE, 0 @cartridge = Cartridge.new game_program end end Updating MMU
  42. 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 Updating MMU
  43. class Emulator def initialize(rom_path) game = File.binread(rom_path).bytes @cpu = CPU.new

    @ppu = PPU.new @screen = Screen.new $mmu = MMU.new(game) end end Emulator
  44. class Emulator def run loop do @cpu.tick @ppu.tick @cpu.cycles @screen.render

    @ppu.framebuffer if @ppu.can_render? end end end def initialize(rom_path) game = File.binread(rom_path).bytes @cpu = CPU.new @ppu = PPU.new @screen = Screen.new $mmu = MMU.new(game) end