rubyboy 1.5.1 → 1.7.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +39 -27
  4. data/exe/rubyboy +16 -5
  5. data/exe/rubyboy-bench +15 -7
  6. data/lib/bench.rb +33 -7
  7. data/lib/executor.rb +19 -10
  8. data/lib/rubyboy/apu.rb +15 -4
  9. data/lib/rubyboy/apu_channels/channel1.rb +4 -4
  10. data/lib/rubyboy/apu_channels/channel2.rb +4 -4
  11. data/lib/rubyboy/apu_channels/channel3.rb +2 -9
  12. data/lib/rubyboy/apu_channels/channel4.rb +0 -7
  13. data/lib/rubyboy/audio.rb +17 -4
  14. data/lib/rubyboy/cartridge/mbc1.rb +30 -18
  15. data/lib/rubyboy/emulator.rb +145 -47
  16. data/lib/rubyboy/emulator_headless.rb +6 -2
  17. data/lib/rubyboy/emulator_wasm.rb +31 -8
  18. data/lib/rubyboy/hardware_state.rb +450 -0
  19. data/lib/rubyboy/lcd.rb +2 -1
  20. data/lib/rubyboy/ram.rb +2 -2
  21. data/lib/rubyboy/rom.rb +26 -0
  22. data/lib/rubyboy/save_file.rb +36 -0
  23. data/lib/rubyboy/sdl.rb +12 -0
  24. data/lib/rubyboy/state_file.rb +49 -0
  25. data/lib/rubyboy/version.rb +1 -1
  26. data/lib/rubyboy.rb +3 -0
  27. metadata +9 -48
  28. data/.rspec +0 -3
  29. data/.rubocop.yml +0 -54
  30. data/CODE_OF_CONDUCT.md +0 -84
  31. data/Rakefile +0 -12
  32. data/docs/favicon.png +0 -0
  33. data/docs/index.html +0 -75
  34. data/docs/index.js +0 -113
  35. data/docs/logo-light-23.svg +0 -1
  36. data/docs/ogp.png +0 -0
  37. data/docs/styles.css +0 -245
  38. data/docs/worker.js +0 -115
  39. data/exe/rubyboy-wasm +0 -26
  40. data/lib/opcodes.json +0 -11657
  41. data/lib/roms/bgbtest.gb +0 -0
  42. data/lib/roms/cpu_instrs/cpu_instrs.gb +0 -0
  43. data/lib/roms/cpu_instrs/individual/01-special.gb +0 -0
  44. data/lib/roms/cpu_instrs/individual/02-interrupts.gb +0 -0
  45. data/lib/roms/cpu_instrs/individual/03-op sp,hl.gb +0 -0
  46. data/lib/roms/cpu_instrs/individual/04-op r,imm.gb +0 -0
  47. data/lib/roms/cpu_instrs/individual/05-op rp.gb +0 -0
  48. data/lib/roms/cpu_instrs/individual/06-ld r,r.gb +0 -0
  49. data/lib/roms/cpu_instrs/individual/07-jr,jp,call,ret,rst.gb +0 -0
  50. data/lib/roms/cpu_instrs/individual/08-misc instrs.gb +0 -0
  51. data/lib/roms/cpu_instrs/individual/09-op r,r.gb +0 -0
  52. data/lib/roms/cpu_instrs/individual/10-bit ops.gb +0 -0
  53. data/lib/roms/cpu_instrs/individual/11-op a,(hl).gb +0 -0
  54. data/lib/roms/cpu_instrs/readme.txt +0 -119
  55. data/lib/roms/dmg-acid2.gb +0 -0
  56. data/lib/roms/hello-world.gb +0 -0
  57. data/lib/roms/instr_timing/instr_timing.gb +0 -0
  58. data/lib/roms/instr_timing/readme.txt +0 -139
  59. data/lib/rubyboy/raylib/audio.rb +0 -32
  60. data/lib/rubyboy/raylib/lcd.rb +0 -40
  61. data/lib/rubyboy/raylib/raylib_loader.rb +0 -36
  62. data/lib/stackprof.rb +0 -14
  63. data/resource/logo/logo.png +0 -0
  64. data/resource/logo/logo.svg +0 -1
  65. data/resource/logo/rubyboy.png +0 -0
  66. data/resource/screenshots/pokemon.png +0 -0
  67. data/resource/screenshots/puyopuyo.png +0 -0
  68. data/sig/rubyboy.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22c8e1f0094ec086f0e7130e6c79623d0e9385c718f0724ca2773c101ff3b976
