using System; using BizHawk.Emulation.Common; using BizHawk.Common.NumberExtensions; using BizHawk.Common; namespace BizHawk.Emulation.Cores.Nintendo.GBHawk { public class GBC_PPU : PPU { // individual byte used in palette colors public byte[] BG_bytes = new byte[64]; public byte[] OBJ_bytes = new byte[64]; public bool BG_bytes_inc; public bool OBJ_bytes_inc; public byte BG_bytes_index; public byte OBJ_bytes_index; public byte BG_transfer_byte; public byte OBJ_transfer_byte; // HDMA is unique to GBC, do it as part of the PPU tick public byte HDMA_src_hi; public byte HDMA_src_lo; public byte HDMA_dest_hi; public byte HDMA_dest_lo; public int HDMA_tick; public byte HDMA_byte; // accessors for derived values public byte BG_pal_ret { get { return (byte)(((BG_bytes_inc ? 1 : 0) << 7) | (BG_bytes_index & 0x3F)); } } public byte OBJ_pal_ret { get { return (byte)(((OBJ_bytes_inc ? 1 : 0) << 7) | (OBJ_bytes_index & 0x3F)); } } public byte HDMA_ctrl { get { return (byte)(((HDMA_active ? 0 : 1) << 7) | ((HDMA_length >> 16) - 1)); } } // controls for tile attributes public int VRAM_sel; public bool BG_V_flip; public bool HDMA_mode; public ushort cur_DMA_src; public ushort cur_DMA_dest; public int HDMA_length; public int HDMA_countdown; public int HBL_HDMA_count; public int last_HBL; public bool HBL_HDMA_go; public bool HBL_test; public override byte ReadReg(int addr) { byte ret = 0; switch (addr) { case 0xFF40: ret = LCDC; break; // LCDC case 0xFF41: ret = STAT; break; // STAT case 0xFF42: ret = scroll_y; break; // SCY case 0xFF43: ret = scroll_x; break; // SCX case 0xFF44: ret = LY; break; // LY case 0xFF45: ret = LYC; break; // LYC case 0xFF46: ret = DMA_addr; break; // DMA case 0xFF47: ret = BGP; break; // BGP case 0xFF48: ret = obj_pal_0; break; // OBP0 case 0xFF49: ret = obj_pal_1; break; // OBP1 case 0xFF4A: ret = window_y; break; // WY case 0xFF4B: ret = window_x; break; // WX // These are GBC specific Regs case 0xFF51: ret = HDMA_src_hi; break; // HDMA1 case 0xFF52: ret = HDMA_src_lo; break; // HDMA2 case 0xFF53: ret = HDMA_dest_hi; break; // HDMA3 case 0xFF54: ret = HDMA_dest_lo; break; // HDMA4 case 0xFF55: ret = HDMA_ctrl; break; // HDMA5 case 0xFF68: ret = BG_pal_ret; break; // BGPI case 0xFF69: ret = BG_bytes[BG_bytes_index]; break; // BGPD case 0xFF6A: ret = OBJ_pal_ret; break; // OBPI case 0xFF6B: ret = OBJ_bytes[OBJ_bytes_index]; break; // OBPD } return ret; } public override void WriteReg(int addr, byte value) { switch (addr) { case 0xFF40: // LCDC if (LCDC.Bit(7) && !value.Bit(7)) { VRAM_access_read = true; VRAM_access_write = true; OAM_access_read = true; OAM_access_write = true; } if (!LCDC.Bit(7) && value.Bit(7)) { // don't draw for one frame after turning on blank_frame = true; } LCDC = value; break; case 0xFF41: // STAT // writing to STAT during mode 0 or 2 causes a STAT IRQ if (LCDC.Bit(7)) { if (((STAT & 3) == 0) || ((STAT & 3) == 1)) { LYC_INT = true; } } STAT = (byte)((value & 0xF8) | (STAT & 7) | 0x80); break; case 0xFF42: // SCY scroll_y = value; break; case 0xFF43: // SCX scroll_x = value; // calculate the column number of the tile to start with x_tile = (int)Math.Floor((float)(scroll_x) / 8); break; case 0xFF44: // LY LY = 0; /*reset*/ break; case 0xFF45: // LYC LYC = value; if (LCDC.Bit(7)) { if (LY != LYC) { STAT &= 0xFB; } else { STAT |= 0x4; } } break; case 0xFF46: // DMA DMA_addr = value; DMA_start = true; DMA_OAM_access = true; DMA_clock = 0; DMA_inc = 0; break; case 0xFF47: // BGP BGP = value; break; case 0xFF48: // OBP0 obj_pal_0 = value; break; case 0xFF49: // OBP1 obj_pal_1 = value; break; case 0xFF4A: // WY window_y = value; break; case 0xFF4B: // WX window_x = value; break; // These are GBC specific Regs case 0xFF51: // HDMA1 HDMA_src_hi = value; cur_DMA_src = (ushort)(((HDMA_src_hi & 0xFF) << 8) | (cur_DMA_src & 0xF0)); break; case 0xFF52: // HDMA2 HDMA_src_lo = value; cur_DMA_src = (ushort)((cur_DMA_src & 0xFF00) | (HDMA_src_lo & 0xF0)); break; case 0xFF53: // HDMA3 HDMA_dest_hi = value; cur_DMA_dest = (ushort)(((HDMA_dest_hi & 0x1F) << 8) | (cur_DMA_dest & 0xF0)); break; case 0xFF54: // HDMA4 HDMA_dest_lo = value; cur_DMA_dest = (ushort)((cur_DMA_dest & 0xFF00) | (HDMA_dest_lo & 0xF0)); break; case 0xFF55: // HDMA5 if (!HDMA_active) { HDMA_mode = value.Bit(7); HDMA_countdown = 4; HDMA_tick = 0; if (value.Bit(7)) { // HDMA during HBlank only HDMA_active = true; HBL_HDMA_count = 0x10; // TODO: DOES HDMA start if triggered in mode 0 immediately? (for now assume no) if ((STAT & 3) == 0) { last_HBL = LY; } else { last_HBL = LY - 1; } HBL_test = true; HBL_HDMA_go = false; } else { // HDMA immediately HDMA_active = true; Core.HDMA_transfer = true; } HDMA_length = ((value & 0x7F) + 1) * 16; } else { //terminate the transfer if (!value.Bit(7)) { HDMA_active = false; } } break; case 0xFF68: // BGPI BG_bytes_index = (byte)(value & 0x3F); BG_bytes_inc = ((value & 0x80) == 0x80); break; case 0xFF69: // BGPD BG_transfer_byte = value; BG_bytes[BG_bytes_index] = value; // change the appropriate palette color color_compute_BG(); if (BG_bytes_inc) { BG_bytes_index++; BG_bytes_index &= 0x3F; } break; case 0xFF6A: // OBPI OBJ_bytes_index = (byte)(value & 0x3F); OBJ_bytes_inc = ((value & 0x80) == 0x80); break; case 0xFF6B: // OBPD OBJ_transfer_byte = value; OBJ_bytes[OBJ_bytes_index] = value; // change the appropriate palette color color_compute_OBJ(); if (OBJ_bytes_inc) { OBJ_bytes_index++; OBJ_bytes_index &= 0x3F; } break; } } public override void tick() { // Do HDMA ticks if (HDMA_active) { if (HDMA_countdown == 0) { if (HDMA_length > 0) { if (!HDMA_mode) { // immediately transfer bytes, 2 bytes per cycles if ((HDMA_tick % 2) == 0) { HDMA_byte = Core.ReadMemory(cur_DMA_src); } else { Core.VRAM[(Core.VRAM_Bank * 0x2000) + cur_DMA_dest] = HDMA_byte; cur_DMA_dest = (ushort)((cur_DMA_dest + 1) & 0x1FFF); cur_DMA_src = (ushort)((cur_DMA_src + 1) & 0xFFFF); HDMA_length--; } HDMA_tick++; } else { // only transfer during mode 0, and only 16 bytes at a time if (((STAT & 3) == 0) && (LY != last_HBL) && HBL_test && (LY_inc == 1)) { HBL_HDMA_go = true; HBL_test = false; } if (HBL_HDMA_go && (HBL_HDMA_count > 0)) { Core.HDMA_transfer = true; if ((HDMA_tick % 2) == 0) { HDMA_byte = Core.ReadMemory(cur_DMA_src); } else { Core.VRAM[(Core.VRAM_Bank * 0x2000) + cur_DMA_dest] = HDMA_byte; cur_DMA_dest = (ushort)((cur_DMA_dest + 1) & 0x1FFF); cur_DMA_src = (ushort)((cur_DMA_src + 1) & 0xFFFF); HDMA_length--; HBL_HDMA_count--; } if ((HBL_HDMA_count == 0) && (HDMA_length != 0)) { HBL_test = true; last_HBL = LY; HBL_HDMA_count = 0x10; HBL_HDMA_go = false; } HDMA_tick++; } else { Core.HDMA_transfer = false; } } } else { HDMA_active = false; Core.HDMA_transfer = false; } } else { HDMA_countdown--; } } // the ppu only does anything if it is turned on via bit 7 of LCDC if (LCDC.Bit(7)) { // start the next scanline if (cycle == 456) { // scanline callback if ((LY + LY_inc) == Core._scanlineCallbackLine) { if (Core._scanlineCallback != null) { Core.GetGPU(); Core._scanlineCallback(LCDC); } } cycle = 0; LY_actual = LY; no_scan = false; if (LY == 0 && LY_inc == 0) { LY_inc = 1; Core.in_vblank = false; // special note here, the y coordiate of the window is kept if the window is deactivated // meaning it will pick up where it left off if re-enabled later // so we don't reset it in the scanline loop window_y_tile = 0; window_y_tile_inc = 0; window_started = false; window_is_reset = true; } Core.cpu.LY = LY; // Automatically restore access to VRAM at this time (force end drawing) // Who Framed Roger Rabbit seems to run into this. VRAM_access_write = true; VRAM_access_read = true; if (LY == 144) { Core.in_vblank = true; } } if (cycle == 452) { LY += LY_inc; // here is where LY = LYC gets cleared (but only if LY isnt 0 as that's a special case) if (LY_inc == 1) { LYC_INT = false; STAT &= 0xFB; } else { VBL_INT = false; if (STAT.Bit(3)) { HBL_INT = true; } STAT &= 0xFC; } Core.cpu.LY = LY; } if ((LY == 153) && (cycle == 0)) { LY = 0; LY_inc = 0; } // exit vblank if LCD went from off to on if (LCD_was_off) { //VBL_INT = false; Core.in_vblank = false; LCD_was_off = false; // we exit vblank into mode 0 for 4 cycles // but no hblank interrupt, presumably this only happens transitioning from mode 3 to 0 STAT &= 0xFC; // also the LCD doesn't turn on right away // also, the LCD does not enter mode 2 on scanline 0 when first turned on no_scan = true; cycle = 4; } // the VBL stat is continuously asserted if ((LY_actual >= 144)) { if (STAT.Bit(4)) { if ((cycle >= 0) && (LY_actual == 144)) { VBL_INT = true; } else if (LY_actual > 144) { VBL_INT = true; } } if ((cycle == 0) && (LY_actual == 144)) { HBL_INT = false; // set STAT mode to 1 (VBlank) and interrupt flag if it is enabled STAT &= 0xFC; STAT |= 0x01; if (Core.REG_FFFF.Bit(0)) { Core.cpu.FlagI = true; } Core.REG_FF0F |= 0x01; } if ((LY_actual >= 144) && (cycle == 0)) { // a special case of OAM mode 2 IRQ assertion, even though PPU Mode still is 1 if (STAT.Bit(5)) { OAM_INT = true; } } if ((LY_actual == 153) && (cycle == 4)) { LY_actual = 0; Core.cpu.LY = LY; } } if (!Core.in_vblank) { if (no_scan) { // timings are slightly different if we just turned on the LCD // there is no mode 2 (presumably it missed the trigger) // mode 3 is very short, probably in some self test mode before turning on? if (cycle == 4) { if (LY != LYC) { LYC_INT = false; STAT &= 0xFB; } if ((LY == LYC) && !STAT.Bit(2)) { // set STAT coincidence FLAG and interrupt flag if it is enabled STAT |= 0x04; if (STAT.Bit(6)) { LYC_INT = true; } } } if (cycle == 80) { STAT &= 0xFC; STAT |= 0x03; OAM_INT = false; OAM_access_read = false; OAM_access_write = false; VRAM_access_read = false; VRAM_access_write = false; } if (cycle == 252) { STAT &= 0xFC; OAM_INT = false; if (STAT.Bit(3)) { HBL_INT = true; } OAM_access_read = true; OAM_access_write = true; VRAM_access_read = true; VRAM_access_write = true; } } else { if (cycle < 76) { if (cycle == 0) { // apparently, writes can make it to OAM one cycle longer then reads OAM_access_write = false; // here mode 2 will be set to true and interrupts fired if enabled STAT &= 0xFC; STAT |= 0x2; if (STAT.Bit(5)) { OAM_INT = true; } HBL_INT = false; } // here OAM scanning is performed OAM_scan(cycle + 4); } else if (cycle >= 76 && LY_actual < 144) { if (cycle == 76) { OAM_access_read = false; OAM_access_write = true; VRAM_access_read = false; } if (cycle == 80) { STAT &= 0xFC; STAT |= 0x03; OAM_INT = false; OAM_access_write = false; VRAM_access_write = false; } // render the screen and handle hblank render(cycle - 76); } } if (cycle >= 452) { OAM_access_read = false; OAM_scan(cycle - 452); } } if ((LY_inc == 0)) { if (cycle == 8) { LYC_INT = false; STAT &= 0xFB; // Special case of LY = LYC if ((LY == LYC) && !STAT.Bit(2)) { // set STAT coincidence FLAG and interrupt flag if it is enabled STAT |= 0x04; if (STAT.Bit(6)) { LYC_INT = true; } } // also a special case of OAM mode 2 IRQ assertion, even though PPU Mode still is 1 if (STAT.Bit(5)) { OAM_INT = true; } } if (cycle == 88) { OAM_INT = false; } } // here LY=LYC will be asserted if ((cycle == 0) && (LY_actual != 0)) { if ((LY == LYC) && !STAT.Bit(2)) { // set STAT coincidence FLAG and interrupt flag if it is enabled STAT |= 0x04; if (STAT.Bit(6)) { LYC_INT = true; } } } cycle++; } else { STAT &= 0xFC; VBL_INT = LYC_INT = HBL_INT = OAM_INT = false; Core.in_vblank = true; LCD_was_off = true; LY = 0; LY_actual = 0; Core.cpu.LY = LY; cycle = 0; } // assert the STAT IRQ line if the line went from zero to 1 stat_line = VBL_INT | LYC_INT | HBL_INT | OAM_INT; if (stat_line && !stat_line_old) { if (Core.REG_FFFF.Bit(1)) { Core.cpu.FlagI = true; } Core.REG_FF0F |= 0x02; } stat_line_old = stat_line; // process latch delays //latch_delay(); } // might be needed, not sure yet public override void latch_delay() { //BGP_l = BGP; } public override void render(int render_cycle) { // we are now in STAT mode 3 // NOTE: presumably the first necessary sprite is fetched at sprite evaulation // i.e. just keeping track of the lowest x-value sprite if (render_cycle == 0) { // window X is latched for the scanline, mid-line changes have no effect window_x_latch = window_x; OAM_scan_index = 0; read_case = 0; internal_cycle = 0; pre_render = true; tile_inc = 0; pixel_counter = -8; sl_use_index = 0; fetch_sprite = false; fetch_sprite_01 = false; fetch_sprite_4 = false; going_to_fetch = false; first_fetch = true; no_sprites = false; evaled_sprites = 0; window_pre_render = false; // TODO: If Window is turned on midscanline what happens? When is this check done exactly? if ((window_started && LCDC.Bit(5)) || (window_is_reset && !LCDC.Bit(5) && (LY > window_y))) { window_y_tile_inc++; if (window_y_tile_inc==8) { window_y_tile_inc = 0; window_y_tile++; window_y_tile %= 32; } } window_started = false; if (SL_sprites_index == 0) { no_sprites = true; } // it is much easier to process sprites if we order them according to the rules of sprite priority first if (!no_sprites) { reorder_and_assemble_sprites(); } } // before anything else, we have to check if windowing is in effect if (LCDC.Bit(5) && !window_started && (LY >= window_y) && (pixel_counter >= (window_x_latch - 7)) && (window_x_latch < 167)) { /* Console.Write(LY); Console.Write(" "); Console.Write(cycle); Console.Write(" "); Console.Write(window_y_tile_inc); Console.Write(" "); Console.Write(window_x_latch); Console.Write(" "); Console.WriteLine(pixel_counter); */ if (pixel_counter == 0 && window_x_latch <= 7) { // if the window starts at zero, we still do the first access to the BG // but then restart all over again at the window window_pre_render = true; read_case = 4; } else { // otherwise, just restart the whole process as if starting BG again window_pre_render = true; read_case = 4; } window_counter = 0; window_x_tile = (int)Math.Floor((float)(pixel_counter - (window_x_latch - 7)) / 8); window_tile_inc = 0; window_started = true; window_is_reset = false; } if (!pre_render && !fetch_sprite && !window_pre_render) { // start shifting data into the LCD if (render_counter >= (render_offset + 8)) { if (tile_data_latch[2].Bit(5) && Core.GBC_compat) { pixel = tile_data_latch[0].Bit(render_counter % 8) ? 1 : 0; pixel |= tile_data_latch[1].Bit(render_counter % 8) ? 2 : 0; } else { pixel = tile_data_latch[0].Bit(7 - (render_counter % 8)) ? 1 : 0; pixel |= tile_data_latch[1].Bit(7 - (render_counter % 8)) ? 2 : 0; } int ref_pixel = pixel; if (!Core.GBC_compat) { if (LCDC.Bit(0)) { pixel = (BGP >> (pixel * 2)) & 3; } else { pixel = 0; } } int pal_num = tile_data_latch[2] & 0x7; bool use_sprite = false; int s_pixel = 0; // now we have the BG pixel, we next need the sprite pixel if (!no_sprites) { bool have_sprite = false; int sprite_attr = 0; if (sprite_present_list[pixel_counter] == 1) { have_sprite = true; s_pixel = sprite_pixel_list[pixel_counter]; sprite_attr = sprite_attr_list[pixel_counter]; } if (have_sprite) { if (LCDC.Bit(1)) { if (!sprite_attr.Bit(7)) { use_sprite = true; } else if (ref_pixel == 0) { use_sprite = true; } if (!LCDC.Bit(0)) { use_sprite = true; } // There is another priority bit in GBC, that can still override sprite priority if (LCDC.Bit(0) && tile_data_latch[2].Bit(7) && Core.GBC_compat) { use_sprite = false; } } if (use_sprite) { pal_num = sprite_attr & 7; if (!Core.GBC_compat) { pal_num = sprite_attr.Bit(4) ? 1 : 0; if (sprite_attr.Bit(4)) { pixel = (obj_pal_1 >> (s_pixel * 2)) & 3; } else { pixel = (obj_pal_0 >> (s_pixel * 2)) & 3; } } } } } // based on sprite priority and pixel values, pick a final pixel color if (Core.GBC_compat) { if (use_sprite) { Core._vidbuffer[LY * 160 + pixel_counter] = (int)OBJ_palette[pal_num * 4 + s_pixel]; } else { Core._vidbuffer[LY * 160 + pixel_counter] = (int)BG_palette[pal_num * 4 + pixel]; } } else { if (use_sprite) { Core._vidbuffer[LY * 160 + pixel_counter] = (int)OBJ_palette[pal_num * 4 + pixel]; } else { Core._vidbuffer[LY * 160 + pixel_counter] = (int)BG_palette[pixel]; } } pixel_counter++; if (pixel_counter == 160) { read_case = 8; hbl_countdown = 5; } } else if ((render_counter >= render_offset) && (pixel_counter < 0)) { pixel_counter++; } render_counter++; } if (!fetch_sprite) { if (!pre_render) { // before we go on to read case 3, we need to know if we stall there or not // Gekkio's tests show that if sprites are at position 0 or 1 (mod 8) // then it takes an extra cycle (1 or 2 more t-states) to process them if (!no_sprites && (pixel_counter < 160)) { for (int i = 0; i < SL_sprites_index; i++) { if ((pixel_counter >= (SL_sprites[i * 4 + 1] - 8)) && (pixel_counter < (SL_sprites[i * 4 + 1])) && !evaled_sprites.Bit(i)) { going_to_fetch = true; fetch_sprite = true; if ((SL_sprites[i * 4 + 1] % 8) < 2) { fetch_sprite_01 = true; } if ((SL_sprites[i * 4 + 1] % 8) > 3) { fetch_sprite_4 = true; } } } } } switch (read_case) { case 0: // read a background tile if ((internal_cycle % 2) == 0) { // calculate the row number of the tiles to be fetched y_tile = ((int)Math.Floor((float)(scroll_y + LY) / 8)) % 32; temp_fetch = y_tile * 32 + (x_tile + tile_inc) % 32; tile_byte = Core.VRAM[0x1800 + (LCDC.Bit(3) ? 1 : 0) * 0x400 + temp_fetch]; tile_data[2] = Core.VRAM[0x3800 + (LCDC.Bit(3) ? 1 : 0) * 0x400 + temp_fetch]; VRAM_sel = tile_data[2].Bit(3) ? 1 : 0; BG_V_flip = tile_data[2].Bit(6) & Core.GBC_compat; } else { read_case = 1; if (!pre_render) { tile_inc++; if (window_pre_render) { read_case = 4; } } } break; case 1: // read from tile graphics (0) if ((internal_cycle % 2) == 0) { y_scroll_offset = (scroll_y + LY) % 8; if (BG_V_flip) { y_scroll_offset = 7 - y_scroll_offset; } if (LCDC.Bit(4)) { tile_data[0] = Core.VRAM[(VRAM_sel * 0x2000) + tile_byte * 16 + y_scroll_offset * 2]; } else { // same as before except now tile byte represents a signed byte if (tile_byte.Bit(7)) { tile_byte -= 256; } tile_data[0] = Core.VRAM[(VRAM_sel * 0x2000) + 0x1000 + tile_byte * 16 + y_scroll_offset * 2]; } } else { read_case = 2; } break; case 2: // read from tile graphics (1) if ((internal_cycle % 2) == 0) { y_scroll_offset = (scroll_y + LY) % 8; if (BG_V_flip) { y_scroll_offset = 7 - y_scroll_offset; } if (LCDC.Bit(4)) { // if LCDC somehow changed between the two reads, make sure we have a positive number if (tile_byte < 0) { tile_byte += 256; } tile_data[1] = Core.VRAM[(VRAM_sel * 0x2000) + tile_byte * 16 + y_scroll_offset * 2 + 1]; } else { // same as before except now tile byte represents a signed byte if (tile_byte.Bit(7) && tile_byte > 0) { tile_byte -= 256; } tile_data[1] = Core.VRAM[(VRAM_sel * 0x2000) + 0x1000 + tile_byte * 16 + y_scroll_offset * 2 + 1]; } } else { if (pre_render) { // here we set up rendering pre_render = false; render_offset = scroll_x % 8; render_counter = 0; latch_counter = 0; read_case = 0; } else { read_case = 3; } } break; case 3: // read from sprite data if ((internal_cycle % 2) == 0) { // nothing to do if not fetching } else { read_case = 0; latch_new_data = true; } break; case 4: // read from window data if ((window_counter % 2) == 0) { temp_fetch = window_y_tile * 32 + (window_x_tile + window_tile_inc) % 32; tile_byte = Core.VRAM[0x1800 + (LCDC.Bit(6) ? 1 : 0) * 0x400 + temp_fetch]; tile_data[2] = Core.VRAM[0x3800 + (LCDC.Bit(6) ? 1 : 0) * 0x400 + temp_fetch]; VRAM_sel = tile_data[2].Bit(3) ? 1 : 0; BG_V_flip = tile_data[2].Bit(6) & Core.GBC_compat; } else { if (!window_pre_render) { window_tile_inc++; } read_case = 5; } window_counter++; break; case 5: // read from tile graphics (for the window) if ((window_counter % 2) == 0) { y_scroll_offset = (window_y_tile_inc) % 8; if (BG_V_flip) { y_scroll_offset = 7 - y_scroll_offset; } if (LCDC.Bit(4)) { tile_data[0] = Core.VRAM[(VRAM_sel * 0x2000) + tile_byte * 16 + y_scroll_offset * 2]; } else { // same as before except now tile byte represents a signed byte if (tile_byte.Bit(7)) { tile_byte -= 256; } tile_data[0] = Core.VRAM[(VRAM_sel * 0x2000) + 0x1000 + tile_byte * 16 + y_scroll_offset * 2]; } } else { read_case = 6; } window_counter++; break; case 6: // read from tile graphics (for the window) if ((window_counter % 2) == 0) { y_scroll_offset = (window_y_tile_inc) % 8; if (BG_V_flip) { y_scroll_offset = 7 - y_scroll_offset; } if (LCDC.Bit(4)) { // if LCDC somehow changed between the two reads, make sure we have a positive number if (tile_byte < 0) { tile_byte += 256; } tile_data[1] = Core.VRAM[(VRAM_sel * 0x2000) + tile_byte * 16 + y_scroll_offset * 2 + 1]; } else { // same as before except now tile byte represents a signed byte if (tile_byte.Bit(7) && tile_byte > 0) { tile_byte -= 256; } tile_data[1] = Core.VRAM[(VRAM_sel * 0x2000) + 0x1000 + tile_byte * 16 + y_scroll_offset * 2 + 1]; } } else { if (window_pre_render) { // here we set up rendering window_pre_render = false; render_offset = 0; render_counter = 0; latch_counter = 0; read_case = 4; } else { read_case = 7; } } window_counter++; break; case 7: // read from sprite data if ((window_counter % 2) == 0) { // nothing to do if not fetching } else { read_case = 4; latch_new_data = true; } window_counter++; break; case 8: // done reading, we are now in phase 0 pre_render = true; // the other interrupts appear to be delayed by 1 CPU cycle, so do the same here if (hbl_countdown > 0) { hbl_countdown--; if (hbl_countdown == 0) { STAT &= 0xFC; STAT |= 0x00; if (STAT.Bit(3)) { HBL_INT = true; } OAM_access_read = true; OAM_access_write = true; VRAM_access_read = true; VRAM_access_write = true; } } break; } internal_cycle++; if (latch_new_data) { latch_new_data = false; tile_data_latch[0] = tile_data[0]; tile_data_latch[1] = tile_data[1]; tile_data_latch[2] = tile_data[2]; } } // every in range sprite takes 6 cycles to process // sprites located at x=0 still take 6 cycles to process even though they don't appear on screen // sprites above x=168 do not take any cycles to process however if (fetch_sprite) { if (going_to_fetch) { going_to_fetch = false; sprite_fetch_counter = first_fetch ? 2 : 0; first_fetch = false; if (fetch_sprite_01) { sprite_fetch_counter += 2; fetch_sprite_01 = false; } if (fetch_sprite_4) { sprite_fetch_counter -= 2; fetch_sprite_4 = false; } int last_eval = 0; // at this time it is unknown what each cycle does, but we only need to accurately keep track of cycles for (int i = 0; i < SL_sprites_index; i++) { if ((pixel_counter >= (SL_sprites[i * 4 + 1] - 8)) && (pixel_counter < (SL_sprites[i * 4 + 1])) && !evaled_sprites.Bit(i)) { sprite_fetch_counter += 6; evaled_sprites |= (1 << i); last_eval = SL_sprites[i * 4 + 1]; } } // if we didn't evaluate all the sprites immediately, 2 more cycles are added to restart it if (evaled_sprites != (Math.Pow(2,SL_sprites_index) - 1)) { if ((last_eval % 8) == 0) { sprite_fetch_counter += 3; } else if ((last_eval % 8) == 1) { sprite_fetch_counter += 2; } else if ((last_eval % 8) == 2) { sprite_fetch_counter += 3; } else if ((last_eval % 8) == 3) { sprite_fetch_counter += 2; } else if ((last_eval % 8) == 4) { sprite_fetch_counter += 3; } else { sprite_fetch_counter += 2; } } } else { sprite_fetch_counter--; if (sprite_fetch_counter == 0) { fetch_sprite = false; } } } } public override void process_sprite() { int y; int VRAM_temp = SL_sprites[sl_use_index * 4 + 3].Bit(3) ? 1 : 0; if (SL_sprites[sl_use_index * 4 + 3].Bit(6)) { if (LCDC.Bit(2)) { y = LY - (SL_sprites[sl_use_index * 4] - 16); y = 15 - y; sprite_sel[0] = Core.VRAM[(VRAM_temp * 0x2000) + (SL_sprites[sl_use_index * 4 + 2] & 0xFE) * 16 + y * 2]; sprite_sel[1] = Core.VRAM[(VRAM_temp * 0x2000) + (SL_sprites[sl_use_index * 4 + 2] & 0xFE) * 16 + y * 2 + 1]; } else { y = LY - (SL_sprites[sl_use_index * 4] - 16); y = 7 - y; sprite_sel[0] = Core.VRAM[(VRAM_temp * 0x2000) + SL_sprites[sl_use_index * 4 + 2] * 16 + y * 2]; sprite_sel[1] = Core.VRAM[(VRAM_temp * 0x2000) + SL_sprites[sl_use_index * 4 + 2] * 16 + y * 2 + 1]; } } else { if (LCDC.Bit(2)) { y = LY - (SL_sprites[sl_use_index * 4] - 16); sprite_sel[0] = Core.VRAM[(VRAM_temp * 0x2000) + (SL_sprites[sl_use_index * 4 + 2] & 0xFE) * 16 + y * 2]; sprite_sel[1] = Core.VRAM[(VRAM_temp * 0x2000) + (SL_sprites[sl_use_index * 4 + 2] & 0xFE) * 16 + y * 2 + 1]; } else { y = LY - (SL_sprites[sl_use_index * 4] - 16); sprite_sel[0] = Core.VRAM[(VRAM_temp * 0x2000) + SL_sprites[sl_use_index * 4 + 2] * 16 + y * 2]; sprite_sel[1] = Core.VRAM[(VRAM_temp * 0x2000) + SL_sprites[sl_use_index * 4 + 2] * 16 + y * 2 + 1]; } } if (SL_sprites[sl_use_index * 4 + 3].Bit(5)) { int b0, b1, b2, b3, b4, b5, b6, b7 = 0; for (int i = 0; i < 2; i++) { b0 = (sprite_sel[i] & 0x01) << 7; b1 = (sprite_sel[i] & 0x02) << 5; b2 = (sprite_sel[i] & 0x04) << 3; b3 = (sprite_sel[i] & 0x08) << 1; b4 = (sprite_sel[i] & 0x10) >> 1; b5 = (sprite_sel[i] & 0x20) >> 3; b6 = (sprite_sel[i] & 0x40) >> 5; b7 = (sprite_sel[i] & 0x80) >> 7; sprite_sel[i] = (byte)(b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7); } } } // normal DMA moves twice as fast in double speed mode on GBC // So give it it's own function so we can seperate it from PPU tick public override void DMA_tick() { // Note that DMA is halted when the CPU is halted if (DMA_start && !Core.cpu.halted) { if (DMA_clock >= 4) { DMA_OAM_access = false; if ((DMA_clock % 4) == 1) { // the cpu can't access memory during this time, but we still need the ppu to be able to. DMA_start = false; // Gekkio reports that A14 being high on DMA transfers always represent WRAM accesses // So transfers nominally from higher memory areas are actually still from there (i.e. FF -> DF) byte DMA_actual = DMA_addr; if (DMA_addr > 0xDF) { DMA_actual &= 0xDF; } DMA_byte = Core.ReadMemory((ushort)((DMA_actual << 8) + DMA_inc)); DMA_start = true; } else if ((DMA_clock % 4) == 3) { Core.OAM[DMA_inc] = DMA_byte; if (DMA_inc < (0xA0 - 1)) { DMA_inc++; } } } DMA_clock++; if (DMA_clock == 648) { DMA_start = false; DMA_OAM_access = true; } } } // order sprites according to x coordinate // note that for sprites of equal x coordinate, priority goes to first on the list public override void reorder_and_assemble_sprites() { sprite_ordered_index = 0; // In CGB mode, sprites are ordered solely based on their position in OAM, so they are already ordered if (Core.GBC_compat) { for (int j = 0; j < SL_sprites_index; j++) { sl_use_index = j; process_sprite(); SL_sprites_ordered[sprite_ordered_index * 4] = SL_sprites[j * 4 + 1]; SL_sprites_ordered[sprite_ordered_index * 4 + 1] = sprite_sel[0]; SL_sprites_ordered[sprite_ordered_index * 4 + 2] = sprite_sel[1]; SL_sprites_ordered[sprite_ordered_index * 4 + 3] = SL_sprites[j * 4 + 3]; sprite_ordered_index++; } } else { for (int i = 0; i < 256; i++) { for (int j = 0; j < SL_sprites_index; j++) { if (SL_sprites[j * 4 + 1] == i) { sl_use_index = j; process_sprite(); SL_sprites_ordered[sprite_ordered_index * 4] = SL_sprites[j * 4 + 1]; SL_sprites_ordered[sprite_ordered_index * 4 + 1] = sprite_sel[0]; SL_sprites_ordered[sprite_ordered_index * 4 + 2] = sprite_sel[1]; SL_sprites_ordered[sprite_ordered_index * 4 + 3] = SL_sprites[j * 4 + 3]; sprite_ordered_index++; } } } } bool have_pixel = false; byte s_pixel = 0; byte sprite_attr = 0; for (int i = 0; i < 160; i++) { have_pixel = false; for (int j = 0; j < SL_sprites_index; j++) { if ((i >= (SL_sprites_ordered[j * 4] - 8)) && (i < SL_sprites_ordered[j * 4]) && !have_pixel) { // we can use the current sprite, so pick out a pixel for it int t_index = i - (SL_sprites_ordered[j * 4] - 8); t_index = 7 - t_index; sprite_data[0] = (byte)((SL_sprites_ordered[j * 4 + 1] >> t_index) & 1); sprite_data[1] = (byte)(((SL_sprites_ordered[j * 4 + 2] >> t_index) & 1) << 1); s_pixel = (byte)(sprite_data[0] + sprite_data[1]); sprite_attr = (byte)SL_sprites_ordered[j * 4 + 3]; // pixel color of 0 is transparent, so if this is the case we dont have a pixel if (s_pixel != 0) { have_pixel = true; } } } if (have_pixel) { sprite_present_list[i] = 1; sprite_pixel_list[i] = s_pixel; sprite_attr_list[i] = sprite_attr; } else { sprite_present_list[i] = 0; } } } public override void OAM_scan(int OAM_cycle) { // we are now in STAT mode 2 // TODO: maybe stat mode 2 flags are set at cycle 0 on visible scanlines? if (OAM_cycle == 0) { OAM_scan_index = 0; SL_sprites_index = 0; write_sprite = 0; } // the gameboy has 80 cycles to scan through 40 sprites, picking out the first 10 it finds to draw // the following is a guessed at implmenentation based on how NES does it, it's probably pretty close if (OAM_cycle < 10) { // start by clearing the sprite table (probably just clears X on hardware, but let's be safe here.) SL_sprites[OAM_cycle * 4] = 0; SL_sprites[OAM_cycle * 4 + 1] = 0; SL_sprites[OAM_cycle * 4 + 2] = 0; SL_sprites[OAM_cycle * 4 + 3] = 0; } else { if (write_sprite == 0) { if (OAM_scan_index < 40) { ushort temp = DMA_OAM_access ? Core.OAM[OAM_scan_index * 4] : (ushort)0xFF; // (sprite Y - 16) equals LY, we have a sprite if ((temp - 16) <= LY && ((temp - 16) + 8 + (LCDC.Bit(2) ? 8 : 0)) > LY) { // always pick the first 10 in range sprites if (SL_sprites_index < 10) { SL_sprites[SL_sprites_index * 4] = temp; write_sprite = 1; } else { // if we already have 10 sprites, there's nothing to do, increment the index OAM_scan_index++; } } else { OAM_scan_index++; } } } else { ushort temp2 = DMA_OAM_access ? Core.OAM[OAM_scan_index * 4 + write_sprite] : (ushort)0xFF; SL_sprites[SL_sprites_index * 4 + write_sprite] = temp2; write_sprite++; if (write_sprite == 4) { write_sprite = 0; SL_sprites_index++; OAM_scan_index++; } } } } public void color_compute_BG() { uint R; uint G; uint B; if ((BG_bytes_index % 2) == 0) { R = (uint)(BG_bytes[BG_bytes_index] & 0x1F); G = (uint)(((BG_bytes[BG_bytes_index] & 0xE0) | ((BG_bytes[BG_bytes_index + 1] & 0x03) << 8)) >> 5); B = (uint)((BG_bytes[BG_bytes_index + 1] & 0x7C) >> 2); } else { R = (uint)(BG_bytes[BG_bytes_index - 1] & 0x1F); G = (uint)(((BG_bytes[BG_bytes_index - 1] & 0xE0) | ((BG_bytes[BG_bytes_index] & 0x03) << 8)) >> 5); B = (uint)((BG_bytes[BG_bytes_index] & 0x7C) >> 2); } uint retR = ((R * 13 + G * 2 + B) >> 1) & 0xFF; uint retG = ((G * 3 + B) << 1) & 0xFF; uint retB = ((R * 3 + G * 2 + B * 11) >> 1) & 0xFF; BG_palette[BG_bytes_index >> 1] = (uint)(0xFF000000 | (retR << 16) | (retG << 8) | retB); } public void color_compute_OBJ() { uint R; uint G; uint B; if ((OBJ_bytes_index % 2) == 0) { R = (uint)(OBJ_bytes[OBJ_bytes_index] & 0x1F); G = (uint)(((OBJ_bytes[OBJ_bytes_index] & 0xE0) | ((OBJ_bytes[OBJ_bytes_index + 1] & 0x03) << 8)) >> 5); B = (uint)((OBJ_bytes[OBJ_bytes_index + 1] & 0x7C) >> 2); } else { R = (uint)(OBJ_bytes[OBJ_bytes_index - 1] & 0x1F); G = (uint)(((OBJ_bytes[OBJ_bytes_index - 1] & 0xE0) | ((OBJ_bytes[OBJ_bytes_index] & 0x03) << 8)) >> 5); B = (uint)((OBJ_bytes[OBJ_bytes_index] & 0x7C) >> 2); } uint retR = ((R * 13 + G * 2 + B) >> 1) & 0xFF; uint retG = ((G * 3 + B) << 1) & 0xFF; uint retB = ((R * 3 + G * 2 + B * 11) >> 1) & 0xFF; OBJ_palette[OBJ_bytes_index >> 1] = (uint)(0xFF000000 | (retR << 16) | (retG << 8) | retB); } public override void SyncState(Serializer ser) { ser.Sync("pal_transfer_byte", ref BG_transfer_byte); ser.Sync("spr_transfer_byte", ref OBJ_transfer_byte); ser.Sync("HDMA_src_hi", ref HDMA_src_hi); ser.Sync("HDMA_src_lo", ref HDMA_src_lo); ser.Sync("HDMA_dest_hi", ref HDMA_dest_hi); ser.Sync("HDMA_dest_lo", ref HDMA_dest_lo); ser.Sync("HDMA_tick", ref HDMA_tick); ser.Sync("HDMA_byte", ref HDMA_byte); ser.Sync("VRAM_sel", ref VRAM_sel); ser.Sync("BG_V_flip", ref BG_V_flip); ser.Sync("HDMA_mode", ref HDMA_mode); ser.Sync("cur_DMA_src", ref cur_DMA_src); ser.Sync("cur_DMA_dest", ref cur_DMA_dest); ser.Sync("HDMA_length", ref HDMA_length); ser.Sync("HDMA_countdown", ref HDMA_countdown); ser.Sync("HBL_HDMA_count", ref HBL_HDMA_count); ser.Sync("last_HBL", ref last_HBL); ser.Sync("HBL_HDMA_go", ref HBL_HDMA_go); ser.Sync("HBL_test", ref HBL_test); ser.Sync("BG_bytes", ref BG_bytes, false); ser.Sync("OBJ_bytes", ref OBJ_bytes, false); ser.Sync("BG_bytes_inc", ref BG_bytes_inc); ser.Sync("OBJ_bytes_inc", ref OBJ_bytes_inc); ser.Sync("BG_bytes_index", ref BG_bytes_index); ser.Sync("OBJ_bytes_index", ref OBJ_bytes_index); base.SyncState(ser); } public override void Reset() { LCDC = 0; STAT = 0x80; scroll_y = 0; scroll_x = 0; LY = 0; LYC = 0; DMA_addr = 0xFF; BGP = 0xFF; obj_pal_0 = 0xFF; obj_pal_1 = 0xFF; window_y = 0x0; window_x = 0x0; window_x_latch = 0xFF; LY_inc = 1; no_scan = false; OAM_access_read = true; VRAM_access_read = true; OAM_access_write = true; VRAM_access_write = true; DMA_OAM_access = true; cycle = 0; LYC_INT = false; HBL_INT = false; VBL_INT = false; OAM_INT = false; stat_line = false; stat_line_old = false; window_counter = 0; window_pre_render = false; window_started = false; window_tile_inc = 0; window_y_tile = 0; window_x_tile = 0; window_y_tile_inc = 0; BG_bytes_inc = false; OBJ_bytes_inc = false; BG_bytes_index = 0; OBJ_bytes_index = 0; BG_transfer_byte = 0; OBJ_transfer_byte = 0; HDMA_src_hi = 0; HDMA_src_lo = 0; HDMA_dest_hi = 0; HDMA_dest_lo = 0; VRAM_sel = 0; BG_V_flip = false; HDMA_active = false; HDMA_mode = false; cur_DMA_src = 0; cur_DMA_dest = 0; HDMA_length = 0; HDMA_countdown = 0; HBL_HDMA_count = 0; last_HBL = 0; HBL_HDMA_go = false; HBL_test = false; } } }