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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +39 -27
  4. data/exe/rubyboy +16 -5
  5. data/exe/rubyboy-bench +15 -7
  6. data/lib/bench.rb +33 -7
  7. data/lib/executor.rb +19 -10
  8. data/lib/rubyboy/apu.rb +15 -4
  9. data/lib/rubyboy/apu_channels/channel1.rb +4 -4
  10. data/lib/rubyboy/apu_channels/channel2.rb +4 -4
  11. data/lib/rubyboy/apu_channels/channel3.rb +2 -9
  12. data/lib/rubyboy/apu_channels/channel4.rb +0 -7
  13. data/lib/rubyboy/audio.rb +17 -4
  14. data/lib/rubyboy/cartridge/mbc1.rb +30 -18
  15. data/lib/rubyboy/emulator.rb +145 -47
  16. data/lib/rubyboy/emulator_headless.rb +6 -2
  17. data/lib/rubyboy/emulator_wasm.rb +31 -8
  18. data/lib/rubyboy/hardware_state.rb +450 -0
  19. data/lib/rubyboy/lcd.rb +2 -1
  20. data/lib/rubyboy/ram.rb +2 -2
  21. data/lib/rubyboy/rom.rb +26 -0
  22. data/lib/rubyboy/save_file.rb +36 -0
  23. data/lib/rubyboy/sdl.rb +12 -0
  24. data/lib/rubyboy/state_file.rb +49 -0
  25. data/lib/rubyboy/version.rb +1 -1
  26. data/lib/rubyboy.rb +3 -0
  27. metadata +9 -48
  28. data/.rspec +0 -3
  29. data/.rubocop.yml +0 -54
  30. data/CODE_OF_CONDUCT.md +0 -84
  31. data/Rakefile +0 -12
  32. data/docs/favicon.png +0 -0
  33. data/docs/index.html +0 -75
  34. data/docs/index.js +0 -113
  35. data/docs/logo-light-23.svg +0 -1
  36. data/docs/ogp.png +0 -0
  37. data/docs/styles.css +0 -245
  38. data/docs/worker.js +0 -115
  39. data/exe/rubyboy-wasm +0 -26
  40. data/lib/opcodes.json +0 -11657
  41. data/lib/roms/bgbtest.gb +0 -0
  42. data/lib/roms/cpu_instrs/cpu_instrs.gb +0 -0
  43. data/lib/roms/cpu_instrs/individual/01-special.gb +0 -0
  44. data/lib/roms/cpu_instrs/individual/02-interrupts.gb +0 -0
  45. data/lib/roms/cpu_instrs/individual/03-op sp,hl.gb +0 -0
  46. data/lib/roms/cpu_instrs/individual/04-op r,imm.gb +0 -0
  47. data/lib/roms/cpu_instrs/individual/05-op rp.gb +0 -0
  48. data/lib/roms/cpu_instrs/individual/06-ld r,r.gb +0 -0
  49. data/lib/roms/cpu_instrs/individual/07-jr,jp,call,ret,rst.gb +0 -0
  50. data/lib/roms/cpu_instrs/individual/08-misc instrs.gb +0 -0
  51. data/lib/roms/cpu_instrs/individual/09-op r,r.gb +0 -0
  52. data/lib/roms/cpu_instrs/individual/10-bit ops.gb +0 -0
  53. data/lib/roms/cpu_instrs/individual/11-op a,(hl).gb +0 -0
  54. data/lib/roms/cpu_instrs/readme.txt +0 -119
  55. data/lib/roms/dmg-acid2.gb +0 -0
  56. data/lib/roms/hello-world.gb +0 -0
  57. data/lib/roms/instr_timing/instr_timing.gb +0 -0
  58. data/lib/roms/instr_timing/readme.txt +0 -139
  59. data/lib/rubyboy/raylib/audio.rb +0 -32
  60. data/lib/rubyboy/raylib/lcd.rb +0 -40
  61. data/lib/rubyboy/raylib/raylib_loader.rb +0 -36
  62. data/lib/stackprof.rb +0 -14
  63. data/resource/logo/logo.png +0 -0
  64. data/resource/logo/logo.svg +0 -1
  65. data/resource/logo/rubyboy.png +0 -0
  66. data/resource/screenshots/pokemon.png +0 -0
  67. data/resource/screenshots/puyopuyo.png +0 -0
  68. data/sig/rubyboy.rbs +0 -4
