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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +39 -27
- data/exe/rubyboy +16 -5
- data/exe/rubyboy-bench +15 -7
- data/lib/bench.rb +33 -7
- data/lib/executor.rb +19 -10
- data/lib/rubyboy/apu.rb +15 -4
- 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 +17 -4
- data/lib/rubyboy/cartridge/mbc1.rb +30 -18
- data/lib/rubyboy/emulator.rb +145 -47
- data/lib/rubyboy/emulator_headless.rb +6 -2
- data/lib/rubyboy/emulator_wasm.rb +31 -8
- data/lib/rubyboy/hardware_state.rb +450 -0
- data/lib/rubyboy/lcd.rb +2 -1
- data/lib/rubyboy/ram.rb +2 -2
- data/lib/rubyboy/rom.rb +26 -0
- data/lib/rubyboy/save_file.rb +36 -0
- data/lib/rubyboy/sdl.rb +12 -0
- data/lib/rubyboy/state_file.rb +49 -0
- data/lib/rubyboy/version.rb +1 -1
- data/lib/rubyboy.rb +3 -0
- metadata +9 -48
- 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
data/lib/rubyboy/emulator.rb
CHANGED
|
@@ -4,78 +4,176 @@ module Rubyboy
|
|
|
4
4
|
class Emulator
|
|
5
5
|
CPU_CLOCK_HZ = 4_194_304
|
|
6
6
|
CYCLE_NANOSEC = 1_000_000_000 / CPU_CLOCK_HZ
|
|
7
|
+
SAVE_STATE_KEYS = [
|
|
8
|
+
SDL::SDL_SCANCODE_1,
|
|
9
|
+
SDL::SDL_SCANCODE_2,
|
|
10
|
+
SDL::SDL_SCANCODE_3,
|
|
11
|
+
SDL::SDL_SCANCODE_4,
|
|
12
|
+
SDL::SDL_SCANCODE_5,
|
|
13
|
+
SDL::SDL_SCANCODE_6,
|
|
14
|
+
SDL::SDL_SCANCODE_7,
|
|
15
|
+
SDL::SDL_SCANCODE_8,
|
|
16
|
+
SDL::SDL_SCANCODE_9,
|
|
17
|
+
SDL::SDL_SCANCODE_0
|
|
18
|
+
].freeze
|
|
7
19
|
|
|
8
20
|
def initialize(rom_path)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
21
|
+
@rom_path = rom_path
|
|
22
|
+
rom_data = File.binread(rom_path).bytes
|
|
23
|
+
@rom = Rom.new(rom_data)
|
|
24
|
+
@ram = Ram.new(@rom)
|
|
25
|
+
@mbc = Cartridge::Factory.create(@rom, @ram)
|
|
26
|
+
@interrupt = Interrupt.new
|
|
27
|
+
@ppu = Ppu.new(@interrupt)
|
|
28
|
+
@timer = Timer.new(@interrupt)
|
|
29
|
+
@joypad = Joypad.new(@interrupt)
|
|
17
30
|
@apu = Apu.new
|
|
18
|
-
@bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu)
|
|
19
|
-
@cpu = Cpu.new(@bus, interrupt)
|
|
31
|
+
@bus = Bus.new(@ppu, @rom, @ram, @mbc, @timer, @interrupt, @joypad, @apu)
|
|
32
|
+
@cpu = Cpu.new(@bus, @interrupt)
|
|
20
33
|
@lcd = Lcd.new
|
|
21
34
|
@audio = Audio.new
|
|
35
|
+
@prev_save_state_keys = Array.new(SAVE_STATE_KEYS.size, 0)
|
|
36
|
+
@save_file = @rom.battery? ? SaveFile.new(default_save_path(rom_path)) : nil
|
|
37
|
+
load_save_file
|
|
22
38
|
end
|
|
23
39
|
|
|
24
|
-
def start
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
catch(:exit_loop) do
|
|
30
|
-
loop do
|
|
31
|
-
elapsed_real_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
|
|
32
|
-
while elapsed_real_time > elapsed_machine_time
|
|
33
|
-
cycles = @cpu.exec
|
|
34
|
-
@timer.step(cycles)
|
|
35
|
-
@audio.queue(@apu.samples) if @apu.step(cycles)
|
|
36
|
-
if @ppu.step(cycles)
|
|
37
|
-
@lcd.draw(@ppu.buffer)
|
|
38
|
-
key_input_check
|
|
39
|
-
throw :exit_loop if @lcd.window_should_close?
|
|
40
|
-
end
|
|
40
|
+
def start(frames: nil, realtime: true)
|
|
41
|
+
raise ArgumentError, 'frames must be a positive integer' unless frames.nil? || (frames.is_a?(Integer) && frames > 0)
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
SDL.InitSubSystem(SDL::INIT_KEYBOARD)
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) if realtime
|
|
47
|
+
elapsed_machine_time = 0
|
|
48
|
+
frame_count = 0
|
|
49
|
+
catch(:exit_loop) do
|
|
50
|
+
loop do
|
|
51
|
+
elapsed_real_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time if realtime
|
|
52
|
+
while !realtime || elapsed_real_time > elapsed_machine_time
|
|
53
|
+
cycles = @cpu.exec
|
|
54
|
+
@timer.step(cycles)
|
|
55
|
+
@audio.queue(@apu.samples, wait: realtime) if @apu.step(cycles)
|
|
56
|
+
if @ppu.step(cycles)
|
|
57
|
+
@lcd.draw(@ppu.buffer)
|
|
58
|
+
key_input_check
|
|
59
|
+
frame_count += 1
|
|
60
|
+
throw :exit_loop if frames && frame_count >= frames
|
|
61
|
+
throw :exit_loop if @lcd.window_should_close?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
elapsed_machine_time += cycles * CYCLE_NANOSEC if realtime
|
|
65
|
+
end
|
|
43
66
|
end
|
|
44
67
|
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
puts e.to_s[0, 100]
|
|
70
|
+
raise e
|
|
71
|
+
ensure
|
|
72
|
+
@lcd.close_window
|
|
73
|
+
save_save_file
|
|
45
74
|
end
|
|
46
|
-
@lcd.close_window
|
|
47
|
-
rescue StandardError => e
|
|
48
|
-
puts e.to_s[0, 100]
|
|
49
|
-
raise e
|
|
50
75
|
end
|
|
51
76
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
56
|
-
while frame_count < frames
|
|
57
|
-
cycles = @cpu.exec
|
|
58
|
-
@timer.step(cycles)
|
|
59
|
-
if @ppu.step(cycles)
|
|
60
|
-
key_input_check
|
|
61
|
-
frame_count += 1
|
|
62
|
-
end
|
|
63
|
-
end
|
|
77
|
+
def save_state(slot: nil, path: nil)
|
|
78
|
+
state_path = path || slot_path(slot)
|
|
79
|
+
return false unless StateFile.write(state_path, rom: @rom) { hardware_state }
|
|
64
80
|
|
|
65
|
-
|
|
81
|
+
puts "Saved state to #{state_path}"
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_state(slot: nil, path: nil)
|
|
86
|
+
state_path = path || slot_path(slot)
|
|
87
|
+
return false unless StateFile.read(state_path, rom: @rom) { |state| restore_hardware_state(state) }
|
|
88
|
+
|
|
89
|
+
@audio.clear_queue
|
|
90
|
+
puts "Loaded state from #{state_path}"
|
|
91
|
+
true
|
|
66
92
|
end
|
|
67
93
|
|
|
68
94
|
private
|
|
69
95
|
|
|
96
|
+
def default_save_path(rom_path)
|
|
97
|
+
dir = File.dirname(rom_path)
|
|
98
|
+
base = File.basename(rom_path, '.*')
|
|
99
|
+
File.join(dir, "#{base}.sav")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def slot_path(slot)
|
|
103
|
+
raise ArgumentError, 'slot must be between 0 and 9' unless (0..9).cover?(slot)
|
|
104
|
+
|
|
105
|
+
dir = File.dirname(@rom_path)
|
|
106
|
+
base = File.basename(@rom_path, '.*')
|
|
107
|
+
File.join(dir, "#{base}.state#{slot}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def hardware_state
|
|
111
|
+
{
|
|
112
|
+
schema: 'game_boy_console_and_cartridge',
|
|
113
|
+
console: {
|
|
114
|
+
cpu: @cpu.hardware_state,
|
|
115
|
+
ram: @ram.hardware_state,
|
|
116
|
+
ppu: @ppu.hardware_state,
|
|
117
|
+
apu: @apu.hardware_state,
|
|
118
|
+
timer: @timer.hardware_state,
|
|
119
|
+
interrupt: @interrupt.hardware_state,
|
|
120
|
+
joypad: @joypad.hardware_state
|
|
121
|
+
},
|
|
122
|
+
cartridge: @mbc.hardware_state
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def restore_hardware_state(state)
|
|
127
|
+
console = state.fetch(:console)
|
|
128
|
+
@cpu.restore_hardware_state(console.fetch(:cpu))
|
|
129
|
+
@ram.restore_hardware_state(console.fetch(:ram))
|
|
130
|
+
@ppu.restore_hardware_state(console.fetch(:ppu))
|
|
131
|
+
@apu.restore_hardware_state(console.fetch(:apu))
|
|
132
|
+
@timer.restore_hardware_state(console.fetch(:timer))
|
|
133
|
+
@interrupt.restore_hardware_state(console.fetch(:interrupt))
|
|
134
|
+
@joypad.restore_hardware_state(console.fetch(:joypad))
|
|
135
|
+
@mbc.restore_hardware_state(state.fetch(:cartridge))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def load_save_file
|
|
139
|
+
return unless @save_file
|
|
140
|
+
|
|
141
|
+
bytes = @save_file.read(@ram.eram.size)
|
|
142
|
+
return unless bytes
|
|
143
|
+
|
|
144
|
+
@ram.eram.replace(bytes)
|
|
145
|
+
puts "Loaded save from #{@save_file.path}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def save_save_file
|
|
149
|
+
return unless @save_file
|
|
150
|
+
|
|
151
|
+
puts "Saved to #{@save_file.path}" if @save_file.write(@ram.eram)
|
|
152
|
+
end
|
|
153
|
+
|
|
70
154
|
def key_input_check
|
|
71
155
|
SDL.PumpEvents
|
|
72
156
|
keyboard = SDL.GetKeyboardState(nil)
|
|
73
157
|
keyboard_state = keyboard.read_array_of_uint8(229)
|
|
74
158
|
|
|
75
|
-
|
|
76
|
-
|
|
159
|
+
check_state_save_keys(keyboard_state)
|
|
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)
|
|
77
162
|
@joypad.direction_button(15 - direction)
|
|
78
163
|
@joypad.action_button(15 - action)
|
|
79
164
|
end
|
|
165
|
+
|
|
166
|
+
def check_state_save_keys(keyboard_state)
|
|
167
|
+
shift = keyboard_state[SDL::SDL_SCANCODE_LSHIFT] > 0
|
|
168
|
+
SAVE_STATE_KEYS.each_with_index do |key, index|
|
|
169
|
+
pressed = keyboard_state[key]
|
|
170
|
+
next unless pressed > 0 && @prev_save_state_keys[index] == 0
|
|
171
|
+
|
|
172
|
+
slot = index == 9 ? 0 : index + 1
|
|
173
|
+
shift ? load_state(slot:) : save_state(slot:)
|
|
174
|
+
ensure
|
|
175
|
+
@prev_save_state_keys[index] = pressed
|
|
176
|
+
end
|
|
177
|
+
end
|
|
80
178
|
end
|
|
81
179
|
end
|
|
@@ -14,9 +14,9 @@ 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
|
-
ram = Ram.new
|
|
19
|
+
ram = Ram.new(rom)
|
|
20
20
|
mbc = Cartridge::Factory.create(rom, ram)
|
|
21
21
|
interrupt = Interrupt.new
|
|
22
22
|
@ppu = Ppu.new(interrupt)
|
|
@@ -27,6 +27,10 @@ module Rubyboy
|
|
|
27
27
|
@cpu = Cpu.new(bus, interrupt)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def framebuffer
|
|
31
|
+
@ppu.buffer
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
def step
|
|
31
35
|
loop do
|
|
32
36
|
cycles = @cpu.exec
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require_relative 'apu'
|
|
4
4
|
require_relative 'bus'
|
|
5
5
|
require_relative 'cpu'
|
|
6
|
-
require_relative 'emulator'
|
|
7
6
|
require_relative 'ppu'
|
|
8
7
|
require_relative 'rom'
|
|
9
8
|
require_relative 'ram'
|
|
@@ -14,12 +13,13 @@ require_relative 'cartridge/factory'
|
|
|
14
13
|
|
|
15
14
|
module Rubyboy
|
|
16
15
|
class EmulatorWasm
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
FRAME_CYCLES = 70_224
|
|
17
|
+
|
|
18
|
+
attr_reader :audio_samples
|
|
19
19
|
|
|
20
20
|
def initialize(rom_data)
|
|
21
21
|
rom = Rom.new(rom_data)
|
|
22
|
-
ram = Ram.new
|
|
22
|
+
ram = Ram.new(rom)
|
|
23
23
|
mbc = Cartridge::Factory.create(rom, ram)
|
|
24
24
|
interrupt = Interrupt.new
|
|
25
25
|
@ppu = Ppu.new(interrupt)
|
|
@@ -28,16 +28,39 @@ module Rubyboy
|
|
|
28
28
|
@apu = Apu.new
|
|
29
29
|
@bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu)
|
|
30
30
|
@cpu = Cpu.new(@bus, interrupt)
|
|
31
|
+
@audio_samples = []
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def step(direction_key, action_key)
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
35
|
+
set_input(direction_key, action_key)
|
|
36
|
+
@audio_samples.clear
|
|
37
|
+
elapsed_cycles = 0
|
|
38
|
+
# LCD-off games must return to the browser event loop as well.
|
|
39
|
+
while elapsed_cycles < FRAME_CYCLES
|
|
37
40
|
cycles = @cpu.exec
|
|
41
|
+
elapsed_cycles += cycles
|
|
38
42
|
@timer.step(cycles)
|
|
39
|
-
|
|
43
|
+
collect_audio(cycles)
|
|
44
|
+
break if @ppu.step(cycles)
|
|
40
45
|
end
|
|
46
|
+
framebuffer
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def framebuffer
|
|
50
|
+
@ppu.buffer
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def set_input(direction_key, action_key)
|
|
56
|
+
@joypad.direction_button(direction_key & 15)
|
|
57
|
+
@joypad.action_button(action_key & 15)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def collect_audio(cycles)
|
|
61
|
+
# Complete 512-frame stereo blocks are copied before the APU reuses its
|
|
62
|
+
# sample buffer. Its incomplete block remains intact between calls.
|
|
63
|
+
@audio_samples.concat(@apu.samples) if @apu.step(cycles)
|
|
41
64
|
end
|
|
42
65
|
end
|
|
43
66
|
end
|