Portada editorial sobre TEXTO EN PORTADA: "0 FRAMES, MUCHAS PISTAS". Una pantalla de depuración ocupa el centro con una ejecución detenida en un callback nulo, mientras al fondo se distinguen una ISO de P

0 frames. 2.708.938 ciclos. Y bastante aprendido.

El objetivo parecía sencillo de formular y bastante menos sencillo de ejecutar: intentar llevar la versión de Grand Theft Auto: San Andreas para PlayStation 2 al navegador mediante recompilación estática, WebAssembly y ps2-web-recomp.

El resultado no fue un GTA jugable en una pestaña. Tampoco llegó a dibujar el primer frame. El proceso terminó después de 2.708.938 ciclos, detenido por un callback nulo. Pero entre el arranque y ese fallo hubo suficiente material para entender qué significa, en la práctica, hacer que un ejecutable de PS2 sobreviva fuera de su consola original.

El punto de partida: una ISO y un ejecutable de PS2

El experimento comenzó con la ISO original del juego. La primera tarea no consistía en “abrir el juego en el navegador”, sino en localizar qué parte del disco contenía el programa que debía ejecutarse. A partir de la información de arranque de la imagen se extrajo el ejecutable de la consola, todavía en código máquina del Emotion Engine.

Ese detalle es importante porque la recompilación estática no funciona como un emulador tradicional. En lugar de interpretar cada instrucción de MIPS mientras el juego corre, la herramienta analiza por adelantado el ejecutable, sigue el flujo de control y genera una representación compilable. El propio proyecto ps2-web-recomp describe este enfoque como una traducción del código del Emotion Engine a C++, que después se compila a WebAssembly y se enlaza con un runtime que recrea servicios y hardware de la PS2. La documentación del repositorio explica el proceso y sus límites.

En nuestro caso, el análisis produjo más de 300 unidades de código. No eran funciones limpias y ordenadas en el sentido habitual de un proyecto moderno, sino fragmentos generados a partir del binario original, con saltos, convenciones y dependencias que había que respetar sin disponer del código fuente.

Recompilar no es eliminar la PS2

La palabra “recompilación” puede dar una impresión equivocada. Traducir el ejecutable a código que WebAssembly pueda ejecutar no convierte automáticamente el juego en una aplicación web convencional. El código recompilado sigue esperando encontrar una máquina con determinados procesadores, registros, módulos, interrupciones, direcciones de memoria y servicios del sistema.

Por eso el runtime es tan importante como el código generado. Hay que ofrecer al programa una versión compatible del entorno de la PS2: memoria, hilos, llamadas del kernel, comunicación con el procesador de entrada y salida, carga de módulos, temporizadores y, más adelante, gráficos y sonido. El proyecto de referencia utiliza WebAssembly para ejecutar el código recompilado y WebGPU para la parte gráfica, mientras que algunos módulos de la IOP se ejecutan mediante una capa específica del runtime. El apartado técnico del proyecto detalla esa separación.

La primera señal positiva llegó cuando el runtime consiguió cargar módulos de PS2. No era todavía una pantalla, pero sí una confirmación de que varias capas estaban conectando: el ejecutable recompilado podía avanzar, solicitar componentes del entorno y recibir respuestas con un formato suficientemente parecido al esperado.

El fallo apareció antes que los gráficos

El proceso avanzó hasta quedar detenido por un callback nulo. Dicho de forma sencilla: una parte del sistema intentó invocar una función a través de un puntero que no contenía una dirección válida. El programa no llegó a la fase en la que tendría sentido hablar de texturas, DMA, rasterización o sincronización con el primer refresco vertical. Se rompió antes.

Ese orden de fallos resulta bastante revelador. Cuando un proyecto así no arranca, es tentador mirar inmediatamente hacia el renderizador. Sin embargo, el primer problema puede estar en una inicialización del kernel, en un módulo cargado parcialmente, en una estructura de memoria mal colocada o en una diferencia entre el comportamiento esperado y el que ofrece el runtime.

El número de ciclos ayudaba a acotar el problema, pero no lo explicaba. La ejecución era larga y suficientemente compleja como para que un simple registro del punto final no bastara. Había que averiguar cuándo se había corrompido el estado y quién había escrito el valor que acabaría convertido en un callback nulo.

GDB, watchpoints y el recorrido hacia atrás

La investigación se apoyó en GDB y en una combinación de puntos de ruptura, análisis de memoria y watchpoints. Un watchpoint permite detener la ejecución cuando cambia el valor de una expresión o de una zona concreta de memoria, en lugar de tener que adivinar qué función está provocando la escritura. El manual de GDB documenta este mecanismo como un punto de ruptura sobre datos.

La estrategia fue seguir el valor problemático hacia atrás. Primero había que confirmar dónde se producía la llamada inválida. Después, identificar qué estructura contenía el callback, en qué dirección de memoria estaba y en qué momento había dejado de tener el valor esperado. A partir de ahí, el análisis consistía en comparar el estado de la memoria con las hipótesis del runtime: qué módulo debía haber registrado la función, qué inicialización faltaba y si la dirección se había perdido al cruzar una de las capas entre el código recompilado y el entorno simulado.

Este tipo de depuración es menos espectacular que ver aparecer una pantalla, pero probablemente es más representativo del trabajo real. El objetivo no es encontrar “la línea que arregla todo”, sino reducir un fallo enorme a una secuencia concreta de hechos: una función se registra, una estructura se escribe, un valor cambia, una llamada se intenta ejecutar y el proceso se detiene.

Aparcado, no abandonado

El experimento queda aparcado por ahora. No porque el enfoque haya demostrado ser inviable, sino porque el siguiente tramo exige resolver cuestiones de infraestructura antes de poder medir algo tan visible como un frame. El callback nulo es un fallo localizado, pero no necesariamente el único obstáculo entre el arranque y el renderizado.

La principal lección ha sido que la recompilación estática no elimina la emulación: cambia dónde ocurre. Parte del código del juego se traduce por adelantado, pero el comportamiento de la consola sigue teniendo que reproducirse en el runtime. WebAssembly aporta un destino de ejecución para el código generado; no proporciona por sí solo el kernel, los módulos, la memoria ni las peculiaridades del hardware de la PS2.

También queda una conclusión más práctica sobre el debugging de sistemas: llegar lejos no significa llegar cerca. Cargar cientos de unidades de código y módulos de la consola es un avance real, pero no garantiza que el primer frame esté a una llamada de distancia. A veces la información más útil de un experimento es precisamente el lugar exacto donde deja de avanzar y las herramientas que permiten demostrar por qué.

En este caso fueron 0 frames, 2.708.938 ciclos y un callback nulo. No es el final del proyecto. Es, de momento, una frontera bien medida.

Fuentes consultadas