rubyboy 1.6.0 → 1.7.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +40 -84
  4. data/exe/rubyboy +5 -7
  5. data/exe/rubyboy-bench +9 -20
  6. data/lib/bench.rb +17 -46
  7. data/lib/executor.rb +19 -10
  8. data/lib/rubyboy/apu_channels/channel1.rb +4 -4
  9. data/lib/rubyboy/apu_channels/channel2.rb +4 -4
  10. data/lib/rubyboy/apu_channels/channel3.rb +2 -9
  11. data/lib/rubyboy/apu_channels/channel4.rb +0 -7
  12. data/lib/rubyboy/audio.rb +7 -2
  13. data/lib/rubyboy/cpu.rb +2 -2
  14. data/lib/rubyboy/emulator.rb +7 -23
  15. data/lib/rubyboy/emulator_headless.rb +1 -1
  16. data/lib/rubyboy/emulator_wasm.rb +30 -7
  17. data/lib/rubyboy/hardware_state.rb +1 -0
  18. data/lib/rubyboy/lcd.rb +2 -10
  19. data/lib/rubyboy/rom.rb +3 -0
  20. data/lib/rubyboy/sdl.rb +1 -1
  21. data/lib/rubyboy/state_file.rb +1 -2
  22. data/lib/rubyboy/version.rb +1 -1
  23. metadata +4 -60
  24. data/.rspec +0 -3
  25. data/.rubocop.yml +0 -54
  26. data/CODE_OF_CONDUCT.md +0 -84
  27. data/Rakefile +0 -12
  28. data/docs/favicon.png +0 -0
  29. data/docs/index.html +0 -75
  30. data/docs/index.js +0 -113
  31. data/docs/logo-light-23.svg +0 -1
  32. data/docs/ogp.png +0 -0
  33. data/docs/styles.css +0 -245
  34. data/docs/worker.js +0 -115
  35. data/exe/rubyboy-wasm +0 -26
  36. data/lib/opcodes.json +0 -11657
  37. data/lib/roms/bgbtest.gb +0 -0
  38. data/lib/roms/cpu_instrs/cpu_instrs.gb +0 -0
  39. data/lib/roms/cpu_instrs/individual/01-special.gb +0 -0
  40. data/lib/roms/cpu_instrs/individual/02-interrupts.gb +0 -0
  41. data/lib/roms/cpu_instrs/individual/03-op sp,hl.gb +0 -0
  42. data/lib/roms/cpu_instrs/individual/04-op r,imm.gb +0 -0
  43. data/lib/roms/cpu_instrs/individual/05-op rp.gb +0 -0
  44. data/lib/roms/cpu_instrs/individual/06-ld r,r.gb +0 -0
  45. data/lib/roms/cpu_instrs/individual/07-jr,jp,call,ret,rst.gb +0 -0
  46. data/lib/roms/cpu_instrs/individual/08-misc instrs.gb +0 -0
  47. data/lib/roms/cpu_instrs/individual/09-op r,r.gb +0 -0
  48. data/lib/roms/cpu_instrs/individual/10-bit ops.gb +0 -0
  49. data/lib/roms/cpu_instrs/individual/11-op a,(hl).gb +0 -0
  50. data/lib/roms/cpu_instrs/readme.txt +0 -119
  51. data/lib/roms/dmg-acid2.gb +0 -0
  52. data/lib/roms/hello-world.gb +0 -0
  53. data/lib/roms/instr_timing/instr_timing.gb +0 -0
  54. data/lib/roms/instr_timing/readme.txt +0 -139
  55. data/lib/rubyboy/raylib/audio.rb +0 -32
  56. data/lib/rubyboy/raylib/lcd.rb +0 -40
  57. data/lib/rubyboy/raylib/raylib_loader.rb +0 -36
  58. data/lib/stackprof.rb +0 -14
  59. data/resource/logo/logo.png +0 -0
  60. data/resource/logo/logo.svg +0 -1
  61. data/resource/logo/rubyboy.png +0 -0
  62. data/resource/screenshots/pokemon.png +0 -0
  63. data/resource/screenshots/puyopuyo.png +0 -0
  64. data/sig/rubyboy.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09e12943a26b3089da7e75f3a26a0994dd71c1a748b63c840c49c3772502b625'
