JESVS

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.

Pantalla de título de Captive: una mazmorra con el cautivo en el suelo, un guardián esqueleto y los créditos del juego

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 $4489 del 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