ruby2600 - an Atari 2600 emulator written in Ruby
Click here to load reader
-
Upload
chester-carlos-duarte-do-nascimento -
Category
Technology
-
view
2.392 -
download
6
description
Transcript of ruby2600 - an Atari 2600 emulator written in Ruby
An Atari 2600 emulator100% written in Ruby
(and RSpec!)
Carlos Duarte do Nascimento (Chester)@chesterbr / http://chester.me
If so, you probably had this...
...or one of these...
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
@chesterbrhttp://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 examplesshared_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)}" endend
More syntactic sugar1.upto 3 do |number| shared_examples_for "advance PC by #{number.humanize}" do it { expect { cpu.step }.to change { cpu.pc }.by number } endend
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 everypossible 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/worddata 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
morelookup!
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 coveredby more than 1,700 tests
Only one CPU bug so farhad to be debugged in-game
Building an Emulator:Building an Emulator:
ArchitectureArchitecture
image CC-BY image 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 theCPU, routing reads and writesto 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! endend
TimingAs 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.rbdef draw_scanline scanline = [] 0.upto(SCANLINE_WIDTH) do |pixel| scanline << topmost_pixel tick_other_chips pixel end return scanlineend
def topmost_pixel ...
def tick_other_chips @cpu.tick if pixel % 3 == 2 @riot.tick if pixel % 3 == 0end
TIA runs 3x fasterthan 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 InheritanceA 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!
@chesterbrhttp://slideshare.net/chesterbr
http://github.com/chesterbr/ruby2600
Credits and LicenseThis presentation is available under the
Creative Commons “by-nc” 3.0 licensenoticing the exceptions below
Images from third parties were included (with due credits) underfair 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.