rubyboy 1.4.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12916ff3bf0b3e855d767980218887793bfb1206675487d9aeaac191897e8c73
4
- data.tar.gz: f8fb9559338f9d0804f05906af939dcf2f735df729a30da6becf5db710934ba4
3
+ metadata.gz: f029df58812310dc308546c77d25e75496e0e7b0aff605a5d0826deba85d9014
4
+ data.tar.gz: c2fffa4e8f4535ae4fad4bdf3c5742f569443fffc925ee02fa06f6b268f5e2c4
5
5
  SHA512:
6
- metadata.gz: 84b0aa4f4731980e90f176a9c03a1aef3b84cdd2ee3ac9f5ead0b0ae286405b582ed5e78fc86169208ba3d6ec620b70d1d68703a77251ae6579675eecdbd6d42
7
- data.tar.gz: 1ecb47bb5111749e58c24e289275fb4f587f994aec722ed933f555b01d84ee198c0a7c0a65b18fb94e8ef434ea3390bfb5c7382c0e5383fd9496d4ad8f5bb070
6
+ metadata.gz: c7f5d75b7ae8ec29488242943aca521b013875c896c8b368622f02408f52803b01e94b548b720fb4bad1ffca30d4223cfbf15ab2e8bd04b89799dc2a41bcf164
7
+ data.tar.gz: b21b63a6c19263c3a72aa9b592505358dc55f450b0fd9fd3c8fb8b907d2057c07b2dae41dc9c0fa1be2fcd4e77010d4dcaf8ca7eafab908f27f7b5d903596a69
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.1] - 2024-12-25
4
+
5
+ - PPU performance optimization through data caching and pixel format changes
6
+
3
7
  ## [1.4.0] - 2024-09-29
4
8
 
5
9
  - Works on browser using ruby.wasm
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  A Game Boy emulator written in Ruby
8
8
 
9
+ **[Try the demo in your browser!](https://sacckey.github.io/rubyboy/)** - Powered by WebAssembly
10
+
9
11
  ## Screenshots
10
12
  <div align="center">
11
13
  <img src="/resource/screenshots/pokemon.png" width="400px"/>
data/docs/index.html CHANGED
@@ -1,13 +1,13 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <title>RUBY BOY</title>
4
+ <title>Ruby Boy</title>
5
5
  <meta charset="utf-8"/>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <meta name="author" content="sacckey" />
8
8
  <meta name="description" content="Game Boy emulator written in Ruby, running on ruby.wasm" />
9
9
  <meta property="og:url" content="https://sacckey.github.io/rubyboy/" />
10
- <meta property="og:title" content="RUBY BOY" />
10
+ <meta property="og:title" content="Ruby Boy" />
11
11
  <meta property="og:description" content="Game Boy emulator written in Ruby, running on ruby.wasm" />
12
12
  <meta property="og:image" content="https://sacckey.github.io/rubyboy/ogp.png" />
13
13
  <meta name="twitter:card" content="summary_large_image" />
data/lib/executor.rb CHANGED
@@ -14,7 +14,7 @@ class Executor
14
14
  end
15
15
 
16
16
  def exec(direction_key = 0b1111, action_key = 0b1111)
17
- bin = @emulator.step(direction_key, action_key).pack('C*')
17
+ bin = @emulator.step(direction_key, action_key).pack('V*')
18
18
  File.binwrite(File.join('/RUBYBOY_TMP', 'video.data'), bin)
19
19
  end
20
20
 
data/lib/rubyboy/apu.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'audio'
4
3
  require_relative 'apu_channels/channel1'
5
4
  require_relative 'apu_channels/channel2'
6
5
  require_relative 'apu_channels/channel3'
@@ -8,8 +7,9 @@ require_relative 'apu_channels/channel4'
8
7
 
9
8
  module Rubyboy
10
9
  class Apu
10
+ attr_reader :samples
11
+
11
12
  def initialize
12
- @audio = Audio.new
13
13
  @nr50 = 0
14
14
  @nr51 = 0
15
15
  @cycles = 0
@@ -67,10 +67,10 @@ module Rubyboy
67
67
  @sample_idx += 1
68
68
  end
69
69
 
70
- return if @sample_idx < 512
70
+ return false if @sample_idx < 512
71
71
 
72
72
  @sample_idx = 0
73
- @audio.queue(@samples)
73
+ true
74
74
  end
75
75
 
76
76
  def read_byte(addr)
@@ -18,6 +18,7 @@ module Rubyboy
18
18
  @bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu)
19
19
  @cpu = Cpu.new(@bus, interrupt)
20
20
  @lcd = Lcd.new
21
+ @audio = Audio.new
21
22
  end
22
23
 
23
24
  def start
@@ -31,7 +32,7 @@ module Rubyboy
31
32
  while elapsed_real_time > elapsed_machine_time
32
33
  cycles = @cpu.exec
