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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +40 -84
- data/exe/rubyboy +5 -7
- data/exe/rubyboy-bench +9 -20
- data/lib/bench.rb +17 -46
- data/lib/executor.rb +19 -10
- data/lib/rubyboy/apu_channels/channel1.rb +4 -4
- data/lib/rubyboy/apu_channels/channel2.rb +4 -4
- data/lib/rubyboy/apu_channels/channel3.rb +2 -9
- data/lib/rubyboy/apu_channels/channel4.rb +0 -7
- data/lib/rubyboy/audio.rb +7 -2
- data/lib/rubyboy/cpu.rb +2 -2
- data/lib/rubyboy/emulator.rb +7 -23
- data/lib/rubyboy/emulator_headless.rb +1 -1
- data/lib/rubyboy/emulator_wasm.rb +30 -7
- data/lib/rubyboy/hardware_state.rb +1 -0
- data/lib/rubyboy/lcd.rb +2 -10
- data/lib/rubyboy/rom.rb +3 -0
- data/lib/rubyboy/sdl.rb +1 -1
- data/lib/rubyboy/state_file.rb +1 -2
- data/lib/rubyboy/version.rb +1 -1
- metadata +4 -60
- data/.rspec +0 -3
- data/.rubocop.yml +0 -54
- data/CODE_OF_CONDUCT.md +0 -84
- data/Rakefile +0 -12
- data/docs/favicon.png +0 -0
- data/docs/index.html +0 -75
- data/docs/index.js +0 -113
- data/docs/logo-light-23.svg +0 -1
- data/docs/ogp.png +0 -0
- data/docs/styles.css +0 -245
- data/docs/worker.js +0 -115
- data/exe/rubyboy-wasm +0 -26
- data/lib/opcodes.json +0 -11657
- data/lib/roms/bgbtest.gb +0 -0
- data/lib/roms/cpu_instrs/cpu_instrs.gb +0 -0
- data/lib/roms/cpu_instrs/individual/01-special.gb +0 -0
- data/lib/roms/cpu_instrs/individual/02-interrupts.gb +0 -0
- data/lib/roms/cpu_instrs/individual/03-op sp,hl.gb +0 -0
- data/lib/roms/cpu_instrs/individual/04-op r,imm.gb +0 -0
- data/lib/roms/cpu_instrs/individual/05-op rp.gb +0 -0
- data/lib/roms/cpu_instrs/individual/06-ld r,r.gb +0 -0
- data/lib/roms/cpu_instrs/individual/07-jr,jp,call,ret,rst.gb +0 -0
- data/lib/roms/cpu_instrs/individual/08-misc instrs.gb +0 -0
- data/lib/roms/cpu_instrs/individual/09-op r,r.gb +0 -0
- data/lib/roms/cpu_instrs/individual/10-bit ops.gb +0 -0
- data/lib/roms/cpu_instrs/individual/11-op a,(hl).gb +0 -0
- data/lib/roms/cpu_instrs/readme.txt +0 -119
- data/lib/roms/dmg-acid2.gb +0 -0
- data/lib/roms/hello-world.gb +0 -0
- data/lib/roms/instr_timing/instr_timing.gb +0 -0
- data/lib/roms/instr_timing/readme.txt +0 -139
- data/lib/rubyboy/raylib/audio.rb +0 -32
- data/lib/rubyboy/raylib/lcd.rb +0 -40
- data/lib/rubyboy/raylib/raylib_loader.rb +0 -36
- data/lib/stackprof.rb +0 -14
- data/resource/logo/logo.png +0 -0
- data/resource/logo/logo.svg +0 -1
- data/resource/logo/rubyboy.png +0 -0
- data/resource/screenshots/pokemon.png +0 -0
- data/resource/screenshots/puyopuyo.png +0 -0
- data/sig/rubyboy.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b4b18354440d4cb9fdfe8d35a4d84414b76d806119167d52e16dfb2b039624b
|
|
4
|
+
data.tar.gz: a6d24e7a4f369e5628c25cc64c51a266454aa94e21ffaccb3e178941ed08e248
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
>
|
|
26
|
+
> Run with YJIT (`RUBYOPT=--yjit`) for better performance.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
$ RUBYOPT=--yjit rubyboy [rom_path]
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Without a ROM path, it runs the bundled [Tobu Tobu Girl](https://tangramgames.dk/tobutobugirl/).
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
### Controls
|
|
32
35
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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](
|
|
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 ||
|
|
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
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
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.
|
|
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 = (
|
|
72
|
+
ret = (0xf & (
|
|
80
73
|
@wave_ram[@wave_duty_position >> 1] >> ((@wave_duty_position & 0x01) << 2)
|
|
81
|
-
))
|
|
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
|
-
|
|
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,
|
data/lib/rubyboy/emulator.rb
CHANGED
|
@@ -17,9 +17,9 @@ module Rubyboy
|
|
|
17
17
|
SDL::SDL_SCANCODE_0
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
|
-
def initialize(rom_path
|
|
20
|
+
def initialize(rom_path)
|
|
21
21
|
@rom_path = rom_path
|
|
22
|
-
rom_data = File.
|
|
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 =
|
|
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)
|
|
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
|
|
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 =
|
|
177
|
-
action =
|
|
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.
|
|
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)
|