Cross-compiled DSperate (Nintendo DS) builds for spruceOS.
Upstream CI builds on ubuntu-24.04-arm. That runner's glibc is 2.39, so the release
tarball will not load on any spruce device — every aarch64 device we ship on is at
glibc 2.31 or older. This repo cross-compiles the same source on Ubuntu 20.04 multiarch
so the binary matches the floor our other universal binaries hold (ra64.universal,
flycast, pcsx_rearmed, vtree.aarch64), and fails the build if it ever drifts above it.
| Build | Output | Devices | Base |
|---|---|---|---|
| Universal | dsperate-aarch64.tar.gz |
Brick, Brick Pro, TSP, TSPS, Flip, Pixel2, Anbernic XX (BaseOS), RGB30 | Ubuntu 20.04 multiarch, GCC 10 (glibc 2.31) |
| A30 | dsperate-a30.tar.gz |
A30 (armhf, Cortex-A7) | A30 buildroot, GCC 13.2 (glibc 2.23) |
dsperate-a30 used to be interpreter + portable C++ renderer, and this section used to
explain at length why it could not be anything else: the recompiler emitted AArch64 and
pinned ~19 host registers (15 of them guest state), which does not shrink onto ARM32's 16
with SP/LR/PC spoken for, and the NEON kernels used 31 A64-only intrinsics. A new backend,
not a port.
Upstream wrote it. As of 1.6.0 there is src/core/cpu/jit/a32 and an ARMv7-expressible
kernel subset, and this target builds both — CMake says ARMv7 host; NEON kernels built from the A32 subset, and the jit test passes under qemu-arm alongside the other 14.
build-a30.sh no longer asserts they are off. It reads DSPERATE_JIT /
DSPERATE_NEON out of compile_commands.json and writes what it finds into BUILD_INFO,
because the hardcoded "jit: OFF / expect it to be slow" text outlived the fact and shipped
a tarball describing a recompiler build as an interpreter one.
JIT and NEON are both on: the cross toolchain reports CMAKE_SYSTEM_PROCESSOR=aarch64,
which is what DSperate keys DSPERATE_JIT / DSPERATE_NEON off. The build hard-fails if
either ends up off, because an interpreter-only binary still runs — just slowly, with
nothing in any log to say why.
# Trigger a build
gh workflow run build-all.yml
# ...against a specific upstream ref
gh workflow run build-all.yml -f dsperate_version=v0.1.0
# Download the output
gh release download beta-main -p "dsperate-aarch64.tar.gz" -R spruceUI/DSperate-sprucedsperate # the SDL2 frontend, what a device runs
dsperate-headless # headless harness (traces, frame dumps, benchmarks)
configs/ # upstream's ready-made settings files, incl. drastic.ini
# and advdrastic.ini (Knulli's "Advanced DraStic" hotkeys)
libs/ # non-device-provided shared libraries (usually absent)
BUILD_INFO # upstream commit, toolchain, flags
LICENSE README.md
DSperate needs a DS BIOS pair and firmware (bios9.bin, bios7.bin, firmware.bin)
dumped from your own console. None are included here, and none can be.
- glibc 2.31 floor, enforced. The build reads the maximum
GLIBC_2.xsymbol version out of both binaries and fails if it exceedsGLIBC_MAX(2.31). It also fails if anyGLIBCXX_/CXXABI_symbol survives, which would mean-static-libstdc++silently did not take. -pthreadis added by this build. DSperate usesstd::thread(the 3D raster workers, the engine-B line worker, the headless harness) but never linksThreads::Threads. Upstream never notices: glibc 2.34 folded libpthread into libc, so on a 24.04 runner the symbols are already there. At 2.31 they are inlibpthread.so.0and must be asked for.-Wl,--no-as-neededgoes with it because CMake puts linker flags ahead of the objects that reference them, and as-needed would drop the library again.- libstdc++ and libgcc are static. Several devices ship a libstdc++ older than this toolchain's. glibc stays dynamic — it is the floor being matched, not something to carry.
- SDL2 is built from source at 2.26.1, linked but not bundled. The device supplies
libSDL2 at runtime —
/usr/trimui/libon TrimUI,spruce/flip/libon Flip,App/PyUI/dll-malion the Anbernic XX line. focal's own libsdl2-dev is 2.0.10 and is deliberately not installed: DSperate needs 2.0.12 forSDL_TouchFingerEvent.windowID(which routes the touchscreen to the window a touch landed in) and 2.0.22 forSDL_SysWMinfo'swl.xdg_toplevel, whichdisplay_wl.cppreaches for behind aSDL_VERSION_ATLEAST(2,0,18)guard — so 2.0.18 through 2.0.20 would compile-fail on that member too. 2.26.1 clears both, and it is the oldest SDL2 spruce ships (2.26.1 / 2.26.5 / 2.30.10 / 2.32.0 across the tree), so compiling against it means we can never reach for an API the oldest device lacks — the same discipline as the glibc floor. SDL 2.26 vendors its own wayland protocol XML and needs onlywayland-client >= 1.18, which focal has exactly, so no wayland-protocols package is involved. The build still assertsSDL_VIDEO_DRIVER_WAYLANDsurvived configure — not because anything needs it now, but as a canary for options configure drops in silence. -DDSPERATE_WAYLAND=OFFon every target. Upstream 1.6.0 builds a Wayland dmabuf scanout tier whenever the SDL2 it compiles against has wayland in it, and ours does. The device SDL2s are KMSDRM or fbdev builds with none and no spruce device runs a compositor, so the tier would be dead code compiled against anSDL_SysWMinfolayout the runtime SDL2 does not share. Upstream's advice to integrators is to pass the flag rather than patch the tier out, which is also what retired our0001patch.- ALSA is dlopen'd by DSperate itself, so it is not a link-time dependency and does not need bundling.
-mtune=cortex-a55(override withARCH_FLAGS). Every spruce aarch64 device is a Cortex-A53 or A55;-mtunechanges scheduling only, never the instruction set, so the binary stays generic ARMv8-A. It is repeated in the link flags because the core is built with LTO, which recompiles at link time against whatever the link command names.- GCC 10, not focal's default 9 (
CROSS_GCC=9to switch). ccache is attached as a CMake compiler launcher, not as a wrapper on the compiler path: CMake finds the LTO archiver by looking forgcc-arnext to the compiler it was given, so a shim makesCMAKE_CXX_COMPILER_ARcome back NOTFOUND and the static library link dies. Thegcc-ar-10/gcc-ranlib-10wrappers are named explicitly as well, and configure is checked for a NOTFOUND archiver before the build starts. - Unit tests run under
qemu-aarch64-staticas part of the build, including the JIT tests that check the recompiler against the interpreter instruction by instruction.-f skip_tests=true(orSKIP_TESTS=1) turns them off.
docker build -t dsperate-builder .
mkdir -p output
docker run --rm -e DSPERATE_VERSION=main -v "$PWD/output:/output" dsperate-builderpatches/ is applied to every target — *.patch via git apply, *.py via python3. A
patch that no longer applies is fatal, so a drifted patch cannot produce a green build with
the feature silently missing.
It is empty right now: upstream 1.6.0 took both patches we carried — the no-wayland
dmabuf guard and DS_ROTATE — and patches/README.md records what they were and what
replaced them.
The A30's panel is mounted at 270°. DS_ROTATE is an environment variable, not a
command-line flag — unset, the image is sideways.
DS_ROTATE=270 ./dsperate game.nds \
--bios9 /mnt/SDCARD/BIOS/nds/bios9.bin \
--bios7 /mnt/SDCARD/BIOS/nds/bios7.bin \
--firmware /mnt/SDCARD/BIOS/nds/firmware.bin \
--fullscreenSince 1.6.0 this is upstream's own, and it belongs to the display-engine scaler tier: the
hardware layer scales a DS-resolution canvas and NEON kernels do the rotate, which is the
whole reason it exists on a Cortex-A7. The tier is auto wherever /dev/disp and /dev/fb0
answer, so nothing has to ask for it. --no-disp forces it off.
The one line to check on stderr, because it says which tier actually opened:
video: display-engine scaler, rot 270, layout …
Without it we are on the SDL renderer, which has no rotation of its own — DS_ROTATE is
read only on the disp path. video.disp: /dev/disp not usable; using SDL is the explicit
version of the same news, and disp: rotation <x> not supported means DS_ROTATE was
something other than 0/90/180/270.
Wiring DSperate into spruceOS as an Emu/NDS emulator option alongside the three DraStic
builds — that is a spruceOS-side change (config.json menuOptions, a launch case, and
the process lists in send_menu_button_to_retroarch, kill_ra_and_standard_emulators
and EMU_PROCESSES).