4
- data.tar.gz: c37fdfa6182bb28cbca6ffe5438670860e0e6c10e32b581509dbfca28e12f70d
3
+ metadata.gz: 5b4b18354440d4cb9fdfe8d35a4d84414b76d806119167d52e16dfb2b039624b
4
+ data.tar.gz: a6d24e7a4f369e5628c25cc64c51a266454aa94e21ffaccb3e178941ed08e248
5
5
  SHA512:
6
- metadata.gz: 35c057be65b5b1143d870d6db5d8a0b769074e42c10281680b411e1d348f2b8b5109ff12dcddae3ca3b803b6a379fff4f7343fd0e16b111e25686d25d5468861
7
- data.tar.gz: 900d258e730fdd03f23b508d1ff4cfd3e80b738ebf0c9ce49e6a064faeb0456a01be84bab269d62f59bcb7895d25b1dfeb08d41a2d39c0fabce4b52427d63a91
6
+ metadata.gz: 2cf41bc728ebde97dff695479aa1b451a68a24f0562cf79d3d630589b78dc2ca7496d605b88edb708f5c402396be7599d41b99a7c51b8a07977cbb461f5ae79b
7
+ data.tar.gz: 5405ed2472b2f53f62c57c01958536dc44bb0859f246d621792f5aeb48b7bb6ae7bcd75075932871ab919eb7931d0dea8114a9ea1e8d0705aa87b82f3ce03bb8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [1.7.1] - 2026-10-11
2
+
3
+ - Fix RLC and RRC on memory writing values over a byte, which could stop the Spinel browser build
4
+
5
+ ## [1.7.0] - 2026-10-10
6
+
7
+ - Require Ruby 4.0 or higher
8
+ - Keep audio output with --unlimited by dropping audio that cannot be played in time
9
+ - Remove the --no-audio player option
10
+ - Remove rubyboy-bench --render and --json, and the SDL 2.0.18 requirement
11
+ - Print a checksum of the final frame from rubyboy-bench to check that a runtime emulated correctly
12
+ - Run rubyboy-bench without loading SDL
13
+ - Run the bundled Tobu Tobu Girl ROM when rubyboy or rubyboy-bench gets no ROM path, from any directory
14
+ - Read ROM files in binary mode, which fixes loading them on Windows
15
+ - Make the APU duty tables shareable between Ractors
16
+ - Ship only the runtime code, the default ROM and documents in the gem, without the rubyboy-wasm command or test ROMs
17
+
1
18
  ## [1.6.0] - 2026-10-04
2
19
 
3
20
  - Add battery-backed saves and hardware-oriented save states
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,103 +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) 2.0.18 or higher.
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>
38
-
39
- | Key | Button |
40
- | :---: | :----: |
41
- | `W` | ↑ |
42
- | `A` | ← |
43
- | `S` | ↓ |
44
- | `D` | → |
45
- | `J` | A |
46
- | `K` | B |
47
- | `U` | Select |
48
- | `I` | Start |
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.
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 |
55
+
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
105
65
 
106
66
  ## Contributing
107
67
 
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).
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).
109
69
 
110
70
  ## License
111
71
 
112
72
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
113
-
114
- ## Code of Conduct
115
-
116
- 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
@@ -6,11 +6,10 @@ require 'optparse'
6
6
 
7
7
  frames = nil
8
8
  realtime = true
9
- audio = true
10
9
 
11
10
  begin
12
11
  OptionParser.new do |opts|
13
- opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
12
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [ROM_PATH]"
14
13
 
