rubyboy 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f906ad2a5676ad89d614d3898dee887cf2b1b0a76c2b5ae906a7f234e806e779
4
- data.tar.gz: d5c133d3ba340a570febc3bd4e4d67245e14a5fef408af6d0aba711a7738c503
3
+ metadata.gz: '09e12943a26b3089da7e75f3a26a0994dd71c1a748b63c840c49c3772502b625'
4
+ data.tar.gz: c37fdfa6182bb28cbca6ffe5438670860e0e6c10e32b581509dbfca28e12f70d
5
5
  SHA512:
6
- metadata.gz: 66db379a951b75f717339c4030e201dbfcf79d85fec15deb2a4c0d03e6513262122411eb4149b00533eae5f18c5ed44ab23d57b0ffb2ed2c7b64076c06f1fb77
7
- data.tar.gz: 648a015c3aeceab1b53b1ae3d20023c573c24a0fd91f3fde2c9da66ad0dd8caddfa6a2013374f96f7ab8dd7b70d510c32437cdd48cc46327f431f8ed06b00440
6
+ metadata.gz: 35c057be65b5b1143d870d6db5d8a0b769074e42c10281680b411e1d348f2b8b5109ff12dcddae3ca3b803b6a379fff4f7343fd0e16b111e25686d25d5468861
7
+ data.tar.gz: 900d258e730fdd03f23b508d1ff4cfd3e80b738ebf0c9ce49e6a064faeb0456a01be84bab269d62f59bcb7895d25b1dfeb08d41a2d39c0fabce4b52427d63a91
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [1.6.0] - 2026-10-04
2
+
3
+ - Add battery-backed saves and hardware-oriented save states
4
+ - Fix MBC1 ROM bank mapping, external RAM size and RAM banking
5
+ - Fix audio drop-outs and APU sample-rate drift
6
+ - Support Spinel compilation and fix SDL event allocation
7
+ - Add --frames, --unlimited and --no-audio player options
8
+ - Add benchmark warmup, SDL rendering and JSON output
9
+ - Disable VSync for rendered benchmarks and require SDL 2.0.18 or higher
10
+ - Update JSON and development dependencies for recent Ruby versions
11
+
12
+ ## [1.5.1] - 2025-02-16
13
+
14
+ - Optimize Cartridge::Mbc1 and Bus
15
+
1
16
  ## [1.5.0] - 2025-02-09
2
17
 
3
18
  - Add EmulatorHeadless and use it for bench
data/README.md CHANGED
@@ -15,7 +15,7 @@ A Game Boy emulator written in Ruby
15
15
  </div>
16
16
 
17
17
  ## Requirements
