Tachibana

Phosphoric › Articles › Phosphoric 2.0

Phosphoric 2.0: a cycle-stepped machine — and the claim we withdraw

What we had said and why it was wrong, what V2 changed, what changes visibly — and what we still do not claim.

By bmarty · Phosphoric 2.0.4 · 12 September 2026

Up to 1.120.0-alpha, Phosphoric presented itself as “cycle-accurate”. That was not true in the sense reference emulators give the word. This article says what was withdrawn, what was built instead, and where the claim stops. Test and option names are those of the repository.

1. What we had said, and why it was wrong

The 6502 core executed an instruction as a block: bus accesses came out in the right order and the cycle total per opcode was right, but internal cycles were caught up by padding at the end of the instruction, the NMOS dummy accesses did not exist, and interrupts were taken at instruction boundaries. The VIA received batches of cycles, the ULA rendered a whole line at once, the PSG ran at the audio sample rate.

That is an honourable level — we named it N2, “bus-cycle ordered” — but it is not “cycle-exact”. We withdrew the wording, wrote a verifiable scale (docs/ACCURACY.md: N1 to N4, each level backed by the test that proves it), and an automatic guard (make test-docs-claims) that rejects any unqualified “cycle-accurate” in showcase documents. Then we did V2.

LevelNameOperational definition
N1Instruction-countedThe cycle total per opcode is exact; peripherals advance in batches after the instruction.
N2Bus-cycle orderedEach real bus access lands on the right intra-instruction cycle; internal cycles are caught up at the end of the instruction; IRQ at instruction boundaries.
N3Cycle-steppedThe machine's unit of advance is the cycle: one bus action per cycle (dummy accesses included), peripherals in lockstep; IRQ/NMI on the penultimate cycle.
N4Sub-cycle (φ1/φ2)The cycle is subdivided; setup/hold races between boards and bus are modelled.

2. What V2 changed

An oracle first

Before touching the core, make test-cycle replays the SingleStepTests/65x02 vectors — 10,000 cases per opcode, with the expected bus trace cycle by cycle — and scores four properties separately (final state, cycle total, bus subsequence, exact bus sequence). The measured, published starting point: 44.26 % exact bus sequences. The oracle also exposed five logic defects in the core, fixed before even starting.

A micro-sequenced core

Each instruction becomes a plan of micro-operations, one per cycle, each performing exactly its bus access — including the dummy ones (zero-page indexing, page crossing, RMW write-back, dead stack reads). Result: 100.00 % over 2,440,000 cases. Interrupts are sampled on the penultimate cycle, which gives for free the delayed I flag of CLI/SEI/PLP and the hijacking of a BRK by an NMI. Both cores share the same computation (flags, BCD, illegal opcodes): only the sequencing differs. The old one remains available (--cpu-legacy).

A master clock

emu_cycle() advances the whole machine by one cycle, in a fixed order: the CPU performs its bus access, peripherals advance by one cycle — never a batch —, then the ULA fetches the cell of that same cycle. That is the order measured on the hardware by Mike Brown; until 2.0.1 the ULA went first, and every split landed one cell too far right. The main loop no longer computes anything: it asks.

The components, one by one

3. What changes visibly

4. What we learnt on the way

Two defects would never have been seen without changing method.

The PSG envelope was declared conformant “by recalculation” — a recalculation assuming 16 states instead of 32. A false assumption is invisible on re-reading; it only shows when measuring the signal. Audio tests now measure frequencies and durations instead of comparing bytes.

The second is even more instructive. An untaken branch made its decision one cycle too late, in a micro-op with no bus access. The CPU counter stayed right, so the oracle was 100 % green. But the master clock had been called one more time: the ULA advanced by a cycle that neither the CPU nor the VIA had lived — about 410 times per frame on the BASIC ROM, i.e. one frame of drift per second between the picture and the rest of the machine. An oracle that only looks at the CPU does not prove the machine's synchronisation. What exposed it: a savestate determinism test, which required raster breakpoints to land on exactly the same cycle.

5. What we do not claim

“Phosphoric is cycle-exact” — no. The FDC is still timed by fixed delays on a flat image (no MFM stream, hence no real byte loss nor CRC). The horizontal reference, on the other hand, is no longer a convention: the (measured) ULA counter puts column 0 at count 0 — but the absolute phase between that counter and the CPU is only observable on a real ORIC with the “VSYNC hack”, which we do not emulate. The VIA one-shot half-cycle is not represented. Default cassette loading remains the ROM patch, by choice: same content loaded, 2.4× fewer cycles.

The exact authorised wording, and the test that would falsify each of its lines, are in docs/ACCURACY.md.

6. Cost

Going cycle-stepped cost, on the reference machine at full speed, 491 → 611 µs per emulated frame: 3 % of the 20 ms budget. make test-bench now rejects any overrun beyond 5 %.

7. Since 2.0.0

Test suite: 1,208 tests in 60 suites, 100 % green; automatic release on tag (Linux binary, Windows zip); browser version online.

Sources & links

← Phosphoric · Features →