The Nintendo Switch side of Arkchemy: it takes
the C emitted by conquertron and
builds it into a real Switch homebrew .nro.
No PowerPC emulation runs at runtime — the recompiled game is compiled to native ARM64 and linked against libnx like any other homebrew app.
This repository contains no game code and no game assets. You supply your own legally-dumped copy.
| Path | What it is |
|---|---|
game/ |
The full-game build: Makefile, regenerate.sh, and source/main.c (the harness/entry point). The recompiled generated_*.c are build artefacts and are not committed. |
native/ |
Smaller native test programs |
gx2_test/ |
GX2 → deko3d graphics experiments |
src/start.s |
Bare-metal ARM64 startup used by the earliest no-libnx milestone |
link.ld, build.sh |
Linker script and build script for that same bare-metal path |
test-results/ |
Dated real-hardware test logs |
docs/ |
Historical milestone notes |
Requires devkitPro with devkitA64, libnx, and
deko3d, plus a checkout of
conquertron — it supplies
ppc_runtime.h, the cafeos_*.h shims and cafeos_state.c that the
generated C compiles against.
You do not have to fetch it yourself. tools/fetch-conquertron.sh resolves
conquertron in this order:
- an explicit
make CONQUERTRON=/path/to/conquertron - a sibling checkout next to this repo
- otherwise, a copy fetched into
deps/conquertron, pinned byconquertron.lock
A sibling checkout deliberately beats the fetched copy. conquertron and jouster are developed together, and a fetched tree silently shadowing local recompiler edits would mean the next hardware run tests the wrong code. An already-present copy is used with no network access at all, so builds work offline.
The side-by-side layout, if you want it:
some-dir/
conquertron/
jouster/
Two helpers, since a sibling and a vendored copy look identical in build output and building against the wrong one is silent:
make -C game conquertron-info # resolved path, commit, local modifications
make -C game conquertron-update # update deps/conquertron and re-pin the lockexport DEVKITPRO=/opt/devkitpro
cd game
make -jIf your checkout is laid out differently, point it at conquertron explicitly:
make -j CONQUERTRON=/path/to/conquertronThat produces game/Jouster.nro. Copy it to your Switch's SD card under
/switch/Jouster/.
The generated C is not in the repository — game/regenerate.sh drives
conquertron against your own dump to produce it before building.
The devkitPro toolchain also ships as a container image, which builds everything here without touching your system package manager (podman or docker, run from the directory that holds both checkouts):
podman run --rm -v "$PWD":/work:z -w /work/jouster/native --userns=keep-id \
docker.io/devkitpro/devkita64 \
bash -lc 'export PATH=$DEVKITPRO/devkitA64/bin:$PATH; make -j'Swap native for gx2_test or game to build the others; the image
already carries libnx and deko3d, so nothing else needs fetching.
game/source/main.c is not a normal entry point; it is a diagnostic harness.
It runs the recompiled game on a worker thread while the main thread logs
periodic checkpoints to sdmc:/switch/Jouster/game-results.log, including
memory-allocation events, watched function arguments, and a stall detector
that exits early if execution stops making forward progress. Test duration can
be overridden by writing a number of seconds to
sdmc:/switch/Jouster/test-seconds.txt.
Those logs are how nearly every bug in this project has been found; dated
examples live in test-results/.
Early, and not playable. The engine starts, runs its 114 static initialisers and gets partway through its reflection registration before stalling, so it never reaches level or asset loading.
What does work:
- Video and audio playback.
bash.movplays start to finish, 526 frames at 29.97fps with audio in sync, decoded by ffmpeg from devkitPro's portlibs. A native Bink shim also serves the game's ownBinkOpen/BinkDoFrameAPI, delivering all 720 luma rows per frame. - The Wii U boot presentation — the
bootTvTex.tgasplash and the 18.9-secondbootSound.btsndjingle, from the game's ownmeta/files. - Filesystem access. All 22 coreinit FS imports the game calls are
implemented, and a boot self-test opens
/vol/content/alchemy.xml,content:/alchemy.xml, a bare relative path and a nestedpermanent/bootstrap.bldthrough the same translation the engine uses.
What does not:
-
The engine boot. Registration reaches 124 of about 1,007 classes, in exactly the retail order, and then the boot stops making progress.
Registration is demand-driven: Alchemy builds a class's metaobject the first time something asks for it, and retail asks 13,567 times during boot against our 105. So 124 is a measure of how far the boot gets, not a fault in registration, and nothing is wrong with the class at the frontier. An earlier version of this file named
igFileandigVirtualStorageDeviceas classes that never register; both are dependencies of classes beyond the frontier, and were never skipped.The current fault is one unchecked null allocation.
igPool::allocateBucketasks for 6,625 elements of 24 bytes, the allocation returns null, nothing checks, and the element constructor writes through a null base until element 563 lands on the engine's memory-context global. On hardware the first of those writes would fault at the bug; guest-memory masking makes every one of them legal, so it surfaced 6,236 calls later as an unrelated null pool. Containing that loop took execution from 710,046 to 1,337,074 calls and dispatch misses from 73,308 to 53 -- what looked like four problems was one.The allocation fails because we run the same three bucket allocations six times. Retail, under a debugger, runs them once, with identical counts and element sizes.
-
Game rendering. Graphics calls are honest no-ops pending a Switch backend; nothing the game itself draws reaches the screen.
Progress is tracked run by run in test-results/, including the wrong turns.
Several confident theories in there were later disproved and the records say
so rather than being quietly rewritten -- the pool allocator, for one, was
chased for hours before it turned out to be reading a block header out of
address 0.
docs/registration-order-real.txt-- 965 classes in the retail game's own registration order, captured from Cemu by breaking onappendToArkCore. Diffing our order against it is how the frontier is located.
Cemu with --enable-gdbstub is the reference for anything about what the
retail game does. Two things make it usable rather than misleading: gdb needs
set architecture powerpc:750 and set endian big, without which
breakpoints silently never fire; and the stub's port is hardcoded to 1337, so
if something else owns it gdb reports "Remote connection closed" as though the
stub were broken. Guest addresses need no rebasing.
See LICENSE — Arkchemy Free & Source-Available License v2.0. It is
not an OSI-approved open source licence and some uses require permission,
so please read it before reusing anything here. Contact details and the
project Discord are in llms.txt.
Contributors are listed in CONTRIBUTORS.csv; the codename
scheme is explained in CODENAMES.md.