@@ -0,0 +1,450 @@
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
+ # Keep channel fields explicit so Spinel can compile them without reflection.
177
+ module ApuChannels
178
+ class Channel1
179
+ def hardware_state
180
+ {
181
+ cycles: @cycles,
182
+ frequency: @frequency,
183
+ frequency_timer: @frequency_timer,
184
+ wave_duty_position: @wave_duty_position,
185
+ enabled: @enabled,
186
+ dac_enabled: @dac_enabled,
187
+ length_enabled: @length_enabled,
188
+ is_upwards: @is_upwards,
189
+ is_decrementing: @is_decrementing,
190
+ sweep_enabled: @sweep_enabled,
191
+ sweep_period: @sweep_period,
192
+ sweep_shift: @sweep_shift,
193
+ period: @period,
194
+ period_timer: @period_timer,
195
+ current_volume: @current_volume,
196
+ initial_volume: @initial_volume,
197
+ shadow_frequency: @shadow_frequency,
198
+ sweep_timer: @sweep_timer,
199
+ length_timer: @length_timer,
200
+ wave_duty_pattern: @wave_duty_pattern
201
+ }
202
+ end
203
+
204
+ def restore_hardware_state(state)
205
+ @cycles = state.fetch(:cycles)
206
+ @frequency = state.fetch(:frequency)
207
+ @frequency_timer = state.fetch(:frequency_timer)
208
+ @wave_duty_position = state.fetch(:wave_duty_position)
209
+ @enabled = state.fetch(:enabled)
210
+ @dac_enabled = state.fetch(:dac_enabled)
211
+ @length_enabled = state.fetch(:length_enabled)
212
+ @is_upwards = state.fetch(:is_upwards)
213
+ @is_decrementing = state.fetch(:is_decrementing)
214
+ @sweep_enabled = state.fetch(:sweep_enabled)
215
+ @sweep_period = state.fetch(:sweep_period)
216
+ @sweep_shift = state.fetch(:sweep_shift)
217
+ @period = state.fetch(:period)
218
+ @period_timer = state.fetch(:period_timer)
219
+ @current_volume = state.fetch(:current_volume)
220
+ @initial_volume = state.fetch(:initial_volume)
221
+ @shadow_frequency = state.fetch(:shadow_frequency)
222
+ @sweep_timer = state.fetch(:sweep_timer)
223
+ @length_timer = state.fetch(:length_timer)
224
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
225
+ end
226
+ end
227
+
228
+ class Channel2
229
+ def hardware_state
230
+ {
231
+ cycles: @cycles,
232
+ frequency: @frequency,
233
+ frequency_timer: @frequency_timer,
234
+ wave_duty_position: @wave_duty_position,
235
+ enabled: @enabled,
236
+ dac_enabled: @dac_enabled,
237
+ length_enabled: @length_enabled,
238
+ is_upwards: @is_upwards,
239
+ is_decrementing: @is_decrementing,
240
+ period: @period,
241
+ period_timer: @period_timer,
242
+ current_volume: @current_volume,
243
+ initial_volume: @initial_volume,
244
+ shadow_frequency: @shadow_frequency,
245
+ length_timer: @length_timer,
246
+ wave_duty_pattern: @wave_duty_pattern
247
+ }
248
+ end
249
+
250
+ def restore_hardware_state(state)
251
+ @cycles = state.fetch(:cycles)
252
+ @frequency = state.fetch(:frequency)
253
+ @frequency_timer = state.fetch(:frequency_timer)
254
+ @wave_duty_position = state.fetch(:wave_duty_position)
255
+ @enabled = state.fetch(:enabled)
256
+ @dac_enabled = state.fetch(:dac_enabled)
257
+ @length_enabled = state.fetch(:length_enabled)
258
+ @is_upwards = state.fetch(:is_upwards)
259
+ @is_decrementing = state.fetch(:is_decrementing)
260
+ @period = state.fetch(:period)
261
+ @period_timer = state.fetch(:period_timer)
262
+ @current_volume = state.fetch(:current_volume)
263
+ @initial_volume = state.fetch(:initial_volume)
264
+ @shadow_frequency = state.fetch(:shadow_frequency)
265
+ @length_timer = state.fetch(:length_timer)
266
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
267
+ end
268
+ end
269
+
270
+ class Channel3
271
+ def hardware_state
272
+ {
273
+ cycles: @cycles,
274
+ frequency: @frequency,
275
+ frequency_timer: @frequency_timer,
276
+ wave_duty_position: @wave_duty_position,
277
+ enabled: @enabled,
278
+ dac_enabled: @dac_enabled,
279
+ length_enabled: @length_enabled,
280
+ is_upwards: @is_upwards,
281
+ is_decrementing: @is_decrementing,
282
+ sweep_enabled: @sweep_enabled,
283
+ sweep_period: @sweep_period,
284
+ sweep_shift: @sweep_shift,
285
+ period: @period,
286
+ period_timer: @period_timer,
287
+ current_volume: @current_volume,
288
+ initial_volume: @initial_volume,
289
+ shadow_frequency: @shadow_frequency,
290
+ sweep_timer: @sweep_timer,
291
+ length_timer: @length_timer,
292
+ wave_duty_pattern: @wave_duty_pattern,
293
+ output_level: @output_level,
294
+ volume_shift: @volume_shift,
295
+ wave_ram: @wave_ram.dup
296
+ }
297
+ end
298
+
299
+ def restore_hardware_state(state)
300
+ @cycles = state.fetch(:cycles)
301
+ @frequency = state.fetch(:frequency)
302
+ @frequency_timer = state.fetch(:frequency_timer)
303
+ @wave_duty_position = state.fetch(:wave_duty_position)
304
+ @enabled = state.fetch(:enabled)
305
+ @dac_enabled = state.fetch(:dac_enabled)
306
+ @length_enabled = state.fetch(:length_enabled)
307
+ @is_upwards = state.fetch(:is_upwards)
308
+ @is_decrementing = state.fetch(:is_decrementing)
309
+ @sweep_enabled = state.fetch(:sweep_enabled)
310
+ @sweep_period = state.fetch(:sweep_period)
311
+ @sweep_shift = state.fetch(:sweep_shift)
312
+ @period = state.fetch(:period)
313
+ @period_timer = state.fetch(:period_timer)
314
+ @current_volume = state.fetch(:current_volume)
315
+ @initial_volume = state.fetch(:initial_volume)
316
+ @shadow_frequency = state.fetch(:shadow_frequency)
317
+ @sweep_timer = state.fetch(:sweep_timer)
318
+ @length_timer = state.fetch(:length_timer)
319
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
320
+ @output_level = state.fetch(:output_level)
321
+ @volume_shift = state.fetch(:volume_shift)
322
+ @wave_ram = state.fetch(:wave_ram).dup
323
+ end
324
+ end
325
+
326
+ class Channel4
327
+ def hardware_state
328
+ {
329
+ cycles: @cycles,
330
+ frequency: @frequency,
331
+ frequency_timer: @frequency_timer,
332
+ wave_duty_position: @wave_duty_position,
333
+ enabled: @enabled,
334
+ dac_enabled: @dac_enabled,
335
+ length_enabled: @length_enabled,
336
+ is_upwards: @is_upwards,
337
+ is_decrementing: @is_decrementing,
338
+ sweep_enabled: @sweep_enabled,
339
+ sweep_period: @sweep_period,
340
+ sweep_shift: @sweep_shift,
341
+ period: @period,
342
+ period_timer: @period_timer,
343
+ current_volume: @current_volume,
344
+ initial_volume: @initial_volume,
345
+ shadow_frequency: @shadow_frequency,
346
+ sweep_timer: @sweep_timer,
347
+ length_timer: @length_timer,
348
+ wave_duty_pattern: @wave_duty_pattern,
349
+ lfsr: @lfsr,
350
+ width_mode: @width_mode,
351
+ shift_amount: @shift_amount,
352
+ divisor_code: @divisor_code
353
+ }
354
+ end
355
+
356
+ def restore_hardware_state(state)
357
+ @cycles = state.fetch(:cycles)
358
+ @frequency = state.fetch(:frequency)
359
+ @frequency_timer = state.fetch(:frequency_timer)
360
+ @wave_duty_position = state.fetch(:wave_duty_position)
361
+ @enabled = state.fetch(:enabled)
362
+ @dac_enabled = state.fetch(:dac_enabled)
363
+ @length_enabled = state.fetch(:length_enabled)
364
+ @is_upwards = state.fetch(:is_upwards)
365
+ @is_decrementing = state.fetch(:is_decrementing)
366
+ @sweep_enabled = state.fetch(:sweep_enabled)
367
+ @sweep_period = state.fetch(:sweep_period)
368
+ @sweep_shift = state.fetch(:sweep_shift)
369
+ @period = state.fetch(:period)
370
+ @period_timer = state.fetch(:period_timer)
371
+ @current_volume = state.fetch(:current_volume)
372
+ @initial_volume = state.fetch(:initial_volume)
373
+ @shadow_frequency = state.fetch(:shadow_frequency)
374
+ @sweep_timer = state.fetch(:sweep_timer)
375
+ @length_timer = state.fetch(:length_timer)
376
+ @wave_duty_pattern = state.fetch(:wave_duty_pattern)
377
+ @lfsr = state.fetch(:lfsr)
378
+ @width_mode = state.fetch(:width_mode)
379
+ @shift_amount = state.fetch(:shift_amount)
380
+ @divisor_code = state.fetch(:divisor_code)
381
+ end
382
+ end
383
+ end
384
+
385
+ class Apu
386
+ def hardware_state
387
+ {
388
+ nr50: @nr50,
389
+ nr51: @nr51,
390
+ frame_sequencer_phase: @cycles,
391
+ sample_phase: @sampling_accumulator,
392
+ frame_sequencer_step: @fs,
393
+ enabled: @enabled,
394
+ channels: {
395
+ channel1: @channel1.hardware_state,
396
+ channel2: @channel2.hardware_state,
397
+ channel3: @channel3.hardware_state,
398
+ channel4: @channel4.hardware_state
399
+ }
400
+ }
401
+ end
402
+
403
+ def restore_hardware_state(state)
404
+ @nr50 = state.fetch(:nr50)
405
+ @nr51 = state.fetch(:nr51)
406
+ @cycles = state.fetch(:frame_sequencer_phase)
407
+ @sampling_accumulator = state.fetch(:sample_phase)
408
+ @fs = state.fetch(:frame_sequencer_step)
409
+ @enabled = state.fetch(:enabled)
410
+ channels = state.fetch(:channels)
411
+ @channel1.restore_hardware_state(channels.fetch(:channel1))
412
+ @channel2.restore_hardware_state(channels.fetch(:channel2))
413
+ @channel3.restore_hardware_state(channels.fetch(:channel3))
414
+ @channel4.restore_hardware_state(channels.fetch(:channel4))
415
+ @samples = Array.new(1024, 0.0)
416
+ @sample_idx = 0
417
+ end
418
+ end
419
+
420
+ module Cartridge
421
+ class Mbc1
422
+ def hardware_state
423
+ {
424
+ type: 'mbc1',
425
+ rom_bank_low: @rom_bank,
426
+ ram_bank_high: @ram_bank,
427
+ ram_enabled: @ram_enable,
428
+ banking_mode: @ram_banking_mode
429
+ }
430
+ end
431
+
432
+ def restore_hardware_state(state)
433
+ @rom_bank = state.fetch(:rom_bank_low)
434
+ @ram_bank = state.fetch(:ram_bank_high)
435
+ @ram_enable = state.fetch(:ram_enabled)
436
+ @ram_banking_mode = state.fetch(:banking_mode)
437
+ end
438
+ end
439
+
440
+ class Nombc
441
+ def hardware_state
442
+ { type: 'nombc' }
443
+ end
444
+
445
+ def restore_hardware_state(_state)
446
+ true
447
+ end
448
+ end
449
+ end
450
+ end
data/lib/rubyboy/lcd.rb CHANGED
@@ -7,6 +7,7 @@ module Rubyboy
7
7
  SCREEN_WIDTH = 160