4
- data.tar.gz: 49adde27b593ef7bf8bf3f874160922ed7994388960db71f5757b2cbf033735f
3
+ metadata.gz: 715ec944430fe6b0f108f1a51dc90722059e754939ca8e23726fb8c9ca226407
4
+ data.tar.gz: 12b42b656684bc9b4255559aaec3600c6fa4d6f32132fc144666de097f7512a0
5
5
  SHA512:
6
- metadata.gz: 48abbc79e8dde1e691f3f76a2736ad909b29af549c3c3b6a99eedd16a5b832f8b84bfb810ed4115483e116f5acbd50e1b07ec333e61778dc82433293f36a8401
7
- data.tar.gz: dcf874037cd1d133687e3a99c5aeb36d97cd8a8f356514bb97b40eec57184b3bed7bd003538925fce18875d13d8bd37e1f50ffdd646568b5fbc351925f710a5a
6
+ metadata.gz: 0d5c32193c766397003bb8d2bfc1072dc8dd0dbca967dbd87c2e041b4559f6f43aa761452fa68acc3036b08e2a5606f96df15733284fa7f1c880e07c9061de57
7
+ data.tar.gz: 1f514d729da361a7ede186479b8ad844c7d57d04fec572d1d2c02fdcd7f86e82d7d8ebb603596369eb3800e5ed389aae79fa3b3a9bb094cfe137c3c3e553a44d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [1.7.0] - 2026-10-10
2
+
3
+ - Require Ruby 4.0 or higher
4
+ - Keep audio output with --unlimited by dropping audio that cannot be played in time
5
+ - Remove the --no-audio player option
6
+ - Remove rubyboy-bench --render and --json, and the SDL 2.0.18 requirement
7
+ - Print a checksum of the final frame from rubyboy-bench to check that a runtime emulated correctly
8
+ - Run rubyboy-bench without loading SDL
9
+ - Run the bundled Tobu Tobu Girl ROM when rubyboy or rubyboy-bench gets no ROM path, from any directory
10
+ - Read ROM files in binary mode, which fixes loading them on Windows
11
+ - Make the APU duty tables shareable between Ractors
12
+ - Ship only the runtime code, the default ROM and documents in the gem, without the rubyboy-wasm command or test ROMs
13
+
14
+ ## [1.6.0] - 2026-10-04
15
+
16
+ - Add battery-backed saves and hardware-oriented save states
17
+ - Fix MBC1 ROM bank mapping, external RAM size and RAM banking
18
+ - Fix audio drop-outs and APU sample-rate drift
19
+ - Support Spinel compilation and fix SDL event allocation
20
+ - Add --frames, --unlimited and --no-audio player options
21
+ - Add benchmark warmup, SDL rendering and JSON output
22
+ - Disable VSync for rendered benchmarks and require SDL 2.0.18 or higher
23
+ - Update JSON and development dependencies for recent Ruby versions
24
+
1
25
  ## [1.5.1] - 2025-02-16
2
26
 
3
27
  - Optimize Cartridge::Mbc1 and Bus
data/README.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  A Game Boy emulator written in Ruby
8
8
 
