I operate on a fictional time system I call "master cycles"
every 1 CPU cycle takes 15 master cycles (on NTSC) or 16 (on PAL).
Every 1 dot (aka PPU cycle) takes 5 master cycles.
Therefore... since an instruction like LDA #$00 takes 2 cycles, it would take 30 master cycles (NTSC).
I keep a timestamp for each subsystem (CPU, PPU, APU, and any external system like a mapper). I run the CPU ahead of everything else, and keep adjusting its timestamp as cycles are emulated. When the CPU does something to interact with another system (usually in the form of a register read or write), I emulate the subsystem until its timestamp reaches or exceeds the CPU's timestamp. This is probably more well known as the "catch up" approach.
Basic Example:
Let's say CPU and PPU timestamps are both 0. My CPU emulator then emulates the following code:
Code:
LDA #$00 ; after this, CPU timestamp is 30
STA $FE ; CPU time = 75
STA $FF ; 120
STA $2001 ; 180
Since the value written to $2001 will affect how the PPU operates (it is a PPU register), I would jump to my PPU emu and "catch it up" -- that is... run it until its timestamp is >= 180,
then I would apply the value written to $2001 and continue with CPU emulation as normal.
I can run my emu for chunks at a time that split cleanly into frames using this method. IE: I would run the CPU for X master cycles, then catch up all subsystems, then output the generated video/audio to the user, then rinse and repeat as needed.
Since frames (practically) are a fixed length (341 dots * 262 scanlines * 5 master cycles per dot = 446710 master cycles per frame)* this system not only indicates how long I need to run each system, but also where in the frame the system is. That is.. .I can perform math on any given timestamp and know which scanline the system is currently on and whatnot.
** another technical note: Odd frames on NTSC are sometimes 5 master cycles shorter (446705 instead of 446710) due to a PPU cycle being skipped under some circumstances. Rather than reconstruct my entire timebase system, I simply have non-ppu related systems skip this cycle by adding an additional 5 to their timestamps where appropriate **
Anyway that's about the jist of it. Just tally up cycles as you go until you reach a target timestamp.