Skip to content
Phosphoricwiki
Contents

Accuracy, word by word

In September 2026, Phosphoric withdrew a claim it could not keep. It replaced it with a verifiable scale, where each word is backed by a test that would make it fall.

On this page

A withdrawn claim

Up to version 1.120, the project presented itself as “exact to the cycle”. An audit of 10 September 2026 showed that this was not true in the sense reference emulators give the word. The 6502 core ran an instruction in one block: accesses came out in the right order and the cycle total was right, but internal cycles were caught up at the end of the instruction, dummy accesses did not exist, and interrupts were taken between two instructions. The VIA received batches of cycles, the ULA drew a line in one go, the PSG ran at the audio output rate.

That is an honourable level, which the project named N2. But it is not “exact to the cycle”. The project withdrew the wording, wrote a scale, added an automatic safeguard, then made version 2.0.

The scale

Phosphoric's accuracy scale, and who is whereFour levels, from weakest to strongest. N1, counted per instruction: the total number of cycles per instruction is right. N2, ordered per bus cycle: each access falls on the right cycle. N3, stepped per cycle: the machine moves one cycle at a time, dummy accesses included. N4, below the cycle: clock phases are modelled. The CPU, the VIA timers, the ULA fetch and signal-level tape are at level N3; the floppy controller is N1+; the LOCI expansion bus is partly N4.Four levels, each proved by a testN1counted per instructionthe cycle totalper opcode is rightWD1793 FDC (N1+)N2ordered per bus cycleeach access on its cycle,internal cycles paddedold core(--cpu-legacy)N3stepped per cycleone cycle at a time, dummyaccesses included6502 CPU (100%)VIA timersULA fetchsignal-level tapeN4below the cycleφ1 and φ2 phases,races on the busLOCI bus (partial)The PSG, apart, is “clocked at hardware rate”: clock/8, integrated output (docs/ACCURACY.md).
Four levels, from weakest to strongest, and the level each component reaches.
LevelNameDefinitionThe test that proves it
N1counted per instructionthe cycle total per opcode is right; peripherals move in batchescompare the counters with the official table
N2ordered per bus cycleeach real access falls on the right cycle; internal cycles are caught up; IRQ at boundariesobserve a register with side effects
N3stepped per cyclethe machine moves one cycle: one access per cycle, dummy accesses included; everything moves together; IRQ on the penultimate cyclecompare the bus trace, cycle by cycle, with an external oracle
N4below the cyclethe cycle is split into φ1 and φ2 phases; races between cards and bus are modelleda race predicate and reproducible edge cases

Each component, at its level

ComponentLevelWhat is missing
6502N3nothing identified: 100.00% on 2,440,000 cases; the only accepted divergence, the 12 JAM opcodes stop the processor instead of locking the bus
VIA 6522N3 for the timersthe half cycle of the one-shot mode (N + 1.5)
ULAN3 for the fetchborder and blanking; the absolute scan/processor phase; the full-screen ULA-NG modes, rendered per line
PSGclocked at hardware ratethe value of the coupling capacitor, unreadable on the schematic
WD1793N1+fixed delays, flat image: no real byte loss nor CRC
TapeN3 at signal levelthe default remains the patched ROM, by choice
LOCI buspartial N4the constants are not yet calibrated on hardware

The VIA, measured on a real 6522

Since version 2.6.0, the VIA’s behaviour takes six fixes measured on a real 6522, brought over from the Neo6502Vic20 project. There they were validated by 61 VIC-20 test programs out of 61, taken from the VICE emulator, whose reference results come from the hardware: countdown starting on the cycle after the write, one-shot reloaded, delayed T2 mode, 8-bit T2 for the shift register, PB7 without DDRB. These programs do not yet run in Phosphoric’s CI; there the rules are locked by make test-io.

Version 2.7.0 added an exact lazy path: a cycle where nothing can happen is only counted, then applied in one block before the next access. The behaviour stays that of stepping cycle by cycle: make test-via-lazy replays 116 command lines against a build without this path, with no difference. The gain is 20 to 30%.

Allowed words, refused words

WordingStatus
“CPU core exact to the cycle”✅ since 1.124: 100% exact bus sequence, interrupts on the penultimate cycle
“cycle-stepped machine”✅ since 2.0.0-alpha.8: the master clock moves everything, never empty, and a save state is an exact resume point
“bus-cycle accurate” for the whole machine✅
“exact cycle counters per opcode (256/256)”✅
“exact to the cycle” alone, for the whole machine❌ the floppy controller is N1+, the VIA half cycle and the absolute phase are not modelled
“WD1793 exact to the cycle”❌ say “WD1793 clocked in cycles, flat image model”

The make test-docs-claims safeguard refuses any unqualified wording in the repository’s showcase documents. This wiki follows the same rule.

Dummy accesses reach the registers

This is the most surprising consequence of level N3, and it is intended: a dummy cycle is a real bus cycle, and a chip does not know that it is a dummy.

The dummy access that eats a byteBASIC's POKE writes with STA (zp),Y. In six cycles, the NMOS 6502 reads the opcode, the zero-page address, then the pointer, then makes a dummy read of the target address before writing to it. If the address is an ACIA's data register, that read consumes the received byte: an echo program written in BASIC loses bytes, on the real machine as in Phosphoric since 2.0.POKE 1021,C to an ACIA's data registerSTA ($zp),Y: 6 cycles, 6 bus accessescycle 1R PCreads the opcode$91cycle 2R PC+1reads the zero-page addresscycle 3R zpreads the pointer(low byte)cycle 4R zp+1reads the pointer(high byte)cycle 5R $03FDdummy readof the targetcycle 6W $03FDthe intendedwritethe received byte is consumed hereA dummy cycle is a real bus cycle: the chip does not know it is a dummy.Before 2.0, Phosphoric did not issue these accesses: it was more lenient than the hardware.Serial driver: write it in assembly, with absolute STA (no dummy cycle).
BASIC’s POKE makes a dummy read before writing: on an ACIA, it eats the received byte.

The same precautions apply to any register with destructive reads: ACIA 6551 and 6850, FIFO registers. Before 2.0, Phosphoric did not issue these accesses: it was more lenient than the hardware.

What was learned along the way

Two defects would not have been seen without changing method:

  1. The PSG envelope, declared conformant by a recomputation that assumed 16 states instead of 32. A wrong assumption does not show on re-reading, only when the signal is measured.
  2. The phantom cycle of a branch not taken. The processor oracle was green at 100%, but the master clock was called once too often, about 410 times per frame. An oracle that only looks at the processor does not prove the machine is synchronised. A save-state determinism test revealed it.

The cost

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

Datasheet audits

On top of the scale, the repository keeps an audit of conformance to datasheets (docs/HARDWARE_CONFORMANCE.md): each core is re-read line by line against the manufacturer’s datasheet. Each difference is classed as fixed (with its test) or accepted deviation (with its reason). For example, for the WD1793: Read Address wrote the wrong byte into the sector register; the step commands ignored the track update flag; Force Interrupt always raised an interrupt, whereas only the D8 variant does. All three are fixed.

Sources in the repository : docs/ACCURACY.md, docs/articles/v2-cycle-stepped.md, docs/specs/V2_CYCLE_ACCURACY.md, docs/HARDWARE_CONFORMANCE.md.