Tachibana

Neo6502 › Artículos › AsteroNeo

Portar: de Astéroric a AsteroNeo

El mismo juego, el mismo C, otra máquina: lo que cambia al pasar de un 6502 a 1 MHz y un bitmap casero a un 65C02 a 6,25 MHz cuyo RP2040 traza las líneas.

Por bmarty · Tachibana · 16 de septiembre de 2026

Astéroric es nuestro clon de estudio de Asteroids (Atari, 1979) para el Oric-1 de 48 K: la lógica de la ROM arcade rev 4 en C cc65, un trazado vectorial XOR casero, 25 Hz en 20 000 ciclos por cuadro — el artículo anterior cuenta las cuarenta fases que hicieron falta. AsteroNeo es su port al Neo6502 de Olimex: un W65C02S real a 6,25 MHz, un RP2040 que hace de chipset y una API en $FF00. Decisiones de encuadre: repositorio dedicado, firmware oficial en modo 0 (320×240, 256 colores), campo a pantalla completa, C cc65 para la lógica y ca65 para la capa API, 30 Hz, render XOR por la API, Phosphoneo como oráculo de los tests y el emulador oficial neo para la prueba de humo. Dos sprints después, el juego es jugable en emulación; esto es lo que cambió y lo que se resistió.

Oric-1 → Neo6502, en una tabla

Oric-1 (Astéroric)Neo6502 (AsteroNeo)
HIRES 240×200 mono, Bresenham XOR caseromodo 0 320×240, líneas y píxeles XOR de la API (5,1 / 5,2 / 5,5)
asteroides como bitmaps prerrenderizados (blit)polígonos derivados de las formas arcade, suavizados (24/16/10 vértices), trazados por el RP2040
teclado: matriz VIA/PSGKey Status por código HID (1,2), cinco acciones remapeables
AY-3-8912 bajo IRQ del Timer 1generador de 4 canales cuadrada/ruido, colas de notas (8,7)
VSync/Timer 1, 25 Hzcontador de cuadros a 60 Hz (5,37), paso de juego a 30 Hz
coordenadas de 8 bitscoordenadas int (320 > 255), integración 8.8 con acarreo

1. Lo que no se mueve

La lógica del juego — oleadas, fragmentación de asteroides, IA de los dos platillos, hiperespacio, récords — se conserva tal cual: es la transposición del desensamblado arcade y no depende de ningún hardware. Los siete módulos C de Astéroric (game, asteroids, ufo, hud, font, title, keys) pasan de una máquina a la otra. Todo lo que tocaba el hardware Oric — HIRES, VIA, AY — se sustituye por una capa Neo6502 de cuatro archivos: neo_gfx.s (trazado), neo_time.c (cadencia), neo_input.c (teclado), neo_sound.c (sonido).

2. El trazado: del Bresenham casero a las líneas del firmware

En el Oric la línea costaba ~97 ciclos por píxel, y fue ella la que dictó los 25 Hz. En el Neo6502 la línea la traza el RP2040: el 65C02 deposita las coordenadas y llama a la función 5,2. Medida en cosimulación sobre el firmware real (Phosphoneo + libemul), una línea cuesta ≈ 22 ciclos 65C02 (544 pasos ARM), un píxel ≈ 5 ciclos — sea cual sea la longitud. El presupuesto cambia de naturaleza: ya no se cuentan píxeles, se cuentan llamadas.

La sutileza está en otra parte. La línea del firmware (algoritmo EFLA) es semiabierta: traza [P0, P1[ y excluye su punto final. En XOR, un polígono trazado segmento a segmento pierde entonces un vértice de cada dos, o lo enciende dos veces. neo_gfx.s la convierte en dos primitivas: una línea cerrada (línea + píxel final) y una línea «abierta» ]P0, P1], trazada al revés, para que cada vértice se XOR-ee exactamente una vez. El programa de prueba t_xor comprueba la idempotencia: trazar dos veces un polígono deja la pantalla en blanco.

3. Las coordenadas: 320 no cabe en un byte

