public unsafe void FrameAdvance() { BGDataRecord *bgdata = stackalloc BGDataRecord[34]; //one at the end is junk, it can never be rendered //262 scanlines if (ppudead != 0) { FrameAdvance_ppudead(); return; } Reg2002_vblank_active_pending = true; ppuphase = PPUPHASE.VBL; ppur.status.sl = 241; //Not sure if this is correct. According to Matt Conte and my own tests, it is. Timing is probably off, though. //NOTE: Not having this here breaks a Super Donkey Kong game. if (reg_2001.show_obj || reg_2001.show_bg) { reg_2003 = 0; } //this was repeatedly finetuned from the fceux days thrugh the old cpu core and into the new one to pass 05-nmi_timing.nes //note that there is still some leniency. for instance, 4,2 will pass in addition to 3,3 const int delay = 6; runppu(3); bool nmi_destiny = reg_2000.vblank_nmi_gen && Reg2002_vblank_active; runppu(3); if (nmi_destiny) { TriggerNMI(); } nes.Board.AtVsyncNMI(); runppu(postNMIlines * kLineTime - delay); //this seems to run just before the dummy scanline begins clear_2002(); idleSynch ^= true; //render 241 scanlines (including 1 dummy at beginning) for (int sl = 0; sl < 241; sl++) { ppur.status.cycle = 0; ppur.status.sl = sl; soam_index = 0; soam_m_index = 0; soam_m_index_aux = 0; oam_index_aux = 0; oam_index = 0; o_bug = 0; is_even_cycle = true; sprite_eval_write = true; sprite_zero_go = false; if (sprite_zero_in_range) { sprite_zero_go = true; } sprite_zero_in_range = false; yp = sl - 1; ppuphase = PPUPHASE.BG; if (NTViewCallback != null && yp == NTViewCallback.Scanline) { NTViewCallback.Callback(); } if (PPUViewCallback != null && yp == PPUViewCallback.Scanline) { PPUViewCallback.Callback(); } //ok, we're also going to draw here. //unless we're on the first dummy scanline if (sl != 0) { //the main scanline rendering loop: //32 times, we will fetch a tile and then render 8 pixels. //two of those tiles were read in the last scanline. int yp_shift = yp << 8; for (int xt = 0; xt < 32; xt++) { int xstart = xt << 3; target = yp_shift + xstart; int rasterpos = xstart; spriteHeight = reg_2000.obj_size_16 ? 16 : 8; //check all the conditions that can cause things to render in these 8px bool renderspritenow = reg_2001.show_obj && (xt > 0 || reg_2001.show_obj_leftmost); bool renderbgnow; for (int xp = 0; xp < 8; xp++, rasterpos++) { ////////////////////////////////////////////////// //Sprite Evaluation Start ////////////////////////////////////////////////// if (ppur.status.cycle <= 63 && !is_even_cycle) { // the first 64 cycles of each scanline are used to initialize sceondary OAM // the actual effect setting a flag that always returns 0xFF from a OAM read // this is a bit of a shortcut to save some instructions // data is read from OAM as normal but never used soam[soam_index] = 0xFF; soam_index++; } if (ppur.status.cycle == 64) { soam_index = 0; } // otherwise, scan through OAM and test if sprites are in range // if they are, they get copied to the secondary OAM if (ppur.status.cycle >= 64) { if (oam_index == 64) { oam_index_aux = 0; oam_index = 0; sprite_eval_write = false; } if (is_even_cycle) { read_value = OAM[oam_index * 4 + soam_m_index]; if (oam_index_aux > 63) { oam_index_aux = 63; } read_value_aux = OAM[oam_index_aux * 4 + soam_m_index_aux]; } else if (sprite_eval_write) { if (soam_index >= 8) { // this code mirrors sprite overflow bug behaviour // see http://wiki.nesdev.com/w/index.php/PPU_sprite_evaluation if (yp >= read_value && yp < read_value + spriteHeight && reg_2001.PPUON) { Reg2002_objoverflow = true; } else { soam_m_index++; oam_index++; if (soam_m_index == 4) { soam_m_index = 0; } } } //look for sprites soam[soam_index * 4] = OAM[oam_index_aux * 4]; if (yp >= read_value_aux && yp < read_value_aux + spriteHeight && soam_m_index_aux == 0) { //a flag gets set if sprite zero is in range if (oam_index_aux == 0) { sprite_zero_in_range = true; } soam_m_index_aux++; } else if (soam_m_index_aux > 0 && soam_m_index_aux < 4) { soam[soam_index * 4 + soam_m_index_aux] = OAM[oam_index_aux * 4 + soam_m_index_aux]; soam_m_index_aux++; if (soam_m_index_aux == 4) { oam_index_aux++; soam_index++; soam_m_index_aux = 0; } } else { oam_index_aux++; } if (soam_index < 8) { soam_m_index = soam_m_index_aux; oam_index = oam_index_aux; } } } ////////////////////////////////////////////////// //Sprite Evaluation End ////////////////////////////////////////////////// //process the current clock's worth of bg data fetching //this needs to be split into 8 pieces or else exact sprite 0 hitting wont work due to the cpu not running while the sprite renders below if (reg_2001.show_obj || reg_2001.show_bg) { Read_bgdata(xp, ref bgdata[xt + 2]); } else { runppu(1); } renderbgnow = reg_2001.show_bg && (xt > 0 || reg_2001.show_bg_leftmost); //bg pos is different from raster pos due to its offsetability. //so adjust for that here int bgpos = rasterpos + ppur.fh; int bgpx = bgpos & 7; int bgtile = bgpos >> 3; int pixel = 0, pixelcolor = PALRAM[pixel]; //according to qeed's doc, use palette 0 or $2006's value if it is & 0x3Fxx //at one point I commented this out to fix bottom-left garbage in DW4. but it's needed for full_nes_palette. //solution is to only run when PPU is actually OFF (left-suppression doesnt count) if (!reg_2001.show_bg && !reg_2001.show_obj) { // if there's anything wrong with how we're doing this, someone please chime in int addr = ppur.get_2007access(); if ((addr & 0x3F00) == 0x3F00) { pixel = addr & 0x1F; } pixelcolor = PALRAM[pixel]; pixelcolor |= 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } //generate the BG data if (renderbgnow) { byte pt_0 = bgdata[bgtile].pt_0; byte pt_1 = bgdata[bgtile].pt_1; int sel = 7 - bgpx; pixel = ((pt_0 >> sel) & 1) | (((pt_1 >> sel) & 1) << 1); if (pixel != 0) { pixel |= bgdata[bgtile].at; } pixelcolor = PALRAM[pixel]; } if (!nes.Settings.DispBackground) { pixelcolor = 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } //look for a sprite to be drawn bool havepixel = false; for (int s = 0; s < soam_index_prev; s++) { int x = t_oam[s].oam_x; if (rasterpos >= x && rasterpos < x + 8) { //build the pixel. //fetch the LSB of the patterns int spixel = t_oam[s].patterns_0 & 1; spixel |= (t_oam[s].patterns_1 & 1) << 1; //shift down the patterns so the next pixel is in the LSB t_oam[s].patterns_0 >>= 1; t_oam[s].patterns_1 >>= 1; //bail out if we already have a pixel from a higher priority sprite. //notice that we continue looping anyway, so that we can shift down the patterns //transparent pixel bailout if (!renderspritenow || havepixel || spixel == 0) { continue; } havepixel = true; //TODO - make sure we dont trigger spritehit if the edges are masked for either BG or OBJ //spritehit: //1. is it sprite#0? //2. is the bg pixel nonzero? //then, it is spritehit. Reg2002_objhit |= (sprite_zero_go && s == 0 && pixel != 0 && rasterpos < 255 && reg_2001.show_bg && reg_2001.show_obj); //priority handling, if in front of BG: bool drawsprite = !(((t_oam[s].oam_attr & 0x20) != 0) && ((pixel & 3) != 0)); if (drawsprite && nes.Settings.DispSprites) { //bring in the palette bits and palettize spixel |= (t_oam[s].oam_attr & 3) << 2; //save it for use in the framebuffer pixelcolor = PALRAM[0x10 + spixel]; } } //rasterpos in sprite range } //oamcount loop /* * if (reg_2001.color_disable) * pixelcolor &= 0x30; * xbuf[target] = PaletteAdjustPixel(pixelcolor); */ pipeline(pixelcolor, target, xt * 32 + xp); target++; } //loop across 8 pixels } //loop across 32 tiles } else { for (int xt = 0; xt < 32; xt++) { Read_bgdata(ref bgdata[xt + 2]); } } // normally only 8 sprites are allowed, but with a particular setting we can have more then that soam_index_prev = soam_index; if (soam_index_prev > 8 && !nes.Settings.AllowMoreThanEightSprites) { soam_index_prev = 8; } ppuphase = PPUPHASE.OBJ; spriteHeight = reg_2000.obj_size_16 ? 16 : 8; // if there are less then 8 evaluated sprites, we still process 8 sprites int bound; if (soam_index_prev > 8) { bound = soam_index_prev; } else { bound = 8; } for (int s = 0; s < bound; s++) { //if this is a real sprite sprite, then it is not above the 8 sprite limit. //this is how we support the no 8 sprite limit feature. //not that at some point we may need a virtual CALL_PPUREAD which just peeks and doesnt increment any counters //this could be handy for the debugging tools also bool realSprite = (s < 8); bool junksprite = (!reg_2001.PPUON); bool extra_sprite = (s >= 8); t_oam[s].oam_y = soam[s * 4]; t_oam[s].oam_ind = soam[s * 4 + 1]; t_oam[s].oam_attr = soam[s * 4 + 2]; t_oam[s].oam_x = soam[s * 4 + 3]; int line = yp - t_oam[s].oam_y; if ((t_oam[s].oam_attr & 0x80) != 0) //vflip { line = spriteHeight - line - 1; } int patternNumber = t_oam[s].oam_ind; int patternAddress; //8x16 sprite handling: if (reg_2000.obj_size_16) { int bank = (patternNumber & 1) << 12; patternNumber = patternNumber & ~1; patternNumber |= (line >> 3) & 1; patternAddress = (patternNumber << 4) | bank; } else { patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 12); } //offset into the pattern for the current line. //tricky: tall sprites have already had lines>8 taken care of by getting a new pattern number above. //so we just need the line offset for the second pattern patternAddress += line & 7; //garbage nametable fetches + scroll resets int garbage_todo = 2; ppubus_read(ppur.get_ntread(), true, true); if (reg_2001.PPUON) { if (sl == 0 && ppur.status.cycle == 304) { runppu(1); read_value = t_oam[s].oam_y; if (reg_2001.PPUON) { ppur.install_latches(); } runppu(1); read_value = t_oam[s].oam_ind; garbage_todo = 0; } if ((sl != 0) && ppur.status.cycle == 256) { runppu(1); if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 256); target++; } read_value = t_oam[s].oam_y; //at 257: 3d world runner is ugly if we do this at 256 if (reg_2001.PPUON) { ppur.install_h_latches(); } runppu(1); if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 257); // last pipeline call option 1 of 2 } read_value = t_oam[s].oam_ind; garbage_todo = 0; } } if (realSprite) { for (int i = 0; i < garbage_todo; i++) { runppu(1); if (i == 0) { if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 256); target++; } read_value = t_oam[s].oam_y; } else { if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 257); // last pipeline call option 2 of 2 } read_value = t_oam[s].oam_ind; } } } ppubus_read(ppur.get_atread(), true, true); //at or nt? if (realSprite) { runppu(1); read_value = t_oam[s].oam_attr; runppu(1); read_value = t_oam[s].oam_x; } // TODO - fake sprites should not come through ppubus_read but rather peek it // (at least, they should not probe it with AddressPPU. maybe the difference between peek and read is not necessary) if (junksprite) { if (realSprite) { ppubus_read(patternAddress, true, false); ppubus_read(patternAddress, true, false); runppu(kFetchTime * 2); } } else { int addr = patternAddress; t_oam[s].patterns_0 = ppubus_read(addr, true, true); if (realSprite) { runppu(kFetchTime); read_value = t_oam[s].oam_x; } addr += 8; t_oam[s].patterns_1 = ppubus_read(addr, true, true); if (realSprite) { runppu(kFetchTime); read_value = t_oam[s].oam_x; } // hflip if ((t_oam[s].oam_attr & 0x40) == 0) { t_oam[s].patterns_0 = BitReverse.Byte8[t_oam[s].patterns_0]; t_oam[s].patterns_1 = BitReverse.Byte8[t_oam[s].patterns_1]; } } } // sprite pattern fetch loop ppuphase = PPUPHASE.BG; // fetch BG: two tiles for next line for (int xt = 0; xt < 2; xt++) { Read_bgdata(ref bgdata[xt]); } // this sequence is tuned to pass 10-even_odd_timing.nes runppu(kFetchTime); bool evenOddDestiny = (reg_2001.show_bg || reg_2001.show_obj); runppu(kFetchTime); // After memory access 170, the PPU simply rests for 4 cycles (or the // equivelant of half a memory access cycle) before repeating the whole // pixel/scanline rendering process. If the scanline being rendered is the very // first one on every second frame, then this delay simply doesn't exist. if (sl == 0 && idleSynch && evenOddDestiny && chopdot) { } else { runppu(1); } } // scanline loop ppur.status.sl = 241; //idle for pre NMI lines runppu(preNMIlines * kLineTime); } //FrameAdvance
public void TickPPU_active() { if (ppur.status.cycle == 0) { ppur.status.cycle = 0; spr_true_count = 0; soam_index = 0; soam_m_index = 0; soam_m_index_aux = 0; oam_index_aux = 0; oam_index = 0; is_even_cycle = true; sprite_eval_write = true; sprite_zero_go = sprite_zero_in_range; sprite_zero_in_range = false; yp = ppur.status.sl - 1; ppuphase = PPUPHASE.BG; // "If PPUADDR is not less then 8 when rendering starts, the first 8 bytes in OAM are written to from // the current location of PPUADDR" if (ppur.status.sl == 0 && PPUON && reg_2003 >= 8 && region == Region.NTSC) { for (int i = 0; i < 8; i++) { OAM[i] = OAM[(reg_2003 & 0xF8) + i]; } } if (NTViewCallback != null && yp == NTViewCallback.Scanline) { NTViewCallback.Callback(); } if (PPUViewCallback != null && yp == PPUViewCallback.Scanline) { PPUViewCallback.Callback(); } // set up intial values to use later yp_shift = yp << 8; xt = 0; xp = 0; sprite_eval_cycle = 0; xstart = xt << 3; target = yp_shift + xstart; rasterpos = xstart; spriteHeight = reg_2000.obj_size_16 ? 16 : 8; //check all the conditions that can cause things to render in these 8px renderspritenow = show_obj_new && (xt > 0 || reg_2001.show_obj_leftmost); hit_pending = false; } if (ppur.status.cycle < 256) { if (ppur.status.sl != 0) { ///////////////////////////////////////////// // Sprite Evaluation End ///////////////////////////////////////////// if (sprite_eval_cycle <= 63 && !is_even_cycle) { // the first 64 cycles of each scanline are used to initialize sceondary OAM // the actual effect setting a flag that always returns 0xFF from a OAM read // this is a bit of a shortcut to save some instructions // data is read from OAM as normal but never used soam[soam_index] = 0xFF; soam_index++; } if (sprite_eval_cycle == 64) { soam_index = 0; oam_index = reg_2003; } // otherwise, scan through OAM and test if sprites are in range // if they are, they get copied to the secondary OAM if (sprite_eval_cycle >= 64) { if (oam_index >= 256) { oam_index = 0; sprite_eval_write = false; } if (is_even_cycle && oam_index < 256) { if ((oam_index + soam_m_index) < 256) { read_value = OAM[oam_index + soam_m_index]; } else { read_value = OAM[oam_index + soam_m_index - 256]; } } else if (!sprite_eval_write) { // if we don't write sprites anymore, just scan through the oam read_value = soam[0]; oam_index += 4; } else if (sprite_eval_write) { //look for sprites if (spr_true_count == 0 && soam_index < 8) { soam[soam_index * 4] = read_value; } if (soam_index < 8) { if (yp >= read_value && yp < read_value + spriteHeight && spr_true_count == 0) { //a flag gets set if sprite zero is in range if (oam_index == reg_2003) { sprite_zero_in_range = true; } spr_true_count++; soam_m_index++; } else if (spr_true_count > 0 && spr_true_count < 4) { soam[soam_index * 4 + soam_m_index] = read_value; soam_m_index++; spr_true_count++; if (spr_true_count == 4) { oam_index += 4; soam_index++; if (soam_index == 8) { // oam_index could be pathologically misaligned at this point, so we have to find the next // nearest actual sprite to work on >8 sprites per scanline option oam_index_aux = (oam_index % 4) * 4; } soam_m_index = 0; spr_true_count = 0; } } else { oam_index += 4; } } else if (soam_index >= 8) { if (yp >= read_value && yp < read_value + spriteHeight && PPUON) { hit_pending = true; //Reg2002_objoverflow = true; } if (yp >= read_value && yp < read_value + spriteHeight && spr_true_count == 0) { spr_true_count++; soam_m_index++; } else if (spr_true_count > 0 && spr_true_count < 4) { soam_m_index++; spr_true_count++; if (spr_true_count == 4) { oam_index += 4; soam_index++; soam_m_index = 0; spr_true_count = 0; } } else { oam_index += 4; if (soam_index == 8) { soam_m_index++; // glitchy increment soam_m_index &= 3; } } read_value = soam[0]; //writes change to reads } } } ///////////////////////////////////////////// // Sprite Evaluation End ///////////////////////////////////////////// //process the current clock's worth of bg data fetching //this needs to be split into 8 pieces or else exact sprite 0 hitting wont work // due to the cpu not running while the sprite renders below if (PPUON) { Read_bgdata(xp, ref bgdata[xt + 2]); } runppu(); if (PPUON && xp == 6) { ppu_was_on = true; } if (PPUON && xp == 7) { if (!race_2006) { ppur.increment_hsc(); } if (ppur.status.cycle == 256 && !race_2006) { ppur.increment_vs(); } ppu_was_on = false; } if (hit_pending) { hit_pending = false; Reg2002_objoverflow = true; } renderbgnow = show_bg_new && (xt > 0 || reg_2001.show_bg_leftmost); //bg pos is different from raster pos due to its offsetability. //so adjust for that here int bgpos = rasterpos + ppur.fh; int bgpx = bgpos & 7; int bgtile = bgpos >> 3; int pixel = 0, pixelcolor = PALRAM[pixel]; //according to qeed's doc, use palette 0 or $2006's value if it is & 0x3Fxx //at one point I commented this out to fix bottom-left garbage in DW4. but it's needed for full_nes_palette. //solution is to only run when PPU is actually OFF (left-suppression doesnt count) if (!PPUON) { // if there's anything wrong with how we're doing this, someone please chime in int addr = ppur.get_2007access(); if ((addr & 0x3F00) == 0x3F00) { pixel = addr & 0x1F; } pixelcolor = PALRAM[pixel]; pixelcolor |= 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } //generate the BG data if (renderbgnow) { byte pt_0 = bgdata[bgtile].pt_0; byte pt_1 = bgdata[bgtile].pt_1; int sel = 7 - bgpx; pixel = ((pt_0 >> sel) & 1) | (((pt_1 >> sel) & 1) << 1); if (pixel != 0) { pixel |= bgdata[bgtile].at; } pixelcolor = PALRAM[pixel]; } if (!nes.Settings.DispBackground) { pixelcolor = 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } //check if the pixel has a sprite in it if (sl_sprites[256 + xt * 8 + xp] != 0 && renderspritenow) { int s = sl_sprites[xt * 8 + xp]; int spixel = sl_sprites[256 + xt * 8 + xp]; int temp_attr = sl_sprites[512 + xt * 8 + xp]; //TODO - make sure we dont trigger spritehit if the edges are masked for either BG or OBJ //spritehit: //1. is it sprite#0? //2. is the bg pixel nonzero? //then, it is spritehit. Reg2002_objhit |= (sprite_zero_go && s == 0 && pixel != 0 && rasterpos < 255 && show_bg_new && show_obj_new); //priority handling, if in front of BG: bool drawsprite = !(((temp_attr & 0x20) != 0) && ((pixel & 3) != 0)); if (drawsprite && nes.Settings.DispSprites) { //bring in the palette bits and palettize spixel |= (temp_attr & 3) << 2; //save it for use in the framebuffer pixelcolor = PALRAM[0x10 + spixel]; } } //oamcount loop pipeline(pixelcolor, target, xt * 8 + xp); target++; // clear out previous sprites from scanline buffer //sl_sprites[xt * 8 + xp] = 0; sl_sprites[256 + xt * 8 + xp] = 0; //sl_sprites[512 + xt * 8 + xp] = 0; // end of visible part of the scanline sprite_eval_cycle++; xp++; rasterpos++; if (xp == 8) { xp = 0; xt++; xstart = xt << 3; target = yp_shift + xstart; rasterpos = xstart; spriteHeight = reg_2000.obj_size_16 ? 16 : 8; //check all the conditions that can cause things to render in these 8px renderspritenow = show_obj_new && (xt > 0 || reg_2001.show_obj_leftmost); hit_pending = false; } } else { // if scanline is the pre-render line, we just read BG data Read_bgdata(xp, ref bgdata[xt + 2]); runppu(); if (PPUON && xp == 6) { ppu_was_on = true; } if (PPUON && xp == 7) { if (!race_2006) { ppur.increment_hsc(); } if (ppur.status.cycle == 256 && !race_2006) { ppur.increment_vs(); } ppu_was_on = false; } xp++; if (xp == 8) { xp = 0; xt++; } } } else if (ppur.status.cycle < 320) { // after we are done with the visible part of the frame, we reach sprite transfer to temp OAM tables and such if (ppur.status.cycle == 256) { // do the more then 8 sprites stuff here where it is convenient // normally only 8 sprites are allowed, but with a particular setting we can have more then that // this extra bit takes care of it quickly soam_index_aux = 8; if (nes.Settings.AllowMoreThanEightSprites) { while (oam_index_aux < 64 && soam_index_aux < 64) { //look for sprites soam[soam_index_aux * 4] = OAM[oam_index_aux * 4]; read_value_aux = OAM[oam_index_aux * 4]; if (yp >= read_value_aux && yp < read_value_aux + spriteHeight) { soam[soam_index_aux * 4 + 1] = OAM[oam_index_aux * 4 + 1]; soam[soam_index_aux * 4 + 2] = OAM[oam_index_aux * 4 + 2]; soam[soam_index_aux * 4 + 3] = OAM[oam_index_aux * 4 + 3]; soam_index_aux++; oam_index_aux++; } else { oam_index_aux++; } } } soam_index_prev = soam_index_aux; if (soam_index_prev > 8 && !nes.Settings.AllowMoreThanEightSprites) { soam_index_prev = 8; } ppuphase = PPUPHASE.OBJ; spriteHeight = reg_2000.obj_size_16 ? 16 : 8; s = 0; ppu_aux_index = 0; junksprite = (!PPUON); t_oam[s].oam_y = soam[s * 4]; t_oam[s].oam_ind = soam[s * 4 + 1]; t_oam[s].oam_attr = soam[s * 4 + 2]; t_oam[s].oam_x = soam[s * 4 + 3]; line = yp - t_oam[s].oam_y; if ((t_oam[s].oam_attr & 0x80) != 0) //vflip { line = spriteHeight - line - 1; } patternNumber = t_oam[s].oam_ind; } switch (ppu_aux_index) { case 0: //8x16 sprite handling: if (reg_2000.obj_size_16) { int bank = (patternNumber & 1) << 12; patternNumber = patternNumber & ~1; patternNumber |= (line >> 3) & 1; patternAddress = (patternNumber << 4) | bank; } else { patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 12); } //offset into the pattern for the current line. //tricky: tall sprites have already had lines>8 taken care of by getting a new pattern number above. //so we just need the line offset for the second pattern patternAddress += line & 7; ppubus_read(ppur.get_ntread(), true, true); read_value = t_oam[s].oam_y; runppu(); break; case 1: if (PPUON && ppur.status.sl == 0 && ppur.status.cycle == 305) { ppur.install_latches(); read_value = t_oam[s].oam_ind; runppu(); } else if (PPUON && (ppur.status.sl != 0) && ppur.status.cycle == 257) { if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 256); target++; } //at 257: 3d world runner is ugly if we do this at 256 if (PPUON) { ppur.install_h_latches(); } read_value = t_oam[s].oam_ind; runppu(); if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 257); // last pipeline call option 1 of 2 } } else { if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 256); target++; } read_value = t_oam[s].oam_ind; runppu(); if (target <= 61441 && target > 0 && s == 0) { pipeline(0, target, 257); // last pipeline call option 2 of 2 } } break; case 2: ppubus_read(ppur.get_atread(), true, true); //at or nt? read_value = t_oam[s].oam_attr; runppu(); break; case 3: read_value = t_oam[s].oam_x; runppu(); break; case 4: // if the PPU is off, we don't put anything on the bus if (junksprite) { ppubus_read(patternAddress, true, false); runppu(); } else { temp_addr = patternAddress; t_oam[s].patterns_0 = ppubus_read(temp_addr, true, true); read_value = t_oam[s].oam_x; runppu(); } break; case 5: // if the PPU is off, we don't put anything on the bus if (junksprite) { runppu(); } else { runppu(); } break; case 6: // if the PPU is off, we don't put anything on the bus if (junksprite) { ppubus_read(patternAddress, true, false); runppu(); } else { temp_addr += 8; t_oam[s].patterns_1 = ppubus_read(temp_addr, true, true); read_value = t_oam[s].oam_x; runppu(); } break; case 7: // if the PPU is off, we don't put anything on the bus if (junksprite) { runppu(); } else { runppu(); // hflip if ((t_oam[s].oam_attr & 0x40) == 0) { t_oam[s].patterns_0 = BitReverse.Byte8[t_oam[s].patterns_0]; t_oam[s].patterns_1 = BitReverse.Byte8[t_oam[s].patterns_1]; } // if the sprites attribute is 0xFF, then this indicates a non-existent sprite // I think the logic here is that bits 2-4 in OAM are disabled, but soam is initialized with 0xFF // so the only way a sprite could have an 0xFF attribute is if it is not in the scope of the scanline if (t_oam[s].oam_attr == 0xFF) { t_oam[s].patterns_0 = 0; t_oam[s].patterns_1 = 0; } } break; } ppu_aux_index++; if (ppu_aux_index == 8) { // now that we have a sprite, we can fill in the next scnaline's sprite pixels with it // this saves quite a bit of processing compared to checking each pixel if (s < soam_index_prev) { int temp_x = t_oam[s].oam_x; for (int i = 0; (temp_x + i) < 256 && i < 8; i++) { if (sl_sprites[256 + temp_x + i] == 0) { if (t_oam[s].patterns_0.Bit(i) || t_oam[s].patterns_1.Bit(i)) { int spixel = t_oam[s].patterns_0.Bit(i) ? 1 : 0; spixel |= (t_oam[s].patterns_1.Bit(i) ? 2 : 0); sl_sprites[temp_x + i] = (byte)s; sl_sprites[256 + temp_x + i] = (byte)spixel; sl_sprites[512 + temp_x + i] = t_oam[s].oam_attr; } } } } ppu_aux_index = 0; s++; if (s < 8) { junksprite = (!PPUON); t_oam[s].oam_y = soam[s * 4]; t_oam[s].oam_ind = soam[s * 4 + 1]; t_oam[s].oam_attr = soam[s * 4 + 2]; t_oam[s].oam_x = soam[s * 4 + 3]; line = yp - t_oam[s].oam_y; if ((t_oam[s].oam_attr & 0x80) != 0) //vflip { line = spriteHeight - line - 1; } patternNumber = t_oam[s].oam_ind; } else { // repeat all the above steps for more then 8 sprites but don't run any cycles if (soam_index_aux > 8) { for (int s = 8; s < soam_index_aux; s++) { t_oam[s].oam_y = soam[s * 4]; t_oam[s].oam_ind = soam[s * 4 + 1]; t_oam[s].oam_attr = soam[s * 4 + 2]; t_oam[s].oam_x = soam[s * 4 + 3]; int line = yp - t_oam[s].oam_y; if ((t_oam[s].oam_attr & 0x80) != 0) //vflip { line = spriteHeight - line - 1; } int patternNumber = t_oam[s].oam_ind; int patternAddress; //8x16 sprite handling: if (reg_2000.obj_size_16) { int bank = (patternNumber & 1) << 12; patternNumber = patternNumber & ~1; patternNumber |= (line >> 3) & 1; patternAddress = (patternNumber << 4) | bank; } else { patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 12); } //offset into the pattern for the current line. //tricky: tall sprites have already had lines>8 taken care of by getting a new pattern number above. //so we just need the line offset for the second pattern patternAddress += line & 7; ppubus_read(ppur.get_ntread(), true, false); ppubus_read(ppur.get_atread(), true, false); //at or nt? int addr = patternAddress; t_oam[s].patterns_0 = ppubus_read(addr, true, false); addr += 8; t_oam[s].patterns_1 = ppubus_read(addr, true, false); // hflip if ((t_oam[s].oam_attr & 0x40) == 0) { t_oam[s].patterns_0 = BitReverse.Byte8[t_oam[s].patterns_0]; t_oam[s].patterns_1 = BitReverse.Byte8[t_oam[s].patterns_1]; } // if the sprites attribute is 0xFF, then this indicates a non-existent sprite // I think the logic here is that bits 2-4 in OAM are disabled, but soam is initialized with 0xFF // so the only way a sprite could have an 0xFF attribute is if it is not in the scope of the scanline if (t_oam[s].oam_attr == 0xFF) { t_oam[s].patterns_0 = 0; t_oam[s].patterns_1 = 0; } int temp_x = t_oam[s].oam_x; for (int i = 0; (temp_x + i) < 256 && i < 8; i++) { if (sl_sprites[256 + temp_x + i] == 0) { if (t_oam[s].patterns_0.Bit(i) || t_oam[s].patterns_1.Bit(i)) { int spixel = t_oam[s].patterns_0.Bit(i) ? 1 : 0; spixel |= (t_oam[s].patterns_1.Bit(i) ? 2 : 0); sl_sprites[temp_x + i] = (byte)s; sl_sprites[256 + temp_x + i] = (byte)spixel; sl_sprites[512 + temp_x + i] = t_oam[s].oam_attr; } } } } } } } } else { if (ppur.status.cycle == 320) { ppuphase = PPUPHASE.BG; xt = 0; xp = 0; } if (ppur.status.cycle < 336) { // if scanline is the pre-render line, we just read BG data Read_bgdata(xp, ref bgdata[xt]); runppu(); if (PPUON && xp == 6) { ppu_was_on = true; } if (PPUON && xp == 7) { if (!race_2006) { ppur.increment_hsc(); } if (ppur.status.cycle == 256 && !race_2006) { ppur.increment_vs(); } ppu_was_on = false; } xp++; if (xp == 8) { xp = 0; xt++; } } else if (ppur.status.cycle < 340) { runppu(); } else { bool evenOddDestiny = PPUON; // After memory access 170, the PPU simply rests for 4 cycles (or the // equivelant of half a memory access cycle) before repeating the whole // pixel/scanline rendering process. If the scanline being rendered is the very // first one on every second frame, then this delay simply doesn't exist. if (ppur.status.sl == 0 && idleSynch && evenOddDestiny && chopdot) { ppur.status.cycle++; } // increment cycle without running ppu else { runppu(); } } } if (ppur.status.cycle == 341) { ppur.status.cycle = 0; ppur.status.sl++; if (ppur.status.sl == 241) { do_active_sl = false; } } }
public unsafe void FrameAdvance() { BGDataRecord *bgdata = stackalloc BGDataRecord[34]; //one at the end is junk, it can never be rendered //262 scanlines if (ppudead != 0) { FrameAdvance_ppudead(); return; } Reg2002_vblank_active_pending = true; ppuphase = PPUPHASE.VBL; ppur.status.sl = 241; //Not sure if this is correct. According to Matt Conte and my own tests, it is. Timing is probably off, though. //NOTE: Not having this here breaks a Super Donkey Kong game. reg_2003 = 0; //this was repeatedly finetuned from the fceux days thrugh the old cpu core and into the new one to pass 05-nmi_timing.nes //note that there is still some leniency. for instance, 4,2 will pass in addition to 3,3 const int delay = 6; runppu(3); bool nmi_destiny = reg_2000.vblank_nmi_gen && Reg2002_vblank_active; runppu(3); if (nmi_destiny) { TriggerNMI(); } runppu(postNMIlines * kLineTime - delay); //this seems to run just before the dummy scanline begins clear_2002(); TempOAM *oams = stackalloc TempOAM[128]; int * oamcounts = stackalloc int[2]; int oamslot = 0; int oamcount = 0; idleSynch ^= true; //render 241 scanlines (including 1 dummy at beginning) for (int sl = 0; sl < 241; sl++) { ppur.status.cycle = 0; ppur.status.sl = sl; int yp = sl - 1; ppuphase = PPUPHASE.BG; if (NTViewCallback != null && yp == NTViewCallback.Scanline) { NTViewCallback.Callback(); } if (PPUViewCallback != null && yp == PPUViewCallback.Scanline) { PPUViewCallback.Callback(); } //twiddle the oam buffers int scanslot = oamslot ^ 1; int renderslot = oamslot; oamslot ^= 1; oamcount = oamcounts[renderslot]; //ok, we're also going to draw here. //unless we're on the first dummy scanline if (sl != 0) { //the main scanline rendering loop: //32 times, we will fetch a tile and then render 8 pixels. //two of those tiles were read in the last scanline. int yp_shift = yp << 8; for (int xt = 0; xt < 32; xt++) { int xstart = xt << 3; oamcount = oamcounts[renderslot]; int target = yp_shift + xstart; int rasterpos = xstart; //check all the conditions that can cause things to render in these 8px bool renderspritenow = reg_2001.show_obj && (xt > 0 || reg_2001.show_obj_leftmost); bool renderbgnow = reg_2001.show_bg && (xt > 0 || reg_2001.show_bg_leftmost); for (int xp = 0; xp < 8; xp++, rasterpos++) { //process the current clock's worth of bg data fetching //this needs to be split into 8 pieces or else exact sprite 0 hitting wont work due to the cpu not running while the sprite renders below Read_bgdata(xp, ref bgdata[xt + 2]); //bg pos is different from raster pos due to its offsetability. //so adjust for that here int bgpos = rasterpos + ppur.fh; int bgpx = bgpos & 7; int bgtile = bgpos >> 3; int pixel = 0, pixelcolor; //generate the BG data if (renderbgnow) { byte pt_0 = bgdata[bgtile].pt_0; byte pt_1 = bgdata[bgtile].pt_1; int sel = 7 - bgpx; pixel = ((pt_0 >> sel) & 1) | (((pt_1 >> sel) & 1) << 1); if (pixel != 0) { pixel |= bgdata[bgtile].at; } pixelcolor = PALRAM[pixel]; } else { if (!renderspritenow) { //according to qeed's doc, use palette 0 or $2006's value if it is & 0x3Fxx //EDIT - this requires corect emulation of PPU OFF state, and seems only to apply when the PPU is OFF // not sure why this was off, but having it on fixes full_nes_palette, and it's a behavior that's been // verified on the decapped PPU // if there's anything wrong with how we're doing this, someone please chime in int addr = ppur.get_2007access(); if ((addr & 0x3F00) == 0x3F00) { // System.Console.WriteLine("{0:X4}", addr); pixel = addr & 0x1F; } } pixelcolor = PALRAM[pixel]; pixelcolor |= 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } if (!nes.Settings.DispBackground) { pixelcolor = 0x8000; //whats this? i think its a flag to indicate a hidden background to be used by the canvas filling logic later } //look for a sprite to be drawn bool havepixel = false; int renderslot_shift = renderslot << 6; for (int s = 0; s < oamcount; s++) { TempOAM *oam = &oams[renderslot_shift + s]; int x = oam->oam[3]; if (rasterpos >= x && rasterpos < x + 8) { //build the pixel. //fetch the LSB of the patterns int spixel = oam->patterns[0] & 1; spixel |= (oam->patterns[1] & 1) << 1; //shift down the patterns so the next pixel is in the LSB oam->patterns[0] >>= 1; oam->patterns[1] >>= 1; //bail out if we already have a pixel from a higher priority sprite. //notice that we continue looping anyway, so that we can shift down the patterns //transparent pixel bailout if (!renderspritenow || havepixel || spixel == 0) { continue; } havepixel = true; //TODO - make sure we dont trigger spritehit if the edges are masked for either BG or OBJ //spritehit: //1. is it sprite#0? //2. is the bg pixel nonzero? //then, it is spritehit. Reg2002_objhit |= (oam->index == 0 && pixel != 0 && rasterpos < 255); //priority handling, if in front of BG: bool drawsprite = !(((oam->oam[2] & 0x20) != 0) && ((pixel & 3) != 0)); if (drawsprite && nes.Settings.DispSprites) { //bring in the palette bits and palettize spixel |= (oam->oam[2] & 3) << 2; //save it for use in the framebuffer pixelcolor = PALRAM[0x10 + spixel]; } } //rasterpos in sprite range } //oamcount loop if (reg_2001.color_disable) { pixelcolor &= 0x30; } xbuf[target] = PaletteAdjustPixel(pixelcolor); target++; } //loop across 8 pixels } //loop across 32 tiles } else { for (int xt = 0; xt < 32; xt++) { Read_bgdata(ref bgdata[xt + 2]); } } //look for sprites (was supposed to run concurrent with bg rendering) oamcounts[scanslot] = 0; oamcount = 0; int spriteHeight = reg_2000.obj_size_16 ? 16 : 8; int scanslot_lshift = scanslot << 6; for (int i = 0; i < 64; i++) { oams[scanslot_lshift + i].present = 0; int spr = i * 4; if (yp >= OAM[spr] && yp < OAM[spr] + spriteHeight) { //if we already have maxsprites, then this new one causes an overflow, //set the flag and bail out. //should we set this flag anyway?? if (oamcount >= 8 && reg_2001.PPUON) { Reg2002_objoverflow = true; if (!nes.Settings.AllowMoreThanEightSprites) { break; } } //just copy some bytes into the internal sprite buffer TempOAM *oam = &oams[scanslot_lshift + oamcount]; for (int j = 0; j < 4; j++) { oam->oam[j] = OAM[spr + j]; } oam->present = 1; //note that we stuff the oam index into [6]. //i need to turn this into a struct so we can have fewer magic numbers oams[scanslot_lshift + oamcount].index = (byte)i; oamcount++; } } oamcounts[scanslot] = oamcount; ppuphase = PPUPHASE.OBJ; //fetch sprite patterns int oam_todo = oamcount; if (oam_todo < 8) { oam_todo = 8; } for (int s = 0; s < oam_todo; s++) { //if this is a real sprite sprite, then it is not above the 8 sprite limit. //this is how we support the no 8 sprite limit feature. //not that at some point we may need a virtual CALL_PPUREAD which just peeks and doesnt increment any counters //this could be handy for the debugging tools also bool realSprite = (s < 8); bool junksprite = (s >= oamcount || !reg_2001.PPUON); TempOAM *oam = &oams[scanslot_lshift + s]; int line = yp - oam->oam[0]; if ((oam->oam[2] & 0x80) != 0) //vflip { line = spriteHeight - line - 1; } int patternNumber = oam->oam[1]; int patternAddress; //create deterministic dummy fetch pattern. if (oam->present == 0) { //according to nintendulator: //* On the first empty sprite slot, read the Y-coordinate of sprite #63 followed by $FF for the remaining 7 cycles //* On all subsequent empty sprite slots, read $FF for all 8 reads //well, we shall just read $FF and that is good enough for now to make mmc3 work patternNumber = 0xFF; line = 0; } //8x16 sprite handling: if (reg_2000.obj_size_16) { int bank = (patternNumber & 1) << 12; patternNumber = patternNumber & ~1; patternNumber |= (line >> 3) & 1; patternAddress = (patternNumber << 4) | bank; } else { patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 12); } //offset into the pattern for the current line. //tricky: tall sprites have already had lines>8 taken care of by getting a new pattern number above. //so we just need the line offset for the second pattern patternAddress += line & 7; //garbage nametable fetches + scroll resets int garbage_todo = 2; ppubus_read(ppur.get_ntread(), true); if (reg_2001.PPUON) { if (sl == 0 && ppur.status.cycle == 304) { runppu(1); if (reg_2001.PPUON) { ppur.install_latches(); } runppu(1); garbage_todo = 0; } if ((sl != 0) && ppur.status.cycle == 256) { runppu(1); //at 257: 3d world runner is ugly if we do this at 256 if (reg_2001.PPUON) { ppur.install_h_latches(); } runppu(1); garbage_todo = 0; } } if (realSprite) { runppu(garbage_todo); } ppubus_read(ppur.get_atread(), true); //at or nt? if (realSprite) { runppu(kFetchTime); } // TODO - fake sprites should not come through ppubus_read but rather peek it // (at least, they should not probe it with AddressPPU. maybe the difference between peek and read is not necessary) if (junksprite) { if (realSprite) { ppubus_read(patternAddress, true); ppubus_read(patternAddress, true); runppu(kFetchTime * 2); } } else { int addr = patternAddress; oam->patterns[0] = ppubus_read(addr, true); if (realSprite) { runppu(kFetchTime); } addr += 8; oam->patterns[1] = ppubus_read(addr, true); if (realSprite) { runppu(kFetchTime); } // hflip if ((oam->oam[2] & 0x40) == 0) { oam->patterns[0] = BitReverse.Byte8[oam->patterns[0]]; oam->patterns[1] = BitReverse.Byte8[oam->patterns[1]]; } } } // sprite pattern fetch loop ppuphase = PPUPHASE.BG; // fetch BG: two tiles for next line for (int xt = 0; xt < 2; xt++) { Read_bgdata(ref bgdata[xt]); } // this sequence is tuned to pass 10-even_odd_timing.nes runppu(kFetchTime); bool evenOddDestiny = reg_2001.show_bg; runppu(kFetchTime); // After memory access 170, the PPU simply rests for 4 cycles (or the // equivelant of half a memory access cycle) before repeating the whole // pixel/scanline rendering process. If the scanline being rendered is the very // first one on every second frame, then this delay simply doesn't exist. if (sl == 0 && idleSynch && evenOddDestiny && chopdot) { } else { runppu(1); } } // scanline loop ppur.status.sl = 241; //idle for pre NMI lines runppu(preNMIlines * kLineTime); } //FrameAdvance