9
- **[Try the demo in your browser!](https://sacckey.github.io/rubyboy/)** - Powered by WebAssembly
9
+ **Try it in your browser:**
10
+ [ruby.wasm version](https://sacckey.github.io/rubyboy/) (CRuby compiled to WebAssembly) /
11
+ [Spinel version](https://sacckey.github.io/rubyboy/spinel/) (the same Ruby code compiled to C by Spinel, then to WebAssembly)
10
12
 
11
13
  ## Screenshots
12
14
  <div align="center">
@@ -14,47 +16,57 @@ A Game Boy emulator written in Ruby
14
16
  <img src="/resource/screenshots/puyopuyo.png" width="400px"/>
15
17
  </div>
16
18
 
17
- ## Requirements
18
- [SDL2](https://wiki.libsdl.org/SDL2/Installation)
19
-
20
19
  ## Installation
21
20
 
22
- This project requires Ruby 3.2.0 or higher.
21
+ Rubyboy requires Ruby 4.0 or higher and [SDL2](https://wiki.libsdl.org/SDL2/Installation).
22
+
23
+ $ gem install rubyboy
23
24
 
24
25
  > [!TIP]
25
- > To enhance performance, it is highly recommended to use Ruby 3.3.x with YJIT.
26
+ > Run with YJIT (`RUBYOPT=--yjit`) for better performance.
27
+
28
+ ## Usage
26
29
 
27
- Install the gem and add to the application's Gemfile by executing:
30
+ $ RUBYOPT=--yjit rubyboy [rom_path]
28
31
 
29
- $ bundle add rubyboy
32
+ Without a ROM path, it runs the bundled [Tobu Tobu Girl](https://tangramgames.dk/tobutobugirl/).
30
33
 
31
- If bundler is not being used to manage dependencies, install the gem by executing:
34
+ ### Controls
32
35
 
33
- $ gem install ffi rubyboy
36
+ | Key | Button |
37
+ | :---: | :---: |
38
+ | `W` `A` `S` `D` | ↑ ← ↓ → |
39
+ | `K` | A |
40
+ | `J` | B |
41
+ | `U` | Select |
42
+ | `I` | Start |
43
+ | `1`–`0` | Save state to slot 1–9, 0 |
44
+ | Left `Shift` + `1`–`0` | Load state from that slot |
34
45
 
35
- ## Usage
46
+ Battery-backed games save to `<rom>.sav`, and save states go to `<rom>.state<N>`,
47
+ next to the ROM file.
48
+
49
+ ### Options
36
50
 
37
- $ RUBYOPT=--yjit rubyboy <rom_path>
51
+ | Option | Effect |
52
+ | --- | --- |
53
+ | `--frames N` | Exit after N frames |
54
+ | `--unlimited` | Run as fast as possible; audio that cannot be played in time is dropped |
38
55
 
39
- | Key | Button |
40
- | :---: | :----: |
41
- | `W` | ↑ |
42
- | `A` | ← |
43
- | `S` | ↓ |
44
- | `D` | → |
45
- | `J` | A |
46
- | `K` | B |
47
- | `U` | Select |
48
- | `I` | Start |
56
+ ## Benchmark
57
+
58
+ On an Apple M4, Rubyboy runs at about 110 FPS on CRuby, 290 FPS with YJIT and
59
+ 800 FPS when compiled with Spinel. See [BENCHMARK.md](BENCHMARK.md) for the method and details.
60
+
61
+ ## Other ways to run
62
+
63
+ - [WebAssembly builds](wasm/README.md): the browser versions (ruby.wasm and Spinel)
64
+ - [Spinel](spinel/README.md): compile Rubyboy to a native executable
49
65
 
50
66
  ## Contributing
51
67
 
52
- 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).
68
+ 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](CODE_OF_CONDUCT.md).
53
69
 
54
70
  ## License
55
71
 
56
72
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
57
-
58
- ## Code of Conduct
59
-
60
- Everyone interacting in the Rubyboy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sacckey/rubyboy/blob/main/CODE_OF_CONDUCT.md).
data/exe/rubyboy CHANGED
@@ -4,22 +4,33 @@
4
4
  require 'rubyboy'
5
5
  require 'optparse'
6
6
 
7
+ frames = nil
8
+ realtime = true
9
+
7
10
  begin
8
11
  OptionParser.new do |opts|
9
- opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
12
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [ROM_PATH]"
13
+
14
+ opts.on('--frames N', 'Exit after N frames (positive integer)') do |value|
15
+ value = Integer(value)
16
+ raise OptionParser::InvalidArgument, 'frames must be a positive integer' unless value > 0
17
+
18
+ frames = value
19
+ end
20
+ opts.on('--unlimited', 'Disable real-time synchronization; excess audio is dropped') { realtime = false }
10
21
 
11
22
  opts.on('-h', '--help', 'Displays this help') do
12
23
  puts opts
13
24
  exit
14
25
  end
15
26
  end.parse!
16
- rescue OptionParser::ParseError => e
27
+ rescue OptionParser::ParseError, ArgumentError => e
17
28
  puts e.message
18
- puts "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
29
+ puts "Usage: #{File.basename($PROGRAM_NAME)} [ROM_PATH]"
19
30
  exit 1
20
31
  end
21
32
 
22
- rom_path = ARGV.shift || 'lib/roms/tobu.gb'
33
+ rom_path = ARGV.shift || Rubyboy::Rom::DEFAULT_PATH
23
34
 
24
35
  puts "Ruby: #{RUBY_VERSION}"
25
36
  if defined?(RubyVM::YJIT)
@@ -28,4 +39,4 @@ else
28
39
  puts 'YJIT is not available in this environment.'
29
40
  end
30
41
 
31
- Rubyboy::Emulator.new(rom_path).start
42
+ Rubyboy::Emulator.new(rom_path).start(frames:, realtime:)
data/exe/rubyboy-bench CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'rubyboy'
5
4
  require 'bench'
6
5
  require 'optparse'
7
6
 
@@ -10,14 +9,18 @@ begin
10
9
  OptionParser.new do |opts|
11
10
  opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]"
12
11
 
13
- opts.on('--count COUNT', Integer, 'Number of counts for the benchmark') do |count|
12
+ opts.on('--count COUNT', Integer, 'Number of trials for the benchmark') do |count|
14
13
  options[:count] = count
15
14
  end
16
15
 
17
- opts.on('--frames FRAMES', Integer, 'Number of frames to process') do |frames|
16
+ opts.on('--frames FRAMES', Integer, 'Number of measured frames per trial') do |frames|
18
17
  options[:frames] = frames
19
18
  end
20
19
 
20
+ opts.on('--warmup-frames FRAMES', Integer, 'Number of unmeasured warmup frames per trial (default: 0)') do |frames|
21
+ options[:warmup_frames] = frames
22
+ end
23
+
21
24
  opts.on('--rom-path ROM_PATH', String, 'Path to the ROM file') do |rom_path|
22
25
  options[:rom_path] = rom_path
23
26
  end
@@ -27,9 +30,9 @@ begin
27
30
  exit
28
31
  end
29
32
  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"
33
+ rescue OptionParser::ParseError, ArgumentError => e
34
+ warn e.message
35
+ warn "Usage: #{File.basename($PROGRAM_NAME)} --count NUM --frames NUM --rom-path FILEPATH"
33
36
  exit 1
34
37
  end
35
38
 
@@ -40,4 +43,9 @@ else
40
43
  puts 'YJIT is not available in this environment.'
41
44
  end
42
45
 
43
- Rubyboy::Bench.new.run(**options)
46
+ begin
47
+ Rubyboy::Bench.new.run(**options)
48
+ rescue ArgumentError => e
49
+ warn e.message
50
+ exit 1
51
+ end
data/lib/bench.rb CHANGED
@@ -4,24 +4,50 @@ require_relative 'rubyboy/emulator_headless'
4
4
 
5
5
  module Rubyboy
6
6
  class Bench
7
- def run(count: 3, frames: 1500, rom_path: 'lib/roms/tobu.gb')
8
- time_sum = 0
7
+ def run(count: 3, frames: 1500, rom_path: Rom::DEFAULT_PATH, warmup_frames: 0)
8
+ validate_integer(:count, count, 1)
9
+ validate_integer(:frames, frames, 1)
10
+ validate_integer(:warmup_frames, warmup_frames, 0)
9
11
 
12
+ time_sum = 0
13
+ frame_checksum = 0
10
14
  count.times do |i|
11
15
  emulator = Rubyboy::EmulatorHeadless.new(rom_path)
12
- frame_count = 0
16
+ run_frames(emulator, warmup_frames)
13
17
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
14
- while frame_count < frames
15
- emulator.step
16
- frame_count += 1
17
- end
18
+ run_frames(emulator, frames)
18
19
  time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
19
20
  puts "#{i + 1}: #{time / 1_000_000_000.0} sec"
20
21
 
21
22
  time_sum += time
23
+ frame_checksum = checksum(emulator.framebuffer)
22
24
  end
23
25
 
24
26
  puts "FPS: #{frames * count * 1_000_000_000.0 / time_sum}"
27
+ # Every trial ends on the same frame. A different value on another runtime means it emulated incorrectly.
28
+ puts "Checksum: #{frame_checksum}"
29
+ end
30
+
31
+ private
32
+
33
+ def validate_integer(name, value, minimum)
34
+ return if value.is_a?(Integer) && value >= minimum
35
+
36
+ requirement = minimum == 0 ? 'a nonnegative integer' : 'a positive integer'
37
+ raise ArgumentError, "#{name} must be #{requirement}"
38
+ end
39
+
40
+ # Integer arithmetic only, so CRuby and ahead-of-time compilers such as Spinel produce the same value.
41
+ def checksum(framebuffer)
42
+ framebuffer.reduce(0) { |hash, pixel| ((hash * 31) + pixel) & 0xffffffff }
43
+ end
44
+
45
+ def run_frames(emulator, frames)
46
+ frame_count = 0
47
+ while frame_count < frames
48
+ emulator.step
49
+ frame_count += 1
50
+ end
25
51
  end
26
52
  end
27
53
  end
data/lib/executor.rb CHANGED
@@ -1,36 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'js'
4
- require 'json'
5
-
6
3
  require_relative 'rubyboy/emulator_wasm'
7
4
 
8
5
  class Executor
9
6
  ALLOWED_ROMS = ['tobu.gb', 'bgbtest.gb'].freeze
10
7
 
11
- def initialize
12
- rom_data = File.open('lib/roms/tobu.gb', 'r') { _1.read.bytes }
8
+ def initialize(rom_path = '/lib/roms/tobu.gb')
9
+ rom_data = File.binread(rom_path).bytes
13
10
  @emulator = Rubyboy::EmulatorWasm.new(rom_data)
14
11
  end
15
12
 
16
13
  def exec(direction_key = 0b1111, action_key = 0b1111)
17
- bin = @emulator.step(direction_key, action_key).pack('V*')
18
- File.binwrite('/video.data', bin)
14
+ @emulator.step(direction_key, action_key)
15
+ write_framebuffer
16
+ write_audio
19
17
  end
20
18
 
21
19
  def read_rom_from_virtual_fs
22
20
  rom_path = '/rom.data'
23
21
  raise "ROM file not found in virtual filesystem at #{rom_path}" unless File.exist?(rom_path)
24
22
 
25
- rom_data = File.open(rom_path, 'rb') { |file| file.read.bytes }
23
+ rom_data = File.binread(rom_path).bytes
26
24
  @emulator = Rubyboy::EmulatorWasm.new(rom_data)
27
25
  end
28
26
 
29
27
  def read_pre_installed_rom(rom_name)
30
28
  raise 'ROM not found in allowed ROMs' unless ALLOWED_ROMS.include?(rom_name)
31
29
 
32
- rom_path = File.join('lib/roms', rom_name)
33
- rom_data = File.open(rom_path, 'r') { _1.read.bytes }
30
+ rom_path = File.join('/lib/roms', rom_name)
31
+ rom_data = File.binread(rom_path).bytes
34
32
  @emulator = Rubyboy::EmulatorWasm.new(rom_data)
35
33
  end
34
+
35
+ private
36
+
37
+ def write_framebuffer
38
+ File.binwrite('/video.data', @emulator.framebuffer.pack('V*'))
39
+ end
40
+
41
+ def write_audio
42
+ samples = @emulator.audio_samples
43
+ File.binwrite('/audio.data', samples.pack('e*')) unless samples.empty?
44
+ end
36
45
  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 +
@@ -6,10 +6,10 @@ module Rubyboy
6
6
  attr_accessor :enabled, :wave_duty_position
7
7
 
8
8
  WAVE_DUTY = [
9
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
- [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
- [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0].freeze, # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0].freeze, # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0].freeze, # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0].freeze # 75%
13
13
  ].freeze
14
14
 
15
15
  def initialize
@@ -6,10 +6,10 @@ module Rubyboy
6
6
  attr_accessor :enabled, :wave_duty_position
7
7
 
8
8
  WAVE_DUTY = [
9
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
- [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
- [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0].freeze, # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0].freeze, # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0].freeze, # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0].freeze # 75%
13
13
  ].freeze
14
14
 
15
15
  def initialize
@@ -5,13 +5,6 @@ module Rubyboy
5
5
  class Channel3
6
6
  attr_accessor :enabled, :wave_duty_position, :wave_ram
7
7
 
8
- WAVE_DUTY = [
9
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
- [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
- [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
- ].freeze
14
-
15
8
  def initialize
16
9
  @cycles = 0
17
10
  @frequency = 0
@@ -76,9 +69,9 @@ module Rubyboy
76
69
  def dac_output
77
70
  return 0.0 unless @dac_enabled && @enabled
78
71
 
79
- ret = ((0xf & (
72
+ ret = (0xf & (
80
73
  @wave_ram[@wave_duty_position >> 1] >> ((@wave_duty_position & 0x01) << 2)
81
- ))) >> @volume_shift
74
+ )) >> @volume_shift
82
75
 
83
76
  (ret / 7.5) - 1.0
84
77
  end
@@ -5,13 +5,6 @@ module Rubyboy
5
5
  class Channel4
6
6
  attr_accessor :enabled, :wave_duty_position
7
7
 
8
- WAVE_DUTY = [
9
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
- [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
- [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
- ].freeze
14
-
15
8
  def initialize
16
9
  @cycles = 0
17
10
  @frequency = 0
data/lib/rubyboy/audio.rb CHANGED
@@ -21,15 +21,28 @@ module Rubyboy
21
21
  SDL.PauseAudioDevice(@device, 0)
22
22
  end
23
23
 
24
- def queue(buffer)
25
- # sleep(0.001) while SDL.GetQueuedAudioSize(@device) > 8192
26
-
27
- SDL.ClearQueuedAudio(@device) if SDL.GetQueuedAudioSize(@device) > 8192
24
+ def queue(buffer, wait: true)
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
+ # Unlimited playback drops this buffer instead, so audio never slows it down.
32
+ while SDL.GetQueuedAudioSize(@device) > 8192
33
+ return unless wait
34
+
35
+ sleep(0.001)
36
+ end
28
37
 
29
38
  buf_ptr = FFI::MemoryPointer.new(:float, buffer.size)
30
39
  buf_ptr.put_array_of_float(0, buffer)
31
40
 
32
41
  SDL.QueueAudio(@device, buf_ptr, buffer.size * buf_ptr.type_size)
33
42
  end
43
+
44
+ def clear_queue
45
+ SDL.ClearQueuedAudio(@device)
46
+ end
34
47
  end
35
48
  end
@@ -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
@@ -15,19 +18,13 @@ module Rubyboy
15
18
  def read_byte(addr)
16
19
  case (addr >> 12)
17
20
  when 0x0, 0x1, 0x2, 0x3
18
- @rom.data[addr]
21
+ @rom.data[rom_offset(addr)] || 0xff
19
22
  when 0x4, 0x5, 0x6, 0x7
20
- @rom.data[addr + ((@rom_bank - 1) << 14)]
23
+ @rom.data[rom_offset(addr)] || 0xff
21
24
  when 0xa, 0xb
22
- if @ram_enable
23
- if @ram_banking_mode
24
- @ram.eram[addr - 0xa000 + (@ram_bank << 11)]
25
- else
26
- @ram.eram[addr - 0xa000]
27
- end
28
- else
29
- 0xff
30
- end
25
+ return 0xff unless @ram_enable
26
+
27
+ @ram.eram[ram_offset(addr)] || 0xff
31
28
  end
32
29
  end
33
30
 
@@ -43,15 +40,30 @@ module Rubyboy
43
40
  when 0x6, 0x7
44
41
  @ram_banking_mode = value & 0x01 == 0x01
45
42
  when 0xa, 0xb
46
- if @ram_enable
47
- if @ram_banking_mode
48
- @ram.eram[addr - 0xa000 + (@ram_bank << 11)] = value
49
- else
50
- @ram.eram[addr - 0xa000] = value
51
- end
52
- end
43
+ return unless @ram_enable
44
+
45
+ offset = ram_offset(addr)
46
+ @ram.eram[offset] = value if offset < @ram.eram.size
53
47
  end
54
48
  end
49
+
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
61
+ end
62
+
63
+ def ram_offset(addr)
64
+ bank = @ram_banking_mode ? @ram_bank : 0
65
+ (addr - 0xa000) + (bank * RAM_BANK_SIZE)
66
+ end
55
67
  end
56
68
  end
57
69
  end