15
14
  opts.on('--frames N', 'Exit after N frames (positive integer)') do |value|
16
15
  value = Integer(value)
@@ -18,8 +17,7 @@ begin
18
17
 
19
18
  frames = value
20
19
  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 }
20
+ opts.on('--unlimited', 'Disable real-time synchronization; excess audio is dropped') { realtime = false }
23
21
 
24
22
  opts.on('-h', '--help', 'Displays this help') do
25
23
  puts opts
@@ -28,11 +26,11 @@ begin
28
26
  end.parse!
29
27
  rescue OptionParser::ParseError, ArgumentError => e
30
28
  puts e.message
31
- puts "Usage: #{File.basename($PROGRAM_NAME)} ROM_PATH"
29
+ puts "Usage: #{File.basename($PROGRAM_NAME)} [ROM_PATH]"
32
30
  exit 1
33
31
  end
34
32
 
35
- rom_path = ARGV.shift || 'lib/roms/tobu.gb'
33
+ rom_path = ARGV.shift || Rubyboy::Rom::DEFAULT_PATH
36
34
 
37
35
  puts "Ruby: #{RUBY_VERSION}"
38
36
  if defined?(RubyVM::YJIT)
@@ -41,4 +39,4 @@ else
41
39
  puts 'YJIT is not available in this environment.'
42
40
  end
43
41
 
44
- Rubyboy::Emulator.new(rom_path, audio:).start(frames:, realtime:)
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
 
@@ -11,23 +10,15 @@ begin
11
10
  opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]"
12
11
 
13
12
  opts.on('--count COUNT', Integer, 'Number of trials for the benchmark') do |count|
14
- options[:count] = Integer(count)
13
+ options[:count] = count
15
14
  end
16
15
 
17
16
  opts.on('--frames FRAMES', Integer, 'Number of measured frames per trial') do |frames|
18
- options[:frames] = Integer(frames)
17
+ options[:frames] = frames
19
18
  end
20
19
 
21
20
  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
21
+ options[:warmup_frames] = frames
31
22
  end
32
23
 
33
24
  opts.on('--rom-path ROM_PATH', String, 'Path to the ROM file') do |rom_path|
@@ -45,18 +36,16 @@ rescue OptionParser::ParseError, ArgumentError => e
45
36
  exit 1
46
37
  end
47
38
 
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
39
+ puts "Ruby: #{RUBY_VERSION}"
40
+ if defined?(RubyVM::YJIT)
41
+ puts "YJIT: #{RubyVM::YJIT.enabled?}"
42
+ else
43
+ puts 'YJIT is not available in this environment.'
55
44
  end
56
45
 
57
46
  begin
58
47
  Rubyboy::Bench.new.run(**options)
59
- rescue ArgumentError, Rubyboy::Bench::WindowClosed => e
48
+ rescue ArgumentError => e
60
49
  warn e.message
61
50
  exit 1
62
51
  end
data/lib/bench.rb CHANGED
@@ -1,44 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require_relative 'rubyboy/emulator_headless'
5
4
 
6
5
  module Rubyboy
7
6
  class Bench
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)
7
+ def run(count: 3, frames: 1500, rom_path: Rom::DEFAULT_PATH, warmup_frames: 0)
11
8
  validate_integer(:count, count, 1)
12
9
  validate_integer(:frames, frames, 1)
13
10
  validate_integer(:warmup_frames, warmup_frames, 0)
14
- require_relative 'rubyboy/lcd' if render
15
11
 
16
- trials = []
17
12
  time_sum = 0
13
+ frame_checksum = 0
18
14
  count.times do |i|
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
15
+ emulator = Rubyboy::EmulatorHeadless.new(rom_path)
16
+ run_frames(emulator, warmup_frames)
17
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
18
+ run_frames(emulator, frames)
19
+ time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
20
+ puts "#{i + 1}: #{time / 1_000_000_000.0} sec"
21
+
23
22
  time_sum += time
