Serie: Aprender a programar con IA

Gráficos rosas, puntaje descontrolado, caída de RX. Cuatro bugs en un sábado.

Cuatro bugs de integración sin relación entre sí, un sábado.

Cuatro bugs, un sábado Cero subsistemas superpuestos, un fallo de integración EL ROSA DE LA MUERTE color de reserva por shader faltante PUNTAJE x2 dos suscriptores, un evento CAÍDA DE RX init antes de que el dispositivo esté listo CÁMARA PERDIDA carrera en el spawn asíncrono de la nave
Cada subsistema funcionaba solo. La integración es donde las demos viven o mueren.

Cuatro bugs en cuatro subsistemas que pasan todas sus pruebas unitarias: ese es el modo de fallo que lanza demos rotas. Atraparlo es la disciplina que convierte una muestra en algo que realmente puedes ejecutar en RakuAI.

Hay un tipo específico de sábado que cualquiera que haya lanzado una demo conoce. La demo funcionaba al final del fin de semana pasado. La retomaste este sábado por la mañana. Ahora está rota de cuatro formas distintas y ninguna de esas formas tiene relación con las otras.

Eso fue hoy. La demo era el juego Shooter que enviamos como una de las muestras canónicas. Los bugs esperaban como invitados a una fiesta a los que nadie invitó.

Qué estaba roto

La demo arrancaba. Esa era la buena noticia. Después de eso:

Los gráficos eran rosas. Cualquiera que haya trabajado en un renderizador en tiempo real conoce el rosa de la muerte. Es el color de reserva de «shader faltante», rosa fuerte, gritándote que algo en el pipeline de renderizado no encontró el shader que esperaba. Cada objeto generado proceduralmente en la demo se renderizaba en este color. Naves. Asteroides. Partículas. Todo rosa. Rosa por todas partes.

El puntaje se descontroló. Cada baja se registraba dos veces. El contador de bajas subía en pares. Veinte enemigos abatidos, «puntaje: cuarenta». Cuarenta enemigos abatidos, «puntaje: ochenta». Sobre el papel parece que el juego estaba siendo generoso, pero en la práctica la tabla de puntuaciones estaba inflada y la lógica de puntaje máximo se disparaba en umbrales completamente incorrectos.

El arranque en RX se caía. La demo en pantalla plana arrancaba sin problemas. En el momento en que conectaba unas gafas y cambiaba a modo RX, salida dura. Ningún backtrace aparecía a través del lanzador. El hilo del renderizador desaparecía antes de que el lanzador pudiera capturar un registro.

La cámara no fijaba el objetivo en la nave. Cámara orbital estándar en tercera persona, se supone que mantiene la nave en cuadro. Al arrancar, la cámara aparecía en el origen del mundo y se quedaba ahí. La nave era visible a través del vacío como un pequeño punto en la distancia.

Cuatro bugs. Cero superposición en subsistemas. El tipo de sábado por la mañana que decide si tienes el flujo de trabajo o no.

Qué causó cada uno

Quiero repasar cada uno porque cada uno enseña una lección distinta sobre cómo una base de código crece y se rompe en un flujo de trabajo impulsado por agentes.

Los gráficos rosas

Causa raíz: los handles de shader se buscaban por nombre de cadena en cada llamada de renderizado, y una refactorización durante las fiestas había movido la inicialización de la caché de shaders a un punto distinto en la secuencia de arranque. Los generadores procedurales ahora se estaban renderizando antes de que la caché de shaders se hubiera calentado. Estaban recibiendo de vuelta el fallback de handle nulo, que el renderizador diligentemente renderizaba como rosa fuerte.

La solución fue crear una utilidad ShaderCache explícita que cachea las referencias de shader resueltas en el primer uso y expone una API síncrona GetOrCreate que los generadores procedurales pueden llamar sin preocuparse por el orden de arranque. Cada generador procedural se actualizó para usarla. Rosa eliminado.

La lección: una suposición de orden de inicialización que vivía implícitamente en la secuencia de arranque se rompió por una refactorización que no marcó esa suposición. El orden implícito es frágil. La nueva caché hace el orden explícito y autorreparable.

El puntaje descontrolado

Causa raíz: el GameplayHUD estaba contando bajas del lado de la interfaz, y ScoreManager también estaba contando bajas del lado de la simulación. Ambos estaban escuchando la misma señal de evento de baja. Ambos sumaban a un campo de puntaje compartido. El puntaje se incrementaba dos veces por baja.

La solución fue elegir al propietario canónico del puntaje. ScoreManager es dueño de la verdad. El GameplayHUD lee de ScoreManager y renderiza. El incremento duplicado en el HUD se eliminó.

