Eskiu en la Nintendo 3DS
La consola
La Nintendo 3DS monta un ARM11 (ARMv6k, 32 bits) como procesador
principal y una GPU PICA200 de pipeline casi fijo. El homebrew
para ella está muy maduro: devkitARM (un toolchain de GCC),
libctru para el hardware, y citro3d / citro2d
para la GPU. Los programas se distribuyen como un .3dsx reubicable
que el Homebrew Launcher carga en cualquier dirección.
Todo ese ecosistema da por hecho C. La idea era volver a Eskiu, un lenguaje de sistemas nativo basado en LLVM, un ciudadano de primera en la consola: compilar Eskiu al ARM11, enlazarlo contra libctru, y manejar la cámara y la GPU desde código Eskiu.
Lo que faltaba
Eskiu ya cross-compila (su kernel de ejemplo arranca en ARM64 bajo QEMU), pero la 3DS dejó ver un hueco concreto: el compilador solo registraba los backends de LLVM AArch64 y x86. La 3DS es ARM de 32 bits, un backend totalmente distinto. Tres cosas se atravesaban entre "compila" y "arranca", y cada una daba una falla diferente:
- El backend mismo.
lookupTarget("armv6k-...")no devolvía nada, porque el target de ARM de 32 bits nunca se inicializaba. Agregar las llamadasLLVMInitializeARM*y enlazar las librerías del backend de ARM fue el costo de entrada. - El ABI de punto flotante. libctru viene compilado en
hard-float (los argumentos van en registros VFP). Los primeros objetos de Eskiu
salían marcados como soft-float, y el linker se negaba a mezclarlos:
"uses VFP register arguments, ... does not." Ahora Eskiu detecta el
sufijo hard-float
hfen el triple (armv6k-none-eabihf) y activa el ABI hard-float, de modo que el objeto lleva el atributoTag_ABI_VFP_args;--mcpu mpcoreelige el VFPv2 del ARM11. - El modelo de reubicación. El primer binario que enlazó limpio
de todos modos crasheaba al arrancar: "undefined instruction, process:
loader." Eskiu emitía código independiente de posición con un GOT, pero a un
.3dsxlo reubica el loader de forma estática, y no hay enlazador dinámico que llene ese GOT, así que cada acceso a una variable global leía basura. Un modelo de reubicación estático (--reloc static) generó las reubicacionesR_ARM_ABS32que emite devkitARM, y el crash desapareció.
Cada una terminó siendo una capacidad de verdad, y general, del compilador: un
backend de ARM de 32 bits, y las banderas --mcpu,
--mattr y --reloc. Con ellas, un "hola mundo" escrito en
Eskiu enlaza contra libctru y arma un .3dsx.
Hablar con el hardware
Un programa necesita el SDK. Solo libctru son cientos de funciones y decenas de
structs, y citro3d/citro2d suman cientos más. Escribir a mano las declaraciones
extern es tedioso y una fuente de errores silenciosos y peligrosos: el
orden equivocado de un campo o un valor de enum mal puesto corrompe la memoria sin
avisar.
Por eso los bindings se generan, al estilo bindgen, a partir del
AST en JSON que clang saca de los headers del SDK. Las funciones
se vuelven declaraciones extern, los structs y unions se copian campo
por campo, los valores de enum se leen ya evaluados, y lo que no se puede mapear
con seguridad se salta con un // TODO en lugar de adivinarlo. Una sola
corrida sacó bindings tipados de Eskiu para libctru (gráficos, entrada, eventos de
GPU, consola), la cámara, citro3d (~140
funciones, 17 structs) y citro2d, del orden de 360
bindings tipados. El generador reprodujo por su cuenta, y bien, justo los
detalles que a mano es fácil equivocar: el orden raro {x, z, y} del
struct del giroscopio, un enum de eventos con valores implícitos, layouts
empaquetados.
Lo que el AST no alcanza a expresar, los muchos static inline y macros
de libctru que no dejan un símbolo enlazable, lo cubre una capa delgada hecha a
mano, igual que los usuarios de bindgen agregan un shim chico.
El demo
La app es una pieza compacta de realidad aumentada: el video de la cámara exterior
de fondo, y un modelo glTF (un shiba low-poly, ~4,300 triángulos)
dibujado encima para que se quede quieto en el mundo mientras inclinas la consola.
Las dos mitades del stack de AR son de ReactVision. ViroCore, el
motor del caso de estudio de ReactVision,
hace el render: carga y maneja el .glb igual que en las plataformas
donde ya corre, y en la 3DS lo único que cambia es el backend del renderizador, que
aquí apunta al PICA200 vía citro3d. El tracking usa el
motor de SLAM propio de ReactVision, el mismo que va a impulsar su
Web AR sin las APIs de WebXR, alimentado en la 3DS por el giroscopio de la consola.
La cámara se captura con doble buffer y se sube a una textura de la GPU en cada
frame, y el modelo se ilumina y se dibuja encima. Corre en hardware real de
3DS.
Su lógica se reescribió después en Eskiu: el bucle principal,
pasar del giroscopio a la rotación, la secuencia de render por frame, las matrices
del modelo y el manejo de memoria están todos en Eskiu, llamando a libctru, citro3d
y el backend de render de ViroCore directo para cada símbolo real, y pasando por el
shim solo para las llamadas de GPU llenas de static inline. El build
de Eskiu genera el mismo .3dsx, y también se verificó corriendo en la
consola.
El dogfooding mejoró el toolchain
Manejar hardware real es el tipo de presión que saca cosas a la luz. Cada uno de los tres bloqueos de arranque terminó siendo una capacidad del compilador que va más allá de la 3DS: ARM de 32 bits es un target soportado, y el modelo de reubicación, el CPU y las features se pueden elegir. El trabajo de cámara, hecho primero en C sobre la consola, dejó una referencia reutilizable y ya depurada: un bug en el tamaño de la unidad de transferencia, una ruta de error de buffer sin manejar, y una interacción de escritura de profundidad entre el fondo 2D y el modelo 3D, todo arreglado ahí antes del port.
El port también sacó dos huecos del lenguaje, y los dos ya están arreglados en el
compilador. extern cubría funciones pero no símbolos de datos, así que
un global de C como un handle de libctru no se podía usar directo; se agregaron las
variables extern, y ahora un extern int x; enlaza un
global de C. Y los literales float de Eskiu son double, algo que la
frontera con C rechazaba al enlazar; ahora el compilador
convierte el argumento float al tipo exacto del parámetro en la
llamada. Restricciones chicas y concretas, de las que solo se aprenden subiendo
código a un dispositivo real.
Así, el mismo lenguaje que arranca un kernel bare-metal en ARM64, incrusta un módulo de memoria ajustada en un motor de renderizado en C++ (ve el caso de estudio de ReactVision) y decodifica un formato criptográfico real sobre HTTP (ve el caso de estudio del INE) ahora compila al ARM11 de una consola de hace 12 años y maneja su cámara y su GPU, corriendo el mismo motor ViroCore sobre citro3d.
eskiu