Yes, on NTSC, 1 CPU cycle = 3 PPU cycles as WedNESday said.
Every scanline is 341 PPU cycles
except for scanline -1 (the prerender scanline)... which alternates between 341 and 340 cycles every other frame on NTSC systems (on PAL, it's always 341)
If you want to be anal... you could say you have less VBlank time than you think -- since the PPU is loading tiles for the next scanline near the end of the scanline... effectively giving you about 320-256= only 64 PPU cycles of HBlank (during which time the PPU is doing only sprite related things). Although like I said I'm just being anal... the only game I know of where this really makes a difference is Micro Machines... and even then if you don't follow things this closely the only thing that'll go wrong is half a scanline on the title screen will be the wrong color.
The only thing that stood out to me in your code is the following:
Code: Select all
if (sl_number == 0)
{
ReloadFromPPUTemp(); // -> Counters
Clear2002Bits();
}
This looks like the start of your pre-render scanline.. which I believe is the right time to clear $2002... however this is way too early to reload Loopy_V (this accually occurs near the end of the scanline -- cycle 304 I believe). If you do this here, some games will act funky... most notably Megaman 2 which will look very ugly when the game scrolls vertically.
Also, I don't know if this is related to your problems, but it doesn't appear as though you have a global PPU timestamp. 'cc_ppu' seems to only be the cycle within the scanline. This is fine..
if CurrentCPUCycle resets itself between calls to this function.
For example... If you call this function with a CurrentCPUCycle of 15 -- the PPU will run for 15*3=45 cycles. After that if you call with a CurrentCPUCycle of 20 -- the PPU will run for 20*3=60 more cycles (not 20-15 * 3 = 15 cycles like you might think?)
I don't really know how you have it set up... and this way would work just fine... as long as CurrentCPUCycle is adjusted accordingly between calls to this function.
And because I'm bored... here's a chopped up version of my current emu's PPU emulator (some things removed, some comments added)
Code: Select all
void CNES::RunPPU(s32 cyc)
{
//don't need to catch up
if(nPPUCycle >= cyc) return;
//Idle scanline
if(nPPUCycle < (341 * PPU_CYCBASE))
{
nPPUCycle = (341 * PPU_CYCBASE);
if(nPPUCycle >= cyc)
return;
}
//after the idle scanline, VBlank flag is raised, and VBlank
if(nPPUCycle == (341 * PPU_CYCBASE))
{
memset(nSpRender,0,256);
n2002Status |= 0x80;
nPPUCycle = nEndOfVBlank;
nScanline = -1;
nScanCyc = 0;
if(nPPUCycle >= cyc)
return;
}
//just after VBlank, $2002 gets cleared
if(nPPUCycle == nEndOfVBlank)
n2002Status = 0;
//
// here, I check to see if full scanlines can be rendered, and if
// so, I render as many full scanlines as I can.
// but for the sake of this example, that part is removed
// assume you can't run any full scanlines:
//
if(bPPUOn) RunPPU_On_Fine(cyc);
else RunPPU_Off_Fine(cyc);
}
void CNES::RunPPU_On_Fine(s32 cyc)
{
if(nPPUCycle >= cyc)
return;
u8* namepage;
u8* pattern;
RESET_NAME_PAGE();
u8 a, at;
u16 c, d, ad;
// scanline -1 -- prerender
//
// **NOTE** prerender scanline stuff removed for sake
// of this example... it looks just like the other scanlines only I
// don't render pixels... and I reload nPPUAddr (LoopyV) at
// cycle 304
// rest of scanlines
while(nScanline < 240)
{
if(!nScanCyc) // **NOTE** for MMC5 IRQs
if(MapperScanlineStart) (this->*MapperScanlineStart)(nScanline);
//cycles 0-255 .. render pixels, load tiles
while(nScanCyc < 256)
{
//render a pixel...
a = nBGRender[nXScroll + nScanCyc];
at = nSpRender[nScanCyc];
if(nScanCyc < nBGClip) a = 0; /* nBGClip is either 0 (no clipping), 8 (clipping), or 256 (BG disabled) */
if(nScanCyc < nSpClip) at = 0; // ditto for nSpClip
if(a && (at & 0x40) && (nScanCyc != 255)) n2002Status |= 0x40; //sprite 0 hit
if(pVidOut)
{
if(at & 0x80) // low sprite priority
{
if(!a) a = at & 0x1F;
}
else if(at)
a = at & 0x1F;
OUTPUT_PIXEL(a);
}
//load a tile (on 3rd cycle)
if((nScanCyc & 7) == 3)
{
LOAD_BG_TILE(nScanCyc - 3 + 16);
INC_PPU_X();
if(nScanCyc == 251)
{
INC_PPU_Y();
}
}
nScanCyc++;
nPPUCycle += PPU_CYCBASE;
if(nPPUCycle >= cyc)
return;
}
// cycle 256 does nothing
if(nScanCyc == 256)
{
nScanCyc++;
nPPUCycle += PPU_CYCBASE;
if(nPPUCycle >= cyc)
return;
}
//cycle 257 -- reset X scroll, load sprite crap
if(nScanCyc == 257)
{
nScanCyc = 260;
nPPUCycle += (260 - 257) * PPU_CYCBASE;
RESET_PPU_X();
PPU_LoadSpriteLine();
if(nPPUCycle >= cyc)
return;
}
// at 260, rising edge **NOTE** for MMC3 IRQs... see below
if(nScanCyc == 260)
{
if(MapperA12Edge)
(this->*MapperA12Edge)(1);
nScanCyc = 323;
nPPUCycle += (323 - 260) * PPU_CYCBASE;
if(nPPUCycle >= cyc)
return;
}
//323, 331, load first two tiles for next line
while(nScanCyc < 339)
{
LOAD_BG_TILE(nScanCyc - 323);
INC_PPU_X();
nScanCyc += 8;
nPPUCycle += 8 * PPU_CYCBASE;
if(nPPUCycle >= cyc)
return;
}
//burn the rest of the scanline
nScanline++;
nScanCyc = 0;
nPPUCycle += 2 * PPU_CYCBASE;
if(pVidOut)
pVidOut += nVidPitchAdd;
if(nPPUCycle >= cyc)
return;
}
}
LOAD_BG_TILE() fills my 'nBGRender' buffer with pixel data... which is later drawn to the screen.
PPU_LoadSpriteLine() does the same kinda thing, but for sprites (nSpRender).
Sprite pixels in nSpRender are 0 if transparent, otherwise they're the palette color to output for this pixel (pattern + attribute bits + 0x10 = between 0x11-0x1F). Additionally, if the sprite pixel is non-transparent and has background priority, bit 7 (0x80) is flipped on. If the pixels in non-transparent and it belongs to sprite 0, bit 6 (0x40) is flipped on.
I know how I do MMC3 IRQs isn't terribly accurate. I tried doing them the proper way but it was just too much work and too much slowdown for too little gain. This way is "good enough" and runs every game I've tried just fine.