Captive: los 700 bytes de vacío que tumbaron a nuestro emulador de Amiga
Hay juegos difíciles de emular porque son exigentes. Y hay juegos difíciles de emular porque no le creen a nadie. Captive (Mindscape, 1990), el dungeon crawler de Antony “Ratt” Crowther, es de los segundos, y en el banco de pruebas de BMAmiga —el emulador bare-metal de Amiga del que les conté— se convirtió en el examen final: la pieza que ha exigido más al blitter, más al renderer y, para rematar, más a la disquetera. Esta es la crónica de lo que costó arrancarlo, que termina —aviso desde ya— en 700 bytes que no contienen nada.
Tres puertas antes del juego
La copia de bench es la versión crackeada, y eso forma parte del sabor. Primero aparece el cracktro —banner cromado del grupo, logo de CAPTIVE sombreteado con tramas, campo de estrellas— cortesía del “Amigados fileloader v2.6 by Ringo Star of Classic”. Espera un clic izquierdo, sondeando el pin PA6 de la CIA-A como se hacía en 1990. Después, un mensaje de CLI que es puro museo:
Press left mousebutton to continue … you don’t have to do this since you can use AMIGADOS formatted disks!!! And remember you MUST *NOT* save on CAPTIVE GAMEDISK!
Otro clic, y arranca lo interesante: “RATT-DOS Version 2, © 1990 Antony ‘Ratt’ Crowther” toma la máquina y empieza a correr el intro como una presentación de diapositivas, cargando imagen tras imagen desde el disquete.
Y decimos “arranca lo interesante” porque RATT-DOS es, literalmente, un
sistema operativo propio para leer el disco. No usa trackdisk.device: se
arma su propia rutina de carga en memoria baja, instala sus manejadores de
interrupción dentro de los slots de la tabla de vectores, programa el DMA
de disco a mano y salta a su propio reino. Cuando un juego trae su propio
DOS, tu emulador ya no emula “una computadora”: emula la computadora
exacta que ese código asume.