33
34
  @timer.step(cycles)
34
- @apu.step(cycles)
35
+ @audio.queue(@apu.samples) if @apu.step(cycles)
35
36
  if @ppu.step(cycles)
36
37
  @lcd.draw(@ppu.buffer)
37
38
  key_input_check
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'apu_wasm'
3
+ require_relative 'apu'
4
4
  require_relative 'bus'
5
5
  require_relative 'cpu'
6
6
  require_relative 'emulator'
7
- require_relative 'ppu_wasm'
7
+ require_relative 'ppu'
8
8
  require_relative 'rom'
9
9
  require_relative 'ram'
10
10
  require_relative 'timer'
@@ -22,10 +22,10 @@ module Rubyboy
22
22
  ram = Ram.new
23
23
  mbc = Cartridge::Factory.create(rom, ram)
24
24
  interrupt = Interrupt.new
25
- @ppu = PpuWasm.new(interrupt)
25
+ @ppu = Ppu.new(interrupt)
26
26
  @timer = Timer.new(interrupt)
27
27
  @joypad = Joypad.new(interrupt)
28
- @apu = ApuWasm.new
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
31
  end
data/lib/rubyboy/lcd.rb CHANGED
@@ -11,21 +11,21 @@ module Rubyboy
11
11
  def initialize
12
12
  raise SDL.GetError() if SDL.InitSubSystem(SDL::INIT_VIDEO) != 0
13
13
 
14
- @buffer = FFI::MemoryPointer.new(:uint8, SCREEN_WIDTH * SCREEN_HEIGHT * 3)
15
- @window = SDL.CreateWindow('RUBY BOY', 0, 0, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL::SDL_WINDOW_RESIZABLE)
14
+ @buffer = FFI::MemoryPointer.new(:uint32, SCREEN_WIDTH * SCREEN_HEIGHT)
15
+ @window = SDL.CreateWindow('Ruby Boy', 0, 0, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL::SDL_WINDOW_RESIZABLE)
16
16
 
17
17
  raise SDL.GetError() if @window.null?
18
18
 
19
19
  @renderer = SDL.CreateRenderer(@window, -1, 0)
20
20
  SDL.SetHint('SDL_HINT_RENDER_SCALE_QUALITY', '2')
21
21
  SDL.RenderSetLogicalSize(@renderer, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE)
22
- @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_RGB24, 1, SCREEN_WIDTH, SCREEN_HEIGHT)
22
+ @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_ABGR8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT)
23
23
  @event = FFI::MemoryPointer.new(:pointer)
24
24
  end
25
25
 
26
26
  def draw(framebuffer)
27
- @buffer.write_array_of_uint8(framebuffer)
28
- SDL.UpdateTexture(@texture, nil, @buffer, SCREEN_WIDTH * 3)
27
+ @buffer.write_array_of_uint32(framebuffer)
28
+ SDL.UpdateTexture(@texture, nil, @buffer, SCREEN_WIDTH * 4)
29
29
  SDL.RenderClear(@renderer)
30
30
  SDL.RenderCopy(@renderer, @texture, nil, nil)
31
31
  SDL.RenderPresent(@renderer)
data/lib/rubyboy/ppu.rb CHANGED
@@ -64,8 +64,14 @@ module Rubyboy
64
64
  @wly = 0x00
65
65
  @cycles = 0
66
66
  @interrupt = interrupt
67
- @buffer = Array.new(144 * 160 * 3, 0x00)
67
+ @buffer = Array.new(144 * 160, 0xffffffff)
68
68
  @bg_pixels = Array.new(LCD_WIDTH, 0x00)
69
+ @tile_cache = Array.new(384) { Array.new(64, 0) }
70
+ @tile_map_cache = Array.new(2048, 0)
71
+ @bgp_cache = Array.new(4, 0xffffffff)
72
+ @obp0_cache = Array.new(4, 0xffffffff)
73
+ @obp1_cache = Array.new(4, 0xffffffff)
74
+ @sprite_cache = Array.new(40) { { y: 0xff, x: 0xff, tile_index: 0, flags: 0 } }
69
75
  end
70
76
 
71
77
  def read_byte(addr)
@@ -102,11 +108,32 @@ module Rubyboy
102
108
  def write_byte(addr, value)
103
109
  case addr
104
110
  when 0x8000..0x9fff
105
- @vram[addr - 0x8000] = value if @mode != MODE[:drawing]
111
+ if @mode != MODE[:drawing]
112
+ @vram[addr - 0x8000] = value
113
+ if addr < 0x9800
114
+ update_tile_cache(addr - 0x8000)
115
+ else
116
+ update_tile_map_cache(addr - 0x8000)
117
+ end
118
+ end
106
119
  when 0xfe00..0xfe9f