8
8
  SCREEN_HEIGHT = 144
9
9
  SCALE = 3
10
+ SDL_EVENT_SIZE = 56
10
11
 
11
12
  def initialize
12
13
  raise SDL.GetError() if SDL.InitSubSystem(SDL::INIT_VIDEO) != 0
@@ -20,7 +21,7 @@ module Rubyboy
20
21
  SDL.SetHint('SDL_HINT_RENDER_SCALE_QUALITY', '2')
21
22
  SDL.RenderSetLogicalSize(@renderer, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE)
22
23
  @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_ABGR8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT)
23
- @event = FFI::MemoryPointer.new(:pointer)
24
+ @event = FFI::MemoryPointer.new(:uint8, SDL_EVENT_SIZE)
24
25
  end
25
26
 
26
27
  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
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Rubyboy
4
4
  class Rom
5
+ # Tobu Tobu Girl, bundled with the gem and used when no ROM is given.
6
+ DEFAULT_PATH = File.expand_path('../roms/tobu.gb', __dir__).freeze
7
+
5
8
  attr_reader :data, :entroy_point, :logo, :title, :new_licensee_code, :sgb_flag, :cartridge_type, :rom_size, :ram_size, :destination_code, :old_licensee_code, :mask_rom_version_number, :header_checksum, :global_checksum
