Tachibana

Phosphoric › Articles › Phosphoric 2.0

Phosphoric 2.0 : une machine cadencée au cycle — et l'annonce qu'on retire

Ce qu'on avait dit et pourquoi c'était faux, ce que la V2 a changé, ce que ça change de visible — et ce qu'on ne dit toujours pas.

Par bmarty · Phosphoric 2.0.4 · 12 septembre 2026

Jusqu'à la 1.120.0-alpha, Phosphoric se présentait comme « cycle-accurate ». Ce n'était pas vrai au sens que les émulateurs de référence donnent à ce mot. Cet article dit ce qui a été retiré, ce qui a été construit à la place, et où s'arrête la revendication. Les noms de tests et d'options sont ceux du dépôt.

1. Ce qu'on avait dit, et pourquoi c'était faux

Le cœur 6502 exécutait une instruction d'un bloc : les accès bus sortaient dans le bon ordre et le total de cycles par opcode était juste, mais les cycles internes étaient rattrapés par bourrage en fin d'instruction, les accès factices du NMOS n'existaient pas, et les interruptions étaient prises aux frontières d'instruction. Le VIA recevait des paquets de cycles, l'ULA rendait une ligne entière d'un coup, le PSG tournait au taux d'échantillonnage audio.

C'est un niveau honorable — nous l'avons nommé N2, « ordonné au cycle bus » — mais ce n'est pas « exact au cycle ». Nous avons retiré la formulation, écrit une échelle vérifiable (docs/ACCURACY.md : N1 à N4, chaque niveau adossé au test qui le prouve), et un garde-fou automatique (make test-docs-claims) qui refuse tout « cycle-accurate » non qualifié dans les documents de vitrine. Puis nous avons fait la V2.

NiveauNomDéfinition opérationnelle
N1Compté à l'instructionLe total de cycles par opcode est exact ; les périphériques avancent par paquets après l'instruction.
N2Ordonné au cycle busChaque accès bus réel tombe au bon cycle intra-instruction ; les cycles internes sont rattrapés en fin d'instruction ; IRQ aux frontières d'instruction.
N3Pas-à-pas au cycleL'unité d'avancement de la machine est le cycle : une action de bus par cycle (accès factices compris), périphériques en verrou ; IRQ/NMI au cycle pénultième.
N4Sous-cycle (φ1/φ2)Le cycle est subdivisé ; les courses setup/hold entre cartes et bus sont modélisées.

2. Ce que la V2 a changé

Un oracle d'abord

Avant de toucher au cœur, make test-cycle rejoue les vecteurs SingleStepTests/65x02 — 10 000 cas par opcode, avec la trace bus attendue cycle par cycle — et note quatre propriétés séparément (état final, total de cycles, sous-séquence bus, séquence bus exacte). Le point de départ, mesuré et publié : 44,26 % de séquences bus exactes. L'oracle a d'ailleurs révélé cinq défauts logiques du cœur, corrigés avant même de commencer.

Un cœur micro-séquencé

Chaque instruction devient un plan de micro-opérations, une par cycle, chacune faisant exactement son accès bus — y compris les accès factices (index page zéro, traversée de page, écriture-retour des RMW, lectures de pile mortes). Résultat : 100,00 % sur 2 440 000 cas. Les interruptions sont échantillonnées au cycle pénultième, ce qui donne gratuitement le drapeau I retardé de CLI/SEI/PLP et le détournement d'un BRK par une NMI. Les deux cœurs partagent le même calcul (drapeaux, BCD, opcodes illégaux) : seul l'ordonnancement diffère. L'ancien reste disponible (--cpu-legacy).

Une horloge maître

emu_cycle() fait avancer toute la machine d'un cycle, dans un ordre figé : le CPU fait son accès bus, les périphériques avancent d'un cycle — jamais d'un paquet —, puis l'ULA fetche la cellule de ce même cycle. C'est l'ordre mesuré sur le matériel par Mike Brown ; jusqu'en 2.0.1 l'ULA passait avant, et chaque split tombait une cellule trop à droite. La boucle principale ne calcule plus rien : elle demande.

Les composants, un par un

3. Ce que ça change de visible

4. Ce qu'on a appris en route

Deux défauts n'auraient jamais été vus sans changer de méthode.

L'enveloppe du PSG était déclarée conforme « par recalcul » — un recalcul qui supposait 16 états au lieu de 32. Une hypothèse fausse est invisible à la relecture ; elle ne se voit qu'en mesurant le signal. Les tests audio mesurent désormais fréquences et durées au lieu de comparer des octets.

Le second est plus instructif encore. Un branchement non pris prenait sa décision un cycle trop tard, dans une micro-op sans accès bus. Le compteur du CPU restait juste, donc l'oracle était vert à 100 %. Mais l'horloge maître avait été appelée une fois de plus : l'ULA avançait d'un cycle que ni le CPU ni le VIA n'avaient vécu — environ 410 fois par trame sur la ROM BASIC, soit une trame de dérive par seconde entre l'image et le reste de la machine. Un oracle qui ne regarde que le CPU ne prouve pas la synchronisation de la machine. Ce qui l'a révélé : un test de déterminisme des savestates, qui exigeait que les arrêts raster tombent exactement au même cycle.

5. Ce qu'on ne dit pas

« Phosphoric est exact au cycle » — non. Le FDC reste cadencé par des délais forfaitaires sur une image plate (pas de flux MFM, donc pas de vraie perte d'octet ni de CRC). Le repère horizontal, lui, n'est plus une convention : le compteur de l'ULA (mesuré) place la colonne 0 au count 0 — mais la phase absolue entre ce compteur et le CPU n'est observable sur un vrai ORIC qu'avec le « VSYNC hack », que nous n'émulons pas. Le demi-cycle du one-shot du VIA n'est pas représenté. Le chargement de cassette par défaut reste le patch ROM, par choix : même contenu chargé, 2,4× moins de cycles.

La formulation exacte autorisée, et le test qui ferait tomber chacune de ses lignes, sont dans docs/ACCURACY.md.

6. Coût

Le passage au cycle a coûté, sur la machine de référence à pleine vitesse, 491 → 611 µs par trame émulée : 3 % du budget de 20 ms. make test-bench refuse désormais tout dépassement de 5 %.

7. Depuis la 2.0.0

Suite de tests : 1 208 tests en 60 suites, 100 % au vert ; release automatique sur tag (binaire Linux, zip Windows) ; version navigateur en ligne.

Sources & liens

← Phosphoric · Fonctionnalités →