From fc0a05144be88b08656f708440a976848af99be1 Mon Sep 17 00:00:00 2001 From: sacckey Date: Thu, 31 Oct 2024 20:58:00 +0900 Subject: [PATCH 01/17] Add PIXEL_FORMATS --- lib/rubyboy/ppu.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index f960180..e80bdbf 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -4,6 +4,11 @@ module Rubyboy class Ppu attr_reader :buffer + PIXEL_FORMATS = { + rgb: 3, + rgba: 4 + }.freeze + MODE = { hblank: 0, vblank: 1, @@ -46,7 +51,9 @@ class Ppu HBLANK_CYCLES = 204 ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES - def initialize(interrupt) + def initialize(interrupt, pixel_format = :rgb) + raise ArgumentError, 'Invalid pixel format' unless PIXEL_FORMATS.key?(pixel_format) + @mode = MODE[:oam_scan] @lcdc = 0x91 @stat = 0x00 @@ -64,7 +71,8 @@ def initialize(interrupt) @wly = 0x00 @cycles = 0 @interrupt = interrupt - @buffer = Array.new(144 * 160 * 3, 0x00) + @channel_count = PIXEL_FORMATS[pixel_format] + @buffer = Array.new(144 * 160 * @channel_count, 0xff) @bg_pixels = Array.new(LCD_WIDTH, 0x00) end @@ -197,7 +205,7 @@ def render_bg 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 + base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @buffer[base + 2] = color @@ -220,7 +228,7 @@ def render_window 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 + base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @buffer[base + 2] = color @@ -267,7 +275,7 @@ def render_sprites next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 color = get_color(pallet, pixel) - base = @ly * LCD_WIDTH * 3 + i * 3 + base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @buffer[base + 2] = color From 2b5162da73e55ffc765bbde530731084b3b71c8e Mon Sep 17 00:00:00 2001 From: sacckey Date: Thu, 31 Oct 2024 20:58:33 +0900 Subject: [PATCH 02/17] Remove PpuWasm class --- lib/rubyboy/emulator_wasm.rb | 4 +- lib/rubyboy/ppu_wasm.rb | 312 ----------------------------------- 2 files changed, 2 insertions(+), 314 deletions(-) delete mode 100644 lib/rubyboy/ppu_wasm.rb diff --git a/lib/rubyboy/emulator_wasm.rb b/lib/rubyboy/emulator_wasm.rb index ea523ea..a221111 100644 --- a/lib/rubyboy/emulator_wasm.rb +++ b/lib/rubyboy/emulator_wasm.rb @@ -4,7 +4,7 @@ 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,7 +22,7 @@ def initialize(rom_data) ram = Ram.new mbc = Cartridge::Factory.create(rom, ram) interrupt = Interrupt.new - @ppu = PpuWasm.new(interrupt) + @ppu = Ppu.new(interrupt, :rgba) @timer = Timer.new(interrupt) @joypad = Joypad.new(interrupt) @apu = ApuWasm.new 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 From a53b2d066bb6923627fb2c02af6b36fc7cd943a3 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 1 Dec 2024 13:34:48 +0900 Subject: [PATCH 03/17] Move audio handling from Apu to Emulator --- lib/rubyboy.rb | 1 + lib/rubyboy/apu.rb | 8 ++++---- lib/rubyboy/emulator.rb | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) 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/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 From 7249bcd2635a453379938a660ea1f099d3f11e11 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 1 Dec 2024 13:43:50 +0900 Subject: [PATCH 04/17] Remove ApuWasm class --- lib/rubyboy/apu_wasm.rb | 118 ----------------------------------- lib/rubyboy/emulator_wasm.rb | 4 +- 2 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 lib/rubyboy/apu_wasm.rb 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_wasm.rb b/lib/rubyboy/emulator_wasm.rb index a221111..25c18dd 100644 --- a/lib/rubyboy/emulator_wasm.rb +++ b/lib/rubyboy/emulator_wasm.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'apu_wasm' +require_relative 'apu' require_relative 'bus' require_relative 'cpu' require_relative 'emulator' @@ -25,7 +25,7 @@ def initialize(rom_data) @ppu = Ppu.new(interrupt, :rgba) @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 From 6eb4f77fd1cf23ade795da92a2eac0857a0f31fc Mon Sep 17 00:00:00 2001 From: sacckey Date: Sat, 7 Dec 2024 15:55:46 +0900 Subject: [PATCH 05/17] Cache tile data --- lib/rubyboy/ppu.rb | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index e80bdbf..7793198 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -74,6 +74,7 @@ def initialize(interrupt, pixel_format = :rgb) @channel_count = PIXEL_FORMATS[pixel_format] @buffer = Array.new(144 * 160 * @channel_count, 0xff) @bg_pixels = Array.new(LCD_WIDTH, 0x00) + @tile_cache = Array.new(384) { Array.new(64, 0) } end def read_byte(addr) @@ -110,7 +111,10 @@ 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 + update_tile_cache(addr - 0x8000) if addr < 0x9800 + end when 0xfe00..0xfe9f @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing] when 0xff40 @@ -200,10 +204,11 @@ def render_bg y = (@ly + @scy) % 256 tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00 tile_map_addr += (y / 8) * 32 + tile_y = y % 8 * 8 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) + pixel = @tile_cache[tile_index][tile_y + (x % 8)] color = get_color(@bgp, pixel) base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @@ -220,13 +225,14 @@ def render_window y = @wly tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00 tile_map_addr += (y / 8) * 32 + tile_y = y % 8 * 8 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) + pixel = @tile_cache[tile_index][tile_y + (x % 8)] color = get_color(@bgp, pixel) base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @@ -263,12 +269,12 @@ def render_sprites 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 + tile_y = y % 8 * 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) + pixel = @tile_cache[tile_index][tile_y + x_flipped] i = (sprite[:x] + x) % 256 next if pixel == 0 || i >= LCD_WIDTH @@ -285,15 +291,27 @@ def render_sprites private + def update_tile_cache(addr) + tile_index = addr >> 4 + + row = (addr % 16) >> 1 + return if row >= 8 + + 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 * 8 + col] = pixel + end + end + 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 From 99a24a0706e83d785f65b39aebbe85932ebe56a5 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sat, 7 Dec 2024 20:46:39 +0900 Subject: [PATCH 06/17] Cache tile_map data --- lib/rubyboy/ppu.rb | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index 7793198..fccd98e 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -75,6 +75,7 @@ def initialize(interrupt, pixel_format = :rgb) @buffer = Array.new(144 * 160 * @channel_count, 0xff) @bg_pixels = Array.new(LCD_WIDTH, 0x00) @tile_cache = Array.new(384) { Array.new(64, 0) } + @tile_map_cache = Array.new(2048, 0) end def read_byte(addr) @@ -113,12 +114,21 @@ def write_byte(addr, value) when 0x8000..0x9fff if @mode != MODE[:drawing] @vram[addr - 0x8000] = value - update_tile_cache(addr - 0x8000) if addr < 0x9800 + 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] when 0xff40 + old_lcdc = @lcdc @lcdc = value + + if old_lcdc[LCDC[:bg_window_tile_data_area]] != @lcdc[LCDC[:bg_window_tile_data_area]] + refresh_tile_map_cache + end when 0xff41 @stat = value & 0x78 when 0xff42 @@ -202,12 +212,12 @@ 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 + tile_map_addr = (y / 8) * 32 + tile_map_addr += 1024 if @lcdc[LCDC[:bg_tile_map_area]] == 1 tile_y = y % 8 * 8 LCD_WIDTH.times do |i| x = (i + @scx) % 256 - tile_index = get_tile_index(tile_map_addr + (x / 8)) + tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] color = get_color(@bgp, pixel) base = (@ly * LCD_WIDTH + i) * @channel_count @@ -223,15 +233,15 @@ 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 / 8) * 32 + tile_map_addr += 1024 if @lcdc[LCDC[:window_tile_map_area]] == 1 tile_y = y % 8 * 8 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)) + tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] color = get_color(@bgp, pixel) base = (@ly * LCD_WIDTH + i) * @channel_count @@ -307,9 +317,20 @@ def update_tile_cache(addr) end end - 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_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 refresh_tile_map_cache + is_8800_mode = @lcdc[LCDC[:bg_window_tile_data_area]] == 0 + + (0x1800..0x1fff).each do |addr| + @tile_map_cache[addr - 0x1800] = is_8800_mode ? to_signed_byte(@vram[addr]) + 256 : @vram[addr] + end end def get_color(pallet, pixel) From 2eea5261743c8846364ddb1651b45495863c9298 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 8 Dec 2024 12:13:15 +0900 Subject: [PATCH 07/17] Cache pallet data --- lib/rubyboy/ppu.rb | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index fccd98e..a106fe3 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -76,6 +76,9 @@ def initialize(interrupt, pixel_format = :rgb) @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, 0xff) + @obp0_cache = Array.new(4, 0xff) + @obp1_cache = Array.new(4, 0xff) end def read_byte(addr) @@ -141,10 +144,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 @@ -219,7 +225,7 @@ def render_bg x = (i + @scx) % 256 tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] - color = get_color(@bgp, pixel) + color = @bgp_cache[pixel] base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @@ -243,7 +249,7 @@ def render_window x = i - (@wx - 7) tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] - color = get_color(@bgp, pixel) + color = @bgp_cache[pixel] base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @@ -273,7 +279,7 @@ def render_sprites 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 @@ -290,7 +296,7 @@ def render_sprites next if pixel == 0 || i >= LCD_WIDTH next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 - color = get_color(pallet, pixel) + color = pallet[pixel] base = (@ly * LCD_WIDTH + i) * @channel_count @buffer[base] = color @buffer[base + 1] = color @@ -333,6 +339,17 @@ def refresh_tile_map_cache end end + def refresh_palette_cache(cache, palette_value) + 4.times do |i| + case (palette_value >> (i * 2)) & 0b11 + when 0 then cache[i] = 0xff + when 1 then cache[i] = 0xaa + when 2 then cache[i] = 0x55 + when 3 then cache[i] = 0x00 + end + end + end + def get_color(pallet, pixel) case (pallet >> (pixel * 2)) & 0b11 when 0 then 0xff From eb1a23efbafe1e2bc53bd573a3270dff55838dc9 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sat, 14 Dec 2024 16:40:06 +0900 Subject: [PATCH 08/17] Cache sprite data --- lib/rubyboy/ppu.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index a106fe3..97b8e66 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -79,6 +79,7 @@ def initialize(interrupt, pixel_format = :rgb) @bgp_cache = Array.new(4, 0xff) @obp0_cache = Array.new(4, 0xff) @obp1_cache = Array.new(4, 0xff) + @sprite_cache = Array.new(40) { { y: 0xff, x: 0xff, tile_index: 0, flags: 0 } } end def read_byte(addr) @@ -124,7 +125,18 @@ def write_byte(addr, value) 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 @@ -266,12 +278,10 @@ 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 From d898688845c09e3ce7f6ae1ed1f303d8d8e8ef24 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 15 Dec 2024 13:28:36 +0900 Subject: [PATCH 09/17] Change pixel format from RGB24 to ABGR8888 --- lib/executor.rb | 2 +- lib/rubyboy/lcd.rb | 8 ++++---- lib/rubyboy/ppu.rb | 37 ++++++++++++++----------------------- lib/rubyboy/sdl.rb | 1 + 4 files changed, 20 insertions(+), 28 deletions(-) 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/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 97b8e66..289a624 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -72,13 +72,13 @@ def initialize(interrupt, pixel_format = :rgb) @cycles = 0 @interrupt = interrupt @channel_count = PIXEL_FORMATS[pixel_format] - @buffer = Array.new(144 * 160 * @channel_count, 0xff) + @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, 0xff) - @obp0_cache = Array.new(4, 0xff) - @obp1_cache = Array.new(4, 0xff) + @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 @@ -233,15 +233,12 @@ def render_bg tile_map_addr = (y / 8) * 32 tile_map_addr += 1024 if @lcdc[LCDC[:bg_tile_map_area]] == 1 tile_y = y % 8 * 8 + buffer_start_index = @ly * LCD_WIDTH LCD_WIDTH.times do |i| x = (i + @scx) % 256 tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] - color = @bgp_cache[pixel] - base = (@ly * LCD_WIDTH + i) * @channel_count - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color + @buffer[buffer_start_index + i] = @bgp_cache[pixel] @bg_pixels[i] = pixel end end @@ -254,6 +251,7 @@ def render_window tile_map_addr = (y / 8) * 32 tile_map_addr += 1024 if @lcdc[LCDC[:window_tile_map_area]] == 1 tile_y = y % 8 * 8 + buffer_start_index = @ly * LCD_WIDTH LCD_WIDTH.times do |i| next if i < @wx - 7 @@ -261,11 +259,7 @@ def render_window x = i - (@wx - 7) tile_index = @tile_map_cache[tile_map_addr + (x / 8)] pixel = @tile_cache[tile_index][tile_y + (x % 8)] - color = @bgp_cache[pixel] - base = (@ly * LCD_WIDTH + i) * @channel_count - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color + @buffer[buffer_start_index + i] = @bgp_cache[pixel] @bg_pixels[i] = pixel end @wly += 1 if rendered @@ -296,6 +290,7 @@ def render_sprites y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1 tile_index = (tile_index + 1) % 256 if y >= 8 tile_y = y % 8 * 8 + buffer_start_index = @ly * LCD_WIDTH + sprite[:x] 8.times do |x| x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x @@ -306,11 +301,7 @@ def render_sprites next if pixel == 0 || i >= LCD_WIDTH next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 - color = pallet[pixel] - base = (@ly * LCD_WIDTH + i) * @channel_count - @buffer[base] = color - @buffer[base + 1] = color - @buffer[base + 2] = color + @buffer[buffer_start_index + x] = pallet[pixel] end end end @@ -352,10 +343,10 @@ def refresh_tile_map_cache def refresh_palette_cache(cache, palette_value) 4.times do |i| case (palette_value >> (i * 2)) & 0b11 - when 0 then cache[i] = 0xff - when 1 then cache[i] = 0xaa - when 2 then cache[i] = 0x55 - when 3 then cache[i] = 0x00 + 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/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 From e6182916a34a4b69bdf8b7b076b5c4d3f170b6a4 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 15 Dec 2024 16:16:36 +0900 Subject: [PATCH 10/17] Use bit manipulation --- lib/rubyboy/ppu.rb | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index 289a624..297113d 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -229,15 +229,15 @@ def step(cycles) def render_bg return if @lcdc[LCDC[:bg_window_enable]] == 0 - y = (@ly + @scy) % 256 - tile_map_addr = (y / 8) * 32 + 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 % 8 * 8 + tile_y = (y & 7) << 3 buffer_start_index = @ly * LCD_WIDTH LCD_WIDTH.times do |i| - x = (i + @scx) % 256 - tile_index = @tile_map_cache[tile_map_addr + (x / 8)] - pixel = @tile_cache[tile_index][tile_y + (x % 8)] + x = (i + @scx) & 0xff + 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 @@ -248,17 +248,17 @@ def render_window rendered = false y = @wly - 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 % 8 * 8 + 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 = @tile_map_cache[tile_map_addr + (x / 8)] - pixel = @tile_cache[tile_index][tile_y + (x % 8)] + 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 @@ -286,17 +286,17 @@ def render_sprites 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 - tile_y = y % 8 * 8 + tile_index = (tile_index + 1) & 0xff if y >= 8 + tile_y = (y & 7) << 3 buffer_start_index = @ly * LCD_WIDTH + sprite[:x] 8.times do |x| x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x pixel = @tile_cache[tile_index][tile_y + x_flipped] - i = (sprite[:x] + x) % 256 + i = (sprite[:x] + x) & 0xff next if pixel == 0 || i >= LCD_WIDTH next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 @@ -311,7 +311,7 @@ def render_sprites def update_tile_cache(addr) tile_index = addr >> 4 - row = (addr % 16) >> 1 + row = (addr & 0xf) >> 1 return if row >= 8 byte1 = @vram[addr & ~1] @@ -320,7 +320,7 @@ def update_tile_cache(addr) 8.times do |col| bit_index = 7 - col pixel = ((byte1 >> bit_index) & 1) | (((byte2 >> bit_index) & 1) << 1) - @tile_cache[tile_index][row * 8 + col] = pixel + @tile_cache[tile_index][(row << 3) + col] = pixel end end @@ -342,7 +342,7 @@ def refresh_tile_map_cache def refresh_palette_cache(cache, palette_value) 4.times do |i| - case (palette_value >> (i * 2)) & 0b11 + case (palette_value >> (i << 1)) & 0b11 when 0 then cache[i] = 0xffffffff when 1 then cache[i] = 0xffaaaaaa when 2 then cache[i] = 0xff555555 @@ -352,7 +352,7 @@ def refresh_palette_cache(cache, palette_value) end def get_color(pallet, pixel) - case (pallet >> (pixel * 2)) & 0b11 + case (pallet >> (pixel << 1)) & 0b11 when 0 then 0xff when 1 then 0xaa when 2 then 0x55 From 7bcf3d2d64d1ea83a6cc20e4cd392b4db9598b46 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sat, 21 Dec 2024 14:58:04 +0900 Subject: [PATCH 11/17] Fix sprite buffer index at screen edge --- lib/rubyboy/ppu.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index 297113d..e88c5b1 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -290,7 +290,7 @@ def render_sprites y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1 tile_index = (tile_index + 1) & 0xff if y >= 8 tile_y = (y & 7) << 3 - buffer_start_index = @ly * LCD_WIDTH + sprite[:x] + buffer_start_index = @ly * LCD_WIDTH 8.times do |x| x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x @@ -301,7 +301,7 @@ def render_sprites next if pixel == 0 || i >= LCD_WIDTH next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0 - @buffer[buffer_start_index + x] = pallet[pixel] + @buffer[buffer_start_index + i] = pallet[pixel] end end end From 50e7ccdd15d1043ef021cbda597a0edf305e5b92 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sat, 21 Dec 2024 17:29:01 +0900 Subject: [PATCH 12/17] Optimize render_bg with tile-based processing --- lib/rubyboy/ppu.rb | 80 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index e88c5b1..cd176b2 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -234,12 +234,80 @@ def render_bg tile_map_addr += 1024 if @lcdc[LCDC[:bg_tile_map_area]] == 1 tile_y = (y & 7) << 3 buffer_start_index = @ly * LCD_WIDTH - LCD_WIDTH.times do |i| - x = (i + @scx) & 0xff - 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 + + 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 + + if 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 end From 731794d73e81a0312c14865dea4de6f5cf210ac2 Mon Sep 17 00:00:00 2001 From: sacckey Date: Sun, 22 Dec 2024 11:26:15 +0900 Subject: [PATCH 13/17] Use reverse and sort for sprite ordering --- lib/rubyboy/ppu.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index cd176b2..1d1d09c 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -347,7 +347,8 @@ def render_sprites 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] From 313a08c3c953d794476bd6610bbecd8371be5df4 Mon Sep 17 00:00:00 2001 From: sacckey Date: Tue, 24 Dec 2024 21:04:59 +0900 Subject: [PATCH 14/17] Rubocop --- lib/rubyboy/ppu.rb | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index 1d1d09c..c392136 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -141,9 +141,7 @@ def write_byte(addr, value) old_lcdc = @lcdc @lcdc = value - if old_lcdc[LCDC[:bg_window_tile_data_area]] != @lcdc[LCDC[:bg_window_tile_data_area]] - refresh_tile_map_cache - end + refresh_tile_map_cache if old_lcdc[LCDC[:bg_window_tile_data_area]] != @lcdc[LCDC[:bg_window_tile_data_area]] when 0xff41 @stat = value & 0x78 when 0xff42 @@ -298,16 +296,16 @@ def render_bg current_tile += 1 end - if 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 + 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 @@ -396,9 +394,7 @@ def update_tile_cache(addr) 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 + @tile_map_cache[map_index] = @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index end def refresh_tile_map_cache From 1fa13fac05ce9f868bf751c8e2f241e3b830b700 Mon Sep 17 00:00:00 2001 From: sacckey Date: Wed, 25 Dec 2024 21:16:52 +0900 Subject: [PATCH 15/17] Refactoring Ppu --- lib/rubyboy/emulator_wasm.rb | 2 +- lib/rubyboy/ppu.rb | 25 +++---------------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/lib/rubyboy/emulator_wasm.rb b/lib/rubyboy/emulator_wasm.rb index 25c18dd..1e3d28e 100644 --- a/lib/rubyboy/emulator_wasm.rb +++ b/lib/rubyboy/emulator_wasm.rb @@ -22,7 +22,7 @@ def initialize(rom_data) ram = Ram.new mbc = Cartridge::Factory.create(rom, ram) interrupt = Interrupt.new - @ppu = Ppu.new(interrupt, :rgba) + @ppu = Ppu.new(interrupt) @timer = Timer.new(interrupt) @joypad = Joypad.new(interrupt) @apu = Apu.new diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index c392136..373c6c8 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -4,11 +4,6 @@ module Rubyboy class Ppu attr_reader :buffer - PIXEL_FORMATS = { - rgb: 3, - rgba: 4 - }.freeze - MODE = { hblank: 0, vblank: 1, @@ -51,9 +46,7 @@ class Ppu HBLANK_CYCLES = 204 ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES - def initialize(interrupt, pixel_format = :rgb) - raise ArgumentError, 'Invalid pixel format' unless PIXEL_FORMATS.key?(pixel_format) - + def initialize(interrupt) @mode = MODE[:oam_scan] @lcdc = 0x91 @stat = 0x00 @@ -71,7 +64,6 @@ def initialize(interrupt, pixel_format = :rgb) @wly = 0x00 @cycles = 0 @interrupt = interrupt - @channel_count = PIXEL_FORMATS[pixel_format] @buffer = Array.new(144 * 160, 0xffffffff) @bg_pixels = Array.new(LCD_WIDTH, 0x00) @tile_cache = Array.new(384) { Array.new(64, 0) } @@ -377,9 +369,7 @@ def render_sprites def update_tile_cache(addr) tile_index = addr >> 4 - - row = (addr & 0xf) >> 1 - return if row >= 8 + row = ((addr & 0xf) >> 1) << 3 byte1 = @vram[addr & ~1] byte2 = @vram[addr | 1] @@ -387,7 +377,7 @@ def update_tile_cache(addr) 8.times do |col| bit_index = 7 - col pixel = ((byte1 >> bit_index) & 1) | (((byte2 >> bit_index) & 1) << 1) - @tile_cache[tile_index][(row << 3) + col] = pixel + @tile_cache[tile_index][row + col] = pixel end end @@ -416,15 +406,6 @@ def refresh_palette_cache(cache, palette_value) end end - def get_color(pallet, pixel) - case (pallet >> (pixel << 1)) & 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 From 0759a711b2fde8833624ede5da60c7e74846245c Mon Sep 17 00:00:00 2001 From: sacckey Date: Wed, 25 Dec 2024 22:45:13 +0900 Subject: [PATCH 16/17] Refactoring Ppu --- lib/rubyboy/ppu.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/rubyboy/ppu.rb b/lib/rubyboy/ppu.rb index 373c6c8..aed013c 100644 --- a/lib/rubyboy/ppu.rb +++ b/lib/rubyboy/ppu.rb @@ -133,7 +133,7 @@ def write_byte(addr, value) old_lcdc = @lcdc @lcdc = value - refresh_tile_map_cache if old_lcdc[LCDC[:bg_window_tile_data_area]] != @lcdc[LCDC[:bg_window_tile_data_area]] + 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 @@ -388,10 +388,14 @@ def update_tile_map_cache(addr) end def refresh_tile_map_cache - is_8800_mode = @lcdc[LCDC[:bg_window_tile_data_area]] == 0 - - (0x1800..0x1fff).each do |addr| - @tile_map_cache[addr - 0x1800] = is_8800_mode ? to_signed_byte(@vram[addr]) + 256 : @vram[addr] + 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 From 9bf264198590c642d2af4719433a9ce824f4bc47 Mon Sep 17 00:00:00 2001 From: sacckey Date: Wed, 25 Dec 2024 23:01:40 +0900 Subject: [PATCH 17/17] Bump version to 1.4.1 --- CHANGELOG.md | 4 ++++ Gemfile.lock | 2 +- lib/rubyboy/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) 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/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