diff --git a/CHANGELOG.md b/CHANGELOG.md index 841014c..6f3ff21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [1.4.1] - 2024-12-25 + +- PPU performance optimization through data caching and pixel format changes + ## [1.4.0] - 2024-09-29 - Works on browser using ruby.wasm diff --git a/Gemfile.lock b/Gemfile.lock index 41c0c6a..2ded902 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rubyboy (1.4.0) + rubyboy (1.4.1) ffi (~> 1.16, >= 1.16.3) GEM diff --git a/lib/executor.rb b/lib/executor.rb index 145456c..440db2f 100644 --- a/lib/executor.rb +++ b/lib/executor.rb @@ -14,7 +14,7 @@ def initialize end def exec(direction_key = 0b1111, action_key = 0b1111) - bin = @emulator.step(direction_key, action_key).pack('C*') + bin = @emulator.step(direction_key, action_key).pack('V*') File.binwrite(File.join('/RUBYBOY_TMP', 'video.data'), bin) end diff --git a/lib/rubyboy.rb b/lib/rubyboy.rb index 1f2ffcc..2c40eba 100644 --- a/lib/rubyboy.rb +++ b/lib/rubyboy.rb @@ -2,6 +2,7 @@ require 'rubyboy/sdl' require_relative 'rubyboy/apu' +require_relative 'rubyboy/audio' require_relative 'rubyboy/bus' require_relative 'rubyboy/cpu' require_relative 'rubyboy/emulator' diff --git a/lib/rubyboy/apu.rb b/lib/rubyboy/apu.rb index eff5d73..46c3f9f 100644 --- a/lib/rubyboy/apu.rb +++ b/lib/rubyboy/apu.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative 'audio' require_relative 'apu_channels/channel1' require_relative 'apu_channels/channel2' require_relative 'apu_channels/channel3' @@ -8,8 +7,9 @@ module Rubyboy class Apu + attr_reader :samples + def initialize - @audio = Audio.new @nr50 = 0 @nr51 = 0 @cycles = 0 @@ -67,10 +67,10 @@ def step(cycles) @sample_idx += 1 end - return if @sample_idx < 512 + return false if @sample_idx < 512 @sample_idx = 0 - @audio.queue(@samples) + true end def read_byte(addr) diff --git a/lib/rubyboy/apu_wasm.rb b/lib/rubyboy/apu_wasm.rb deleted file mode 100644 index 20ddb1a..0000000 --- a/lib/rubyboy/apu_wasm.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -# require_relative 'audio' -require_relative 'apu_channels/channel1' -require_relative 'apu_channels/channel2' -require_relative 'apu_channels/channel3' -require_relative 'apu_channels/channel4' - -module Rubyboy - class ApuWasm - def initialize - @audio = nil - @nr50 = 0 - @nr51 = 0 - @cycles = 0 - @sampling_cycles = 0 - @fs = 0 - @samples = Array.new(1024, 0.0) - @sample_idx = 0 - @channel1 = ApuChannels::Channel1.new - @channel2 = ApuChannels::Channel2.new - @channel3 = ApuChannels::Channel3.new - @channel4 = ApuChannels::Channel4.new - end - - def step(cycles) - @cycles += cycles - @sampling_cycles += cycles - - @channel1.step(cycles) - @channel2.step(cycles) - @channel3.step(cycles) - @channel4.step(cycles) - - if @cycles >= 0x2000 - @cycles -= 0x2000 - - @channel1.step_fs(@fs) - @channel2.step_fs(@fs) - @channel3.step_fs(@fs) - @channel4.step_fs(@fs) - - @fs = (@fs + 1) % 8 - end - - if @sampling_cycles >= 87 - @sampling_cycles -= 87 - - left_sample = ( - @nr51[7] * @channel4.dac_output + - @nr51[6] * @channel3.dac_output + - @nr51[5] * @channel2.dac_output + - @nr51[4] * @channel1.dac_output - ) / 4.0 - - right_sample = ( - @nr51[3] * @channel4.dac_output + - @nr51[2] * @channel3.dac_output + - @nr51[1] * @channel2.dac_output + - @nr51[0] * @channel1.dac_output - ) / 4.0 - - 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 - - @samples[@sample_idx * 2] = (@nr50[4..6] / 7.0) * left_sample / 8.0 - @samples[@sample_idx * 2 + 1] = (@nr50[0..2] / 7.0) * right_sample / 8.0 - @sample_idx += 1 - end - - return if @sample_idx < 512 - - @sample_idx = 0 - @audio.queue(@samples) - end - - def read_byte(addr) - case addr - when 0xff10..0xff14 then @channel1.read_nr1x(addr - 0xff10) - when 0xff15..0xff19 then @channel2.read_nr2x(addr - 0xff15) - when 0xff1a..0xff1e then @channel3.read_nr3x(addr - 0xff1a) - when 0xff1f..0xff23 then @channel4.read_nr4x(addr - 0xff1f) - when 0xff24 then @nr50 - when 0xff25 then @nr51 - when 0xff26 then (@channel1.enabled ? 0x01 : 0x00) | (@channel2.enabled ? 0x02 : 0x00) | (@channel3.enabled ? 0x04 : 0x00) | (@channel4.enabled ? 0x08 : 0x00) | 0x70 | (@enabled ? 0x80 : 0x00) - when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)] - else raise "Invalid APU read at #{addr.to_s(16)}" - end - end - - def write_byte(addr, val) - return if !@enabled && ![0xff11, 0xff16, 0xff1b, 0xff20, 0xff26].include?(addr) && !(0xff30..0xff3f).include?(addr) - - val &= 0x3f if !@enabled && [0xff11, 0xff16, 0xff1b, 0xff20].include?(addr) - - case addr - when 0xff10..0xff14 then @channel1.write_nr1x(addr - 0xff10, val) - when 0xff15..0xff19 then @channel2.write_nr2x(addr - 0xff15, val) - when 0xff1a..0xff1e then @channel3.write_nr3x(addr - 0xff1a, val) - when 0xff1f..0xff23 then @channel4.write_nr4x(addr - 0xff1f, val) - when 0xff24 then @nr50 = val - when 0xff25 then @nr51 = val - when 0xff26 - flg = val & 0x80 > 0 - if !flg && @enabled - (0xff10..0xff25).each { |a| write_byte(a, 0) } - elsif flg && !@enabled - @fs = 0 - @channel1.wave_duty_position = 0 - @channel2.wave_duty_position = 0 - @channel3.wave_duty_position = 0 - end - @enabled = flg - when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)] = val - else raise "Invalid APU write at #{addr.to_s(16)}" - end - end - end -end diff --git a/lib/rubyboy/emulator.rb b/lib/rubyboy/emulator.rb index 7f68d41..6c6300d 100644 --- a/lib/rubyboy/emulator.rb +++ b/lib/rubyboy/emulator.rb @@ -18,6 +18,7 @@ def initialize(rom_path) @bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu) @cpu = Cpu.new(@bus, interrupt) @lcd = Lcd.new + @audio = Audio.new end def start @@ -31,7 +32,7 @@ def start while elapsed_real_time > elapsed_machine_time cycles = @cpu.exec @timer.step(cycles) - @apu.step(cycles) + @audio.queue(@apu.samples) if @apu.step(cycles) if @ppu.step(cycles) @lcd.draw(@ppu.buffer) key_input_check diff --git a/lib/rubyboy/emulator_wasm.rb b/lib/rubyboy/emulator_wasm.rb index ea523ea..1e3d28e 100644 --- a/lib/rubyboy/emulator_wasm.rb +++ b/lib/rubyboy/emulator_wasm.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require_relative 'apu_wasm' +require_relative 'apu' require_relative 'bus' require_relative 'cpu' require_relative 'emulator' -require_relative 'ppu_wasm' +require_relative 'ppu' require_relative 'rom' require_relative 'ram' require_relative 'timer' @@ -22,10 +22,10 @@ def initialize(rom_data) ram = Ram.new mbc = Cartridge::Factory.create(rom, ram) interrupt = Interrupt.new - @ppu = PpuWasm.new(interrupt) + @ppu = Ppu.new(interrupt) @timer = Timer.new(interrupt) @joypad = Joypad.new(interrupt) - @apu = ApuWasm.new + @apu = Apu.new @bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu) @cpu = Cpu.new(@bus, interrupt) end diff --git a/lib/rubyboy/lcd.rb b/lib/rubyboy/lcd.rb index e14a2fd..b1cba83 100644 --- a/lib/rubyboy/lcd.rb +++ b/lib/rubyboy/lcd.rb @@ -11,7 +11,7 @@ class Lcd def initialize raise SDL.GetError() if SDL.InitSubSystem(SDL::INIT_VIDEO) != 0 - @buffer = FFI::MemoryPointer.new(:uint8, SCREEN_WIDTH * SCREEN_HEIGHT * 3) + @buffer = FFI::MemoryPointer.new(:uint32, SCREEN_WIDTH * SCREEN_HEIGHT) @window = SDL.CreateWindow('Ruby Boy', 0, 0, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL::SDL_WINDOW_RESIZABLE) raise SDL.GetError() if @window.null? @@ -19,13 +19,13 @@ def initialize @renderer = SDL.CreateRenderer(@window, -1, 0) SDL.SetHint('SDL_HINT_RENDER_SCALE_QUALITY', '2') SDL.RenderSetLogicalSize(@renderer, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE) - @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_RGB24, 1, SCREEN_WIDTH, SCREEN_HEIGHT) + @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_ABGR8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT) @event = FFI::MemoryPointer.new(:pointer) end def draw(framebuffer) - @buffer.write_array_of_uint8(framebuffer) - SDL.UpdateTexture(@texture, nil, @buffer, SCREEN_WIDTH * 3) + @buffer.write_array_of_uint32(framebuffer) + SDL.UpdateTexture(@texture, nil, @buffer, SCREEN_WIDTH * 4) SDL.RenderClear(@renderer) SDL.RenderCopy(@renderer, @texture, nil, nil) SDL.RenderPresent(@renderer) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index f960180..aed013c 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -64,8 +64,14 @@ def initialize(interrupt) @wly = 0x00 @cycles = 0 @interrupt = interrupt - @buffer = Array.new(144 * 160 * 3, 0x00) + @buffer = Array.new(144 * 160, 0xffffffff) @bg_pixels = Array.new(LCD_WIDTH, 0x00) + @tile_cache = Array.new(384) { Array.new(64, 0) } + @tile_map_cache = Array.new(2048, 0) + @bgp_cache = Array.new(4, 0xffffffff) + @obp0_cache = Array.new(4, 0xffffffff) + @obp1_cache = Array.new(4, 0xffffffff) + @sprite_cache = Array.new(40) { { y: 0xff, x: 0xff, tile_index: 0, flags: 0 } } end def read_byte(addr) @@ -102,11 +108,32 @@ def read_byte(addr) def write_byte(addr, value) case addr when 0x8000..0x9fff - @vram[addr - 0x8000] = value if @mode != MODE[:drawing] + if @mode != MODE[:drawing] + @vram[addr - 0x8000] = value + if addr < 0x9800 + update_tile_cache(addr - 0x8000) + else + update_tile_map_cache(addr - 0x8000) + end + end when 0xfe00..0xfe9f - @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing] + if @mode != MODE[:oam_scan] && @mode != MODE[:drawing] + @oam[addr - 0xfe00] = value + sprite_index = (addr - 0xfe00) >> 2 + attribute = (addr - 0xfe00) & 3 + + case attribute + when 0 then @sprite_cache[sprite_index][:y] = (value - 16) & 0xff + when 1 then @sprite_cache[sprite_index][:x] = (value - 8) & 0xff + when 2 then @sprite_cache[sprite_index][:tile_index] = value + when 3 then @sprite_cache[sprite_index][:flags] = value + end + end when 0xff40 + old_lcdc = @lcdc @lcdc = value + + refresh_tile_map_cache if old_lcdc[LCDC[:bg_window_tile_data_area]] != value[LCDC[:bg_window_tile_data_area]] when 0xff41 @stat = value & 0x78 when 0xff42 @@ -119,10 +146,13 @@ def write_byte(addr, value) @lyc = value when 0xff47 @bgp = value + refresh_palette_cache(@bgp_cache, value) when 0xff48 @obp0 = value + refresh_palette_cache(@obp0_cache, value) when 0xff49 @obp1 = value + refresh_palette_cache(@obp1_cache, value) when 0xff4a @wy = value when 0xff4b @@ -189,19 +219,85 @@ def step(cycles) def render_bg return if @lcdc[LCDC[:bg_window_enable]] == 0 - y = (@ly + @scy) % 256 - tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00 - tile_map_addr += (y / 8) * 32 - LCD_WIDTH.times do |i| - x = (i + @scx) % 256 - tile_index = get_tile_index(tile_map_addr + (x / 8)) - pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2) - color = get_color(@bgp, pixel) - base = @ly * LCD_WIDTH * 3 + i * 3 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color - @bg_pixels[i] = pixel + y = (@ly + @scy) & 0xff + tile_map_addr = (y >> 3) << 5 + tile_map_addr += 1024 if @lcdc[LCDC[:bg_tile_map_area]] == 1 + tile_y = (y & 7) << 3 + buffer_start_index = @ly * LCD_WIDTH + + scx = @scx + buffer = @buffer + bg_pixels = @bg_pixels + tile_cache = @tile_cache + tile_map_cache = @tile_map_cache + bgp_cache = @bgp_cache + + i = 0 + current_tile = scx >> 3 + x_offset = scx & 7 + + if x_offset > 0 + tile = tile_cache[tile_map_cache[tile_map_addr + current_tile]] + while (x_offset + i) < 8 + pixel = tile[tile_y + x_offset + i] + buffer[buffer_start_index + i] = bgp_cache[pixel] + bg_pixels[i] = pixel + i += 1 + end + current_tile += 1 + end + + while i < LCD_WIDTH - 7 + tile = tile_cache[tile_map_cache[tile_map_addr + (current_tile & 0x1f)]] + idx = buffer_start_index + i + + # Unroll the 8-pixel loop + pixel = tile[tile_y] + buffer[idx] = bgp_cache[pixel] + bg_pixels[i] = pixel + + pixel = tile[tile_y + 1] + buffer[idx + 1] = bgp_cache[pixel] + bg_pixels[i + 1] = pixel + + pixel = tile[tile_y + 2] + buffer[idx + 2] = bgp_cache[pixel] + bg_pixels[i + 2] = pixel + + pixel = tile[tile_y + 3] + buffer[idx + 3] = bgp_cache[pixel] + bg_pixels[i + 3] = pixel + + pixel = tile[tile_y + 4] + buffer[idx + 4] = bgp_cache[pixel] + bg_pixels[i + 4] = pixel + + pixel = tile[tile_y + 5] + buffer[idx + 5] = bgp_cache[pixel] + bg_pixels[i + 5] = pixel + + pixel = tile[tile_y + 6] + buffer[idx + 6] = bgp_cache[pixel] + bg_pixels[i + 6] = pixel + + pixel = tile[tile_y + 7] + buffer[idx + 7] = bgp_cache[pixel] + bg_pixels[i + 7] = pixel + + i += 8 + current_tile += 1 + end + + return unless i < LCD_WIDTH + + tile = tile_cache[tile_map_cache[tile_map_addr + (current_tile & 0x1f)]] + x = 0 + while i < LCD_WIDTH + pixel = tile[tile_y + x] + buffer[buffer_start_index + i] = bgp_cache[pixel] + bg_pixels[i] = pixel + x += 1 + i += 1 end end @@ -210,20 +306,18 @@ def render_window rendered = false y = @wly - tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00 - tile_map_addr += (y / 8) * 32 + tile_map_addr = (y >> 3) << 5 + tile_map_addr += 1024 if @lcdc[LCDC[:window_tile_map_area]] == 1 + tile_y = (y & 7) << 3 + buffer_start_index = @ly * LCD_WIDTH LCD_WIDTH.times do |i| next if i < @wx - 7 rendered = true x = i - (@wx - 7) - tile_index = get_tile_index(tile_map_addr + (x / 8)) - pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2) - color = get_color(@bgp, pixel) - base = @ly * LCD_WIDTH * 3 + i * 3 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color + tile_index = @tile_map_cache[tile_map_addr + (x >> 3)] + pixel = @tile_cache[tile_index][tile_y + (x & 7)] + @buffer[buffer_start_index + i] = @bgp_cache[pixel] @bg_pixels[i] = pixel end @wly += 1 if rendered @@ -236,62 +330,83 @@ def render_sprites sprites = [] cnt = 0 - @oam.each_slice(4) do |y, x, tile_index, flags| - y = (y - 16) % 256 - x = (x - 8) % 256 - next if y > @ly || y + sprite_height <= @ly + @sprite_cache.each do |sprite| + next if sprite[:y] > @ly || sprite[:y] + sprite_height <= @ly - sprites << { y:, x:, tile_index:, flags: } + sprites << sprite cnt += 1 break if cnt == 10 end - sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] } + sprites.reverse! + sprites.sort! { |a, b| b[:x] <=> a[:x] } sprites.each do |sprite| flags = sprite[:flags] - pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1 + pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0_cache : @obp1_cache tile_index = sprite[:tile_index] tile_index &= 0xfe if sprite_height == 16 - y = (@ly - sprite[:y]) % 256 + y = (@ly - sprite[:y]) & 0xff y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1 - tile_index = (tile_index + 1) % 256 if y >= 8 - y %= 8 + tile_index = (tile_index + 1) & 0xff if y >= 8 + tile_y = (y & 7) << 3 + buffer_start_index = @ly * LCD_WIDTH 8.times do |x| x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x - pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2) - i = (sprite[:x] + x) % 256 + pixel = @tile_cache[tile_index][tile_y + x_flipped] + i = (sprite[:x] + x) & 0xff next if pixel == 0 || i >= LCD_WIDTH next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 - color = get_color(pallet, pixel) - base = @ly * LCD_WIDTH * 3 + i * 3 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color + @buffer[buffer_start_index + i] = pallet[pixel] end end end private - def get_tile_index(tile_map_addr) - tile_index = @vram[tile_map_addr] - @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index + def update_tile_cache(addr) + tile_index = addr >> 4 + row = ((addr & 0xf) >> 1) << 3 + + byte1 = @vram[addr & ~1] + byte2 = @vram[addr | 1] + + 8.times do |col| + bit_index = 7 - col + pixel = ((byte1 >> bit_index) & 1) | (((byte2 >> bit_index) & 1) << 1) + @tile_cache[tile_index][row + col] = pixel + end + end + + def update_tile_map_cache(addr) + map_index = addr - 0x1800 + tile_index = @vram[addr] + @tile_map_cache[map_index] = @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index end - def get_pixel(tile_index, c, r) - @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1) + def refresh_tile_map_cache + if @lcdc[LCDC[:bg_window_tile_data_area]] == 0 + (0x1800..0x1fff).each do |addr| + @tile_map_cache[addr - 0x1800] = to_signed_byte(@vram[addr]) + 256 + end + else + (0x1800..0x1fff).each do |addr| + @tile_map_cache[addr - 0x1800] = @vram[addr] + end + end end - def get_color(pallet, pixel) - case (pallet >> (pixel * 2)) & 0b11 - when 0 then 0xff - when 1 then 0xaa - when 2 then 0x55 - when 3 then 0x00 + def refresh_palette_cache(cache, palette_value) + 4.times do |i| + case (palette_value >> (i << 1)) & 0b11 + when 0 then cache[i] = 0xffffffff + when 1 then cache[i] = 0xffaaaaaa + when 2 then cache[i] = 0xff555555 + when 3 then cache[i] = 0xff000000 + end end end diff --git a/lib/rubyboy/ppu_wasm.rb b/lib/rubyboy/ppu_wasm.rb deleted file mode 100644 index 501f480..0000000 --- a/lib/rubyboy/ppu_wasm.rb +++ /dev/null @@ -1,312 +0,0 @@ -# frozen_string_literal: true - -module Rubyboy - class PpuWasm - attr_reader :buffer - - MODE = { - hblank: 0, - vblank: 1, - oam_scan: 2, - drawing: 3 - }.freeze - - LCDC = { - bg_window_enable: 0, - sprite_enable: 1, - sprite_size: 2, - bg_tile_map_area: 3, - bg_window_tile_data_area: 4, - window_enable: 5, - window_tile_map_area: 6, - lcd_ppu_enable: 7 - }.freeze - - STAT = { - ly_eq_lyc: 2, - hblank: 3, - vblank: 4, - oam_scan: 5, - lyc: 6 - }.freeze - - SPRITE_FLAGS = { - bank: 3, - dmg_palette: 4, - x_flip: 5, - y_flip: 6, - priority: 7 - }.freeze - - LCD_WIDTH = 160 - LCD_HEIGHT = 144 - - OAM_SCAN_CYCLES = 80 - DRAWING_CYCLES = 172 - HBLANK_CYCLES = 204 - ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES - - def initialize(interrupt) - @mode = MODE[:oam_scan] - @lcdc = 0x91 - @stat = 0x00 - @scy = 0x00 - @scx = 0x00 - @ly = 0x00 - @lyc = 0x00 - @obp0 = 0x00 - @obp1 = 0x00 - @wy = 0x00 - @wx = 0x00 - @bgp = 0x00 - @vram = Array.new(0x2000, 0x00) - @oam = Array.new(0xa0, 0x00) - @wly = 0x00 - @cycles = 0 - @interrupt = interrupt - @buffer = Array.new(144 * 160 * 4, 0xff) - @bg_pixels = Array.new(LCD_WIDTH, 0x00) - end - - def read_byte(addr) - case addr - when 0x8000..0x9fff - @mode == MODE[:drawing] ? 0xff : @vram[addr - 0x8000] - when 0xfe00..0xfe9f - @mode == MODE[:oam_scan] || @mode == MODE[:drawing] ? 0xff : @oam[addr - 0xfe00] - when 0xff40 - @lcdc - when 0xff41 - @stat | 0x80 | @mode - when 0xff42 - @scy - when 0xff43 - @scx - when 0xff44 - @ly - when 0xff45 - @lyc - when 0xff47 - @bgp - when 0xff48 - @obp0 - when 0xff49 - @obp1 - when 0xff4a - @wy - when 0xff4b - @wx - end - end - - def write_byte(addr, value) - case addr - when 0x8000..0x9fff - @vram[addr - 0x8000] = value if @mode != MODE[:drawing] - when 0xfe00..0xfe9f - @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing] - when 0xff40 - @lcdc = value - when 0xff41 - @stat = value & 0x78 - when 0xff42 - @scy = value - when 0xff43 - @scx = value - when 0xff44 - # ly is read only - when 0xff45 - @lyc = value - when 0xff47 - @bgp = value - when 0xff48 - @obp0 = value - when 0xff49 - @obp1 = value - when 0xff4a - @wy = value - when 0xff4b - @wx = value - end - end - - def step(cycles) - return false if @lcdc[LCDC[:lcd_ppu_enable]] == 0 - - res = false - @cycles += cycles - - case @mode - when MODE[:oam_scan] - if @cycles >= OAM_SCAN_CYCLES - @cycles -= OAM_SCAN_CYCLES - @mode = MODE[:drawing] - end - when MODE[:drawing] - if @cycles >= DRAWING_CYCLES - render_bg - render_window - render_sprites - @cycles -= DRAWING_CYCLES - @mode = MODE[:hblank] - @interrupt.request(:lcd) if @stat[STAT[:hblank]] == 1 - end - when MODE[:hblank] - if @cycles >= HBLANK_CYCLES - @cycles -= HBLANK_CYCLES - @ly += 1 - handle_ly_eq_lyc - - if @ly == LCD_HEIGHT - @mode = MODE[:vblank] - @interrupt.request(:vblank) - @interrupt.request(:lcd) if @stat[STAT[:vblank]] == 1 - else - @mode = MODE[:oam_scan] - @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1 - end - end - when MODE[:vblank] - if @cycles >= ONE_LINE_CYCLES - @cycles -= ONE_LINE_CYCLES - @ly += 1 - handle_ly_eq_lyc - - if @ly == 154 - @ly = 0 - @wly = 0 - handle_ly_eq_lyc - @mode = MODE[:oam_scan] - @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1 - res = true - end - end - end - - res - end - - def render_bg - return if @lcdc[LCDC[:bg_window_enable]] == 0 - - y = (@ly + @scy) % 256 - tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00 - tile_map_addr += (y / 8) * 32 - LCD_WIDTH.times do |i| - x = (i + @scx) % 256 - tile_index = get_tile_index(tile_map_addr + (x / 8)) - pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2) - color = get_color(@bgp, pixel) - base = @ly * LCD_WIDTH * 4 + i * 4 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color - @bg_pixels[i] = pixel - end - end - - def render_window - return if @lcdc[LCDC[:bg_window_enable]] == 0 || @lcdc[LCDC[:window_enable]] == 0 || @ly < @wy - - rendered = false - y = @wly - tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00 - tile_map_addr += (y / 8) * 32 - LCD_WIDTH.times do |i| - next if i < @wx - 7 - - rendered = true - x = i - (@wx - 7) - tile_index = get_tile_index(tile_map_addr + (x / 8)) - pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2) - color = get_color(@bgp, pixel) - base = @ly * LCD_WIDTH * 4 + i * 4 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color - @bg_pixels[i] = pixel - end - @wly += 1 if rendered - end - - def render_sprites - return if @lcdc[LCDC[:sprite_enable]] == 0 - - sprite_height = @lcdc[LCDC[:sprite_size]] == 0 ? 8 : 16 - sprites = [] - cnt = 0 - - @oam.each_slice(4) do |y, x, tile_index, flags| - y = (y - 16) % 256 - x = (x - 8) % 256 - next if y > @ly || y + sprite_height <= @ly - - sprites << { y:, x:, tile_index:, flags: } - cnt += 1 - break if cnt == 10 - end - sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] } - - sprites.each do |sprite| - flags = sprite[:flags] - pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1 - tile_index = sprite[:tile_index] - tile_index &= 0xfe if sprite_height == 16 - y = (@ly - sprite[:y]) % 256 - y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1 - tile_index = (tile_index + 1) % 256 if y >= 8 - y %= 8 - - 8.times do |x| - x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x - - pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2) - i = (sprite[:x] + x) % 256 - - next if pixel == 0 || i >= LCD_WIDTH - next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 - - color = get_color(pallet, pixel) - base = @ly * LCD_WIDTH * 4 + i * 4 - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color - end - end - end - - private - - def get_tile_index(tile_map_addr) - tile_index = @vram[tile_map_addr] - @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index - end - - def get_pixel(tile_index, c, r) - @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1) - end - - def get_color(pallet, pixel) - case (pallet >> (pixel * 2)) & 0b11 - when 0 then 0xff - when 1 then 0xaa - when 2 then 0x55 - when 3 then 0x00 - end - end - - def to_signed_byte(byte) - byte &= 0xff - byte > 127 ? byte - 256 : byte - end - - def handle_ly_eq_lyc - if @ly == @lyc - @stat |= 0x04 - @interrupt.request(:lcd) if @stat[STAT[:lyc]] == 1 - else - @stat &= 0xfb - end - end - end -end diff --git a/lib/rubyboy/sdl.rb b/lib/rubyboy/sdl.rb index 324a938..8da0e03 100644 --- a/lib/rubyboy/sdl.rb +++ b/lib/rubyboy/sdl.rb @@ -13,6 +13,7 @@ module SDL INIT_KEYBOARD = 0x200 WINDOW_RESIZABLE = 0x20 PIXELFORMAT_RGB24 = 386930691 + PIXELFORMAT_ABGR8888 = 376840196 SDL_WINDOW_RESIZABLE = 0x20 QUIT = 0x100 diff --git a/lib/rubyboy/version.rb b/lib/rubyboy/version.rb index 4570a70..59170af 100644 --- a/lib/rubyboy/version.rb +++ b/lib/rubyboy/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Rubyboy - VERSION = '1.4.0' + VERSION = '1.4.1' end