Eskiueskiu v0.7.0
Case study

Eskiu on the Nintendo 3DS

A camera-and-gyroscope AR demo, a 3D model held steady over the live camera feed, running on a Nintendo 3DS with its logic written in Eskiu and its rendering going through ViroCore. Verified running on real 3DS hardware.
A New Nintendo 3DS running the ReactVision and Eskiu demo: a low-poly shiba 3D model composited over the live outer-camera feed on the top screen, with the demo's controls and per-frame diagnostics on the bottom screen.
The demo on a New Nintendo 3DS: the shiba composited over the outer-camera feed, with the Eskiu-driven UI and per-frame diagnostics (camera frames, transfer unit, GPU state) below.

The target

The Nintendo 3DS runs an ARM11 (ARMv6k, 32-bit) application processor and a fixed-function-ish PICA200 GPU. Its homebrew scene is mature: devkitARM (a GCC toolchain), libctru for the hardware, and citro3d / citro2d for the GPU. Programs ship as a relocatable .3dsx that the Homebrew Launcher loads at an arbitrary address.

Everything in that ecosystem assumes C. The goal was to make Eskiu, a native, LLVM-based systems language, a first-class citizen on the device: compile Eskiu to the ARM11, link it against libctru, and drive the camera and the GPU from Eskiu code.

What was missing

Eskiu already cross-compiles (its example kernel boots on ARM64 in QEMU), but the 3DS exposed a specific gap: the compiler only registered the AArch64 and x86 LLVM backends. The 3DS is 32-bit ARM, a different backend entirely. Three things stood between "compiles" and "boots", and each produced a different failure:

Those became real, general compiler features: a 32-bit ARM backend, and the --mcpu, --mattr, and --reloc flags. With them, a "hello world" written in Eskiu links against libctru and builds a .3dsx.

Talking to the hardware

A program needs the SDK. libctru alone is hundreds of functions and dozens of structs, and citro3d/citro2d add hundreds more. Hand-writing extern declarations is tedious and a place for silent, dangerous mistakes: a wrong struct field order or enum value corrupts memory with no error.

So the bindings are generated, bindgen-style, from clang's JSON AST of the SDK headers. Functions become extern declarations, structs and unions are reproduced field-for-field, enum values are read already-evaluated, and anything that can't be mapped safely is skipped with a // TODO rather than guessed. One run produced typed Eskiu bindings for libctru (graphics, input, GPU events, console), the camera, citro3d (~140 functions, 17 structs), and citro2d, on the order of 360 typed bindings. The generator reproduced automatically exactly the details that are easy to get wrong by hand: the gyroscope struct's unusual {x, z, y} field order, an event enum whose values are implicit, packed layouts.

What the AST can't express, libctru's many static inline and macro helpers that have no linkable symbol, is bridged by a thin hand-written layer, the same way bindgen users add a small shim.

The demo

The application is a compact piece of augmented reality: the outer camera feed as the background, and a glTF model (a low-poly shiba, ~4,300 triangles) drawn over it so it sits still in the world as you tilt the console. Both halves of the AR stack are ReactVision's own. ViroCore, the engine from the ReactVision case study, does the rendering: it loads and drives the .glb the same way it does on the platforms it already ships to, and only the renderer's backend changes on the 3DS, retargeted to the PICA200 through citro3d. Tracking is ReactVision's in-house SLAM engine, the same one slated to power its Web AR without the WebXR APIs, fed on the 3DS by the console's gyroscope. The camera is captured double-buffered and uploaded to a GPU texture each frame, and the model is lit and drawn on top. It runs on real 3DS hardware.

Its logic was then rewritten in Eskiu: the main loop, the gyroscope-to-rotation integration, per-frame render sequencing, the model's matrices, and memory management all live in Eskiu, calling libctru, citro3d, and the ViroCore renderer backend directly for every real symbol and going through the shim only for the inline-heavy GPU calls. The Eskiu build produces the same .3dsx, and it too was verified running on device.

Dogfooding made the toolchain better

Driving real hardware is the kind of pressure that finds things. Each of the three boot blockers turned into a compiler capability that outlives the 3DS: 32-bit ARM is a supported target, and the relocation model, CPU, and features are selectable. The camera work, done first in C on the device, produced a reusable, debugged reference: a transfer-unit sizing bug, an unhandled buffer-error path, and a depth-write interaction between the 2D background and the 3D model, all fixed there before the port.

The port surfaced two language gaps, and both are now fixed in the compiler. extern covered functions but not data symbols, so a C global like a libctru handle could not be reached directly; extern variables were added, and a extern int x; now binds a C global. And Eskiu's float literals are double, which the C float boundary rejected at link time; the compiler now narrows a float argument to the parameter type at the call site. Small, concrete constraints, learned by shipping code onto a device.


So the same language that boots a bare-metal ARM64 kernel, drops a memory-tight module into a C++ rendering engine (see the ReactVision case study), and decodes a real cryptographic format over HTTP (see the INE case study) now compiles to a 12-year-old handheld's ARM11 and drives its camera and GPU, running the same ViroCore engine through citro3d.