Skip to content

Lockstep Scheduler

DoubleGate edited this page Jul 8, 2026 · 1 revision

Lockstep Scheduler

The scheduler is the heart of the cycle-exact emulator: it advances the TIA, the 6507 CPU, the RIOT timer, and any cart coprocessor in tight lockstep at TIA color-clock resolution. It lives in rusty2600-core (the System type) and is the single owner of the run loop — see Architecture-Overview fact 1 and Architecture-Decision-Records ADR 0001.

Clock topology

The master clock is the NTSC color subcarrier, 3.579545 MHz (PAL 3.546894 MHz). Everything else is a fixed integer divisor of it.

Unit Advances every N color clocks Rate (NTSC) Notes
TIA color clock (master) 1 3.579545 MHz one pixel emitted per visible color clock
6507 CPU 3 ~1.193182 MHz offset by the seeded power-on phase (0/1/2)
RIOT interval timer 3 (one CPU cycle) ~1.193182 MHz further prescaled by 1 / 8 / 64 / 1024
Cart coprocessor (DPC etc.) 3 (Board::tick) ~1.193182 MHz default no-op; DPC oscillator may free-run
TIA audio 114 ~31.4 kHz color/114; CPU/114 for the four CPU-clocked AUDC distortion modes

PAL keeps the same /3 and /114 integer divisors; only the absolute master frequency and the frame line budget differ.

Resolution: integer color-clock lockstep, not catch-up

Resolution is integer color clocks. A scanline is 228 color clocks = 68 HBLANK + 160 visible = exactly 76 CPU cycles. Because a 2600 program already races the beam at color-clock granularity, integer resolution is the natural — and sufficient — timebase: there is no sub-color-clock event the program can observe. This is why Architecture-Decision-Records ADR 0002's fractional-timebase refactor is marked "likely unneeded" for this console, in contrast to systems where sub-cycle bus-phase timing matters.

The run loop (conceptually)

tick_one_color_clock():
    tia.tick_color_clock()          # emit one dot, advance the beam
    if color_clocks % 3 == phase:   # CPU phi0 fires every 3rd clock
        if tia.rdy_stall():
            # RDY held: CPU frozen this cycle (the WSYNC beam-stall)
        else:
            cpu.tick(bus)
            riot.tick()              # RIOT timer advances on the CPU cycle
            board.tick()             # cart coprocessor hook, if any
    color_clocks += 1

phase is the per-power-on CPU/color-clock alignment (0, 1, or 2), seeded from a deterministic PRNG — never the OS RNG. Reset preserves it; a cold power-cycle re-rolls it from the seed, per the determinism contract (Architecture-Decision-Records ADR 0004).

WSYNC / RDY: the CPU beam-stall

Writing WSYNC clears the RDY latch and halts the CPU until the next HBLANK starts. The TIA owns the signal; the scheduler reads it and simply skips the CPU step while it's asserted — the color clock keeps running, only the CPU is frozen. The TIA releases RDY when its color-clock counter wraps at the end of HBLANK.

A real edge case this project found and fixed: because the CPU access ticks its own cycle before the register write is applied, a STA WSYNC whose final cycle lands exactly on the 228→0 wrap arrives with the beam already at the start of the very scanline it was waiting for. Arming the stall in that case would over-wait a full extra scanline — a phantom line. Rusty2600 correctly skips arming rdy_stall when the strobe coincides with the boundary itself, matching Gopher2600/Stella's own "RDY released at the leading edge of the next HBLANK" behavior. This was Frogger's positioning-kernel jitter bug, found and fixed by direct comparison against Gopher2600's own byte-for-byte instruction trace.

Bus design

The Bus owns the TIA, the RIOT, the cart (boxed Board), and the open-bus latch — there is no wram field (the 2600's only RAM is in the RIOT; see RIOT). The CPU borrows &mut Bus through a narrow CpuBus adapter. The video and audio paths each see a smaller trait: VideoBus for cart-mediated reads, and AudioBus for drawing the TIA's mixed sample.

No interrupt dispatch

The 6507 has no IRQ or NMI pins — see CPU-6507. There is no interrupt path in the scheduler at all; the only "timing interrupt" a 2600 program gets is polling the RIOT timer (INTIM) or WSYNC. The BRK software interrupt and the RESET vector still exist and work normally.

See also

Architecture-Overview · CPU-6507 · TIA · RIOT · Architecture-Decision-Records

Clone this wiki locally