18
- [SDL2](https://wiki.libsdl.org/SDL2/Installation)
18
+ [SDL2](https://wiki.libsdl.org/SDL2/Installation) 2.0.18 or higher.
19
19
 
20
20
  ## Installation
21
21
 
@@ -47,6 +47,62 @@ If bundler is not being used to manage dependencies, install the gem by executin
47
47
  | `U` | Select |
48
48
  | `I` | Start |
49
49
 
50
+ ## Bounded execution
51
+
52
+ The player accepts `--frames N` to stop after N completed frames, `--no-audio`
53
+ to disable audio output while continuing APU emulation, and `--unlimited` to
54
+ disable real-time synchronization:
55
+
56
+ ```sh
57
+ bundle exec ruby --yjit -Ilib exe/rubyboy --frames 600 --unlimited --no-audio lib/roms/tobu.gb
58
+ ```
59
+
60
+ Normal playback remains synchronized to real time with audio enabled.
61
+ `--unlimited` alone still permits audio queue backpressure; use `--no-audio`
62
+ for maximum speed. These player options retain keyboard input and battery save
63
+ loading/writing. Use the benchmark below for comparisons with a fixed start state.
64
+
65
+ ## Benchmarking
66
+
67
+ Run a headless benchmark with an explicit warmup before each measured trial:
68
+
69
+ ```sh
70
+ bundle exec ruby --yjit -Ilib exe/rubyboy-bench \
71
+ --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 --json
72
+ ```
73
+
74
+ Add `--render` to include SDL drawing, with audio output and VSync disabled.
75
+ No environment variable is required; the benchmark overrides the VSync setting
76
+ for its renderer, including `SDL_RENDER_VSYNC=1`:
77
+
78
+ ```sh
79
+ bundle exec ruby --yjit -Ilib exe/rubyboy-bench \
80
+ --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 --render --json
81
+ ```
82
+
83
+ Both modes run the same CPU, timer, PPU and APU frame processing. Each trial
84
+ starts with a fresh emulator, ignores battery saves, accepts no game input and
85
+ runs without real-time synchronization or audio output. Initialization, warmup
86
+ and display cleanup are excluded from the measured duration. Warmup advances
87
+ the machine state by the requested number of frames in both modes. The defaults
88
+ are 3 trials, 1500 measured frames and no warmup; counts and measured frames must
89
+ be positive, and warmup frames may be zero.
90
+
91
+ Without `--json`, the command prints each trial's duration and aggregate FPS.
92
+ With `--json`, stdout contains one JSON document with individual durations/FPS,
93
+ aggregate FPS, runtime identity and execution conditions. Aggregate FPS is the
94
+ total measured frame count divided by the sum of measured durations. Closing
95
+ the window before a trial finishes aborts the benchmark with a nonzero exit
96
+ status instead of reporting a result for that incomplete trial.
97
+
98
+ For comparisons, use the same source revision, ROM, frame counts, warmup,
99
+ window/display settings and SDL render driver. Normal playback retains its
100
+ existing VSync setting. Rendered FPS measures frame
101
+ processing and SDL drawing calls; it does not count distinct frames visible on
102
+ the monitor. Measure performance without recording, then use a separate
103
+ `--render --count 1` run for a reference recording. Preserve elapsed real time
104
+ when converting the recording to GIF so that speed differences remain visible.
105
+
50
106
  ## Contributing
51
107
 
52
108
  Bug reports and pull requests are welcome on GitHub at https://github.com/sacckey/rubyboy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sacckey/rubyboy/blob/main/CODE_OF_CONDUCT.md).
data/exe/rubyboy CHANGED
@@ -4,16 +4,29 @@
4
4
  require 'rubyboy'
5
5
  require 'optparse'
6
6
 
7
+ frames = nil
8
+ realtime = true
9
+ audio = true
10
+
7
11
  begin
8
12
  OptionParser.new do |opts|
9
13
  opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
10
14
 
15
+ opts.on('--frames N', 'Exit after N frames (positive integer)') do |value|
16
+ value = Integer(value)
17
+ raise OptionParser::InvalidArgument, 'frames must be a positive integer' unless value > 0
18
+
19
+ frames = value
20
+ end
21
+ opts.on('--unlimited', 'Disable real-time synchronization') { realtime = false }
22
+ opts.on('--no-audio', 'Disable audio output while keeping APU emulation') { audio = false }
23
+
11
24
  opts.on('-h', '--help', 'Displays this help') do
12
25
  puts opts
13
26
  exit
14
27
  end
15
28
  end.parse!
16
- rescue OptionParser::ParseError => e
29
+ rescue OptionParser::ParseError, ArgumentError => e
17
30
  puts e.message
18
31
  puts "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
19
32
  exit 1
@@ -28,4 +41,4 @@ else
28
41
  puts 'YJIT is not available in this environment.'
29
42
  end
30
43
 
31
- Rubyboy::Emulator.new(rom_path).start
44
+ Rubyboy::Emulator.new(rom_path, audio:).start(frames:, realtime:)
data/exe/rubyboy-bench CHANGED
@@ -10,12 +10,24 @@ begin
10
10
  OptionParser.new do |opts|
11
11
  opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]"
12
12
 
13
- opts.on('--count COUNT', Integer, 'Number of counts for the benchmark') do |count|
14
- options[:count] = count
13
+ opts.on('--count COUNT', Integer, 'Number of trials for the benchmark') do |count|
14
+ options[:count] = Integer(count)
15
15
  end
16
16
 
17
- opts.on('--frames FRAMES', Integer, 'Number of frames to process') do |frames|
18
- options[:frames] = frames
17
+ opts.on('--frames FRAMES', Integer, 'Number of measured frames per trial') do |frames|
18
+ options[:frames] = Integer(frames)
19
+ end
20
+
21
+ opts.on('--warmup-frames FRAMES', Integer, 'Number of unmeasured warmup frames per trial (default: 0)') do |frames|
22
+ options[:warmup_frames] = Integer(frames)
23
+ end
24
+
25
+ opts.on('--render', 'Draw each frame without audio output or realtime synchronization') do
26
+ options[:render] = true
27
+ end
28
+
29
+ opts.on('--json', 'Output benchmark results and runtime information as JSON') do
30
+ options[:json] = true
19
31
  end
20
32
 
21
33
  opts.on('--rom-path ROM_PATH', String, 'Path to the ROM file') do |rom_path|
@@ -27,17 +39,24 @@ begin
27
39
  exit
28
40
  end
29
41
  end.parse!
30
- rescue OptionParser::ParseError => e
31
- puts e.message
32
- puts "Usage: #{File.basename($PROGRAM_NAME)} --count NUM --frames NUM --rom-path FILEPATH"
42
+ rescue OptionParser::ParseError, ArgumentError => e
43
+ warn e.message
44
+ warn "Usage: #{File.basename($PROGRAM_NAME)} --count NUM --frames NUM --rom-path FILEPATH"
33
45
  exit 1
34
46
  end
35
47
 
36
- puts "Ruby: #{RUBY_VERSION}"
37
- if defined?(RubyVM::YJIT)
38
- puts "YJIT: #{RubyVM::YJIT.enabled?}"
39
- else
40
- puts 'YJIT is not available in this environment.'
48
+ unless options[:json]
49
+ puts "Ruby: #{RUBY_VERSION}"
50
+ if defined?(RubyVM::YJIT)
51
+ puts "YJIT: #{RubyVM::YJIT.enabled?}"
52
+ else
53
+ puts 'YJIT is not available in this environment.'
54
+ end
41
55
  end
42
56
 
43
- Rubyboy::Bench.new.run(**options)
57
+ begin
58
+ Rubyboy::Bench.new.run(**options)
59
+ rescue ArgumentError, Rubyboy::Bench::WindowClosed => e
60
+ warn e.message
61
+ exit 1
62
+ end
data/lib/bench.rb CHANGED
@@ -1,27 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require_relative 'rubyboy/emulator_headless'
4
5
 
5
6
  module Rubyboy
6
7
  class Bench
7
- def run(count: 3, frames: 1500, rom_path: 'lib/roms/tobu.gb')
8
- time_sum = 0
8
+ class WindowClosed < StandardError; end
9
+
10
+ def run(count: 3, frames: 1500, rom_path: 'lib/roms/tobu.gb', warmup_frames: 0, render: false, json: false)
11
+ validate_integer(:count, count, 1)
12
+ validate_integer(:frames, frames, 1)
13
+ validate_integer(:warmup_frames, warmup_frames, 0)
14
+ require_relative 'rubyboy/lcd' if render
9
15
 
16
+ trials = []
17
+ time_sum = 0
10
18
  count.times do |i|
11
- emulator = Rubyboy::EmulatorHeadless.new(rom_path)
12
- frame_count = 0
19
+ time = run_trial(rom_path, frames, warmup_frames, render)
20
+ elapsed_seconds = time / 1_000_000_000.0
21
+ trials << { trial: i + 1, frames:, elapsed_seconds:, fps: frames / elapsed_seconds }
22
+ puts "#{i + 1}: #{elapsed_seconds} sec" unless json
23
+ time_sum += time
24
+ end
25
+
26
+ result = {
27
+ schema_version: 1,
28
+ rom_path: File.expand_path(rom_path),
29
+ count:,
30
+ frames:,
31
+ warmup_frames:,
32
+ render:,
33
+ runtime:,
34
+ conditions: { apu: true, audio_output: false, realtime_sync: false, vsync: render ? false : nil, input: false, save: false },
35
+ trials:,
36
+ total_frames: frames * count,
37
+ elapsed_seconds: time_sum / 1_000_000_000.0,
38
+ fps: frames * count * 1_000_000_000.0 / time_sum
39
+ }
40
+ puts(json ? JSON.generate(result) : "FPS: #{result[:fps]}")
41
+ result
42
+ end
43
+
44
+ private
45
+
46
+ def validate_integer(name, value, minimum)
47
+ return if value.is_a?(Integer) && value >= minimum
48
+
49
+ requirement = minimum == 0 ? 'a nonnegative integer' : 'a positive integer'
50
+ raise ArgumentError, "#{name} must be #{requirement}"
51
+ end
52
+
53
+ def run_trial(rom_path, frames, warmup_frames, render)
54
+ emulator = Rubyboy::EmulatorHeadless.new(rom_path)
55
+ lcd = Rubyboy::Lcd.new(vsync: false) if render
56
+ begin
57
+ run_frames(emulator, lcd, warmup_frames)
13
58
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
14
- while frame_count < frames
15
- emulator.step
16
- frame_count += 1
17
- end
18
- time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
19
- puts "#{i + 1}: #{time / 1_000_000_000.0} sec"
59
+ run_frames(emulator, lcd, frames)
60
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
61
+ ensure
62
+ lcd&.close_window
63
+ end
64
+ end
20
65
 
21
- time_sum += time
66
+ def run_frames(emulator, lcd, frames)
67
+ frame_count = 0
68
+ while frame_count < frames
69
+ raise WindowClosed, 'Benchmark stopped because the window was closed.' if lcd&.window_should_close?
70
+
71
+ emulator.step
72
+ lcd&.draw(emulator.framebuffer)
73
+ frame_count += 1
22
74
  end
75
+ end
23
76
 
24
- puts "FPS: #{frames * count * 1_000_000_000.0 / time_sum}"
77
+ def runtime
78
+ yjit_enabled = defined?(RubyVM::YJIT) ? RubyVM::YJIT.enabled? : false
79
+ { engine: RUBY_ENGINE, version: RUBY_VERSION, description: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, yjit_enabled: }
25
80
  end
26
81
  end
27
82
  end
data/lib/rubyboy/apu.rb CHANGED
@@ -7,13 +7,24 @@ require_relative 'apu_channels/channel4'
7
7
 
8
8
  module Rubyboy
9
9
  class Apu
10
+ # APU samples are pushed to SDL at 48 kHz (must match Rubyboy::Audio::SAMPLE_RATE).
11
+ # The CPU runs at 4_194_304 Hz, so the real divisor is 87.3813...
12
+ # The previous implementation used `>= 87` which effectively ran the sampler
13
+ # at ~48_210 Hz — a 0.4% drift that slowly over-filled the SDL audio queue
14
+ # and triggered the clear-on-overflow in Audio#queue, producing audible gaps
15
+ # in BGM. We fix this with a fixed-point accumulator that targets the exact
16
+ # ratio: every step adds `cycles * SAMPLE_RATE` and we emit one sample
17
+ # whenever the accumulator reaches CPU_CLOCK_HZ.
18
+ CPU_CLOCK_HZ = 4_194_304
19
+ SAMPLE_RATE = 48_000
20
+
10
21
  attr_reader :samples
11
22
 
12
23
  def initialize
13
24
  @nr50 = 0
14
25
  @nr51 = 0
15
26
  @cycles = 0
16
- @sampling_cycles = 0
27
+ @sampling_accumulator = 0
17
28
  @fs = 0
18
29
  @samples = Array.new(1024, 0.0)
19
30
  @sample_idx = 0
@@ -25,7 +36,7 @@ module Rubyboy
25
36
 
26
37
  def step(cycles)
27
38
  @cycles += cycles
28
- @sampling_cycles += cycles
39
+ @sampling_accumulator += cycles * SAMPLE_RATE
29
40
 
30
41
  @channel1.step(cycles)
31
42
  @channel2.step(cycles)
@@ -43,8 +54,8 @@ module Rubyboy
43
54
  @fs = (@fs + 1) % 8
44
55
  end
45
56
 
46
- if @sampling_cycles >= 87
47
- @sampling_cycles -= 87
57
+ if @sampling_accumulator >= CPU_CLOCK_HZ
58
+ @sampling_accumulator -= CPU_CLOCK_HZ
48
59
 
49
60
  left_sample = (
50
61
  @nr51[7] * @channel4.dac_output +
data/lib/rubyboy/audio.rb CHANGED
@@ -22,14 +22,22 @@ module Rubyboy
22
22
  end
23
23
 
24
24
  def queue(buffer)
25
- # sleep(0.001) while SDL.GetQueuedAudioSize(@device) > 8192
26
-
27
- SDL.ClearQueuedAudio(@device) if SDL.GetQueuedAudioSize(@device) > 8192
25
+ # Block until the SDL audio queue has drained enough to accept another
26
+ # buffer. The previous implementation called SDL.ClearQueuedAudio here
27
+ # instead, which dropped the entire queue whenever the emulator ran ahead
28
+ # of real time — that produced audible pops/gaps in BGM (most noticeable
29
+ # in Pokemon-style tracks). Blocking here costs a tiny bit of frame
30
+ # budget but keeps audio continuous.
31
+ sleep(0.001) while SDL.GetQueuedAudioSize(@device) > 8192
28
32
 
29
33
  buf_ptr = FFI::MemoryPointer.new(:float, buffer.size)
30
34
  buf_ptr.put_array_of_float(0, buffer)
31
35
 
32
36
  SDL.QueueAudio(@device, buf_ptr, buffer.size * buf_ptr.type_size)
33
37
  end
38
+
39
+ def clear_queue
40
+ SDL.ClearQueuedAudio(@device)
41
+ end
34
42
  end
35
43
  end
data/lib/rubyboy/bus.rb CHANGED
@@ -11,74 +11,93 @@ module Rubyboy
11
11
  @apu = apu
12
12
  @interrupt = interrupt
13
13
  @timer = timer
14
+ end
15
+
16
+ def read_byte(addr)
17
+ case addr >> 12
18
+ when 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0xa, 0xb
19
+ return @mbc.read_byte(addr)
20
+ when 0x8, 0x9
21
+ return @ppu.read_byte(addr)
22
+ when 0xc
23
+ return @ram.wram1[addr - 0xc000]
24
+ when 0xd
25
+ return @ram.wram2[addr - 0xd000]
26
+ when 0xf
27
+ case addr >> 8
28
+ when 0xfe
29
+ return @ppu.read_byte(addr) if addr <= 0xfe9f
30
+ when 0xff
31
+ last_byte = addr & 0xFF
14
32
 
15
- @read_methods = Array.new(0x10000)
16
- @write_methods = Array.new(0x10000)
33
+ case last_byte
34
+ when 0x00
35
+ return @joypad.read_byte(addr)
36
+ when 0x04, 0x05, 0x06, 0x07
37
+ return @timer.read_byte(addr)
38
+ when 0x0f
39
+ return @interrupt.read_byte(addr)
40
+ when 0x46
41
+ return @ppu.read_byte(addr)
42
+ when 0xff
43
+ return @interrupt.read_byte(addr)
44
+ end
17
45
 
18
- set_methods
19
- end
46
+ return @apu.read_byte(addr) if last_byte <= 0x26 && last_byte >= 0x10
20
47
 
21
- def set_methods
22
- 0x10000.times do |addr|
23
- case addr
24
- when 0x0000..0x7fff
25
- @read_methods[addr] = -> { @mbc.read_byte(addr) }
26
- @write_methods[addr] = ->(value) { @mbc.write_byte(addr, value) }
27
- when 0x8000..0x9fff
28
- @read_methods[addr] = -> { @ppu.read_byte(addr) }
29
- @write_methods[addr] = ->(value) { @ppu.write_byte(addr, value) }
30
- when 0xa000..0xbfff
31
- @read_methods[addr] = -> { @mbc.read_byte(addr) }
32
- @write_methods[addr] = ->(value) { @mbc.write_byte(addr, value) }
33
- when 0xc000..0xcfff
34
- @read_methods[addr] = -> { @ram.wram1[addr - 0xc000] }
35
- @write_methods[addr] = ->(value) { @ram.wram1[addr - 0xc000] = value }
36
- when 0xd000..0xdfff
37
- @read_methods[addr] = -> { @ram.wram2[addr - 0xd000] }
38
- @write_methods[addr] = ->(value) { @ram.wram2[addr - 0xd000] = value }
39
- when 0xfe00..0xfe9f
40
- @read_methods[addr] = -> { @ppu.read_byte(addr) }
41
- @write_methods[addr] = ->(value) { @ppu.write_byte(addr, value) }
42
- when 0xff00
43
- @read_methods[addr] = -> { @joypad.read_byte(addr) }
44
- @write_methods[addr] = ->(value) { @joypad.write_byte(addr, value) }
45
- when 0xff04..0xff07
46
- @read_methods[addr] = -> { @timer.read_byte(addr) }
47
- @write_methods[addr] = ->(value) { @timer.write_byte(addr, value) }
48
- when 0xff0f
49
- @read_methods[addr] = -> { @interrupt.read_byte(addr) }
50
- @write_methods[addr] = ->(value) { @interrupt.write_byte(addr, value) }
51
- when 0xff10..0xff26
52
- @read_methods[addr] = -> { @apu.read_byte(addr) }
53
- @write_methods[addr] = ->(value) { @apu.write_byte(addr, value) }
54
- when 0xff30..0xff3f
55
- @read_methods[addr] = -> { @apu.read_byte(addr) }
56
- @write_methods[addr] = ->(value) { @apu.write_byte(addr, value) }
57
- when 0xff46
58
- @read_methods[addr] = -> { @ppu.read_byte(addr) }
59
- @write_methods[addr] = ->(value) { 0xa0.times { |i| write_byte(0xfe00 + i, read_byte((value << 8) + i)) } }
60
- when 0xff40..0xff4b
61
- @read_methods[addr] = -> { @ppu.read_byte(addr) }
62
- @write_methods[addr] = ->(value) { @ppu.write_byte(addr, value) }
63
- when 0xff80..0xfffe
64
- @read_methods[addr] = -> { @ram.hram[addr - 0xff80] }
65
- @write_methods[addr] = ->(value) { @ram.hram[addr - 0xff80] = value }
66
- when 0xffff
67
- @read_methods[addr] = -> { @interrupt.read_byte(addr) }
68
- @write_methods[addr] = ->(value) { @interrupt.write_byte(addr, value) }
69
- else
70
- @read_methods[addr] = -> { 0xff }
71
- @write_methods[addr] = ->(_value) {}
48
+ return @apu.read_byte(addr) if last_byte <= 0x3f && last_byte >= 0x30
49
+
50
+ return @ppu.read_byte(addr) if last_byte <= 0x4b && last_byte >= 0x40
51
+
52
+ return @ram.hram[addr - 0xff80] if last_byte <= 0xfe && last_byte >= 0x80
72
53
  end
73
54
  end
74
- end
75
55
 
76
- def read_byte(addr)
77
- @read_methods[addr].call
56
+ 0xff
78
57
  end
79
58
 
80
59
  def write_byte(addr, value)
81
- @write_methods[addr].call(value)
60
+ case addr >> 12
61
+ when 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0xa, 0xb
62
+ return @mbc.write_byte(addr, value)
63
+ when 0x8, 0x9
64
+ return @ppu.write_byte(addr, value)
65
+ when 0xc
66
+ return @ram.wram1[addr - 0xc000] = value
67
+ when 0xd
68
+ return @ram.wram2[addr - 0xd000] = value
69
+ when 0xf
70
+ case addr >> 8
71
+ when 0xfe
72
+ return @ppu.write_byte(addr, value) if addr <= 0xfe9f
73
+ when 0xff
74
+ last_byte = addr & 0xFF
75
+
76
+ case last_byte
77
+ when 0x00
78
+ return @joypad.write_byte(addr, value)
79
+ when 0x04, 0x05, 0x06, 0x07
80
+ return @timer.write_byte(addr, value)
81
+ when 0x0f
82
+ return @interrupt.write_byte(addr, value)
83
+ when 0x46
84
+ 0xa0.times { |i| write_byte(0xfe00 + i, read_byte((value << 8) + i)) }
85
+ return
86
+ when 0xff
87
+ return @interrupt.write_byte(addr, value)
88
+ end
89
+
90
+ return @apu.write_byte(addr, value) if last_byte <= 0x26 && last_byte >= 0x10
91
+
92
+ return @apu.write_byte(addr, value) if last_byte <= 0x3f && last_byte >= 0x30
93
+
94
+ return @ppu.write_byte(addr, value) if last_byte <= 0x4b && last_byte >= 0x40
95
+
96
+ return @ram.hram[addr - 0xff80] = value if last_byte <= 0xfe && last_byte >= 0x80
97
+ end
98
+ end
99
+
100
+ nil
82
101
  end
83
102
 
84
103
  def read_word(addr)
@@ -3,6 +3,9 @@
3
3
  module Rubyboy
4
4
  module Cartridge
5
5
  class Mbc1
6
+ ROM_BANK_SIZE = 0x4000
7
+ RAM_BANK_SIZE = 0x2000
8
+
6
9
  def initialize(rom, ram)
7
10
  @rom = rom
8
11
  @ram = ram
@@ -10,65 +13,56 @@ module Rubyboy
10
13
  @ram_bank = 0
11
14
  @ram_enable = false
12
15
  @ram_banking_mode = false
16
+ end
13
17
 
14
- @read_methods = Array.new(0xc000)
15
- @write_methods = Array.new(0xc000)
18
+ def read_byte(addr)
19
+ case (addr >> 12)
20
+ when 0x0, 0x1, 0x2, 0x3
21
+ @rom.data[rom_offset(addr)] || 0xff
22
+ when 0x4, 0x5, 0x6, 0x7
23
+ @rom.data[rom_offset(addr)] || 0xff
24
+ when 0xa, 0xb
25
+ return 0xff unless @ram_enable
16
26
 
17
- set_methods
27
+ @ram.eram[ram_offset(addr)] || 0xff
28
+ end
18
29
  end
19
30
 
20
- def set_methods
21
- 0xc000.times do |addr|
22
- @read_methods[addr] =
23
- case addr
24
- when 0x0000..0x3fff then -> { @rom.data[addr] }
25
- when 0x4000..0x7fff then -> { @rom.data[addr + (@rom_bank - 1) * 0x4000] }
26
- when 0xa000..0xbfff
27
- lambda do
28
- if @ram_enable
29
- if @ram_banking_mode
30
- @ram.eram[addr - 0xa000 + @ram_bank * 0x800]
31
- else
32
- @ram.eram[addr - 0xa000]
33
- end
34
- else
35
- 0xff
36
- end
37
- end
38
- end
39
- end
31
+ def write_byte(addr, value)
32
+ case addr >> 12
33
+ when 0x0, 0x1
34
+ @ram_enable = value & 0x0f == 0x0a
35
+ when 0x2, 0x3
36
+ @rom_bank = value & 0x1f
37
+ @rom_bank = 1 if @rom_bank == 0
38
+ when 0x4, 0x5
39
+ @ram_bank = value & 0x03
40
+ when 0x6, 0x7
41
+ @ram_banking_mode = value & 0x01 == 0x01
42
+ when 0xa, 0xb
43
+ return unless @ram_enable
40
44
 
41
- 0xc000.times do |addr|
42
- @write_methods[addr] =
43
- case addr
44
- when 0x0000..0x1fff then ->(value) { @ram_enable = value & 0x0f == 0x0a }
45
- when 0x2000..0x3fff
46
- lambda do |value|
47
- @rom_bank = value & 0x1f
48
- @rom_bank = 1 if @rom_bank == 0
49
- end
50
- when 0x4000..0x5fff then ->(value) { @ram_bank = value & 0x03 }
51
- when 0x6000..0x7fff then ->(value) { @ram_banking_mode = value & 0x01 == 0x01 }
52
- when 0xa000..0xbfff
53
- lambda do |value|
54
- if @ram_enable
55
- if @ram_banking_mode
56
- @ram.eram[addr - 0xa000 + @ram_bank * 0x800] = value
57
- else
58
- @ram.eram[addr - 0xa000] = value
59
- end
60
- end
61
- end
62
- end
45
+ offset = ram_offset(addr)
46
+ @ram.eram[offset] = value if offset < @ram.eram.size
63
47
  end
64
48
  end
65
49
 
66
- def read_byte(addr)
67
- @read_methods[addr].call
50
+ private
51
+
52
+ def rom_offset(addr)
53
+ bank =
54
+ if addr < ROM_BANK_SIZE
55
+ @ram_banking_mode ? (@ram_bank << 5) : 0
56
+ else
57
+ (@ram_bank << 5) | @rom_bank
58
+ end
59
+
60
+ ((bank * ROM_BANK_SIZE) + (addr & (ROM_BANK_SIZE - 1))) % @rom.data.size
68
61
  end
69
62
 
70
- def write_byte(addr, value)
71
- @write_methods[addr].call(value)
63
+ def ram_offset(addr)
64
+ bank = @ram_banking_mode ? @ram_bank : 0
65
+ (addr - 0xa000) + (bank * RAM_BANK_SIZE)
72
66
  end
73
67
  end
74
68
  end