rubyboy 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,49 +4,74 @@ 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
- def initialize(rom_path)
20
+ def initialize(rom_path, audio: true)
21
+ @rom_path = rom_path
9
22
  rom_data = File.open(rom_path, 'r') { _1.read.bytes }
10
- rom = Rom.new(rom_data)
11
- ram = Ram.new
12
- mbc = Cartridge::Factory.create(rom, ram)
13
- interrupt = Interrupt.new
14
- @ppu = Ppu.new(interrupt)
15
- @timer = Timer.new(interrupt)
16
- @joypad = Joypad.new(interrupt)
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
- @audio = Audio.new
34
+ @audio = audio ? Audio.new : nil
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
- SDL.InitSubSystem(SDL::INIT_KEYBOARD)
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)
26
42
 
27
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
28
- elapsed_machine_time = 0
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
43
+ begin
44
+ SDL.InitSubSystem(SDL::INIT_KEYBOARD)
41
45
 
42
- elapsed_machine_time += cycles * CYCLE_NANOSEC
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) if @apu.step(cycles) && @audio
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
77
  def bench(frames)
@@ -65,17 +90,106 @@ module Rubyboy
65
90
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
66
91
  end
67
92
 
93
+ def save_state(slot: nil, path: nil)
94
+ state_path = path || slot_path(slot)
95
+ return false unless StateFile.write(state_path, rom: @rom) { hardware_state }
96
+
97
+ puts "Saved state to #{state_path}"
98
+ true
99
+ end
100
+
101
+ def load_state(slot: nil, path: nil)
102
+ state_path = path || slot_path(slot)
103
+ return false unless StateFile.read(state_path, rom: @rom) { |state| restore_hardware_state(state) }
104
+
105
+ @audio&.clear_queue
106
+ puts "Loaded state from #{state_path}"
107
+ true
108
+ end
109
+
68
110
  private
69
111
 
112
+ def default_save_path(rom_path)
113
+ dir = File.dirname(rom_path)
114
+ base = File.basename(rom_path, '.*')
115
+ File.join(dir, "#{base}.sav")
116
+ end
117
+
118
+ def slot_path(slot)
119
+ raise ArgumentError, 'slot must be between 0 and 9' unless (0..9).cover?(slot)
120
+
121
+ dir = File.dirname(@rom_path)
122
+ base = File.basename(@rom_path, '.*')
123
+ File.join(dir, "#{base}.state#{slot}")
124
+ end
125
+
126
+ def hardware_state
127
+ {
128
+ schema: 'game_boy_console_and_cartridge',
129
+ console: {
130
+ cpu: @cpu.hardware_state,
131
+ ram: @ram.hardware_state,
132
+ ppu: @ppu.hardware_state,
133
+ apu: @apu.hardware_state,
134
+ timer: @timer.hardware_state,
135
+ interrupt: @interrupt.hardware_state,
136
+ joypad: @joypad.hardware_state
137
+ },
138
+ cartridge: @mbc.hardware_state
139
+ }
140
+ end
141
+
142
+ def restore_hardware_state(state)
143
+ console = state.fetch(:console)
144
+ @cpu.restore_hardware_state(console.fetch(:cpu))
145
+ @ram.restore_hardware_state(console.fetch(:ram))
146
+ @ppu.restore_hardware_state(console.fetch(:ppu))
147
+ @apu.restore_hardware_state(console.fetch(:apu))
148
+ @timer.restore_hardware_state(console.fetch(:timer))
149
+ @interrupt.restore_hardware_state(console.fetch(:interrupt))
150
+ @joypad.restore_hardware_state(console.fetch(:joypad))
151
+ @mbc.restore_hardware_state(state.fetch(:cartridge))
152
+ end
153
+
154
+ def load_save_file
155
+ return unless @save_file
156
+
157
+ bytes = @save_file.read(@ram.eram.size)
158
+ return unless bytes
159
+
160
+ @ram.eram.replace(bytes)
161
+ puts "Loaded save from #{@save_file.path}"
162
+ end
163
+
164
+ def save_save_file
165
+ return unless @save_file
166
+
167
+ puts "Saved to #{@save_file.path}" if @save_file.write(@ram.eram)
168
+ end
169
+
70
170
  def key_input_check
