Here's a snippit of what my DoFrame() function consists of (stuff relating to video output removed, some extra comments added):
//figure out how many cycles there are going to be this frame
s32 framecycles = ((242 * 341) * PPUCYC_BASE) + nVBlankCycles;
if(bOddFrame) framecycles -= PPUCYC_BASE;
//trigger NMI (if enabled), and flip on VBlank flag
n2002Status |= 0x80;
flgN = 0x80; //This is a tweak I added... see below for why
//Run for a frame
// APU would be run here too, but I haven't implimented it yet
//adjust cycle counters
nCPUCycle -= framecycles;
nPPUCycle -= framecycles;
// APU cycles would be adjusted here, but APU not implimented
//toggle odd frames (if NTSC)
if(!bPALMode) bOddFrame ^= 1;
The 'nVBlankCycles' variable is set like so (on new ROM load and Hard Reset):
nVBlankCycles = (bPALMode ? 70 : 20) * 341 * PPUCYC_BASE;
Which is the length of VBlank for the current system (70 scanlines on PAL, 20 on NTSC). If the 'nPPUCycle' var is less than nVBlankCycles, that indicates the PPU is in VBlank. RunPPU() basically does nothing until the PPU cycle reaches nVBlankCycles.. at which time my scanline counter and scanline cycle counter get prepped and I begin actually performing things based on the cycle within the scanline.
RunCPU() runs the 6502 emulator until nCPUCycle reaches or exceeds the passed timestamp (framecycles). RunPPU() runs the PPU (and renders pixels) until nPPUCycle reaches or exceeds nCPUCycle. Because things almost always "spill over" -- by that I mean, nCPUCycle usually is a little higher than framecycles by the time RunCPU() returns -- I cannot simply reset the counters to zero. Instead I substract framecycles so that any cycles that spilled over the last frame take away from the next frame's VBlank time.
PPUCYC_BASE is defined as 5. I do things in this base so that the interaction between CPU/PPU cycle bases sync up properly. As I'm sure you know... on an NTSC system, 1 CPU cycle == 3 PPU cycles... However on a PAL system, 1 CPU cycle is a little longer, so I have things defined like so:
#define PPUCYC_BASE 5
#define CPUCYC_BASE_NTSC 15
#define CPUCYC_BASE_PAL 16
RunCPU uses the appropriate CPU cycle base depending on whether or not it's in PAL mode.
Also notice I take a cycle out of the frame on odd frames (NTSC only). Not only do I take the cycle out here, but RunPPU() takes the cycle out of scanline 20 (first line after VBlank).
The whole 'bJustRead2002' thing is to handle the sideeffect when and NMI occurs in the middle of a 2002 read (I think Brad Taylor made a post on the boards about it... I'd post a link but I don't have one... do a search if interested). bJustRead2002 is cleared every CPU instruction, but set on 2002 reads. The jist of what happens (my understanding, anyway), is that if VBlank happens while 2002 is being read, the NMI takes effect immidately (as expected), but status gets pushed on the stack with the N flag set. ANYWAY... basically, this tweak fixes games which won't otherwise work because they do loops to check 2002 AND have NMI enabled (Baseball Stars is the first one to come to mind). But it doesn't push NMI back any cycles, so other games aren't broken in the process.
This was probably way more than you wanted to hear XD. Sorry about that. Anyway I hope it's helpful. Feel free to speak up if something is still confusing.