107
- @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing]
120
+ if @mode != MODE[:oam_scan] && @mode != MODE[:drawing]
121
+ @oam[addr - 0xfe00] = value
122
+ sprite_index = (addr - 0xfe00) >> 2
123
+ attribute = (addr - 0xfe00) & 3
124
+
125
+ case attribute
126
+ when 0 then @sprite_cache[sprite_index][:y] = (value - 16) & 0xff
127
+ when 1 then @sprite_cache[sprite_index][:x] = (value - 8) & 0xff
128
+ when 2 then @sprite_cache[sprite_index][:tile_index] = value
129
+ when 3 then @sprite_cache[sprite_index][:flags] = value
130
+ end
131
+ end
108
132
  when 0xff40
133
+ old_lcdc = @lcdc
109
134
  @lcdc = value
135
+
136
+ refresh_tile_map_cache if old_lcdc[LCDC[:bg_window_tile_data_area]] != value[LCDC[:bg_window_tile_data_area]]
110
137
  when 0xff41
111
138
  @stat = value & 0x78
112
139
  when 0xff42
@@ -119,10 +146,13 @@ module Rubyboy
119
146
  @lyc = value
120
147
  when 0xff47
121
148
  @bgp = value
149
+ refresh_palette_cache(@bgp_cache, value)
122
150
  when 0xff48
123
151
  @obp0 = value
152
+ refresh_palette_cache(@obp0_cache, value)
124
153
  when 0xff49
125
154
  @obp1 = value
155
+ refresh_palette_cache(@obp1_cache, value)
126
156
  when 0xff4a
127
157
  @wy = value
128
158
  when 0xff4b
@@ -189,19 +219,85 @@ module Rubyboy
189
219
  def render_bg
190
220
  return if @lcdc[LCDC[:bg_window_enable]] == 0
191
221
 
192
- y = (@ly + @scy) % 256
193
- tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00
194
- tile_map_addr += (y / 8) * 32
195
- LCD_WIDTH.times do |i|
196
- x = (i + @scx) % 256
197
- tile_index = get_tile_index(tile_map_addr + (x / 8))
198
- pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
199
- color = get_color(@bgp, pixel)
200
- base = @ly * LCD_WIDTH * 3 + i * 3
201
- @buffer[base] = color
202
- @buffer[base + 1] = color
203
- @buffer[base + 2] = color
204
- @bg_pixels[i] = pixel
222
+ y = (@ly + @scy) & 0xff
223
+ tile_map_addr = (y >> 3) << 5
224
+ tile_map_addr += 1024 if @lcdc[LCDC[:bg_tile_map_area]] == 1
225
+ tile_y = (y & 7) << 3
226
+ buffer_start_index = @ly * LCD_WIDTH
227
+
228
+ scx = @scx
229
+ buffer = @buffer
230
+ bg_pixels = @bg_pixels
231
+ tile_cache = @tile_cache
232
+ tile_map_cache = @tile_map_cache
233
+ bgp_cache = @bgp_cache
234
+
235
+ i = 0
236
+ current_tile = scx >> 3
237
+ x_offset = scx & 7
238
+
239
+ if x_offset > 0
240
+ tile = tile_cache[tile_map_cache[tile_map_addr + current_tile]]
241
+ while (x_offset + i) < 8
242
+ pixel = tile[tile_y + x_offset + i]
243
+ buffer[buffer_start_index + i] = bgp_cache[pixel]
244
+ bg_pixels[i] = pixel
245
+ i += 1
246
+ end
247
+ current_tile += 1
248
+ end
249
+
250
+ while i < LCD_WIDTH - 7
251
+ tile = tile_cache[tile_map_cache[tile_map_addr + (current_tile & 0x1f)]]
252
+ idx = buffer_start_index + i
253
+
254
+ # Unroll the 8-pixel loop
255
+ pixel = tile[tile_y]
256
+ buffer[idx] = bgp_cache[pixel]
257
+ bg_pixels[i] = pixel
258
+
259
+ pixel = tile[tile_y + 1]
260
+ buffer[idx + 1] = bgp_cache[pixel]
261
+ bg_pixels[i + 1] = pixel
262
+
263
+ pixel = tile[tile_y + 2]
264
+ buffer[idx + 2] = bgp_cache[pixel]
265
+ bg_pixels[i + 2] = pixel
266
+
267
+ pixel = tile[tile_y + 3]
268
+ buffer[idx + 3] = bgp_cache[pixel]
269
+ bg_pixels[i + 3] = pixel
270
+
271
+ pixel = tile[tile_y + 4]
272
+ buffer[idx + 4] = bgp_cache[pixel]
273
+ bg_pixels[i + 4] = pixel
274
+
275
+ pixel = tile[tile_y + 5]
276
+ buffer[idx + 5] = bgp_cache[pixel]
277
+ bg_pixels[i + 5] = pixel
278
+
279
+ pixel = tile[tile_y + 6]
280
+ buffer[idx + 6] = bgp_cache[pixel]
281
+ bg_pixels[i + 6] = pixel
282
+
283
+ pixel = tile[tile_y + 7]
284
+ buffer[idx + 7] = bgp_cache[pixel]
285
+ bg_pixels[i + 7] = pixel
286
+
287
+ i += 8
288
+ current_tile += 1
289
+ end
290
+
291
+ return unless i < LCD_WIDTH
292
+
293
+ tile = tile_cache[tile_map_cache[tile_map_addr + (current_tile & 0x1f)]]
294
+ x = 0
295
+ while i < LCD_WIDTH
296
+ pixel = tile[tile_y + x]
297
+ buffer[buffer_start_index + i] = bgp_cache[pixel]
298
+ bg_pixels[i] = pixel
299
+ x += 1
300
+ i += 1
205
301
  end