23
+ frame_checksum = checksum(emulator.framebuffer)
24
24
  end
25
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
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}"
42
29
  end
43
30
 
44
31
  private
@@ -50,33 +37,17 @@ module Rubyboy
50
37
  raise ArgumentError, "#{name} must be #{requirement}"
51
38
  end
52
39
 
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)
58
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
59
- run_frames(emulator, lcd, frames)
60
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
61
- ensure
62
- lcd&.close_window
63
- end
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 }
64
43
  end
65
44
 
66
- def run_frames(emulator, lcd, frames)
45
+ def run_frames(emulator, frames)
67
46
  frame_count = 0
68
47
  while frame_count < frames
69
- raise WindowClosed, 'Benchmark stopped because the window was closed.' if lcd&.window_should_close?
70
-
71
48
  emulator.step
72
- lcd&.draw(emulator.framebuffer)
73
49
  frame_count += 1
74
50
  end
75
51
  end
76
-
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: }
80
- end
81
52
  end
82
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
@@ -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,14 +21,19 @@ module Rubyboy
21
21
  SDL.PauseAudioDevice(@device, 0)
22
22
  end
23
23
 
24
- def queue(buffer)
24
+ def queue(buffer, wait: true)
25
25
  # Block until the SDL audio queue has drained enough to accept another
26
26
  # buffer. The previous implementation called SDL.ClearQueuedAudio here
27
27
  # instead, which dropped the entire queue whenever the emulator ran ahead
28
28
  # of real time — that produced audible pops/gaps in BGM (most noticeable
29
29
  # in Pokemon-style tracks). Blocking here costs a tiny bit of frame
30
30
  # budget but keeps audio continuous.
31
- sleep(0.001) while SDL.GetQueuedAudioSize(@device) > 8192
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
32
37
 
33
38
  buf_ptr = FFI::MemoryPointer.new(:float, buffer.size)
34
39
  buf_ptr.put_array_of_float(0, buffer)
data/lib/rubyboy/cpu.rb CHANGED
@@ -1071,7 +1071,7 @@ module Rubyboy
1071
1071
 
1072
1072
  def rlc8(x, cycles:)
1073
1073
  value = get_value(x)
1074
- value = (value << 1) | (value >> 7)
1074
+ value = ((value << 1) | (value >> 7)) & 0xff
1075
1075
  set_value(x, value)
