Tachibana

Phosphoric › Artículos › Phosphoric 2.0

Phosphoric 2.0: una máquina cadenciada por ciclo — y la afirmación que retiramos

Lo que habíamos dicho y por qué era falso, lo que cambió la V2, lo que cambia de forma visible — y lo que seguimos sin afirmar.

Por bmarty · Phosphoric 2.0.4 · 12 de septiembre de 2026

Hasta la 1.120.0-alpha, Phosphoric se presentaba como «cycle-accurate». No era cierto en el sentido que los emuladores de referencia dan a esa palabra. Este artículo dice qué se retiró, qué se construyó en su lugar y dónde termina la afirmación. Los nombres de pruebas y opciones son los del repositorio.

1. Lo que habíamos dicho, y por qué era falso

El núcleo 6502 ejecutaba una instrucción en bloque: los accesos al bus salían en el orden correcto y el total de ciclos por opcode era justo, pero los ciclos internos se recuperaban por relleno al final de la instrucción, los accesos ficticios del NMOS no existían, y las interrupciones se tomaban en las fronteras de instrucción. El VIA recibía lotes de ciclos, la ULA renderizaba una línea entera de golpe, el PSG giraba a la frecuencia de muestreo de audio.

Es un nivel honorable — lo llamamos N2, «ordenado al ciclo de bus» — pero no es «exacto al ciclo». Retiramos la formulación, escribimos una escala verificable (docs/ACCURACY.md: N1 a N4, cada nivel respaldado por la prueba que lo demuestra) y un guardián automático (make test-docs-claims) que rechaza cualquier «cycle-accurate» sin matizar en los documentos de escaparate. Luego hicimos la V2.

NivelNombreDefinición operativa
N1Contado por instrucciónEl total de ciclos por opcode es exacto; los periféricos avanzan por lotes tras la instrucción.
N2Ordenado al ciclo de busCada acceso real al bus cae en el ciclo intrainstrucción correcto; los ciclos internos se recuperan al final de la instrucción; IRQ en las fronteras de instrucción.
N3Paso a paso por cicloLa unidad de avance de la máquina es el ciclo: una acción de bus por ciclo (accesos ficticios incluidos), periféricos en sincronía; IRQ/NMI en el penúltimo ciclo.
N4Subciclo (φ1/φ2)El ciclo se subdivide; se modelan las carreras setup/hold entre tarjetas y bus.

2. Lo que cambió la V2

Primero un oráculo

Antes de tocar el núcleo, make test-cycle reproduce los vectores SingleStepTests/65x02 — 10 000 casos por opcode, con la traza de bus esperada ciclo a ciclo — y puntúa cuatro propiedades por separado (estado final, total de ciclos, subsecuencia de bus, secuencia de bus exacta). El punto de partida, medido y publicado: 44,26 % de secuencias de bus exactas. El oráculo reveló además cinco defectos lógicos del núcleo, corregidos antes incluso de empezar.

Un núcleo microsecuenciado

Cada instrucción se convierte en un plan de microoperaciones, una por ciclo, cada una haciendo exactamente su acceso al bus — incluidos los ficticios (indexación en página cero, cruce de página, reescritura de los RMW, lecturas muertas de pila). Resultado: 100,00 % en 2 440 000 casos. Las interrupciones se muestrean en el penúltimo ciclo, lo que da gratis la bandera I retardada de CLI/SEI/PLP y el desvío de un BRK por una NMI. Ambos núcleos comparten el mismo cálculo (banderas, BCD, opcodes ilegales): solo difiere la secuenciación. El antiguo sigue disponible (--cpu-legacy).

Un reloj maestro

emu_cycle() hace avanzar toda la máquina un ciclo, en un orden fijo: el CPU hace su acceso al bus, los periféricos avanzan un ciclo — nunca un lote —, luego la ULA hace fetch de la celda de ese mismo ciclo. Es el orden medido en el hardware por Mike Brown; hasta la 2.0.1 la ULA iba antes, y cada split caía una celda demasiado a la derecha. El bucle principal ya no calcula nada: pide.

Los componentes, uno a uno

3. Lo que cambia de forma visible

4. Lo que aprendimos por el camino

Dos defectos nunca se habrían visto sin cambiar de método.

La envolvente del PSG se declaraba conforme «por recálculo» — un recálculo que suponía 16 estados en lugar de 32. Una hipótesis falsa es invisible en la relectura; solo se ve midiendo la señal. Las pruebas de audio miden ahora frecuencias y duraciones en lugar de comparar bytes.

El segundo es aún más instructivo. Un salto no tomado decidía un ciclo demasiado tarde, en una microoperación sin acceso al bus. El contador del CPU seguía siendo justo, así que el oráculo estaba en verde al 100 %. Pero el reloj maestro se había llamado una vez más: la ULA avanzaba un ciclo que ni el CPU ni el VIA habían vivido — unas 410 veces por cuadro en la ROM BASIC, es decir, un cuadro de deriva por segundo entre la imagen y el resto de la máquina. Un oráculo que solo mira el CPU no demuestra la sincronización de la máquina. Lo que lo reveló: una prueba de determinismo de los savestates, que exigía que las paradas de raster cayeran exactamente en el mismo ciclo.

5. Lo que no decimos

«Phosphoric es exacto al ciclo» — no. El FDC sigue cadenciado por retardos fijos sobre una imagen plana (sin flujo MFM, así que sin verdadera pérdida de byte ni CRC). La referencia horizontal, en cambio, ya no es una convención: el contador de la ULA (medido) sitúa la columna 0 en el count 0 — pero la fase absoluta entre ese contador y el CPU solo es observable en un ORIC real con el «VSYNC hack», que no emulamos. El medio ciclo del one-shot del VIA no está representado. La carga de casete por defecto sigue siendo el parche ROM, por elección: mismo contenido cargado, 2,4× menos ciclos.

La formulación exacta autorizada, y la prueba que haría caer cada una de sus líneas, están en docs/ACCURACY.md.

6. Coste

El paso al ciclo costó, en la máquina de referencia a plena velocidad, 491 → 611 µs por cuadro emulado: 3 % del presupuesto de 20 ms. make test-bench rechaza ahora cualquier exceso superior al 5 %.

7. Desde la 2.0.0

Batería de pruebas: 1 208 pruebas en 60 suites, 100 % en verde; release automática por etiqueta (binario Linux, zip Windows); versión para navegador en línea.

Fuentes y enlaces

← Phosphoric · Funcionalidades →