71
171
  SDL.PumpEvents
72
172
  keyboard = SDL.GetKeyboardState(nil)
73
173
  keyboard_state = keyboard.read_array_of_uint8(229)
74
174
 
175
+ check_state_save_keys(keyboard_state)
75
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)
76
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)
77
178
  @joypad.direction_button(15 - direction)
78
179
  @joypad.action_button(15 - action)
79
180
  end
181
+
182
+ def check_state_save_keys(keyboard_state)
183
+ shift = keyboard_state[SDL::SDL_SCANCODE_LSHIFT] > 0
184
+ SAVE_STATE_KEYS.each_with_index do |key, index|
185
+ pressed = keyboard_state[key]
186
+ next unless pressed > 0 && @prev_save_state_keys[index] == 0
187
+
188
+ slot = index == 9 ? 0 : index + 1
189
+ shift ? load_state(slot:) : save_state(slot:)
190
+ ensure
191
+ @prev_save_state_keys[index] = pressed
192
+ end
193
+ end
80
194
  end
81
195
  end
@@ -16,7 +16,7 @@ module Rubyboy
16
16
  def initialize(rom_path)
17
17
  rom_data = File.open(rom_path, 'r') { _1.read.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
@@ -19,7 +19,7 @@ module Rubyboy
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)
@@ -0,0 +1,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class Registers
5
+ def hardware_state
6
+ { af:, bc:, de:, hl: }
7
+ end
8
+
9
+ def restore_hardware_state(state)
10
+ self.af = state.fetch(:af)
11
+ self.bc = state.fetch(:bc)
12
+ self.de = state.fetch(:de)
13
+ self.hl = state.fetch(:hl)
14
+ end
15
+ end
16
+
17
+ class Cpu
18
+ def hardware_state
19
+ {
20
+ registers: @registers.hardware_state,
21
+ pc: @pc,
22
+ sp: @sp,
23
+ ime: @ime,
24
+ ime_delay: @ime_delay,
25
+ halted: @halted
26
+ }
27
+ end
28
+
29
+ def restore_hardware_state(state)
30
+ @registers.restore_hardware_state(state.fetch(:registers))
31
+ @pc = state.fetch(:pc)
32
+ @sp = state.fetch(:sp)
33
+ @ime = state.fetch(:ime)
34
+ @ime_delay = state.fetch(:ime_delay)
35
+ @halted = state.fetch(:halted)
36
+ end
37
+ end
38
+
39
+ class Timer
40
+ def hardware_state
41
+ {
42
+ div: @div,
43
+ tima: @tima,
44
+ tma: @tma,
45
+ tac: @tac,
46
+ divider_phase: @cycles
47
+ }
48
+ end
49
+
50
+ def restore_hardware_state(state)
51
+ @div = state.fetch(:div)
52
+ @tima = state.fetch(:tima)
53
+ @tma = state.fetch(:tma)
54
+ @tac = state.fetch(:tac)
55
+ @cycles = state.fetch(:divider_phase)
56
+ end
57
+ end
58
+
59
+ class Interrupt
60
+ def hardware_state
61
+ { ie: @ie, if: @if }
62
+ end
63
+
64
+ def restore_hardware_state(state)
65
+ @ie = state.fetch(:ie)
66
+ @if = state.fetch(:if)
67
+ end
68
+ end
69
+
70
+ class Joypad
71
+ def hardware_state
72
+ {
73
+ p1_select: @mode,
74
+ action_buttons: @action,
75
+ direction_buttons: @direction
76
+ }
77
+ end
78
+
79
+ def restore_hardware_state(state)
80
+ @mode = state.fetch(:p1_select)
81
+ @action = state.fetch(:action_buttons)
82
+ @direction = state.fetch(:direction_buttons)
83
+ end
84
+ end
85
+
86
+ class Ram
87
+ def hardware_state
88
+ {
89
+ eram: @eram.dup,
90
+ wram1: @wram1.dup,
91
+ wram2: @wram2.dup,
92
+ hram: @hram.dup
93
+ }
94
+ end
95
+
96
+ def restore_hardware_state(state)
97
+ @eram = state.fetch(:eram).dup
98
+ @wram1 = state.fetch(:wram1).dup
99
+ @wram2 = state.fetch(:wram2).dup
100
+ @hram = state.fetch(:hram).dup
101
+ end
102
+ end
103
+
104
+ class Ppu
105
+ def hardware_state
106
+ {
107
+ registers: {
108
+ lcdc: @lcdc,
109
+ stat: @stat,
110
+ scy: @scy,
111
+ scx: @scx,
112
+ ly: @ly,
113
+ lyc: @lyc,
114
+ obp0: @obp0,
115
+ obp1: @obp1,
116
+ wy: @wy,
117
+ wx: @wx,
118
+ bgp: @bgp
119
+ },
120
+ vram: @vram.dup,
121
+ oam: @oam.dup,
122
+ mode: @mode,
123
+ dot_phase: @cycles,
124
+ window_line: @wly
125
+ }
126
+ end
127
+
128
+ def restore_hardware_state(state)
129
+ registers = state.fetch(:registers)
130
+ @lcdc = registers.fetch(:lcdc)
131
+ @stat = registers.fetch(:stat)
132
+ @scy = registers.fetch(:scy)
133
+ @scx = registers.fetch(:scx)
134
+ @ly = registers.fetch(:ly)
135
+ @lyc = registers.fetch(:lyc)
136
+ @obp0 = registers.fetch(:obp0)
137
+ @obp1 = registers.fetch(:obp1)
138
+ @wy = registers.fetch(:wy)
139
+ @wx = registers.fetch(:wx)
140
+ @bgp = registers.fetch(:bgp)
141
+ @vram = state.fetch(:vram).dup
142
+ @oam = state.fetch(:oam).dup
143
+ @mode = state.fetch(:mode)
144
+ @cycles = state.fetch(:dot_phase)
145
+ @wly = state.fetch(:window_line)
146
+ rebuild_render_caches
147
+ end
148
+
149
+ private
150
+
151
+ def rebuild_render_caches
152
+ @buffer = Array.new(144 * 160, 0xffffffff)
153
+ @bg_pixels = Array.new(LCD_WIDTH, 0x00)
154
+ @tile_cache = Array.new(384) { Array.new(64, 0) }
155
+ @tile_map_cache = Array.new(2048, 0)
156
+ @bgp_cache = Array.new(4, 0xffffffff)
157
+ @obp0_cache = Array.new(4, 0xffffffff)
158
+ @obp1_cache = Array.new(4, 0xffffffff)
159
+ @sprite_cache = Array.new(40) { { y: 0xff, x: 0xff, tile_index: 0, flags: 0 } }
160
+
161
+ (0...0x1800).step(2) { |addr| update_tile_cache(addr) }
162
+ refresh_tile_map_cache
163
+ refresh_palette_cache(@bgp_cache, @bgp)
164
+ refresh_palette_cache(@obp0_cache, @obp0)
165
+ refresh_palette_cache(@obp1_cache, @obp1)
166
+ 40.times do |sprite_index|
167
+ base = sprite_index << 2
168
+ @sprite_cache[sprite_index][:y] = (@oam[base] - 16) & 0xff
169
+ @sprite_cache[sprite_index][:x] = (@oam[base + 1] - 8) & 0xff
170
+ @sprite_cache[sprite_index][:tile_index] = @oam[base + 2]
171
+ @sprite_cache[sprite_index][:flags] = @oam[base + 3]
172
+ end
173
+ end
174
+ end
175
+
176
+ module ApuChannels
177
+ class Channel1
178
+ def hardware_state
179
+ {
180
+ cycles: @cycles,
181
+ frequency: @frequency,
182
+ frequency_timer: @frequency_timer,
183
+ wave_duty_position: @wave_duty_position,
184
+ enabled: @enabled,
185
+ dac_enabled: @dac_enabled,
186
+ length_enabled: @length_enabled,
187
+ is_upwards: @is_upwards,
188
+ is_decrementing: @is_decrementing,
189
+ sweep_enabled: @sweep_enabled,
190
+ sweep_period: @sweep_period,
191
+ sweep_shift: @sweep_shift,
192
+ period: @period,
193
+ period_timer: @period_timer,
194
+ current_volume: @current_volume,
195
+ initial_volume: @initial_volume,
196
+ shadow_frequency: @shadow_frequency,
197
+ sweep_timer: @sweep_timer,
198
+ length_timer: @length_timer,
199
+ wave_duty_pattern: @wave_duty_pattern
200
+ }
201
+ end
202
+
203
+ def restore_hardware_state(state)
204
+ @cycles = state.fetch(:cycles)
205
+ @frequency = state.fetch(:frequency)
206
+ @frequency_timer = state.fetch(:frequency_timer)
207
+ @wave_duty_position = state.fetch(:wave_duty_position)
208
+ @enabled = state.fetch(:enabled)
209
+ @dac_enabled = state.fetch(:dac_enabled)
210
+ @length_enabled = state.fetch(:length_enabled)
211
+ @is_upwards = state.fetch(:is_upwards)
212
+ @is_decrementing = state.fetch(:is_decrementing)
213
+ @sweep_enabled = state.fetch(:sweep_enabled)
214
+ @sweep_period = state.fetch(:sweep_period)
215
+ @sweep_shift = state.fetch(:sweep_shift)
216
+ @period = state.fetch(:period)
217
+ @period_timer = state.fetch(:period_timer)
218
+ @current_volume = state.fetch(:current_volume)
219
+ @initial_volume = state.fetch(:initial_volume)
220
+ @shadow_frequency = state.fetch(:shadow_frequency)
221
+ @sweep_timer = state.fetch(:sweep_timer)
222
+ @length_timer = state.fetch(:length_timer)
223
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
224
+ end
225
+ end
226
+
227
+ class Channel2
228
+ def hardware_state
229
+ {
230
+ cycles: @cycles,
231
+ frequency: @frequency,
232
+ frequency_timer: @frequency_timer,
233
+ wave_duty_position: @wave_duty_position,
234
+ enabled: @enabled,
235
+ dac_enabled: @dac_enabled,
236
+ length_enabled: @length_enabled,
237
+ is_upwards: @is_upwards,
238
+ is_decrementing: @is_decrementing,
239
+ period: @period,
240
+ period_timer: @period_timer,
241
+ current_volume: @current_volume,
242
+ initial_volume: @initial_volume,
243
+ shadow_frequency: @shadow_frequency,
244
+ length_timer: @length_timer,
245
+ wave_duty_pattern: @wave_duty_pattern
246
+ }
247
+ end
248
+
249
+ def restore_hardware_state(state)
250
+ @cycles = state.fetch(:cycles)
251
+ @frequency = state.fetch(:frequency)
252
+ @frequency_timer = state.fetch(:frequency_timer)
253
+ @wave_duty_position = state.fetch(:wave_duty_position)
254
+ @enabled = state.fetch(:enabled)
255
+ @dac_enabled = state.fetch(:dac_enabled)
256
+ @length_enabled = state.fetch(:length_enabled)
257
+ @is_upwards = state.fetch(:is_upwards)
258
+ @is_decrementing = state.fetch(:is_decrementing)
259
+ @period = state.fetch(:period)
260
+ @period_timer = state.fetch(:period_timer)
261
+ @current_volume = state.fetch(:current_volume)
262
+ @initial_volume = state.fetch(:initial_volume)
263
+ @shadow_frequency = state.fetch(:shadow_frequency)
264
+ @length_timer = state.fetch(:length_timer)
265
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
266
+ end
267
+ end
268
+
269
+ class Channel3
270
+ def hardware_state
271
+ {
272
+ cycles: @cycles,
273
+ frequency: @frequency,
274
+ frequency_timer: @frequency_timer,
275
+ wave_duty_position: @wave_duty_position,
276
+ enabled: @enabled,
277
+ dac_enabled: @dac_enabled,
278
+ length_enabled: @length_enabled,
279
+ is_upwards: @is_upwards,
280
+ is_decrementing: @is_decrementing,
281
+ sweep_enabled: @sweep_enabled,
282
+ sweep_period: @sweep_period,
283
+ sweep_shift: @sweep_shift,
284
+ period: @period,
285
+ period_timer: @period_timer,
286
+ current_volume: @current_volume,
287
+ initial_volume: @initial_volume,
288
+ shadow_frequency: @shadow_frequency,
289
+ sweep_timer: @sweep_timer,
290
+ length_timer: @length_timer,
291
+ wave_duty_pattern: @wave_duty_pattern,
292
+ output_level: @output_level,
293
+ volume_shift: @volume_shift,
294
+ wave_ram: @wave_ram.dup
295
+ }
296
+ end
297
+
298
+ def restore_hardware_state(state)
299
+ @cycles = state.fetch(:cycles)
300
+ @frequency = state.fetch(:frequency)
301
+ @frequency_timer = state.fetch(:frequency_timer)
302
+ @wave_duty_position = state.fetch(:wave_duty_position)
303
+ @enabled = state.fetch(:enabled)
304
+ @dac_enabled = state.fetch(:dac_enabled)
305
+ @length_enabled = state.fetch(:length_enabled)
306
+ @is_upwards = state.fetch(:is_upwards)
307
+ @is_decrementing = state.fetch(:is_decrementing)
308
+ @sweep_enabled = state.fetch(:sweep_enabled)
309
+ @sweep_period = state.fetch(:sweep_period)
310
+ @sweep_shift = state.fetch(:sweep_shift)
311
+ @period = state.fetch(:period)
312
+ @period_timer = state.fetch(:period_timer)
313
+ @current_volume = state.fetch(:current_volume)
314
+ @initial_volume = state.fetch(:initial_volume)
315
+ @shadow_frequency = state.fetch(:shadow_frequency)
316
+ @sweep_timer = state.fetch(:sweep_timer)
317
+ @length_timer = state.fetch(:length_timer)
318
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
319
+ @output_level = state.fetch(:output_level)
320
+ @volume_shift = state.fetch(:volume_shift)
321
+ @wave_ram = state.fetch(:wave_ram).dup
322
+ end
323
+ end
324
+
325
+ class Channel4
326
+ def hardware_state
327
+ {
328
+ cycles: @cycles,
329
+ frequency: @frequency,
330
+ frequency_timer: @frequency_timer,
331
+ wave_duty_position: @wave_duty_position,
332
+ enabled: @enabled,
333
+ dac_enabled: @dac_enabled,
334
+ length_enabled: @length_enabled,
335
+ is_upwards: @is_upwards,
336
+ is_decrementing: @is_decrementing,
337
+ sweep_enabled: @sweep_enabled,
338
+ sweep_period: @sweep_period,
339
+ sweep_shift: @sweep_shift,
340
+ period: @period,
341
+ period_timer: @period_timer,
342
+ current_volume: @current_volume,
343
+ initial_volume: @initial_volume,
344
+ shadow_frequency: @shadow_frequency,
345
+ sweep_timer: @sweep_timer,
346
+ length_timer: @length_timer,
347
+ wave_duty_pattern: @wave_duty_pattern,
348
+ lfsr: @lfsr,
349
+ width_mode: @width_mode,
350
+ shift_amount: @shift_amount,
351
+ divisor_code: @divisor_code
352
+ }
353
+ end
354
+
355
+ def restore_hardware_state(state)
356
+ @cycles = state.fetch(:cycles)
357
+ @frequency = state.fetch(:frequency)
358
+ @frequency_timer = state.fetch(:frequency_timer)
359
+ @wave_duty_position = state.fetch(:wave_duty_position)
360
+ @enabled = state.fetch(:enabled)
361
+ @dac_enabled = state.fetch(:dac_enabled)
362
+ @length_enabled = state.fetch(:length_enabled)
363
+ @is_upwards = state.fetch(:is_upwards)
364
+ @is_decrementing = state.fetch(:is_decrementing)
365
+ @sweep_enabled = state.fetch(:sweep_enabled)
366
+ @sweep_period = state.fetch(:sweep_period)
367
+ @sweep_shift = state.fetch(:sweep_shift)
368
+ @period = state.fetch(:period)
369
+ @period_timer = state.fetch(:period_timer)
370
+ @current_volume = state.fetch(:current_volume)
371
+ @initial_volume = state.fetch(:initial_volume)
372
+ @shadow_frequency = state.fetch(:shadow_frequency)
373
+ @sweep_timer = state.fetch(:sweep_timer)
374
+ @length_timer = state.fetch(:length_timer)
375
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
376
+ @lfsr = state.fetch(:lfsr)
377
+ @width_mode = state.fetch(:width_mode)
378
+ @shift_amount = state.fetch(:shift_amount)
379
+ @divisor_code = state.fetch(:divisor_code)
380
+ end
381
+ end
382
+ end
383
+
384
+ class Apu
385
+ def hardware_state
386
+ {
387
+ nr50: @nr50,
388
+ nr51: @nr51,
389
+ frame_sequencer_phase: @cycles,
390
+ sample_phase: @sampling_accumulator,
391
+ frame_sequencer_step: @fs,
392
+ enabled: @enabled,
393
+ channels: {
394
+ channel1: @channel1.hardware_state,
395
+ channel2: @channel2.hardware_state,
396
+ channel3: @channel3.hardware_state,
397
+ channel4: @channel4.hardware_state
398
+ }
399
+ }
400
+ end
401
+
402
+ def restore_hardware_state(state)
403
+ @nr50 = state.fetch(:nr50)
404
+ @nr51 = state.fetch(:nr51)
405
+ @cycles = state.fetch(:frame_sequencer_phase)
406
+ @sampling_accumulator = state.fetch(:sample_phase)
407
+ @fs = state.fetch(:frame_sequencer_step)
408
+ @enabled = state.fetch(:enabled)
409
+ channels = state.fetch(:channels)
410
+ @channel1.restore_hardware_state(channels.fetch(:channel1))
411
+ @channel2.restore_hardware_state(channels.fetch(:channel2))
412
+ @channel3.restore_hardware_state(channels.fetch(:channel3))
413
+ @channel4.restore_hardware_state(channels.fetch(:channel4))
414
+ @samples = Array.new(1024, 0.0)
415
+ @sample_idx = 0
416
+ end
417
+ end
418
+
419
+ module Cartridge
420
+ class Mbc1
421
+ def hardware_state
422
+ {
423
+ type: 'mbc1',
424
+ rom_bank_low: @rom_bank,
425
+ ram_bank_high: @ram_bank,
426
+ ram_enabled: @ram_enable,
427
+ banking_mode: @ram_banking_mode
428
+ }
429
+ end
430
+
431
+ def restore_hardware_state(state)
432
+ @rom_bank = state.fetch(:rom_bank_low)
433
+ @ram_bank = state.fetch(:ram_bank_high)
434
+ @ram_enable = state.fetch(:ram_enabled)
435
+ @ram_banking_mode = state.fetch(:banking_mode)
436
+ end
437
+ end
438
+
439
+ class Nombc
440
+ def hardware_state
441
+ { type: 'nombc' }
442
+ end
443
+
444
+ def restore_hardware_state(_state)
445
+ true
446
+ end
447
+ end
448
+ end
449
+ end