La lección: en un motor impulsado por eventos, dos suscriptores al mismo evento correrán ambos. Si ambos escriben al mismo estado, el estado estará mal en proporción a cuántos suscriptores escribieron. La solución es trazar una línea clara sobre quién es dueño de qué estado. Una vez trazada, el bug se vuelve imposible.

La caída al arrancar en RX

Causa raíz: el subsistema de RX intentaba inicializarse antes de que el dispositivo gráfico subyacente estuviera listo. En el arranque de pantalla plana, el orden resultaba funcionar porque el dispositivo gráfico siempre estaba listo para cuando el usuario presionaba «Start». En el arranque de RX, la detección del headset disparaba la ruta de inicialización de RX antes de que se confirmara que el dispositivo gráfico estaba listo. El subsistema de RX desreferenciaba un handle de dispositivo nulo. Caída.

La solución fue un XRBootstrap que detecta RX en el punto más temprano posible del arranque, difiere la inicialización real de RX hasta que el dispositivo gráfico confirma que está listo, y se apaga con elegancia si algo en la cadena reporta una falla. La caída ahora se convierte en un mensaje de error limpio y el usuario regresa a pantalla plana.

La lección: las caídas duras son el peor tipo de error porque matan el registro que te habría dicho qué pasó. Detectar el fallo antes y reportarlo con limpieza vale el costo de ingeniería.

La cámara que no fijaba el objetivo

Causa raíz: la cámara orbital intenta apuntar a la nave del jugador al arrancar. La nave la genera el generador procedural, de forma asíncrona, unos fotogramas después de que la cámara se inicializa. La cámara estaba buscando la nave en el primerísimo fotograma, no encontraba nada, y luego nunca volvía a intentarlo.

La solución fue darle a la cámara orbital un mecanismo de reintento. Busca la nave durante un número acotado de fotogramas antes de rendirse, y una vez que fija el objetivo, se queda fija. La cámara ahora aparece brevemente en el origen, y luego salta a la nave dentro del primer segundo de juego.

La lección: las condiciones de carrera entre sistemas que se inicializan en calendarios ligeramente distintos te van a morder. La solución es un pequeño reintento, acotado para que no dé vueltas para siempre, con un timeout explícito que produce un error útil si el reintento se agota.

Lo que aprendí sobre la demo Shooter específicamente

Tres cosas, todas incómodas.

Cada subsistema funcionaba de forma aislada. La integración estaba rota. El renderizador funcionaba. El sistema de puntaje funcionaba. El subsistema de RX funcionaba. La cámara funcionaba. La demo Shooter como experiencia integrada no funcionaba. Este es el tipo de fallo que se escapa de las pruebas unitarias siempre, porque las pruebas unitarias por naturaleza ejercitan las cosas de forma aislada.

La refactorización de fin de año durante el receso de fines de diciembre fue la causa próxima de al menos dos de estos bugs. La reorganización de la caché de shaders y la división de suscripción al evento de baja llegaron ambas en la ventana del sprint festivo. Ambas eran PRs correctas de forma aislada. Ambas rompieron la demo de formas no obvias. La lección es correr las demos integradas como parte de CI, no solo las pruebas unitarias. Ese trabajo llegó esta mañana como una PR separada.

La ruta de arranque de RX necesitaba su propio guardián de arranque. Las caídas duras son inaceptables porque matan el diagnóstico. Cada ruta de arranque que toca hardware no trivial necesita un guardián. Ahora tenemos eso para RX. No lo tenemos para audio espacial ni para el enlace Wi-Fi 7. Agregar ambos está en la cola para los próximos dos sábados.

Lo que socios y constructores deberían llevarse de esto

Si estás construyendo algo que integra múltiples subsistemas con dependencias de orden de inicialización, la lección es la misma que la demo Shooter acaba de aprender. Haz el orden de inicialización explícito. Haz que los modos de fallo sean elegantes. Corre la demo integrada en CI, no solo las pruebas unitarias.

Si eres un equipo indie pensando en adoptar Raku, la demo Shooter es una muestra real que probamos contra cada lanzamiento. Si se rompe, la ves romperse. Ese es el tipo de señal de disciplina pública que importa más que una lista de funciones.

Si eres un laboratorio de IA pensando en conectar tu modelo a una experiencia de RA, la ruta de arranque de RX ahora tiene un guardián defendible. Tu modelo no verá una caída dura al entrar. Obtendrás un reporte de error limpio si algo en la cadena falla. La infraestructura está en su lugar.

Cuatro bugs. Un sábado. La demo compila limpia ahora. A los invitados de la fiesta se los escoltó afuera.

De vuelta a construir.

Lanza sobre un runtime que ejecuta sus propias muestras

RakuAI prueba sus demos contra cada lanzamiento con CI de integración y guardianes elegantes de arranque de RX. Esa es la señal de disciplina pública que importa más que una lista de funciones. Empieza a construir sobre él.

← Todas las entradas