206
302
  end
207
303
 
@@ -210,20 +306,18 @@ module Rubyboy
210
306
 
211
307
  rendered = false
212
308
  y = @wly
213
- tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00
214
- tile_map_addr += (y / 8) * 32
309
+ tile_map_addr = (y >> 3) << 5
310
+ tile_map_addr += 1024 if @lcdc[LCDC[:window_tile_map_area]] == 1
311
+ tile_y = (y & 7) << 3
312
+ buffer_start_index = @ly * LCD_WIDTH
215
313
  LCD_WIDTH.times do |i|
216
314
  next if i < @wx - 7
217
315
 
218
316
  rendered = true
219
317
  x = i - (@wx - 7)
220
- tile_index = get_tile_index(tile_map_addr + (x / 8))
221
- pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
222
- color = get_color(@bgp, pixel)
223
- base = @ly * LCD_WIDTH * 3 + i * 3
224
- @buffer[base] = color
225
- @buffer[base + 1] = color
226
- @buffer[base + 2] = color
318
+ tile_index = @tile_map_cache[tile_map_addr + (x >> 3)]
319
+ pixel = @tile_cache[tile_index][tile_y + (x & 7)]
320
+ @buffer[buffer_start_index + i] = @bgp_cache[pixel]
227
321
  @bg_pixels[i] = pixel
228
322
  end
229
323
  @wly += 1 if rendered
@@ -236,62 +330,83 @@ module Rubyboy
236
330
  sprites = []
237
331
  cnt = 0
238
332
 
239
- @oam.each_slice(4) do |y, x, tile_index, flags|
240
- y = (y - 16) % 256
241
- x = (x - 8) % 256
242
- next if y > @ly || y + sprite_height <= @ly
333
+ @sprite_cache.each do |sprite|
334
+ next if sprite[:y] > @ly || sprite[:y] + sprite_height <= @ly
243
335
 
244
- sprites << { y:, x:, tile_index:, flags: }
336
+ sprites << sprite
245
337
  cnt += 1
246
338
  break if cnt == 10
247
339
  end
248
- sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
340
+ sprites.reverse!
341
+ sprites.sort! { |a, b| b[:x] <=> a[:x] }
249
342
 
250
343
  sprites.each do |sprite|
251
344
  flags = sprite[:flags]
252
- pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1
345
+ pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0_cache : @obp1_cache
253
346
  tile_index = sprite[:tile_index]
254
347
  tile_index &= 0xfe if sprite_height == 16
255
- y = (@ly - sprite[:y]) % 256
348
+ y = (@ly - sprite[:y]) & 0xff
256
349
  y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1
257
- tile_index = (tile_index + 1) % 256 if y >= 8
258
- y %= 8
350
+ tile_index = (tile_index + 1) & 0xff if y >= 8
351
+ tile_y = (y & 7) << 3
352
+ buffer_start_index = @ly * LCD_WIDTH
259
353
 
260
354
  8.times do |x|
261
355
  x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x
262
356
 
263
- pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2)
264
- i = (sprite[:x] + x) % 256
357
+ pixel = @tile_cache[tile_index][tile_y + x_flipped]
358
+ i = (sprite[:x] + x) & 0xff
265
359
 
266
360
  next if pixel == 0 || i >= LCD_WIDTH
267
361
  next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0
268
362
 
269
- color = get_color(pallet, pixel)
270
- base = @ly * LCD_WIDTH * 3 + i * 3
271
- @buffer[base] = color
272
- @buffer[base + 1] = color
273
- @buffer[base + 2] = color
363
+ @buffer[buffer_start_index + i] = pallet[pixel]
274
364
  end
275
365
  end
276
366
  end
277
367
 
278
368
  private
279
369
 