Astéroric vivía en 8 bits: 240 columnas, todo cabía en un registro. A 320 píxeles de ancho, las posiciones pasan a int, y la integración de velocidades en coma fija 8.8 se hace con acarreo (phys.c). La colisión toroidal y el wraparound por duplicación (un objeto a caballo de un borde se traza dos veces) se reescriben sobre 320×240 — y las constantes de spawn, wrap y HUD se adaptan al campo a pantalla completa.

4. La cadencia: 30 Hz sobre cuadros a 60 Hz

Ya no hay VSync ni Timer 1: el firmware expone un contador de cuadros a 60 Hz (5,37). El paso de juego se fija en 30 Hz — dos cuadros — y todas las constantes calibradas para 25 Hz se recalculan. Con la latencia medida, el sprint 1 mantenía la cadencia con 3 pasos de retraso de 255.

Luego el playtest pidió asteroides más redondeados: las siluetas Atari suavizadas por gen_shapes.py pasan de 11-13 a 24/16/10 vértices. El bucle C que encadenaba los segmentos costaba ~1 500 ciclos por segmento y el juego caía a 200 pasos de retraso de 255. El trazado de polígonos se reescribe en ensamblador (poly_xor: recorte por segmento, segmentos semiabiertos): 2 pasos de retraso de 255. En esta máquina el cuello de botella no es el píxel, es el envoltorio C alrededor de cada llamada.

5. Teclado y sonido

La matriz VIA/PSG del Oric deja paso al estado de cada tecla por código HID (1,2). Las cinco acciones — rotación, empuje, hiperespacio, disparo, salir — se pueden remapear a cualquier tecla con nombre desde la pantalla CONTROLS; el mapeo vive en RAM. El sonido deja el AY y su IRQ por el generador de 4 canales del firmware (8,7): cada efecto de Astéroric se recrea como cola de notas cuadrada/ruido, jingle incluido. Sin envolvente hardware, las caídas son escaleras de volumen; validadas de oído en neo el 16 de septiembre — en emulador, no en placa.

6. Dos puntos de atención en la cadena de herramientas

cc65 2.19 y OptStackOps. Los asteroides aparecían como una maraña de segmentos mientras la misma lógica compilada con gcc en el host era correcta. El optimizador, al fusionar operaciones en la pila C, indexaba la tabla del número de vértices con el byte bajo de un puntero en lugar del identificador de forma. Solución: --disable-opt OptStackOps en todo el proyecto (unos ciclos por llamada, sin efecto a 6,25 MHz) y lectura de n antes de los punteros. Lección: cualquier pasada de optimización que se active debe volver a pasar las capturas de referencia.

Salir limpiamente. Un jmp ($FFFC) para volver a NeoBASIC relanzaba el juego: el vector de reset está parcheado a la dirección de ejecución del .neo. La salida pasa ahora por la API (1,3: recargar NeoBASIC) y luego jmp (0), como el núcleo al reset; el test t_exit teclea PRINT 6*7 tras salir y espera 42.

7. Los tests

Tres niveles, todos en make test. En el host, la lógica portable se compila con gcc contra stubs que reproducen la EFLA del firmware (integración 8.8, colisiones, tablas de formas, idempotencia XOR, wraparound, fragmentación, tabla de teclas). En el objetivo, Phosphoneo ejecuta escenarios deterministas — pantalla de título, partida, CONTROLS, programas t_line, t_xor, t_exit — y las capturas se comparan bit a bit con las referencias; la cadencia se mide bajo la latencia registrada en cosimulación. Por último, una prueba de humo en el emulador oficial. Esa red es la que atrapó el caso de cc65, los vértices perdidos y el falso retorno a NeoBASIC.

8. Lo que no decimos

No se ha conectado ninguna placa Neo6502: todo se verifica en Phosphoneo, en cosimulación con el firmware real y en neo. La latencia real de las llamadas en placa queda por medir (plan B: sprites hardware). El sonido está validado de oído solo en emulador. El sprint 2 está en curso: mando USB, opción de 60 Hz, persistencia de récords y del mapeo en SD/USB. El binario asteroneo.neo (v0.2.0) está publicado en GitHub y Framagit; el CHANGELOG del repositorio es el que manda.

Fuentes y enlaces

← Neo6502 · Prophet → · Neo6502 →