The master clock
All of Phosphoric rests on one promise: one call to emu_cycle(), one cycle of the whole machine. Here is the contract, the order of the chips within the cycle, and the defect that taught how to keep it.
On this page
Before: three clocks
Before version 2.0, the machine’s time lived in three places. The processor advanced its own clock; a
callback defined in main.c moved the peripherals; and the main loop computed the scan position
itself to know when to draw a line. As a result, only the main loop knew how to clock the machine.
The debugger, the tests or the replay could not ask for “one cycle forward”, only for “run one
instruction”.
The contract
bool emu_cycle(emulator_t* emu); /* one cycle of the WHOLE machine */
int emu_step (emulator_t* emu); /* one instruction, through emu_cycle */
emu_cycle() (src/emu_clock.c) returns true when the cycle it has just run ended an instruction.
Any caller can now clock the machine: the main loop, the single-stepping debugger, the tests, the
replay of a movie.
The order within the cycle
| Order | Who | What |
|---|---|---|
| 1 | the 6502 | the cycle’s single bus access: read, write, or NMOS dummy access |
| 2 | the φ2 peripherals | VIA, floppy controllers, ACIA, DTL 2000, MIDI, tape: moved forward by one cycle |
| 3 | the ULA | reads one 6-pixel cell (this cycle’s column), then advances the scan |
Why the processor comes before the ULA
The ULA and the 6502 share memory in two halves of the same microsecond, without ever competing for it: there is no stolen cycle to model. The order of these two halves was measured with an oscilloscope on a real machine and published in an unofficial ULA guide: on the rising edge of the clock, the 6502 makes its access; then the ULA’s read begins.
Observable consequence: a processor write at cycle c is seen by cell c. The test
test_cpu_write_visible_in_the_same_cell fails if the order is reversed.
No batches
With the micro-sequenced core, each cycle is a bus access. The clock callback therefore always
receives cycles = 1, and peripherals move one cycle at a time, never in batches. The test
test_peripherals_get_one_cycle_at_a_time would fail if a batch came back.
The order of the peripherals among themselves is written down too, in a separate table
(io_bus_tick_order[], decision 0003): the VIA first, which drives the
timers and the interrupt, then the tape, then the cards. This order has observable effects; it is
not changed without proof.
Never an empty call
The contract has a converse: each call costs the processor exactly one cycle. A micro-operation that returned without a bus access would move the ULA forward by a cycle that neither the processor nor the VIA had lived.
It happened, in 2.0.0-alpha.8. A branch not taken made its decision one cycle too late, in a micro-operation with no bus access. The processor’s counter stayed right, the 65x02 oracle was green at 100%. But the master clock was called once too often: about 410 cycles of scan lead per frame on the BASIC ROM, one frame of drift per second between the picture and the rest of the machine.
What the emulator carries
The scan counters used to be local variables of the main loop. Carrying them in emulator_t is what
lets anyone clock the machine:
| Field | Role |
|---|---|
raster_cycle | the current cycle in the frame (0 to 19,967) |
raster_rendered | the visible lines already drawn (0 to 224) |
raster_ng_line | the line already processed by the ULA-NG (0 to 311) |
raster_next_line | the cycle of the next line change |
emu_raster_pos() gives the beam position: PAL line (0 to 311) and cycle within the line (0 to 63).
Resuming mid-frame
Because the scan is in the emulator’s state, a save state (.ost) can be taken in the middle of
a frame. On loading, Phosphoric re-aligns the scan, redraws from RAM the lines already scanned, and
replays the cells already read on the current line to recover the attribute state. The test
make test-savestate-determinism checks that resuming gives the same raster stops, the same VIA and
the same RAM as an uninterrupted run.
Finer than the cycle
Only one place in the project goes below the cycle: the expansion bus of the LOCI cartridge, where a race is run between the card and the edge of the PHI2 clock. The motherboard chips do not need it, since they always win that race. See The LOCI.
The loop of a frame
Around the clock, the main loop (emulator_run()) runs each frame as named steps, in a fixed and
observable order: the frame’s instructions (with debugger, trace and profiler), then the LOCI, sound
with no display, external commands, fast loading, automatic typing, display and events, screenshots,
recording, and finally the pacing at 50 frames per second. A screenshot taken at a given cycle
therefore sees the screen after the automatic typing of the same frame, never before.