280
- def get_tile_index(tile_map_addr)
281
- tile_index = @vram[tile_map_addr]
282
- @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index
370
+ def update_tile_cache(addr)
371
+ tile_index = addr >> 4
372
+ row = ((addr & 0xf) >> 1) << 3
373
+
374
+ byte1 = @vram[addr & ~1]
375
+ byte2 = @vram[addr | 1]
376
+
377
+ 8.times do |col|
378
+ bit_index = 7 - col
379
+ pixel = ((byte1 >> bit_index) & 1) | (((byte2 >> bit_index) & 1) << 1)
380
+ @tile_cache[tile_index][row + col] = pixel
381
+ end
382
+ end
383
+
384
+ def update_tile_map_cache(addr)
385
+ map_index = addr - 0x1800
386
+ tile_index = @vram[addr]
387
+ @tile_map_cache[map_index] = @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index
283
388
  end
284
389
 
285
- def get_pixel(tile_index, c, r)
286
- @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1)
390
+ def refresh_tile_map_cache
391
+ if @lcdc[LCDC[:bg_window_tile_data_area]] == 0
392
+ (0x1800..0x1fff).each do |addr|
393
+ @tile_map_cache[addr - 0x1800] = to_signed_byte(@vram[addr]) + 256
394
+ end
395
+ else
396
+ (0x1800..0x1fff).each do |addr|
397
+ @tile_map_cache[addr - 0x1800] = @vram[addr]
398
+ end
399
+ end
287
400
  end
288
401
 
289
- def get_color(pallet, pixel)
290
- case (pallet >> (pixel * 2)) & 0b11
291
- when 0 then 0xff
292
- when 1 then 0xaa
293
- when 2 then 0x55
294
- when 3 then 0x00
402
+ def refresh_palette_cache(cache, palette_value)
403
+ 4.times do |i|
404
+ case (palette_value >> (i << 1)) & 0b11
405
+ when 0 then cache[i] = 0xffffffff
406
+ when 1 then cache[i] = 0xffaaaaaa
407
+ when 2 then cache[i] = 0xff555555
408
+ when 3 then cache[i] = 0xff000000
409
+ end
295
410
  end
296
411
  end
297
412
 
@@ -12,7 +12,7 @@ module Rubyboy
12
12
  SCALE = 4
13
13
 
14
14
  def initialize
15
- InitWindow(WIDTH * SCALE, HEIGHT * SCALE, 'RUBY BOY')
15
+ InitWindow(WIDTH * SCALE, HEIGHT * SCALE, 'Ruby Boy')
16
16
  image = GenImageColor(WIDTH, HEIGHT, BLACK)
17
17
  image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8
18
18
  @texture = LoadTextureFromImage(image)
data/lib/rubyboy/sdl.rb CHANGED
@@ -13,6 +13,7 @@ module Rubyboy
13
13
  INIT_KEYBOARD = 0x200
14
14
  WINDOW_RESIZABLE = 0x20
15
15
  PIXELFORMAT_RGB24 = 386930691
16
+ PIXELFORMAT_ABGR8888 = 376840196
16
17
  SDL_WINDOW_RESIZABLE = 0x20
17
18
  QUIT = 0x100
18
19
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyboy
4
- VERSION = '1.4.0'
4
+ VERSION = '1.4.1'
5
5
  end
data/lib/rubyboy.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rubyboy/sdl'
4
4
  require_relative 'rubyboy/apu'
5
+ require_relative 'rubyboy/audio'
5
6
  require_relative 'rubyboy/bus'
6
7
  require_relative 'rubyboy/cpu'
7
8
  require_relative 'rubyboy/emulator'
Binary file
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyboy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - sacckey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-29 00:00:00.000000000 Z
11
+ date: 2024-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -84,7 +84,6 @@ files:
84
84
  - lib/rubyboy/apu_channels/channel2.rb
85
85
  - lib/rubyboy/apu_channels/channel3.rb
86
86
  - lib/rubyboy/apu_channels/channel4.rb
87
- - lib/rubyboy/apu_wasm.rb
88
87
  - lib/rubyboy/audio.rb
89
88
  - lib/rubyboy/bus.rb
90
89
  - lib/rubyboy/cartridge/factory.rb
@@ -97,7 +96,6 @@ files:
97
96
  - lib/rubyboy/joypad.rb
98
97
  - lib/rubyboy/lcd.rb
99
98
  - lib/rubyboy/ppu.rb
100
- - lib/rubyboy/ppu_wasm.rb
101
99
  - lib/rubyboy/ram.rb
102
100
  - lib/rubyboy/raylib/audio.rb
