diff --git a/src/crab/gb/display.cr b/src/crab/gb/display.cr index d0483fb..cc4ac31 100644 --- a/src/crab/gb/display.cr +++ b/src/crab/gb/display.cr @@ -5,8 +5,8 @@ module GB WIDTH = 160 HEIGHT = 144 - PIXELFORMAT_RGB24 = (1 << 28) | (7 << 24) | (1 << 20) | (0 << 16) | (24 << 8) | (3 << 0) - TEXTUREACCESS_STREAMING = 1 + PIXELFORMAT_BGR5 = 357764866 + TEXTUREACCESS_STREAMING = 1 @window : SDL::Window @renderer : SDL::Renderer @@ -23,15 +23,15 @@ module GB @window = SDL::Window.new(window_title, WIDTH * DISPLAY_SCALE, HEIGHT * DISPLAY_SCALE, flags: flags) @renderer = SDL::Renderer.new @window @renderer.logical_size = {WIDTH, HEIGHT} - @texture = LibSDL.create_texture @renderer, PIXELFORMAT_RGB24, TEXTUREACCESS_STREAMING, WIDTH, HEIGHT + @texture = LibSDL.create_texture @renderer, PIXELFORMAT_BGR5, TEXTUREACCESS_STREAMING, WIDTH, HEIGHT end def window_title : String "CryBoy - #{@title} - #{@fps} fps" end - def draw(framebuffer : Array(RGB)) : Nil - LibSDL.update_texture @texture, nil, framebuffer, WIDTH * sizeof(RGB) + def draw(framebuffer : Slice(UInt16)) : Nil + LibSDL.update_texture @texture, nil, framebuffer, WIDTH * sizeof(UInt16) @renderer.clear @renderer.copy @texture @renderer.present diff --git a/src/crab/gb/fifo_ppu.cr b/src/crab/gb/fifo_ppu.cr index 2b9f2be..289fcd5 100644 --- a/src/crab/gb/fifo_ppu.cr +++ b/src/crab/gb/fifo_ppu.cr @@ -196,14 +196,14 @@ module GB if !sprite_pixel.nil? && sprite_wins? bg_pixel, sprite_pixel pixel = sprite_pixel palette = sprite_pixel.palette == 0 ? @obp0 : @obp1 - palettes = @obj_palettes + arr = @obj_pram else pixel = bg_pixel palette = @bgp - palettes = @palettes + arr = @pram end color = @cgb_ptr.value ? pixel.color : palette[pixel.color] - @framebuffer[Display::WIDTH * @ly + @lx] = palettes[pixel.palette][color] + @framebuffer[Display::WIDTH * @ly + @lx] = arr.to_unsafe.as(UInt16*)[4 * pixel.palette + color] end @lx += 1 if @lx == Display::WIDTH diff --git a/src/crab/gb/ppu.cr b/src/crab/gb/ppu.cr index 955d9dd..a1747f2 100644 --- a/src/crab/gb/ppu.cr +++ b/src/crab/gb/ppu.cr @@ -1,469 +1,403 @@ - # This file is simply designed to hold shared features of the scanline and FIFO - # renderers while the FIFO renderer is in active development. The purpose of - # this file is solely to reduce common changes to both renderers. +# This file is simply designed to hold shared features of the scanline and FIFO +# renderers while the FIFO renderer is in active development. The purpose of +# this file is solely to reduce common changes to both renderers. - module GB - struct Sprite - getter oam_idx : UInt8 = 0_u8 +module GB + struct Sprite + getter oam_idx : UInt8 = 0_u8 - def initialize(@y : UInt8, @x : UInt8, @tile_num : UInt8, @attributes : UInt8) - end + def initialize(@y : UInt8, @x : UInt8, @tile_num : UInt8, @attributes : UInt8) + end - def initialize(oam : Bytes, @oam_idx : UInt8) - initialize oam[oam_idx], oam[oam_idx + 1], oam[oam_idx + 2], oam[oam_idx + 3] - end + def initialize(oam : Bytes, @oam_idx : UInt8) + initialize oam[oam_idx], oam[oam_idx + 1], oam[oam_idx + 2], oam[oam_idx + 3] + end - def to_s(io : IO) - io << "Sprite(y:#{@y}, x:#{@x}, tile_num:#{@tile_num}, tile_ptr: #{hex_str tile_ptr}, visible:#{visible?}, priority:#{priority}, y_flip:#{y_flip?}, x_flip:#{x_flip?}, dmg_palette_number:#{dmg_palette_numpalette_number}" - end + def to_s(io : IO) + io << "Sprite(y:#{@y}, x:#{@x}, tile_num:#{@tile_num}, tile_ptr: #{hex_str tile_ptr}, visible:#{visible?}, priority:#{priority}, y_flip:#{y_flip?}, x_flip:#{x_flip?}, dmg_palette_number:#{dmg_palette_numpalette_number}" + end - def on_line(line : Int, sprite_height = 8) : Bool - y <= line + 16 < y + sprite_height - end + def on_line(line : Int, sprite_height = 8) : Bool + y <= line + 16 < y + sprite_height + end - # behavior is undefined if sprite is not on given line - def bytes(line : Int, sprite_height = 8) : Tuple(UInt16, UInt16) - actual_y = -16 + y - if sprite_height == 8 - tile_ptr = 16_u16 * @tile_num - else # 8x16 tile - if (actual_y + 8 <= line) ^ y_flip? - tile_ptr = 16_u16 * (@tile_num | 0x01) - else - tile_ptr = 16_u16 * (@tile_num & 0xFE) - end - end - sprite_row = (line.to_i16 - actual_y) & 7 - if y_flip? - {tile_ptr + (7 - sprite_row) * 2, tile_ptr + (7 - sprite_row) * 2 + 1} + # behavior is undefined if sprite is not on given line + def bytes(line : Int, sprite_height = 8) : Tuple(UInt16, UInt16) + actual_y = -16 + y + if sprite_height == 8 + tile_ptr = 16_u16 * @tile_num + else # 8x16 tile + if (actual_y + 8 <= line) ^ y_flip? + tile_ptr = 16_u16 * (@tile_num | 0x01) else - {tile_ptr + sprite_row * 2, tile_ptr + sprite_row * 2 + 1} + tile_ptr = 16_u16 * (@tile_num & 0xFE) end end - - def visible? : Bool - ((1...160).includes? y) && ((1...168).includes? x) - end - - def y : UInt8 - @y - end - - def x : UInt8 - @x - end - - def priority : UInt8 - (@attributes >> 7) & 0x1 - end - - def y_flip? : Bool - (@attributes >> 6) & 0x1 == 1 - end - - def x_flip? : Bool - (@attributes >> 5) & 0x1 == 1 - end - - def dmg_palette_number : UInt8 - (@attributes >> 4) & 0x1 - end - - def bank_num : UInt8 - (@attributes >> 3) & 0x1 - end - - def cgb_palette_number : UInt8 - @attributes & 0b111 + sprite_row = (line.to_i16 - actual_y) & 7 + if y_flip? + {tile_ptr + (7 - sprite_row) * 2, tile_ptr + (7 - sprite_row) * 2 + 1} + else + {tile_ptr + sprite_row * 2, tile_ptr + sprite_row * 2 + 1} end end - struct RGB - property red, green, blue - - def initialize(@red : UInt8, @green : UInt8, @blue : UInt8) - end - - def initialize(grey : UInt8) - @red = @green = @blue = grey - end - - def self.from_bgr16(bgr : UInt16, should_convert : Bool) : RGB - color = RGB.new( - 0x1F_u8 & bgr, - 0x1F_u8 & (bgr >> 5), - 0x1F_u8 & (bgr >> 10) - ) - should_convert ? color.convert_from_cgb : color - end - - def convert_from_cgb : RGB - {% unless flag? :graphics_test %} - # correction algorithm from: https://byuu.net/video/color-emulation - RGB.new( - Math.min(240, (26_u32 * @red + 4_u32 * @green + 2_u32 * @blue) >> 2).to_u8, - Math.min(240, (24_u32 * @green + 8_u32 * @blue) >> 2).to_u8, - Math.min(240, (6_u32 * @red + 4_u32 * @green + 22_u32 * @blue) >> 2).to_u8 - ) - {% else %} - # documented in https://github.com/mattcurrie/mealybug-tearoom-tests - RGB.new( - @red << 3 | @red >> 2, - @green << 3 | @green >> 2, - @blue << 3 | @blue >> 2 - ) - {% end %} - end + def visible? : Bool + ((1...160).includes? y) && ((1...168).includes? x) end - POST_BOOT_VRAM = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xF0, 0x00, 0xF0, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xF3, 0x00, 0xF3, 0x00, - 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, - 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0x00, 0xCF, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x0F, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x0F, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF0, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, - 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC3, 0x00, 0xC3, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, - 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, - 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x3C, 0x00, - 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, - 0xF3, 0x00, 0xF3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, - 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, - 0x3C, 0x00, 0x3C, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x0F, 0x00, - 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, - 0xFC, 0x00, 0xFC, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, - 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, - 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xC3, 0x00, 0xC3, 0x00, - 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0xFC, 0x00, 0xFC, 0x00, - 0x3C, 0x00, 0x42, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0x42, 0x00, 0x3C, 0x00, - ] + def y : UInt8 + @y + end - abstract class PPU - @ran_bios : Bool # determine if colors should be adjusted for cgb - @cgb_ptr : Pointer(Bool) + def x : UInt8 + @x + end - @framebuffer = Array(RGB).new Display::WIDTH * Display::HEIGHT, RGB.new(0, 0, 0) + def priority : UInt8 + (@attributes >> 7) & 0x1 + end - @pram = Bytes.new 64 - @palettes = Array(Array(RGB)).new 8 { Array(GB::RGB).new 4, GB::RGB.new(0, 0, 0) } - @palette_index : UInt8 = 0 - @auto_increment = false + def y_flip? : Bool + (@attributes >> 6) & 0x1 == 1 + end - @obj_pram = Bytes.new 64 - @obj_palettes = Array(Array(RGB)).new 8 { Array(GB::RGB).new 4, GB::RGB.new(0, 0, 0) } - @obj_palette_index : UInt8 = 0 - @obj_auto_increment = false + def x_flip? : Bool + (@attributes >> 5) & 0x1 == 1 + end - @vram = Array(Bytes).new 2 { Bytes.new GB::Memory::VRAM.size } # 0x8000..0x9FFF - @vram_bank : UInt8 = 0 # track which bank is active - @sprite_table = Bytes.new Memory::OAM.size # 0xFE00..0xFE9F - @lcd_control : UInt8 = 0x00_u8 # 0xFF40 - @lcd_status : UInt8 = 0x80_u8 # 0xFF41 - @scy : UInt8 = 0x00_u8 # 0xFF42 - @scx : UInt8 = 0x00_u8 # 0xFF43 - @ly : UInt8 = 0x00_u8 # 0xFF44 - @lyc : UInt8 = 0x00_u8 # 0xFF45 - @dma : UInt8 = 0x00_u8 # 0xFF46 - @bgp : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF47 - @obp0 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF48 - @obp1 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF49 - @wy : UInt8 = 0x00_u8 # 0xFF4A - @wx : UInt8 = 0x00_u8 # 0xFF4B + def dmg_palette_number : UInt8 + (@attributes >> 4) & 0x1 + end - # At some point, wy _must_ equal ly to enable the window - @window_trigger = false - @current_window_line = 0 + def bank_num : UInt8 + (@attributes >> 3) & 0x1 + end - @old_stat_flag = false - - @hdma1 : UInt8 = 0xFF - @hdma2 : UInt8 = 0xFF - @hdma3 : UInt8 = 0xFF - @hdma4 : UInt8 = 0xFF - @hdma5 : UInt8 = 0xFF - @hdma_src : UInt16 = 0x0000 - @hdma_dst : UInt16 = 0x8000 - @hdma_pos : UInt16 = 0x0000 - @hdma_active : Bool = false - - @first_line = true - - # count number of cycles into current line on fifo, or the number of cycles into the current mode on scanline - @cycle_counter : Int32 = 0 - - def initialize(@gb : GB) - @cgb_ptr = gb.cgb_ptr - unless @cgb_ptr.value # fill default color palettes - {% if flag? :pink %} - @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ - RGB.new(0xFF, 0xF6, 0xD3), RGB.new(0xF9, 0xA8, 0x75), - RGB.new(0xEB, 0x6B, 0x6F), RGB.new(0x7C, 0x3F, 0x58), - ] - {% elsif flag? :graphics_test %} - @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ - RGB.new(0xFF, 0xFF, 0xFF), RGB.new(0xAA, 0xAA, 0xAA), - RGB.new(0x55, 0x55, 0x55), RGB.new(0x00, 0x00, 0x00), - ] - {% else %} - @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ - RGB.new(0xE0, 0xF8, 0xCF), RGB.new(0x86, 0xC0, 0x6C), - RGB.new(0x30, 0x68, 0x50), RGB.new(0x07, 0x17, 0x20), - ] - {% end %} - end - @ran_bios = @cgb_ptr.value - end - - def skip_boot : Nil - POST_BOOT_VRAM.each_with_index do |byte, idx| - @vram[0][idx] = byte.to_u8 - end - end - - # handle stat interrupts - # stat interrupts are only requested on the rising edge - def handle_stat_interrupt : Nil - self.coincidence_flag = @ly == @lyc - stat_flag = (coincidence_flag && coincidence_interrupt_enabled) || - (mode_flag == 2 && oam_interrupt_enabled) || - (mode_flag == 0 && hblank_interrupt_enabled) || - (mode_flag == 1 && vblank_interrupt_enabled) - if !@old_stat_flag && stat_flag - @gb.interrupts.lcd_stat_interrupt = true - end - @old_stat_flag = stat_flag - end - - # Copy 16-byte block from hdma_src to hdma_dst, then decrement value in hdma5 - def copy_hdma_block(block_number : Int) : Nil - 0x10.times do |byte| - offset = 0x10 * block_number + byte - @gb.memory.write_byte @hdma_dst &+ offset, @gb.memory.read_byte @hdma_src &+ offset - @gb.memory.tick_components 2, from_cpu: false, ignore_speed: true - end - @hdma5 &-= 1 - end - - def start_hdma(value : UInt8) : Nil - @hdma_src = ((@hdma1.to_u16 << 8) | @hdma2) & 0xFFF0 - @hdma_dst = 0x8000_u16 + (((@hdma3.to_u16 << 8) | @hdma4) & 0x1FF0) - @hdma5 = value & 0x7F - if value & 0x80 > 0 # hdma - @hdma_active = true - @hdma_pos = 0 - else # gdma - unless @hdma_active - (@hdma5 + 1).times do |block_num| - copy_hdma_block block_num - end - end - @hdma_active = false - end - end - - def step_hdma : Nil - copy_hdma_block @hdma_pos - @hdma_pos += 1 - @hdma_active = false if @hdma5 == 0xFF - end - - # read from ppu memory - def [](index : Int) : UInt8 - case index - when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] - when Memory::OAM then @sprite_table[index - Memory::OAM.begin] - when 0xFF40 then @lcd_control - when 0xFF41 - if @first_line && mode_flag == 2 - @lcd_status & 0b11111100 - else - @lcd_status - end - when 0xFF42 then @scy - when 0xFF43 then @scx - when 0xFF44 then @ly - when 0xFF45 then @lyc - when 0xFF46 then @dma - when 0xFF47 then palette_from_array @bgp - when 0xFF48 then palette_from_array @obp0 - when 0xFF49 then palette_from_array @obp1 - when 0xFF4A then @wy - when 0xFF4B then @wx - when 0xFF4F then @cgb_ptr.value ? 0xFE_u8 | @vram_bank : 0xFF_u8 - when 0xFF51 then @cgb_ptr.value ? @hdma1 : 0xFF_u8 - when 0xFF52 then @cgb_ptr.value ? @hdma2 : 0xFF_u8 - when 0xFF53 then @cgb_ptr.value ? @hdma3 : 0xFF_u8 - when 0xFF54 then @cgb_ptr.value ? @hdma4 : 0xFF_u8 - when 0xFF55 then @cgb_ptr.value ? @hdma5 : 0xFF_u8 - when 0xFF68 then @cgb_ptr.value ? 0x40_u8 | (@auto_increment ? 0x80 : 0) | @palette_index : 0xFF_u8 - when 0xFF69 then @cgb_ptr.value ? @pram[@palette_index] : 0xFF_u8 - when 0xFF6A then @cgb_ptr.value ? 0x40_u8 | (@obj_auto_increment ? 0x80 : 0) | @obj_palette_index : 0xFF_u8 - when 0xFF6B then @cgb_ptr.value ? @obj_pram[@obj_palette_index] : 0xFF_u8 - else raise "Reading from invalid ppu register: #{hex_str index.to_u16!}" - end - end - - # write to ppu memory - def []=(index : Int, value : UInt8) : Nil - case index - when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] = value - when Memory::OAM then @sprite_table[index - Memory::OAM.begin] = value - when 0xFF40 - if value & 0x80 > 0 && !lcd_enabled? - @ly = 0 - self.mode_flag = 2 - @first_line = true - end - @lcd_control = value - handle_stat_interrupt - when 0xFF41 - @lcd_status = (@lcd_status & 0b10000111) | (value & 0b01111000) - handle_stat_interrupt - when 0xFF42 then @scy = value - when 0xFF43 then @scx = value - when 0xFF44 then nil # read only - when 0xFF45 - @lyc = value - handle_stat_interrupt - when 0xFF46 then @dma = value - when 0xFF47 then @bgp = palette_to_array value - when 0xFF48 then @obp0 = palette_to_array value - when 0xFF49 then @obp1 = palette_to_array value - when 0xFF4A then @wy = value - when 0xFF4B then @wx = value - when 0xFF4F then @vram_bank = value & 1 if @cgb_ptr.value - when 0xFF51 then @hdma1 = value if @cgb_ptr.value - when 0xFF52 then @hdma2 = value if @cgb_ptr.value - when 0xFF53 then @hdma3 = value if @cgb_ptr.value - when 0xFF54 then @hdma4 = value if @cgb_ptr.value - when 0xFF55 then start_hdma value if @cgb_ptr.value - when 0xFF68 - if @cgb_ptr.value - @palette_index = value & 0x3F - @auto_increment = value & 0x80 > 0 - end - when 0xFF69 - if @cgb_ptr.value - @pram[@palette_index] = value - bgr16 = @pram[@palette_index | 1].to_u16 << 8 | @pram[@palette_index & ~1] - palette_number = @palette_index >> 3 - color_number = (@palette_index & 7) >> 1 - @palettes[palette_number][color_number] = RGB.from_bgr16 bgr16, @ran_bios - @palette_index += 1 if @auto_increment - @palette_index &= 0x3F - end - when 0xFF6A - if @cgb_ptr.value - @obj_palette_index = value & 0x3F - @obj_auto_increment = value & 0x80 > 0 - end - when 0xFF6B - if @cgb_ptr.value - @obj_pram[@obj_palette_index] = value - bgr16 = @obj_pram[@obj_palette_index | 1].to_u16 << 8 | @obj_pram[@obj_palette_index & ~1] - palette_number = @obj_palette_index >> 3 - color_number = (@obj_palette_index & 7) >> 1 - @obj_palettes[palette_number][color_number] = RGB.from_bgr16 bgr16, @ran_bios - @obj_palette_index += 1 if @obj_auto_increment - @obj_palette_index &= 0x3F - end - else raise "Writing to invalid ppu register: #{hex_str index.to_u16!}" - end - end - - # LCD Control Register - - def lcd_enabled? : Bool - @lcd_control & (0x1 << 7) != 0 - end - - def window_tile_map : UInt8 - @lcd_control & (0x1 << 6) - end - - def window_enabled? : Bool - @lcd_control & (0x1 << 5) != 0 - end - - def bg_window_tile_data : UInt8 - @lcd_control & (0x1 << 4) - end - - def bg_tile_map : UInt8 - @lcd_control & (0x1 << 3) - end - - def sprite_height - @lcd_control & (0x1 << 2) != 0 ? 16 : 8 - end - - def sprite_enabled? : Bool - @lcd_control & (0x1 << 1) != 0 - end - - def bg_display? : Bool - @lcd_control & 0x1 != 0 - end - - # LCD Status Register - - def coincidence_interrupt_enabled : Bool - @lcd_status & (0x1 << 6) != 0 - end - - def oam_interrupt_enabled : Bool - @lcd_status & (0x1 << 5) != 0 - end - - def vblank_interrupt_enabled : Bool - @lcd_status & (0x1 << 4) != 0 - end - - def hblank_interrupt_enabled : Bool - @lcd_status & (0x1 << 3) != 0 - end - - def coincidence_flag : Bool - @lcd_status & (0x1 << 2) != 0 - end - - def coincidence_flag=(on : Bool) : Nil - @lcd_status = (@lcd_status & ~(0x1 << 2)) | (on ? (0x1 << 2) : 0) - end - - def mode_flag : UInt8 - @lcd_status & 0x3 - end - - def mode_flag=(mode : UInt8) - step_hdma if mode == 0 && @hdma_active - @first_line = false if @first_line && mode_flag == 0 && mode == 2 - @window_trigger = false if mode == 1 - @lcd_status = (@lcd_status & 0b11111100) | mode - handle_stat_interrupt - end - - # palettes - - def palette_to_array(palette : UInt8) : Array(UInt8) - [palette & 0x3, (palette >> 2) & 0x3, (palette >> 4) & 0x3, (palette >> 6) & 0x3] - end - - def palette_from_array(palette_array : Array(UInt8)) : UInt8 - palette_array.each_with_index.reduce(0x00_u8) do |palette, (color, idx)| - palette | color << (idx * 2) - end - end - - def write_png : Nil - @gb.display.write_png @framebuffer - end + def cgb_palette_number : UInt8 + @attributes & 0b111 end end + + POST_BOOT_VRAM = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0xF0, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, + 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0x00, 0xCF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x3C, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0xFC, 0x00, 0xFC, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0x3C, 0x00, 0x42, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0x42, 0x00, 0x3C, 0x00, + ] + + abstract class PPU + @ran_bios : Bool # determine if colors should be adjusted for cgb + @cgb_ptr : Pointer(Bool) + + @framebuffer = Slice(UInt16).new Display::WIDTH * Display::HEIGHT + + @pram = Bytes.new 64 + @palette_index : UInt8 = 0 + @auto_increment = false + + @obj_pram = Bytes.new 64 + @obj_palette_index : UInt8 = 0 + @obj_auto_increment = false + + @vram = Array(Bytes).new 2 { Bytes.new GB::Memory::VRAM.size } # 0x8000..0x9FFF + @vram_bank : UInt8 = 0 # track which bank is active + @sprite_table = Bytes.new Memory::OAM.size # 0xFE00..0xFE9F + @lcd_control : UInt8 = 0x00_u8 # 0xFF40 + @lcd_status : UInt8 = 0x80_u8 # 0xFF41 + @scy : UInt8 = 0x00_u8 # 0xFF42 + @scx : UInt8 = 0x00_u8 # 0xFF43 + @ly : UInt8 = 0x00_u8 # 0xFF44 + @lyc : UInt8 = 0x00_u8 # 0xFF45 + @dma : UInt8 = 0x00_u8 # 0xFF46 + @bgp : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF47 + @obp0 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF48 + @obp1 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF49 + @wy : UInt8 = 0x00_u8 # 0xFF4A + @wx : UInt8 = 0x00_u8 # 0xFF4B + + # At some point, wy _must_ equal ly to enable the window + @window_trigger = false + @current_window_line = 0 + + @old_stat_flag = false + + @hdma1 : UInt8 = 0xFF + @hdma2 : UInt8 = 0xFF + @hdma3 : UInt8 = 0xFF + @hdma4 : UInt8 = 0xFF + @hdma5 : UInt8 = 0xFF + @hdma_src : UInt16 = 0x0000 + @hdma_dst : UInt16 = 0x8000 + @hdma_pos : UInt16 = 0x0000 + @hdma_active : Bool = false + + @first_line = true + + # count number of cycles into current line on fifo, or the number of cycles into the current mode on scanline + @cycle_counter : Int32 = 0 + + def initialize(@gb : GB) + @cgb_ptr = gb.cgb_ptr + @ran_bios = @cgb_ptr.value + end + + def skip_boot : Nil + POST_BOOT_VRAM.each_with_index do |byte, idx| + @vram[0][idx] = byte.to_u8 + end + end + + # handle stat interrupts + # stat interrupts are only requested on the rising edge + def handle_stat_interrupt : Nil + self.coincidence_flag = @ly == @lyc + stat_flag = (coincidence_flag && coincidence_interrupt_enabled) || + (mode_flag == 2 && oam_interrupt_enabled) || + (mode_flag == 0 && hblank_interrupt_enabled) || + (mode_flag == 1 && vblank_interrupt_enabled) + if !@old_stat_flag && stat_flag + @gb.interrupts.lcd_stat_interrupt = true + end + @old_stat_flag = stat_flag + end + + # Copy 16-byte block from hdma_src to hdma_dst, then decrement value in hdma5 + def copy_hdma_block(block_number : Int) : Nil + 0x10.times do |byte| + offset = 0x10 * block_number + byte + @gb.memory.write_byte @hdma_dst &+ offset, @gb.memory.read_byte @hdma_src &+ offset + @gb.memory.tick_components 2, from_cpu: false, ignore_speed: true + end + @hdma5 &-= 1 + end + + def start_hdma(value : UInt8) : Nil + @hdma_src = ((@hdma1.to_u16 << 8) | @hdma2) & 0xFFF0 + @hdma_dst = 0x8000_u16 + (((@hdma3.to_u16 << 8) | @hdma4) & 0x1FF0) + @hdma5 = value & 0x7F + if value & 0x80 > 0 # hdma + @hdma_active = true + @hdma_pos = 0 + else # gdma + unless @hdma_active + (@hdma5 + 1).times do |block_num| + copy_hdma_block block_num + end + end + @hdma_active = false + end + end + + def step_hdma : Nil + copy_hdma_block @hdma_pos + @hdma_pos += 1 + @hdma_active = false if @hdma5 == 0xFF + end + + # read from ppu memory + def [](index : Int) : UInt8 + case index + when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] + when Memory::OAM then @sprite_table[index - Memory::OAM.begin] + when 0xFF40 then @lcd_control + when 0xFF41 + if @first_line && mode_flag == 2 + @lcd_status & 0b11111100 + else + @lcd_status + end + when 0xFF42 then @scy + when 0xFF43 then @scx + when 0xFF44 then @ly + when 0xFF45 then @lyc + when 0xFF46 then @dma + when 0xFF47 then palette_from_array @bgp + when 0xFF48 then palette_from_array @obp0 + when 0xFF49 then palette_from_array @obp1 + when 0xFF4A then @wy + when 0xFF4B then @wx + when 0xFF4F then @cgb_ptr.value ? 0xFE_u8 | @vram_bank : 0xFF_u8 + when 0xFF51 then @cgb_ptr.value ? @hdma1 : 0xFF_u8 + when 0xFF52 then @cgb_ptr.value ? @hdma2 : 0xFF_u8 + when 0xFF53 then @cgb_ptr.value ? @hdma3 : 0xFF_u8 + when 0xFF54 then @cgb_ptr.value ? @hdma4 : 0xFF_u8 + when 0xFF55 then @cgb_ptr.value ? @hdma5 : 0xFF_u8 + when 0xFF68 then @cgb_ptr.value ? 0x40_u8 | (@auto_increment ? 0x80 : 0) | @palette_index : 0xFF_u8 + when 0xFF69 then @cgb_ptr.value ? @pram[@palette_index] : 0xFF_u8 + when 0xFF6A then @cgb_ptr.value ? 0x40_u8 | (@obj_auto_increment ? 0x80 : 0) | @obj_palette_index : 0xFF_u8 + when 0xFF6B then @cgb_ptr.value ? @obj_pram[@obj_palette_index] : 0xFF_u8 + else raise "Reading from invalid ppu register: #{hex_str index.to_u16!}" + end + end + + # write to ppu memory + def []=(index : Int, value : UInt8) : Nil + case index + when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] = value + when Memory::OAM then @sprite_table[index - Memory::OAM.begin] = value + when 0xFF40 + if value & 0x80 > 0 && !lcd_enabled? + @ly = 0 + self.mode_flag = 2 + @first_line = true + end + @lcd_control = value + handle_stat_interrupt + when 0xFF41 + @lcd_status = (@lcd_status & 0b10000111) | (value & 0b01111000) + handle_stat_interrupt + when 0xFF42 then @scy = value + when 0xFF43 then @scx = value + when 0xFF44 then nil # read only + when 0xFF45 + @lyc = value + handle_stat_interrupt + when 0xFF46 then @dma = value + when 0xFF47 then @bgp = palette_to_array value + when 0xFF48 then @obp0 = palette_to_array value + when 0xFF49 then @obp1 = palette_to_array value + when 0xFF4A then @wy = value + when 0xFF4B then @wx = value + when 0xFF4F then @vram_bank = value & 1 if @cgb_ptr.value + when 0xFF51 then @hdma1 = value if @cgb_ptr.value + when 0xFF52 then @hdma2 = value if @cgb_ptr.value + when 0xFF53 then @hdma3 = value if @cgb_ptr.value + when 0xFF54 then @hdma4 = value if @cgb_ptr.value + when 0xFF55 then start_hdma value if @cgb_ptr.value + when 0xFF68 + if @cgb_ptr.value + @palette_index = value & 0x3F + @auto_increment = value & 0x80 > 0 + end + when 0xFF69 + if @cgb_ptr.value + @pram[@palette_index] = value + @palette_index += 1 if @auto_increment + @palette_index &= 0x3F + end + when 0xFF6A + if @cgb_ptr.value + @obj_palette_index = value & 0x3F + @obj_auto_increment = value & 0x80 > 0 + end + when 0xFF6B + if @cgb_ptr.value + @obj_pram[@obj_palette_index] = value + @obj_palette_index += 1 if @obj_auto_increment + @obj_palette_index &= 0x3F + end + else raise "Writing to invalid ppu register: #{hex_str index.to_u16!}" + end + end + + # LCD Control Register + + def lcd_enabled? : Bool + @lcd_control & (0x1 << 7) != 0 + end + + def window_tile_map : UInt8 + @lcd_control & (0x1 << 6) + end + + def window_enabled? : Bool + @lcd_control & (0x1 << 5) != 0 + end + + def bg_window_tile_data : UInt8 + @lcd_control & (0x1 << 4) + end + + def bg_tile_map : UInt8 + @lcd_control & (0x1 << 3) + end + + def sprite_height + @lcd_control & (0x1 << 2) != 0 ? 16 : 8 + end + + def sprite_enabled? : Bool + @lcd_control & (0x1 << 1) != 0 + end + + def bg_display? : Bool + @lcd_control & 0x1 != 0 + end + + # LCD Status Register + + def coincidence_interrupt_enabled : Bool + @lcd_status & (0x1 << 6) != 0 + end + + def oam_interrupt_enabled : Bool + @lcd_status & (0x1 << 5) != 0 + end + + def vblank_interrupt_enabled : Bool + @lcd_status & (0x1 << 4) != 0 + end + + def hblank_interrupt_enabled : Bool + @lcd_status & (0x1 << 3) != 0 + end + + def coincidence_flag : Bool + @lcd_status & (0x1 << 2) != 0 + end + + def coincidence_flag=(on : Bool) : Nil + @lcd_status = (@lcd_status & ~(0x1 << 2)) | (on ? (0x1 << 2) : 0) + end + + def mode_flag : UInt8 + @lcd_status & 0x3 + end + + def mode_flag=(mode : UInt8) + step_hdma if mode == 0 && @hdma_active + @first_line = false if @first_line && mode_flag == 0 && mode == 2 + @window_trigger = false if mode == 1 + @lcd_status = (@lcd_status & 0b11111100) | mode + handle_stat_interrupt + end + + # palettes + + def palette_to_array(palette : UInt8) : Array(UInt8) + [palette & 0x3, (palette >> 2) & 0x3, (palette >> 4) & 0x3, (palette >> 6) & 0x3] + end + + def palette_from_array(palette_array : Array(UInt8)) : UInt8 + palette_array.each_with_index.reduce(0x00_u8) do |palette, (color, idx)| + palette | color << (idx * 2) + end + end + + def write_png : Nil + @gb.display.write_png @framebuffer + end + end +end diff --git a/src/crab/gb/scanline_ppu.cr b/src/crab/gb/scanline_ppu.cr index fb67403..5aa3f9d 100644 --- a/src/crab/gb/scanline_ppu.cr +++ b/src/crab/gb/scanline_ppu.cr @@ -57,9 +57,9 @@ module GB color = (msb << 1) | lsb @scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0} if @cgb_ptr.value - @framebuffer[Display::WIDTH * @ly + x] = @palettes[@vram[1][tile_num_addr] & 0b111][color] + @framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color] else - @framebuffer[Display::WIDTH * @ly + x] = @palettes[0][@bgp[color]] + @framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]] end elsif bg_display? || @cgb_ptr.value tile_num_addr = background_map + (((x + @scx) >> 3) & 0x1F) + ((((@ly.to_u16 + @scy) >> 3) * 32) & 0x3FF) @@ -84,9 +84,9 @@ module GB color = (msb << 1) | lsb @scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0} if @cgb_ptr.value - @framebuffer[Display::WIDTH * @ly + x] = @palettes[@vram[1][tile_num_addr] & 0b111][color] + @framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color] else - @framebuffer[Display::WIDTH * @ly + x] = @palettes[0][@bgp[color]] + @framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]] end end end @@ -112,12 +112,12 @@ module GB # objects are always on top of bg/window color 0 # objects are on top of bg/window colors 1-3 if bg_priority and object priority are both unset if !bg_display? || @scanline_color_vals[x][0] == 0 || (!@scanline_color_vals[x][1] && sprite.priority == 0) - @framebuffer[Display::WIDTH * @ly + x] = @obj_palettes[sprite.cgb_palette_number][color] + @framebuffer[Display::WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[4 * sprite.cgb_palette_number + color] end else if sprite.priority == 0 || @scanline_color_vals[x][0] == 0 palette = sprite.dmg_palette_number == 0 ? @obp0 : @obp1 - @framebuffer[Display::WIDTH * @ly + x] = @obj_palettes[0][palette[color]] + @framebuffer[Display::WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[palette[color]] end end end