rubyboy 1.5.1 → 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.
@@ -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
data/lib/rubyboy/lcd.rb CHANGED
@@ -7,8 +7,9 @@ module Rubyboy
7
7
  SCREEN_WIDTH = 160
8
8
  SCREEN_HEIGHT = 144
9
9
  SCALE = 3
10
+ SDL_EVENT_SIZE = 56
10
11
 
11
- def initialize
12
+ def initialize(vsync: nil)
12
13
  raise SDL.GetError() if SDL.InitSubSystem(SDL::INIT_VIDEO) != 0
13
14
 
14
15
  @buffer = FFI::MemoryPointer.new(:uint32, SCREEN_WIDTH * SCREEN_HEIGHT)
@@ -16,11 +17,19 @@ module Rubyboy
16
17
 
17
18
  raise SDL.GetError() if @window.null?
18
19
 
19
- @renderer = SDL.CreateRenderer(@window, -1, 0)
20
+ begin
21
+ @renderer = SDL.CreateRenderer(@window, -1, 0)
22
+ raise SDL.GetError() if @renderer.null?
23
+ raise SDL.GetError() if !vsync.nil? && SDL.RenderSetVSync(@renderer, vsync ? 1 : 0) != 0
24
+ rescue StandardError
25
+ close_window
26
+ raise
27
+ end
28
+
20
29
  SDL.SetHint('SDL_HINT_RENDER_SCALE_QUALITY', '2')
21
30
  SDL.RenderSetLogicalSize(@renderer, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE)
22
31
  @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_ABGR8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT)
23
- @event = FFI::MemoryPointer.new(:pointer)
32
+ @event = FFI::MemoryPointer.new(:uint8, SDL_EVENT_SIZE)
24
33
  end
25
34
 
26
35
  def draw(framebuffer)
data/lib/rubyboy/ram.rb CHANGED
@@ -4,8 +4,8 @@ module Rubyboy
4
4
  class Ram
5
5
  attr_accessor :eram, :wram1, :wram2, :hram
6
6
 
7
- def initialize
8
- @eram = Array.new(0x2000, 0)
7
+ def initialize(rom)
8
+ @eram = Array.new(rom.ram_size_bytes, 0)
9
9
  @wram1 = Array.new(0x1000, 0)
10
10
  @wram2 = Array.new(0x1000, 0)
11
11
  @hram = Array.new(0x80, 0)
data/lib/rubyboy/rom.rb CHANGED
@@ -10,11 +10,34 @@ module Rubyboy
10
10
  BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E
11
11
  ].map(&:hex).freeze
12
12
 
13
+ RAM_SIZE_BYTES = {
14
+ 0x00 => 0,
15
+ 0x01 => 2 * 1024,
16
+ 0x02 => 8 * 1024,
17
+ 0x03 => 32 * 1024,
18
+ 0x04 => 128 * 1024,
19
+ 0x05 => 64 * 1024
20
+ }.freeze
21
+
22
+ BATTERY_CARTRIDGE_TYPES = [0x03, 0x06, 0x09, 0x0d, 0x0f, 0x10, 0x13, 0x1b, 0x1e, 0x22, 0xff].freeze
23
+
13
24
  def initialize(data)
14
25
  @data = data
15
26
  load_data
16
27
  end
17
28
 
29
+ def ram_size_bytes
30
+ RAM_SIZE_BYTES.fetch(@ram_size, 0)
31
+ end
32
+
33
+ def battery?
34
+ BATTERY_CARTRIDGE_TYPES.include?(@cartridge_type)
35
+ end
36
+
37
+ def global_checksum_value
38
+ (@global_checksum[0] << 8) | @global_checksum[1]
39
+ end
40
+
18
41
  private
19
42
 
20
43
  def load_data
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class SaveFile
5
+ attr_reader :path
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ end
10
+
11
+ def read(expected_size)
12
+ return nil unless File.exist?(@path)
13
+
14
+ bytes = File.binread(@path).bytes
15
+ if bytes.size != expected_size
16
+ warn "[rubyboy] save file size mismatch (expected #{expected_size} bytes, got #{bytes.size}); ignoring #{@path}"
17
+ return nil
18
+ end
19
+
20
+ bytes
21
+ rescue StandardError => e
22
+ warn "[rubyboy] failed to read save file #{@path}: #{e.message}"
23
+ nil
24
+ end
25
+
26
+ def write(bytes)
27
+ tmp_path = "#{@path}.tmp"
28
+ File.binwrite(tmp_path, bytes.pack('C*'))
29
+ File.rename(tmp_path, @path)
30
+ true
31
+ rescue StandardError => e
32
+ warn "[rubyboy] failed to write save file #{@path}: #{e.message}"
33
+ false
34
+ end
35
+ end
36
+ end
data/lib/rubyboy/sdl.rb CHANGED
@@ -25,6 +25,17 @@ module Rubyboy
25
25
  SDL_SCANCODE_K = 14