103
101
  - lib/rubyboy/raylib/lcd.rb
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # require_relative 'audio'
4
- require_relative 'apu_channels/channel1'
5
- require_relative 'apu_channels/channel2'
6
- require_relative 'apu_channels/channel3'
7
- require_relative 'apu_channels/channel4'
8
-
9
- module Rubyboy
10
- class ApuWasm
11
- def initialize
12
- @audio = nil
13
- @nr50 = 0
14
- @nr51 = 0
15
- @cycles = 0
16
- @sampling_cycles = 0
17
- @fs = 0
18
- @samples = Array.new(1024, 0.0)
19
- @sample_idx = 0
20
- @channel1 = ApuChannels::Channel1.new
21
- @channel2 = ApuChannels::Channel2.new
22
- @channel3 = ApuChannels::Channel3.new
23
- @channel4 = ApuChannels::Channel4.new
24
- end
25
-
26
- def step(cycles)
27
- @cycles += cycles
28
- @sampling_cycles += cycles
29
-
30
- @channel1.step(cycles)
31
- @channel2.step(cycles)
32
- @channel3.step(cycles)
33
- @channel4.step(cycles)
34
-
35
- if @cycles >= 0x2000
36
- @cycles -= 0x2000
37
-
38
- @channel1.step_fs(@fs)
39
- @channel2.step_fs(@fs)
40
- @channel3.step_fs(@fs)
41
- @channel4.step_fs(@fs)
42
-
43
- @fs = (@fs + 1) % 8
44
- end
45
-
46
- if @sampling_cycles >= 87
47
- @sampling_cycles -= 87
48
-
49
- left_sample = (
50
- @nr51[7] * @channel4.dac_output +
51
- @nr51[6] * @channel3.dac_output +
52
- @nr51[5] * @channel2.dac_output +
53
- @nr51[4] * @channel1.dac_output
54
- ) / 4.0
55
-
56
- right_sample = (
57
- @nr51[3] * @channel4.dac_output +
58
- @nr51[2] * @channel3.dac_output +
59
- @nr51[1] * @channel2.dac_output +
60
- @nr51[0] * @channel1.dac_output
61
- ) / 4.0
62
-
63
- raise "#{@nr51} #{@channel4.dac_output}, #{@channel3.dac_output}, #{@channel2.dac_output},#{@channel1.dac_output}" if left_sample.abs > 1.0 || right_sample.abs > 1.0
64
-
65
- @samples[@sample_idx * 2] = (@nr50[4..6] / 7.0) * left_sample / 8.0
66
- @samples[@sample_idx * 2 + 1] = (@nr50[0..2] / 7.0) * right_sample / 8.0
67
- @sample_idx += 1
68
- end
69
-
70
- return if @sample_idx < 512
71
-
72
- @sample_idx = 0
73
- @audio.queue(@samples)
74
- end
75
-
76
- def read_byte(addr)
77
- case addr
78
- when 0xff10..0xff14 then @channel1.read_nr1x(addr - 0xff10)
79
- when 0xff15..0xff19 then @channel2.read_nr2x(addr - 0xff15)
80
- when 0xff1a..0xff1e then @channel3.read_nr3x(addr - 0xff1a)
81
- when 0xff1f..0xff23 then @channel4.read_nr4x(addr - 0xff1f)
82
- when 0xff24 then @nr50
83
- when 0xff25 then @nr51
84
- when 0xff26 then (@channel1.enabled ? 0x01 : 0x00) | (@channel2.enabled ? 0x02 : 0x00) | (@channel3.enabled ? 0x04 : 0x00) | (@channel4.enabled ? 0x08 : 0x00) | 0x70 | (@enabled ? 0x80 : 0x00)
85
- when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)]
86
- else raise "Invalid APU read at #{addr.to_s(16)}"
87
- end
88
- end
89
-
90
- def write_byte(addr, val)
91
- return if !@enabled && ![0xff11, 0xff16, 0xff1b, 0xff20, 0xff26].include?(addr) && !(0xff30..0xff3f).include?(addr)
92
-
93
- val &= 0x3f if !@enabled && [0xff11, 0xff16, 0xff1b, 0xff20].include?(addr)
94
-
95
- case addr
96
- when 0xff10..0xff14 then @channel1.write_nr1x(addr - 0xff10, val)
97
- when 0xff15..0xff19 then @channel2.write_nr2x(addr - 0xff15, val)
98
- when 0xff1a..0xff1e then @channel3.write_nr3x(addr - 0xff1a, val)
99
- when 0xff1f..0xff23 then @channel4.write_nr4x(addr - 0xff1f, val)
100
- when 0xff24 then @nr50 = val
101
- when 0xff25 then @nr51 = val
102
- when 0xff26
103
- flg = val & 0x80 > 0
104
- if !flg && @enabled
105
- (0xff10..0xff25).each { |a| write_byte(a, 0) }
106
- elsif flg && !@enabled
107
- @fs = 0
108
- @channel1.wave_duty_position = 0
109
- @channel2.wave_duty_position = 0
110
- @channel3.wave_duty_position = 0
111
- end
112
- @enabled = flg
113
- when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)] = val
114
- else raise "Invalid APU write at #{addr.to_s(16)}"
115
- end
116
- end
117
- end
118
- end
@@ -1,312 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rubyboy
4
- class PpuWasm
5
- attr_reader :buffer
6
-
7
- MODE = {
8
- hblank: 0,
9
- vblank: 1,
10
- oam_scan: 2,
11
- drawing: 3
12
- }.freeze
13
-
14
- LCDC = {
15
- bg_window_enable: 0,
16
- sprite_enable: 1,
17
- sprite_size: 2,
18
- bg_tile_map_area: 3,
19
- bg_window_tile_data_area: 4,
20
- window_enable: 5,
21
- window_tile_map_area: 6,
22
- lcd_ppu_enable: 7
23
- }.freeze
24
-
25
- STAT = {
26
- ly_eq_lyc: 2,
27
- hblank: 3,
28
- vblank: 4,
29
- oam_scan: 5,
30
- lyc: 6
31
- }.freeze
32
-
33
- SPRITE_FLAGS = {
34
- bank: 3,
35
- dmg_palette: 4,
36
- x_flip: 5,
37
- y_flip: 6,
38
- priority: 7
39
- }.freeze
40
-
41
- LCD_WIDTH = 160
42
- LCD_HEIGHT = 144
43
-
44
- OAM_SCAN_CYCLES = 80
45
- DRAWING_CYCLES = 172
46
- HBLANK_CYCLES = 204
47
- ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES
48
-
49
- def initialize(interrupt)
50
- @mode = MODE[:oam_scan]
51
- @lcdc = 0x91
52
- @stat = 0x00
53
- @scy = 0x00
54
- @scx = 0x00
55
- @ly = 0x00
56
- @lyc = 0x00
57
- @obp0 = 0x00
58
- @obp1 = 0x00
59
- @wy = 0x00
60
- @wx = 0x00
61
- @bgp = 0x00
62
- @vram = Array.new(0x2000, 0x00)
63
- @oam = Array.new(0xa0, 0x00)
64
- @wly = 0x00
65
- @cycles = 0
66
- @interrupt = interrupt
67
- @buffer = Array.new(144 * 160 * 4, 0xff)
68
- @bg_pixels = Array.new(LCD_WIDTH, 0x00)
69
- end
70
-
71
- def read_byte(addr)
72
- case addr
73
- when 0x8000..0x9fff
74
- @mode == MODE[:drawing] ? 0xff : @vram[addr - 0x8000]
75
- when 0xfe00..0xfe9f
76
- @mode == MODE[:oam_scan] || @mode == MODE[:drawing] ? 0xff : @oam[addr - 0xfe00]
77
- when 0xff40
78
- @lcdc
79
- when 0xff41
80
- @stat | 0x80 | @mode
81
- when 0xff42
82
- @scy
83
- when 0xff43
84
- @scx
85
- when 0xff44
86
- @ly
87
- when 0xff45
88
- @lyc
89
- when 0xff47
90
- @bgp
91
- when 0xff48
92
- @obp0
93
- when 0xff49
94
- @obp1
95
- when 0xff4a
96
- @wy
97
- when 0xff4b
98
- @wx
99
- end
100
- end
101
-
102
- def write_byte(addr, value)
103
- case addr
104
- when 0x8000..0x9fff
105
- @vram[addr - 0x8000] = value if @mode != MODE[:drawing]
106
- when 0xfe00..0xfe9f
107
- @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing]
108
- when 0xff40
109
- @lcdc = value
110
- when 0xff41
111
- @stat = value & 0x78
112
- when 0xff42
113
- @scy = value
114
- when 0xff43
115
- @scx = value
116
- when 0xff44
117
- # ly is read only
118
- when 0xff45
119
- @lyc = value
120
- when 0xff47
121
- @bgp = value
122
- when 0xff48
123
- @obp0 = value
124
- when 0xff49
125
- @obp1 = value
126
- when 0xff4a
127
- @wy = value
128
- when 0xff4b
129
- @wx = value
130
- end
131
- end
132
-
133
- def step(cycles)
134
- return false if @lcdc[LCDC[:lcd_ppu_enable]] == 0
135
-
136
- res = false
137
- @cycles += cycles
138
-
139
- case @mode
140
- when MODE[:oam_scan]
141
- if @cycles >= OAM_SCAN_CYCLES
142
- @cycles -= OAM_SCAN_CYCLES
143
- @mode = MODE[:drawing]
144
- end
145
- when MODE[:drawing]
146
- if @cycles >= DRAWING_CYCLES
147
- render_bg
148
- render_window
149
- render_sprites
150
- @cycles -= DRAWING_CYCLES
151
- @mode = MODE[:hblank]
152
- @interrupt.request(:lcd) if @stat[STAT[:hblank]] == 1
153
- end
154
- when MODE[:hblank]
155
- if @cycles >= HBLANK_CYCLES
156
- @cycles -= HBLANK_CYCLES
157
- @ly += 1
158
- handle_ly_eq_lyc
159
-
160
- if @ly == LCD_HEIGHT
161
- @mode = MODE[:vblank]
162
- @interrupt.request(:vblank)
163
- @interrupt.request(:lcd) if @stat[STAT[:vblank]] == 1
164
- else
165
- @mode = MODE[:oam_scan]
166
- @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
167
- end
168
- end
169
- when MODE[:vblank]
170
- if @cycles >= ONE_LINE_CYCLES
171
- @cycles -= ONE_LINE_CYCLES
172
- @ly += 1
173
- handle_ly_eq_lyc
174
-
175
- if @ly == 154
176
- @ly = 0
177
- @wly = 0
178
- handle_ly_eq_lyc
179
- @mode = MODE[:oam_scan]
180
- @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
181
- res = true
182
- end
183
- end
184
- end
185
-
186
- res
187
- end
188
-
189
- def render_bg
190
- return if @lcdc[LCDC[:bg_window_enable]] == 0
191
-
192
- y = (@ly + @scy) % 256
193
- tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00
194
- tile_map_addr += (y / 8) * 32
195
- LCD_WIDTH.times do |i|
196
- x = (i + @scx) % 256
197
- tile_index = get_tile_index(tile_map_addr + (x / 8))
198
- pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
199
- color = get_color(@bgp, pixel)
200
- base = @ly * LCD_WIDTH * 4 + i * 4
201
- @buffer[base] = color
202
- @buffer[base + 1] = color
203
- @buffer[base + 2] = color
204
- @bg_pixels[i] = pixel
205
- end
206
- end
207
-
208
- def render_window
209
- return if @lcdc[LCDC[:bg_window_enable]] == 0 || @lcdc[LCDC[:window_enable]] == 0 || @ly < @wy
210
-
211
- rendered = false
212
- y = @wly
213
- tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00
214
- tile_map_addr += (y / 8) * 32
215
- LCD_WIDTH.times do |i|
216
- next if i < @wx - 7
217
-
218
- rendered = true
219
- x = i - (@wx - 7)
220
- tile_index = get_tile_index(tile_map_addr + (x / 8))
221
- pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
222
- color = get_color(@bgp, pixel)
223
- base = @ly * LCD_WIDTH * 4 + i * 4
224
- @buffer[base] = color
225
- @buffer[base + 1] = color
226
- @buffer[base + 2] = color
227
- @bg_pixels[i] = pixel
228
- end
229
- @wly += 1 if rendered
230
- end
231
-
232
- def render_sprites
233
- return if @lcdc[LCDC[:sprite_enable]] == 0
234
-
235
- sprite_height = @lcdc[LCDC[:sprite_size]] == 0 ? 8 : 16
236
- sprites = []
237
- cnt = 0
238
-
239
- @oam.each_slice(4) do |y, x, tile_index, flags|
240
- y = (y - 16) % 256
241
- x = (x - 8) % 256
242
- next if y > @ly || y + sprite_height <= @ly
243
-
244
- sprites << { y:, x:, tile_index:, flags: }
245
- cnt += 1
246
- break if cnt == 10
247
- end
248
- sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
249
-
250
- sprites.each do |sprite|
251
- flags = sprite[:flags]
252
- pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1
253
- tile_index = sprite[:tile_index]
254
- tile_index &= 0xfe if sprite_height == 16
255
- y = (@ly - sprite[:y]) % 256
256
- y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1
257
- tile_index = (tile_index + 1) % 256 if y >= 8
258
- y %= 8
259
-
260
- 8.times do |x|
261
- x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x
262
-
263
- pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2)
264
- i = (sprite[:x] + x) % 256
265
-
266
- next if pixel == 0 || i >= LCD_WIDTH
267
- next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0
268
-
269
- color = get_color(pallet, pixel)
270
- base = @ly * LCD_WIDTH * 4 + i * 4
271
- @buffer[base] = color
272
- @buffer[base + 1] = color
273
- @buffer[base + 2] = color
274
- end
275
- end
276
- end
277
-
278
- private
279
-
280
- def get_tile_index(tile_map_addr)
281
- tile_index = @vram[tile_map_addr]
282
- @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index
283
- end
284
-
285
- def get_pixel(tile_index, c, r)
286
- @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1)
287
- end
288
-
289
- def get_color(pallet, pixel)
290
- case (pallet >> (pixel * 2)) & 0b11
291
- when 0 then 0xff
292
- when 1 then 0xaa
293
- when 2 then 0x55
294
- when 3 then 0x00
295
- end
296
- end
297
-
298
- def to_signed_byte(byte)
299
- byte &= 0xff
300
- byte > 127 ? byte - 256 : byte
301
- end
302
-
303
- def handle_ly_eq_lyc
304
- if @ly == @lyc
305
- @stat |= 0x04
306
- @interrupt.request(:lcd) if @stat[STAT[:lyc]] == 1
307
- else
308
- @stat &= 0xfb
309
- end
310
- end
311
- end
312
- end