6
9
 
7
10
  LOGO_DUMP = %w[
@@ -10,11 +13,34 @@ module Rubyboy
10
13
  BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E
11
14
  ].map(&:hex).freeze
12
15
 
16
+ RAM_SIZE_BYTES = {
17
+ 0x00 => 0,
18
+ 0x01 => 2 * 1024,
19
+ 0x02 => 8 * 1024,
20
+ 0x03 => 32 * 1024,
21
+ 0x04 => 128 * 1024,
22
+ 0x05 => 64 * 1024
23
+ }.freeze
24
+
25
+ BATTERY_CARTRIDGE_TYPES = [0x03, 0x06, 0x09, 0x0d, 0x0f, 0x10, 0x13, 0x1b, 0x1e, 0x22, 0xff].freeze
26
+
13
27
  def initialize(data)
14
28
  @data = data
15
29
  load_data
16
30
  end
17
31
 
32
+ def ram_size_bytes
33
+ RAM_SIZE_BYTES.fetch(@ram_size, 0)
34
+ end
35
+
36
+ def battery?
37
+ BATTERY_CARTRIDGE_TYPES.include?(@cartridge_type)
38
+ end
39
+
40
+ def global_checksum_value
41
+ (@global_checksum[0] << 8) | @global_checksum[1]
42
+ end
43
+
18
44
  private
