An Atari 2600 emulator
100% written in Ruby
(and RSpec!)
Carlos Duarte do Nascimento (Chester)
@chesterbr / http://chester.me
If so, you probably had this...
...or one of these...
...or you used an emulator
http://stella.sourceforge.net
Emulator
A program that runs software written
for one type of computer system in
another type by simulating the
hardware of the original system
ruby2600
● Atari 2600 emulator
● Written in Ruby
● Runs quite a few classic games
● Open-source
http://github.com/chesterbr/ruby2600
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? ☺)
Work in progress!
● A few subtle bugs
● Does not run every game
● Not full-speed
● No sound
http://github.com/chesterbr/ruby2600
We'll see
● How the Atari works
● CPU emulation
● Architecture (just a bit)
● Ruby2600 in action
● The future
@chesterbr
http://chester.me
About me (Chester)
© Ila Fox - http://www.ilafox.com
Building an Emulator:
How the Atari 2600 works
Let's peek inside...
(Atari 2600 Jr. printed circuit board)
Cartridge connector
CPU: 6507
Video:TIA
Everything else: RIOT (6532)
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)
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
Building an Emulator:
CPU
image CC-BY Konstantin Lanzet
65xx family: in the past...
http://en.wikipedia.org/wiki/MOS_Technology_6502#Computers_and_games
...and in the future!
http://en.wikipedia.org/wiki/MOS_Technology_6502#In_popular_culture
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
Emulated 6507
As it executes each instruction, it
keeps instance variables for registers
(@a, @x, @y), flags (@n, @z, ...) and
@memory as an array
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
...
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!
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
6507 Instruction Set, 1/2
6507 Instruction Set, 2/2
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
...
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
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
“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
Less effort → better coverage
● Each instruction tested in every
possible addressing mode
● Easy to understand, high-level tests
● Tests become a specifcation - specs!
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
...
def step
fetch
decode
execute
return @time_in_cycles
end
CPU.rb
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
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
CPU.rb
def execute
case @instruction
when LDA
flag_nz @a = load
when STA
store @a
when JMPabs
@pc = @param
when BXX
@pc = branch
...
Final result
Full instruction set covered
by more than 1,700 tests
Only one CPU bug so far
had to be debugged in-game
Building an Emulator:Building an Emulator:
ArchitectureArchitecture
image CC-BYimage CC-BY Benjamin EshanBenjamin Eshan
ruby2600 class diagram
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)
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)
ruby2600 class diagram
Ruby spice: duck typing
Instead of defning read/write
methods, make Bus,TIA, RIOT and
Cart classes “quack” like arrays
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
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.)
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
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
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
BALL
PLAYFIELD
PLAYERS (2)
MISSILES (2)
Graphic Objects
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?
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
ruby2600 class diagram
Building an Emulator:
Let's run it!
reproduction:reproduction: Young FrankensteinYoung Frankenstein
Building an Emulator:
Speeding up
TBBT 1-06 "The Middle-Earth Paradigm", © 2007 Chuck Lorre Productions / WB Television
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"
Refactoring
Thanks to test coverage, we can safely
play around and refactor for speed
(while keeping it readable)
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
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
Questions?
Thank you!
@chesterbr
http://slideshare.net/chesterbr
http://github.com/chesterbr/ruby2600
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.

ruby2600 - an Atari 2600 emulator written in Ruby

  • 1.
    An Atari 2600emulator 100% written in Ruby (and RSpec!) Carlos Duarte do Nascimento (Chester) @chesterbr / http://chester.me
  • 3.
    If so, youprobably had this...
  • 4.
    ...or one ofthese...
  • 5.
    ...or you usedan emulator http://stella.sourceforge.net
  • 6.
    Emulator A program thatruns software written for one type of computer system in another type by simulating the hardware of the original system
  • 7.
    ruby2600 ● Atari 2600emulator ● Written in Ruby ● Runs quite a few classic games ● Open-source http://github.com/chesterbr/ruby2600
  • 8.
    Why? There are greatemulators 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? ☺)
  • 9.
    Work in progress! ●A few subtle bugs ● Does not run every game ● Not full-speed ● No sound http://github.com/chesterbr/ruby2600
  • 10.
    We'll see ● Howthe Atari works ● CPU emulation ● Architecture (just a bit) ● Ruby2600 in action ● The future
  • 11.
  • 12.
    Building an Emulator: Howthe Atari 2600 works
  • 13.
    Let's peek inside... (Atari2600 Jr. printed circuit board)
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
    Challenging specs ● CPUspeed: 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)
  • 19.
    Atari 2600 ina 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
  • 20.
    Building an Emulator: CPU imageCC-BY Konstantin Lanzet
  • 21.
    65xx family: inthe past... http://en.wikipedia.org/wiki/MOS_Technology_6502#Computers_and_games
  • 22.
    ...and in thefuture! http://en.wikipedia.org/wiki/MOS_Technology_6502#In_popular_culture
  • 23.
    The 6507 CPU Readsinstructions 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
  • 24.
    Emulated 6507 As itexecutes each instruction, it keeps instance variables for registers (@a, @x, @y), flags (@n, @z, ...) and @memory as an array
  • 25.
    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 ...
  • 26.
    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!
  • 28.
    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
  • 29.
  • 30.
  • 31.
    CPU_spec.rb context 'INX' do beforedo 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 ...
  • 32.
    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
  • 33.
    More syntactic sugar 1.upto3 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
  • 34.
    “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
  • 35.
    Less effort →better coverage ● Each instruction tested in every possible addressing mode ● Easy to understand, high-level tests ● Tests become a specifcation - specs!
  • 36.
    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 ...
  • 38.
  • 39.
    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
  • 40.
    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
  • 41.
    CPU.rb def execute case @instruction whenLDA flag_nz @a = load when STA store @a when JMPabs @pc = @param when BXX @pc = branch ...
  • 42.
    Final result Full instructionset covered by more than 1,700 tests Only one CPU bug so far had to be debugged in-game
  • 43.
    Building an Emulator:Buildingan Emulator: ArchitectureArchitecture image CC-BYimage CC-BY Benjamin EshanBenjamin Eshan
  • 44.
  • 45.
    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)
  • 46.
    Bus Acts as amemory 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)
  • 47.
  • 48.
    Ruby spice: ducktyping Instead of defning read/write methods, make Bus,TIA, RIOT and Cart classes “quack” like arrays
  • 49.
    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
  • 50.
    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.)
  • 51.
    Cart.rb (full source) classCart 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
  • 52.
    Timing As TIA generateseach pixel for each scanline, it will "tick" the CPU and RIOT to keep everything in sync image cc-by Steve Evans
  • 53.
    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
  • 54.
  • 55.
    Graphic Objects To keepTIA'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?
  • 56.
    Composition and Inheritance Acommon 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
  • 57.
  • 58.
    Building an Emulator: Let'srun it! reproduction:reproduction: Young FrankensteinYoung Frankenstein
  • 60.
    Building an Emulator: Speedingup TBBT 1-06 "The Middle-Earth Paradigm", © 2007 Chuck Lorre Productions / WB Television
  • 61.
    Knuth, Donald (December1974). "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"
  • 62.
    Refactoring Thanks to testcoverage, we can safely play around and refactor for speed (while keeping it readable)
  • 63.
    JRuby We have asmall 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
  • 64.
    If nothing elseworks 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
  • 66.
  • 67.
    Credits and License Thispresentation 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.