La pantalla de título de Captive: la mazmorra, el cautivo tirado en el suelo y su guardián esqueleto. Créditos de RATT — código y gráficos — y música de SuperCC en SoundTracker. Todo lo que se ve aquí se cargó del disco con el lector casero de RATT-DOS.
Lo que ya nos había costado
Captive no atacó por un solo frente. Antes de llegar a la disquetera, ya nos había enseñado dos lecciones caras:
El blitter en modo línea. Todo el arte del título está dibujado con
líneas tramadas — unas 19 operaciones de línea por frame en estado estable.
Cada detalle del modo línea del blitter tenía que estar bien: que BLTSIZE
lleva el mágico “2” del modo línea y no un ancho de fila, que el canal B se
recupera una vez por fila de píxeles (el juego lee una tabla de dither
$AAAA/$14AA por ese camino, con el minterm $CA y el bhold girando),
que el relleno no aplica en modo línea. Sin eso, el logo no se dibuja: cada
fila de la trama se colapsa.
La escalera. El título usa una geometría retorcida —lores, 4 bitplanes,
dual playfield, DDF $30/$D8— y nuestro contador de palabras de fetch le
daba 23 donde el hardware da 22. El síntoma es hermoso: la imagen se
escurría 32 píxeles por línea, como una escalera diagonal. Se midió el
desplazamiento fila por fila en el harness del host, se hizo A/B, y con 22
palabras quedó perfecta.
Con el título dibujándose y los dos clics funcionando, lo único que faltaba era que el juego cargara. Ahí empezó lo bueno.
El crimen
El síntoma en la bench: pantalla negra con barra superior verde y un
recuadro rojo — el propio dump de registros del manejador de errores
del juego, con el LED de DF0 parpadeando el número del track en retry
infinito. En el host, el CPU acababa descarrilando dentro de la tabla de
vectores: un jsr $68(a6) contra la tabla de thunks de RATT-DOS caía en un
rts a media instrucción, popeaba una dirección basura y se perdía
navegando por memoria. Crashes distintos en host y bench (el stub de arranque
se reubica según el tope de RAM: 1 MB de chip en el host, 512 KB + slow en
la bench) — la clase de detalle que te hace dudar de tu cordura.
RATT-DOS no es un formato: es un navegante
El reverse engineering pagó la primera gran revelación: RATT-DOS no usa un
formato de disco propietario. Es un lector compatible con ADOS con su
propio decodificador en software. El juego arma DSKSYNC=$4489, enciende
WORDSYNC, y hace una lectura cruda de 6400 palabras —una revolución
completa más un poco— directo a chip RAM. Su decodificador (copiado a
$614 desde el stub de arranque) escanea el flujo buscando la marca
$4489, fusiona el par odd/even siguiente, y lee lo que resulta: el campo
de información de ADOS, FF <track> <sector> <count>. Valida track, valida
bloque, decodifica checksum y datos.
¿Y qué hace cuando el sector no es el que quiere? No vuelve a buscar
desde el principio. Salta a ciegas. Se adelanta $430 bytes y re-escanea;
y cuando ve un sector con count==1 —el último de la revolución— salta
otros $200 extra y sigue escaneando hacia adelante, esperando caer en el
sector 0 de la siguiente vuelta del disco.
Ahí está el corazón del asunto: ese juego de 1990 no parsea el track, lo navega por estima. Sus offsets no están calculados sobre “donde están los bytes”, sino sobre “cuánto dura una revolución real de un disco real dando vueltas”. El software mide física — y tu emulador tiene que reproducir la física, no solo el payload.
Todos inocentes
Antes de encontrar al culpable, hubo que eliminar a todos los sospechosos, uno por measurement, no por intuición:
- Nuestro encoder MFM: declarado inocente al 100% — es byte a byte idéntico al de WinUAE para cada palabra entregada (solo difieren bytes de gap entre sectores, que nunca se entregan).
- El arranque de entrega con WORDSYNC: arranca en el segundo
$4489del par, exactamente como el modelo bit-serial de WinUAE. - El stepper de la cabeza: 2,335 pasos monitoreados, cero rechazos por rate-limit. La cabeza estaba exactamente donde el juego pidió.
- El latch de DSKBYTR, el tamaño de chip RAM, el manejo de interrupciones: verificados, no eran.
Para esto se construyó instrumentación nueva en el harness del host:
EXCLOG (cada excepción con vector y PC), DISKCAP (cada palabra de DMA
entregada con su dirección de chip), DISKSTEP (cada pulso de STEP, el
cilindro resultante y su veredicto de rate-limit), y un PCWATCH que
imprime los registros que el decodificador del juego usa en sus
comparaciones. En la Pi, los hooks son punteros nulos: la instrumentación
se compila a nada en el binario de producción. El flujo que lo clavó:
capturar el stream entregado, decodificar los headers FF/trk/sec/cnt
offline, poner watchpoints en los compares del decodificador — y el walk
mostró exactamente qué sector estaba siendo saltado.
Y sí: por el camino hubo una teoría hermosa y equivocada —una “desalineación de una palabra en el MFM crudo”— que convivió un buen rato con el expediente antes de caer desmentida por los datos. Las teorías muertas se quedan en el expediente, anotadas como tales. La honestidad con uno mismo es parte del método.
$630 > $440
El bug, al final, es aritmética pura. Nuestro track sintético medía
11 × 1088 = 11,968 bytes: sectores empacados espalda con espalda, sin
hueco alguno. Pero una revolución real PAL, a 2 µs por celda de bit, son
12,668 bytes. Esos 700 bytes sobrantes, en un disco formateado por el
propio trackdisk, no se reparten: forman el gap delante del sector 0,
donde termina una vuelta y empieza la siguiente.
Con nuestra pista sin gap, el salto de estima del juego después del último
sector ($430 + $200 = $630 bytes) superaba la distancia hasta el sync del
sector 0 (solo $440): el escaneo aterrizaba más allá de la marca y,
como solo escanea hacia adelante, el bloque 0 dejaba de existir para él.
Para siempre. Cada lectura de esos tracks reintentaba infinito, la máquina
de estados del juego se caía a basura, y su propio handler dibujaba el
recuadro rojo.
El fix es de una línea conceptual: MFM_TRACK_BYTES 11968 → 12668, con el
TRACK_LEAD_GAP de 700 bytes delante del sector 0, tal como lo deja el
formateador de trackdisk y tal como WinUAE tiende un track ADF. Con la
geometría real, el salto del juego aterriza ~100 palabras antes del
sector 0 y el escaneo lo encuentra.
La lección
Lo que me deja fascinado de este bug: nuestro emulador era byte-perfecto y estaba roto. Cada sector, cada sync, cada checksum — impecables, verificables, idénticos a WinUAE. Y aun así el juego no arrancaba, porque el software de 1990 no leía el disco como un archivo: lo leía como un cilindro que gira, con topología y con tiempos. Los 700 bytes de vacío no son “espacio desperdiciado” — son parte del contrato del medio. Un emulador que solo reproduce el contenido y no la geometría está condenado a fallar exactamente aquí: en el software que confiaba en que el mundo físico iba a estar donde lo dejó.
Es la misma filosofía que sostiene todo el proyecto — el bus con un slot por color clock, el blitter que se estira bajo seis bitplanes — llevada hasta el plato del disquete: se emula el hardware, no la intención del hardware. El vacío incluido.
Final feliz
Estado: arreglado. En el host, runs de 12,000 frames con el intro cargando imagen tras imagen y cero excepciones de CPU. En la bench, el camino completo: cracktro, dos gates de mouse, y RATT-DOS desfilando su presentación. Todo el ciclo de depuración — captura, decodificación offline, watchpoints, fix — vivió 90% en la laptop, sobre el mismo core que corre en la Pi, gracias a esa regla de arquitectura de no dejar que el emulador sepa qué máquina lo hospeda.
Captive ya no es el examen final. Es el trofeo del pasillo — y el recordatorio de que, en emulación, hasta la nada tiene dirección y tamaño.
← volver a posts