1076
1076
  update_flags(
1077
1077
  z: value == 0,
@@ -1085,7 +1085,7 @@ module Rubyboy
1085
1085
 
1086
1086
  def rrc8(x, cycles:)
1087
1087
  value = get_value(x)
1088
- value = (value >> 1) | (value << 7)
1088
+ value = ((value >> 1) | (value << 7)) & 0xff
1089
1089
  set_value(x, value)
1090
1090
  update_flags(
1091
1091
  z: value == 0,
@@ -17,9 +17,9 @@ module Rubyboy
17
17
  SDL::SDL_SCANCODE_0
18
18
  ].freeze
19
19
 
20
- def initialize(rom_path, audio: true)
20
+ def initialize(rom_path)
21
21
  @rom_path = rom_path
22
- rom_data = File.open(rom_path, 'r') { _1.read.bytes }
22
+ rom_data = File.binread(rom_path).bytes
23
23
  @rom = Rom.new(rom_data)
24
24
  @ram = Ram.new(@rom)
25
25
  @mbc = Cartridge::Factory.create(@rom, @ram)
@@ -31,7 +31,7 @@ module Rubyboy
31
31
  @bus = Bus.new(@ppu, @rom, @ram, @mbc, @timer, @interrupt, @joypad, @apu)
32
32
  @cpu = Cpu.new(@bus, @interrupt)
33
33
  @lcd = Lcd.new
34
- @audio = audio ? Audio.new : nil
34
+ @audio = Audio.new
35
35
  @prev_save_state_keys = Array.new(SAVE_STATE_KEYS.size, 0)
36
36
  @save_file = @rom.battery? ? SaveFile.new(default_save_path(rom_path)) : nil
37
37
  load_save_file
@@ -52,7 +52,7 @@ module Rubyboy
52
52
  while !realtime || elapsed_real_time > elapsed_machine_time
53
53
  cycles = @cpu.exec
54
54
  @timer.step(cycles)
55
- @audio.queue(@apu.samples) if @apu.step(cycles) && @audio
55
+ @audio.queue(@apu.samples, wait: realtime) if @apu.step(cycles)
56
56
  if @ppu.step(cycles)
57
57
  @lcd.draw(@ppu.buffer)
58
58
  key_input_check
@@ -74,22 +74,6 @@ module Rubyboy
74
74
  end
75
75
  end
76
76
 
77
- def bench(frames)
78
- @lcd.close_window
79
- frame_count = 0
80
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
81
- while frame_count < frames
82
- cycles = @cpu.exec
83
- @timer.step(cycles)
84
- if @ppu.step(cycles)
85
- key_input_check
86
- frame_count += 1
87
- end
88
- end
89
-
90
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
91
- end
92
-
93
77
  def save_state(slot: nil, path: nil)
94
78
  state_path = path || slot_path(slot)
95
79
  return false unless StateFile.write(state_path, rom: @rom) { hardware_state }
@@ -102,7 +86,7 @@ module Rubyboy
102
86
  state_path = path || slot_path(slot)
103
87
  return false unless StateFile.read(state_path, rom: @rom) { |state| restore_hardware_state(state) }
104
88
 
105
- @audio&.clear_queue
89
+ @audio.clear_queue
106
90
  puts "Loaded state from #{state_path}"
107
91
  true
108
92
  end
@@ -173,8 +157,8 @@ module Rubyboy
173
157
  keyboard_state = keyboard.read_array_of_uint8(229)
174
158
 
175
159
  check_state_save_keys(keyboard_state)
176
- direction = (keyboard_state[SDL::SDL_SCANCODE_D]) | (keyboard_state[SDL::SDL_SCANCODE_A] << 1) | (keyboard_state[SDL::SDL_SCANCODE_W] << 2) | (keyboard_state[SDL::SDL_SCANCODE_S] << 3)
177
- action = (keyboard_state[SDL::SDL_SCANCODE_K]) | (keyboard_state[SDL::SDL_SCANCODE_J] << 1) | (keyboard_state[SDL::SDL_SCANCODE_U] << 2) | (keyboard_state[SDL::SDL_SCANCODE_I] << 3)
160
+ direction = keyboard_state[SDL::SDL_SCANCODE_D] | (keyboard_state[SDL::SDL_SCANCODE_A] << 1) | (keyboard_state[SDL::SDL_SCANCODE_W] << 2) | (keyboard_state[SDL::SDL_SCANCODE_S] << 3)
161
+ action = keyboard_state[SDL::SDL_SCANCODE_K] | (keyboard_state[SDL::SDL_SCANCODE_J] << 1) | (keyboard_state[SDL::SDL_SCANCODE_U] << 2) | (keyboard_state[SDL::SDL_SCANCODE_I] << 3)
178
162
  @joypad.direction_button(15 - direction)
179
163
  @joypad.action_button(15 - action)
180
164
  end
@@ -14,7 +14,7 @@ require_relative 'cartridge/factory'
14
14
  module Rubyboy
15
15
  class EmulatorHeadless
16
16
  def initialize(rom_path)
17
- rom_data = File.open(rom_path, 'r') { _1.read.bytes }
17
+ rom_data = File.binread(rom_path).bytes
18
18
  rom = Rom.new(rom_data)
19
19
  ram = Ram.new(rom)
20
20
  mbc = Cartridge::Factory.create(rom, ram)