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.
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:
lookupTarget("armv6k-...") no
devolvía nada, porque el target de ARM de 32 bits nunca se inicializaba. Agregar
las llamadas LLVMInitializeARM* y enlazar las librerías del backend
de ARM fue el costo de entrada.hf en el triple (armv6k-none-eabihf)
y activa el ABI hard-float, de modo que el objeto lleva el atributo
Tag_ABI_VFP_args; --mcpu mpcore elige el VFPv2 del
ARM11..3dsx lo 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 reubicaciones R_ARM_ABS32 que 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.
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.
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.
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.