26
26
  SDL_SCANCODE_U = 24
27
27
  SDL_SCANCODE_I = 12
28
+ SDL_SCANCODE_1 = 30
29
+ SDL_SCANCODE_2 = 31
30
+ SDL_SCANCODE_3 = 32
31
+ SDL_SCANCODE_4 = 33
32
+ SDL_SCANCODE_5 = 34
33
+ SDL_SCANCODE_6 = 35
34
+ SDL_SCANCODE_7 = 36
35
+ SDL_SCANCODE_8 = 37
36
+ SDL_SCANCODE_9 = 38
37
+ SDL_SCANCODE_0 = 39
38
+ SDL_SCANCODE_LSHIFT = 225
28
39
 
29
40
  attach_function :Init, 'SDL_Init', [:uint32], :int
30
41
  attach_function :InitSubSystem, 'SDL_InitSubSystem', [:uint32], :int
@@ -42,6 +53,7 @@ module Rubyboy
42
53
  attach_function :GetKeyboardState, 'SDL_GetKeyboardState', [:pointer], :pointer
43
54
  attach_function :SetHint, 'SDL_SetHint', %i[string string], :int
44
55
  attach_function :RenderSetLogicalSize, 'SDL_RenderSetLogicalSize', %i[pointer int int], :int
56
+ attach_function :RenderSetVSync, 'SDL_RenderSetVSync', %i[pointer int], :int
45
57
  attach_function :SetWindowTitle, 'SDL_SetWindowTitle', %i[pointer string], :void
46
58
  attach_function :RaiseWindow, 'SDL_RaiseWindow', [:pointer], :void
47
59
  attach_function :GetError, 'SDL_GetError', [], :string
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class StateFile
5
+ MAGIC = 'RBYSTATE'
6
+ FORMAT_VERSION = 1
7
+ HEADER_SIZE = 16
8
+
9
+ def self.write(path, rom:)
10
+ payload = Marshal.dump(yield)
11
+ header = MAGIC.b + [FORMAT_VERSION, rom.global_checksum_value, 0].pack('L<S<S<')
12
+ tmp_path = "#{path}.tmp"
13
+ File.binwrite(tmp_path, header + payload)
14
+ File.rename(tmp_path, path)
15
+ true
16
+ rescue StandardError => e
17
+ warn "[rubyboy] failed to write state file #{path}: #{e.message}"
18
+ false
19
+ end
20
+
21
+ def self.read(path, rom:)
22
+ return warn_and_false("state file does not exist: #{path}") unless File.exist?(path)
23
+
24
+ data = File.binread(path)
25
+ return warn_and_false("state file is too small: #{path}") if data.bytesize < HEADER_SIZE
26
+
27
+ magic = data.byteslice(0, MAGIC.bytesize)
28
+ return warn_and_false("invalid state file magic: #{path}") unless magic == MAGIC
29
+
30
+ version, checksum = data.byteslice(8, 6).unpack('L<S<')
31
+ return warn_and_false("unsupported state file version #{version} (expected #{FORMAT_VERSION})") unless version == FORMAT_VERSION
32
+ return warn_and_false("ROM checksum mismatch for state file #{path}") unless checksum == rom.global_checksum_value
33
+
34
+ # State files are local emulator snapshots written by this process.
35
+ # rubocop:disable Security/MarshalLoad
36
+ yield Marshal.load(data.byteslice(HEADER_SIZE..))
37
+ # rubocop:enable Security/MarshalLoad
38
+ true
39
+ rescue StandardError => e
40
+ warn "[rubyboy] failed to read state file #{path}: #{e.message}"
41
+ false
42
+ end
43
+
44
+ def self.warn_and_false(message)
45
+ warn "[rubyboy] #{message}"
46
+ false
47
+ end
48
+ private_class_method :warn_and_false
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyboy
4
- VERSION = '1.5.1'
4
+ VERSION = '1.6.0'
5
5
  end
data/lib/rubyboy.rb CHANGED
@@ -9,8 +9,11 @@ require_relative 'rubyboy/emulator'
9
9
  require_relative 'rubyboy/ppu'
10
10
  require_relative 'rubyboy/rom'
11
11
  require_relative 'rubyboy/ram'
12
+ require_relative 'rubyboy/save_file'
13
+ require_relative 'rubyboy/state_file'
12
14
  require_relative 'rubyboy/timer'
13
15
  require_relative 'rubyboy/lcd'
14
16
  require_relative 'rubyboy/joypad'
15
17
  require_relative 'rubyboy/interrupt'
16
18
  require_relative 'rubyboy/cartridge/factory'
19
+ require_relative 'rubyboy/hardware_state'