19
45
 
20
46
  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
@@ -5,6 +5,7 @@ require 'ffi'
5
5
  module Rubyboy
6
6
  module SDL
7
7
  extend FFI::Library
8
+
8
9
  ffi_lib 'SDL2'
9
10
 
10
11
  INIT_TIMER = 0x01
@@ -25,6 +26,17 @@ module Rubyboy
25
26
  SDL_SCANCODE_K = 14
26
27
  SDL_SCANCODE_U = 24
27
28
  SDL_SCANCODE_I = 12
29
+ SDL_SCANCODE_1 = 30
30
+ SDL_SCANCODE_2 = 31
31
+ SDL_SCANCODE_3 = 32
32
+ SDL_SCANCODE_4 = 33
33
+ SDL_SCANCODE_5 = 34
34
+ SDL_SCANCODE_6 = 35
35
+ SDL_SCANCODE_7 = 36
36
+ SDL_SCANCODE_8 = 37
37
+ SDL_SCANCODE_9 = 38
38
+ SDL_SCANCODE_0 = 39
39
+ SDL_SCANCODE_LSHIFT = 225
28
40
 
29
41
  attach_function :Init, 'SDL_Init', [:uint32], :int
30
42
  attach_function :InitSubSystem, 'SDL_InitSubSystem', [:uint32], :int
@@ -0,0 +1,49 @@
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-next Security/MarshalLoad
36
+ yield Marshal.load(data.byteslice(HEADER_SIZE..))
37
+ true
38
+ rescue StandardError => e
39
+ warn "[rubyboy] failed to read state file #{path}: #{e.message}"
40
+ false
41
+ end
42
+
43
+ def self.warn_and_false(message)
44
+ warn "[rubyboy] #{message}"
45
+ false
46
+ end
47
+ private_class_method :warn_and_false
48
+ end
49
+ 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.7.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'