Aller au contenu
Phosphoricwiki
Sommaire

L'architecture du code

Un émulateur grossit par sa périphérie : chaque carte, chaque option, chaque sortie ajoute du code. Voici comment Phosphoric est découpé, et comment il a été remis en ordre sans changer son comportement.

Dans cette page

Le dépôt en un coup d’œil

Phosphoric est écrit en C11, environ 45 000 lignes dans src/. Le cœur est découpé en couches sans dépendance circulaire :

DossierContenu
src/cpu/le 6502 : les deux moteurs, le micro-séquenceur, le calcul commun
src/memory/la mémoire, les ROM, la surcouche du Microdisc
src/video/l’ULA, l’ULA-NG, le rendu, le menu F1, les exports d’image
src/audio/le PSG, le mixage, les sources des cartes
src/io/le VIA, le clavier, la cassette, les puces série, le LOCI, le bus d’entrées-sorties
src/storage/les contrôleurs de disquette, les formats .dsk, Sedoric
src/cards/une carte d’extension par fichier
src/network/l’API HTTP, le serveur GDB, la diffusion Chromecast
src/cli/l’analyse de la ligne de commande et l’aide
racine de src/l’horloge maître, le débogueur, le protocole de pilotage, les sauvegardes d’état, main.c

Le Makefile est le seul système de construction (décision 0001). Les objets sont construits hors des sources, un dossier par configuration (build/<config>/ : SDL2, HTTPAPI, CAST, MIDI, TLS, TUI, LOCI, DEBUG, COVERAGE…) : changer d’options ne mélange jamais des objets.

Un audit, puis six chantiers

En juillet 2026, un audit d’architecture mesuré (graphe des inclusions, découpage de main.c, ordre des ticks, fils d’exécution) a conclu : le cœur est proprement modularisé, mais la périphérie s’entasse dans un main.c de 4 690 lignes, dont 1 487 pour la seule fonction main(). La dette est périphérique, pas centrale : elle se règle par extraction, sans toucher au moteur.

Le plan qui a suivi, mené de la 2.1 à la 2.19, a enchaîné des chantiers :

ChantierVersionCe qui a changé
A, hygiène2.1.3un seul système de construction
B, construction fiable2.2.0objets hors des sources, un dossier par configuration
C, main() découpée2.3.0de 2 043 à 32 lignes
D, contrat des périphériques2.4.0ticks et présence dans io_device_t, ordre explicite
E, qualité et sécurité2.8.0 à 2.9.xserveurs sur 127.0.0.1 par défaut, analyse HTTP fuzzée, assainisseurs en CI
F, monolithes découpés2.12.0control.c de 1 352 à 261 lignes, debugger.c de 2 395 à 637
G, cartes en modules2.13.0 à 2.19.0une carte = un fichier et une ligne

Chaque déplacement a été prouvé sans effet par cli_golden : 116 lignes de commande rejouées sur l’ancien et le nouveau binaire, zéro écart.

main() en 32 lignes

cli_parse_args() remplit une structure cli_opts_t, puis 14 étapes nommées main_setup_* montent la machine : le processus, phosphoric.cfg, la machine, les entrées et l’imprimante, les cartes série, les enregistrements, le LOCI, les outils de débogage, les captures, la ROM et le modèle, la cassette, les disquettes et la synthèse vocale, les services, les traces. emulator_run() fait tourner la machine, main_finish() écrit les sorties. Chaque étape renvoie −1 pour continuer, ou un code de sortie.

La boucle d’une trame

emulator_run() déroule chaque trame en étapes nommées, dans un ordre fixe : les instructions de la trame (avec débogueur, trace, profileur, corrections de la cassette), les crochets du LOCI, le son sans affichage, les commandes externes, le chargement rapide, la frappe automatique, la présentation et les événements (menu F1, affichage à l’écran, clavier, souris), les captures datées, la vidéo, les captures conditionnelles, les POKE injectés, la cadence (limiteur à 50 Hz ou --realtime), et les conditions de sortie.

Avant la 2.0.3, tout cela tenait dans une seule fonction de 1 300 lignes. Chaque étape est désormais une fonction static de 150 lignes au plus, qui partage un run_state_t.

Les commandes viennent d’une seule file

Le processeur, le VIA et la vidéo tournent dans un seul fil d’exécution, une trame après l’autre. Or des commandes arrivent d’ailleurs : --control sur l’entrée standard, l’API HTTP depuis son propre fil, le menu F1. Exécutées au milieu d’une instruction, elles corrompraient l’état.

D’où la décision 0002 : un seul interpréteur, control_dispatch(), et une file que la boucle de l’émulateur vide une fois par trame, à la frontière de trame. Une commande HTTP attend donc au plus 20 ms plus son exécution. À l’arrêt, la file est fermée avant le serveur, pour libérer les producteurs en attente.

Le contrat des périphériques et les cartes

Chaque périphérique du bus est une structure io_device_t : est-ce à moi ?, lecture, écriture (qui peut décliner vers le VIA), lecture sans effet de bord, section de sauvegarde, tick. Chaque carte est un module de src/cards/, listé une fois dans include/cards_list.h. Le détail, avec le schéma, est dans La page 3 et les cartes.

Deux règles de performance : l’ordre des ticks est une boucle déroulée (une boucle générique coûtait 22 % par trame), et les ticks des cartes sont des appels directs générés depuis la liste (une table de pointeurs coûtait 5 % d’instructions).

Changer de cartes : relancer

Une carte se branche au démarrage : sa ROM, sa surcouche, sa place sur le bus, son transport ouvert, et pour le LOCI en co-simulation, des fils d’exécution et un firmware en marche. Les rebrancher dans le même processus réutiliserait des états globaux. Décision 0004 : « Appliquer et redémarrer » dans le menu F1 remplace le processus (execv) par l’émulateur relancé avec la même ligne de commande, moins les options de cartes, plus celles des cartes choisies. C’est un vrai démarrage à froid, et le menu le dit.

Deux langues, un seul code

Le projet est écrit en français : commentaires, documentation, messages de commit. Il est publié aussi pour un public anglophone. Décision 0005 : deux branches miroir. main (française) est publiée sur Framagit et sur la forge du projet ; main-en est main plus des commits de traduction, publiée en main sur GitHub et Codeberg. Seuls les commentaires et la documentation diffèrent : le code, les chaînes affichées et les données sont identiques, ce que vérifie tools/check_comment_only_diff.py.

Les décisions sont résumées dans Les décisions.

Sources dans le dépôt : README.md, docs/architecture/AUDIT-ARCHITECTURE.md, docs/architecture/io-bus.md, docs/architecture/master-clock.md, docs/adr/README.md.