rubyboy 1.4.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
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