ruby2600 - an Atari 2600 emulator written in Ruby

3,571 views

Published on

The emulator was presented to the public at RubyConfBr 2013. Its source code can be downloaded at http://github.com/chesterbr/ruby2600

The video is on YouTube: http://www.youtube.com/watch?v=S3qAOu41CxE

Published in: Technology
0 Comments
5 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
3,571
On SlideShare
0
From Embeds
0
Number of Embeds
60
Actions
Shares
0
Downloads
14
Comments
0
Likes
5
Embeds 0
No embeds

No notes for slide

ruby2600 - an Atari 2600 emulator written in Ruby

  1. 1. An Atari 2600 emulator 100% written in Ruby (and RSpec!) Carlos Duarte do Nascimento (Chester) @chesterbr / http://chester.me
  2. 2. If so, you probably had this...
  3. 3. ...or one of these...
  4. 4. ...or you used an emulator http://stella.sourceforge.net
  5. 5. Emulator A program that runs software written for one type of computer system in another type by simulating the hardware of the original system
  6. 6. ruby2600 ● Atari 2600 emulator ● Written in Ruby ● Runs quite a few classic games ● Open-source http://github.com/chesterbr/ruby2600
  7. 7. Why? There are great emulators out there, but they strive for speed above readability A test-driven emulator in a high-level language is a great learning tool Always wondered how much TDD would help on wildly unfamiliar territory (also: why not? ☺)
  8. 8. Work in progress! ● A few subtle bugs ● Does not run every game ● Not full-speed ● No sound http://github.com/chesterbr/ruby2600
  9. 9. We'll see ● How the Atari works ● CPU emulation ● Architecture (just a bit) ● Ruby2600 in action ● The future
  10. 10. @chesterbr http://chester.me About me (Chester) © Ila Fox - http://www.ilafox.com
  11. 11. Building an Emulator: How the Atari 2600 works
  12. 12. Let's peek inside... (Atari 2600 Jr. printed circuit board)
  13. 13. Cartridge connector
  14. 14. CPU: 6507
  15. 15. Video:TIA
  16. 16. Everything else: RIOT (6532)
  17. 17. Challenging specs ● CPU speed: 1.19 MHz (not GHz) ● Max cart (program) size: 4 KB ● RAM: 128 bytes ● Video RAM: 0KB (game code has to drive TIA into generating each scanline in realtime)
  18. 18. Atari 2600 in a nutshell The CPU reads a game program from the ROM chip on the cartrigde It confgures the pixels generated by TIA, using RAM, I/O and timers provided by the RIOT chip Our goal: simulate this in software
  19. 19. Building an Emulator: CPU image CC-BY Konstantin Lanzet
  20. 20. 65xx family: in the past... http://en.wikipedia.org/wiki/MOS_Technology_6502#Computers_and_games
  21. 21. ...and in the future! http://en.wikipedia.org/wiki/MOS_Technology_6502#In_popular_culture
  22. 22. The 6507 CPU Reads instructions from the cartridge that manipulate and transfer bytes between chips, keeping state on internal registers and flags http://en.wikipedia.org/wiki/Von_Neumann_architecture
  23. 23. Emulated 6507 As it executes each instruction, it keeps instance variables for registers (@a, @x, @y), flags (@n, @z, ...) and @memory as an array
  24. 24. CPU.rb module Ruby2600 class CPU attr_accessor :memory attr_accessor :pc, :a, :x, :y, :s # Flags (P register): nv--dizc attr_accessor :n, :v, :d, :i, :z, :c def step ...runs a instruction... end ...
  25. 25. module Ruby2600 class CPU attr_accessor :memory attr_accessor :pc, :a, :x, :y, :s # Flags (P register): nv--dizc attr_accessor :n, :v, :d, :i, :z, :c def step ...runs a instruction... end ... CPU.rb TESTS FIRST!TESTS FIRST!
  26. 26. Assembly debugging == PAIN! To avoid it, we need technical specifcations that are easy to read, yet detail-oriented enough to test emulator code RSpec does the job! http://rspec.info
  27. 27. 6507 Instruction Set, 1/2
  28. 28. 6507 Instruction Set, 2/2
  29. 29. CPU_spec.rb context 'INX' do before do cpu.memory[0] = 0xE8 # INX cpu.pc = 0x0000 cpu.x = 0x07 end it 'should advance PC by one' do cpu.step cpu.pc.should == 0x0001 end it 'should set X value' do cpu.step cpu.x.should == 0x08 end ...
  30. 30. Using shared examples shared_examples_for 'advance PC by one' do it { expect { cpu.step }.to change { cpu.pc }.by 1 } end shared_examples_for 'set X value' do |expected| it do cpu.step value = cpu.x value.should be(expected), "Expected: #{hex_bye(expected)}, " + "found: #{hex_byte(value)}" end end
  31. 31. More syntactic sugar 1.upto 3 do |number| shared_examples_for "advance PC by #{number.humanize}" do it { expect { cpu.step }.to change { cpu.pc }.by number } end end RSpec.configure do |c| c.alias_it_should_behave_like_to :it_should, 'should' end
  32. 32. “Literate Testing” context 'INX' do before do cpu.memory[0] = 0xE8 # INX cpu.x = 0x07 end it_should 'advance PC by one' it_should 'take two cycles' it_should 'set X value', 0x08 it_should 'reset Z flag' it_should 'reset N flag' ... http://en.wikipedia.org/wiki/Literate_programming
  33. 33. Less effort → better coverage ● Each instruction tested in every possible addressing mode ● Easy to understand, high-level tests ● Tests become a specifcation - specs!
  34. 34. CPU.rb module Ruby2600 class CPU attr_accessor :memory attr_accessor :pc, :a, :x, :y, :s # Flags (P register): nv--dizc attr_accessor :n, :v, :d, :i, :z, :c def step ...runs a instruction... end ...
  35. 35. def step fetch decode execute return @time_in_cycles end CPU.rb
  36. 36. No byte/word data types :-(CPU.rb def fetch @opcode = memory[@pc] @param_lo = memory[word(@pc + 1)] @param_hi = memory[word(@pc + 2)] @param = @param_hi * 0x100 + @param_lo @pc = word(@pc + OPCODE_SIZES[@opcode]) end Lookup table
  37. 37. more lookup! CPU.rb def decode if (@opcode & 0b11111) == BXX @instruction = BXX elsif (@opcode & 0b11111) == SCX @instruction = SCX else @instruction_group = @opcode & 0b11 mode_in_group = (@opcode & 0b11100) >> 2 @addressing_mode = ADDRESSING[ @instruction_group][mode_in_group] @instruction = @opcode & 0b11100011 end @time_in_cycles = time_in_cycles end
  38. 38. CPU.rb def execute case @instruction when LDA flag_nz @a = load when STA store @a when JMPabs @pc = @param when BXX @pc = branch ...
  39. 39. Final result Full instruction set covered by more than 1,700 tests Only one CPU bug so far had to be debugged in-game
  40. 40. Building an Emulator:Building an Emulator: ArchitectureArchitecture image CC-BYimage CC-BY Benjamin EshanBenjamin Eshan
  41. 41. ruby2600 class diagram
  42. 42. Memory-based I/O CPU “talks” to other chips by reading and writing specifc memory locations: 0000-002C – TIA (write) 0030-003D – TIA (read) 0080-00FF – RIOT (RAM) 0280-0297 – RIOT (I/O,Timer) F000-FFFF – Cartridge (ROM) (very simplified, see: http://nocash.emubase.de/2k6specs.htm)
  43. 43. Bus Acts as a memory façade to the CPU, routing reads and writes to the appropriate chip class It is also the interface where we “plug” an UI (anything that displays images and reads keypresses)
  44. 44. ruby2600 class diagram
  45. 45. Ruby spice: duck typing Instead of defning read/write methods, make Bus,TIA, RIOT and Cart classes “quack” like arrays
  46. 46. TIA.rb / RIOT.rb / Cart.rb (and also bus.rb!) def [](position) ...return value for position... end def []=(position, value) ...react to writing value to position... end
  47. 47. Benefts ● High decoupling: CPU,TIA, RIOT and Cart are (mostly) independent ● We can use regular arrays as mocks for TIA, RIOT, Cart and Bus itself! ● We can “plug” different UIs (e.g.: text-mode rendering, network multiplayer, joystick interface, etc.)
  48. 48. Cart.rb (full source) class Cart def initialize(rom_file) @bytes = File.open(rom_file, "rb") { |f| f.read }.unpack('C*') @bytes += @bytes if @bytes.count == 2048 end def [](address) @bytes[address] end def []=(address, value) # Don't write to Read-Only Memory, duh! end end
  49. 49. Timing As TIA generates each pixel for each scanline, it will "tick" the CPU and RIOT to keep everything in sync image cc-by Steve Evans
  50. 50. TIA.rb def draw_scanline scanline = [] 0.upto(SCANLINE_WIDTH) do |pixel| scanline << topmost_pixel tick_other_chips pixel end return scanline end def topmost_pixel ... def tick_other_chips @cpu.tick if pixel % 3 == 2 @riot.tick if pixel % 3 == 0 end TIA runs 3x faster than CPU and RIOT
  51. 51. BALL PLAYFIELD PLAYERS (2) MISSILES (2) Graphic Objects
  52. 52. Graphic Objects To keep TIA's responsibility focused on building the frames, we will offload object drawing to separate classes (Playfeld, Player, Ball, Missile) For the common behavior, should we use composition or inheritance?
  53. 53. Composition and Inheritance A common ancestor (Graphic) contains the common behavior, and each class adds its own flavor Behavior that does not defne a Graphic (position counters) is better suited for a separate class, using composition instead of inheritance
  54. 54. ruby2600 class diagram
  55. 55. Building an Emulator: Let's run it! reproduction:reproduction: Young FrankensteinYoung Frankenstein
  56. 56. Building an Emulator: Speeding up TBBT 1-06 "The Middle-Earth Paradigm", © 2007 Chuck Lorre Productions / WB Television
  57. 57. Knuth, Donald (December 1974). "Structured Programmingwith go to Statements",ACM Journal Me and Prof. Knuth hanging out. We're, like, bros. Really. "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil"
  58. 58. Refactoring Thanks to test coverage, we can safely play around and refactor for speed (while keeping it readable)
  59. 59. JRuby We have a small working set (no more than 5KB) handled in very tight loops Have to measure, but it smells like the kind of modern-CPU-friendly code that could be generated via JIT http://en.wikipedia.org/wiki/Just-in-time_compilation
  60. 60. If nothing else works The modular design gives us freedom to rewrite any part using other languages and/or replace them with existing code http://www.6502.org/tools/emu/#emulation
  61. 61. Questions? Thank you! @chesterbr http://slideshare.net/chesterbr http://github.com/chesterbr/ruby2600
  62. 62. Credits and License This presentation is available under the Creative Commons “by-nc” 3.0 license noticing the exceptions below Images from third parties were included (with due credits) under fair use assumption and/or under their respective licenses. These are excluded from the license above. Atari™ and likewise characters/games/systems are mentioned uniquely for illustrative purposes under fair use assumption. They are property of their rights holders, and are also excluded from the license above.

×