snx-build
Build the ScarletNX runtime and recompile games correctly. Avoid the setups that build "successfully" into something that does not work. It covers the CPM.cmake dependencies, the presets for Clang onl
SKILL.md.claude/skills/snx-build/SKILL.md
GitHubBuild
Order
Run
cmake --preset default -B build && cmake --build build. It builds intobuild/, as the docs show. Without-B, the preset builds intobuild/default. The first time, the configure fetches the third-party packages (cmake/dependencies.cmake) into.cache/cpm. Thus a fresh clone needs the network one time.The packages build in the same tree as the runtime. FFmpeg builds with its own configure and make, which
cmake/ffmpeg.cmakeruns. Its logs are inbuild/ffmpeg_build-prefix/src/ffmpeg_build-stamp/. The other packages build through their CMake.Use Clang only: any other compiler stops the configure. The NX runtime requires FFmpeg. The configure also stops in two other conditions:
- The FFmpeg source path (the checkout, or
CPM_SOURCE_CACHE) has a space in it. FFmpeg's configure cannot build from such a path. - The host is x86-64 and it has no NASM (see below).
On Windows,
scripts\bootstrap.ps1downloads the toolchain intodeps\toolsone time. The toolchain is llvm-mingw, CMake, Ninja, NASM, Python, and w64devkit for the sh and GNU make that FFmpeg needs. The default execution policy of Windows runs no scripts. Thus, in a new PowerShell window, runSet-ExecutionPolicy -Scope Process Bypassfirst. Then. scripts\env.ps1puts the toolchain on PATH in that window.- The FFmpeg source path (the checkout, or
hactool is our own code, in the tree (
src/nx/runtime/driver/hactool).cmake/hactool.cmakebuilds it. It runs insidebuild/scarletnx -x Game.xci.The last step of the build copies the runtime to
games/<id>/<name>for eachgames/<id>/<name>_code.so. These are the module of the game and the module of its update (<id>_<update id>). For an older version'sgames/<Game>_code.so, the step copies the runtime togames/<Game>(cmake/deploy.cmake). That copy is the binary that runs. The build printsRuntime deployed to ...for each copy. If there is no such line, no game got the new runtime.
Silent failures
| What happens | Why | What to do |
|---|---|---|
| Movies stay black. The log says that there is no H.264 decoder. | When a build other than CMake makes the runtime, the runtime drops the H.264 decoder with no message. The recompiler had its own build at one time. | Build the runtime with CMake, which includes FFmpeg. build/scarletnx -r also uses CMake. |
The configure stops with NASM was not found |
The x86-64 assembly of FFmpeg needs NASM. A build without it decodes movies several times slower. Thus the build never drops the assembly silently. | Install nasm. -DSNX_FFMPEG_ASM=OFF builds FFmpeg without its assembly on purpose (slower movies). |
| A fix has no effect | The game ran an old runtime. The build did not deploy (there was no _code.so in games/ yet), or the game ran during the build. |
Check the deploy lines. On Linux, a game that runs keeps its old copy (the deploy renames the new file over it). On Windows, the deploy cannot replace a .exe that runs. Thus stop the game before you build. |
| Two build folders do not agree on the deployed runtime | Every build folder with SNX_DEPLOY on deploys to the same games/ |
The last build wins. Configure extra folders with -DSNX_DEPLOY=OFF. The recompiler builds its runtime in build/native-recomp or build/win64-recomp with deployment off (src/nx/runtime/driver/toolchain.c). |
| The deploy wrote over a test binary next to the game | The deploy targets every *_code.so in games/, and symlinks too |
Keep A/B copies outside games/, or do not build while they exist. |
| Nobody sees a new warning | The runtime builds with only the default warnings of Clang. The third-party packages and the generated game module build with -w. A warning does not stop the build. |
Check a changed file with more warnings on. Take the line of the file from build/compile_commands.json. Run it with -Wall -Wextra -fsyntax-only. Fix the warnings that come from the change. |
The game crashes at startup with recompiled code is from another ScarletNX version |
The build of the game module used another SNX_ABI (src/common/cpu/aarch64/cpu.h) |
Recompile the game. |
The Vulkan renderer
The renderer is Vulkan. The sdl3 backend is the only window backend on Linux and Windows. The project removed the OpenGL renderer and the x11, x11es, win32 and android backends on 2026-10-11. A GPU or driver without Vulkan 1.3 stops the runtime at startup with a message.
The runtime always links SDL3. -H skips the SDL video initialization and renders offscreen with Vulkan. The
interface is the Renderer table in gpu/renderer.h, which vk_renderer fills in. All other code calls
g_renderer->draw(...) and the other functions of the table.
The build also makes snx-shaderc. Check a translator change on a shader corpus before you render with it. Record a
corpus with --shader_corpus=dir. Then run build/snx-shaderc dir --validate (spirv-val must be on PATH). Use
--out to save direct SPIR-V modules for disassembly or for a comparison between builds.
A corpus holds the shaders of the game. Keep it under games/ or in the scratchpad, never in a tracked folder of the
repo. --vk_validation=1 turns on the Khronos validation layer if the layer is on the machine. On Arch, the user
installs it from vulkan-validation-layers.
A screenshot (--shot) reads back the frame that Vulkan presented. The log gives the hash of the screenshot. Compare
a change against the build before it at the same frame, on a quiet machine. A boot logo is a good frame
(--seconds 4 --script ""). Use the same frame because logos animate per frame.
A rebuild can change the runtime: a new commit, or any change in a tree with edits (pf_key). The first run after
such a rebuild starts the shader files again from zero. Thus the run translates the game's own shaders again behind
the load screen (prepare_shaders=2), before --seconds starts to count. It also translates again every shader that
its draws use. Thus the timed presses of a script can miss. Before you suspect the renderer, run again with warm
files.
Windows
CI (.github/workflows/build.yml) builds Windows natively from scripts/bootstrap.ps1, and cross-builds it on
Linux. Before you push Windows code, cross-build it here to check it. Put the Linux build of llvm-mingw on PATH. Its
release is llvm-mingw-<date>-ucrt-ubuntu-22.04-x86_64.tar.xz, which gives the x86_64-w64-mingw32-* names. Then
run cmake --preset windows-cross && cmake --build --preset windows-cross.
Only that build checks the code that only Windows compiles: platform/plat_win32.c, platform/profiler_win32.c and
the _WIN32 parts of the SDL3 backend. A header that a file needs but does not include, or a global that must be
static, shows only in that build.
The runtime builds with the default warnings of Clang and two errors: -Werror=missing-prototypes and
-Werror=missing-variable-declarations. When another file uses a function or a global, declare it in the header next
to the file that defines it. That file also includes the header, so the compiler checks that the two agree. All other
functions and globals are static. This rule replaced an older pattern: an extern inside a function, or a prototype
copied into another .c file. At one time there were 237 of them, and some did not agree with the definition.
The Windows runtime is the SDL3 backend with the Vulkan renderer, as on Linux. Build it with
cmake --preset windows-cross && cmake --build --preset windows-cross. In that build, SDL3 is a CPM package, and the
build links it statically.
To run a Windows build, use Wine with a game module for Windows. Keep that module out of games/:
- First, make
build/wingame/<id>/<id>_data/romfsa symlink to the Linux copy. If you do not, the recompiler copies 13 GB. Then make the module with a Windowsscarletnx.exe -r, with--outdir build/wingame. The recompiler builds modules only for the host that it runs on. The Linux recompiler refuses--target win64(src/nx/runtime/driver/recompile.c). - The Mono and Gecko installers wait for a click and show nothing on screen. Thus use a prefix of its own, without
these installers. Make the prefix with
WINEPREFIX=$PWD/build/wineprefix WINEDLLOVERRIDES="mscoree,mshtml=" WINEDEBUG=-all wineboot -i. Use the same variables forwine <id>.exe --seconds=20 --shot=shotinbuild/wingame/<id>/. The log is<id>.lognext to it. - Under Wine 11.19 on this machine, the SDL3 backend with Vulkan works.
When you must recompile the game module
Recompile it only when the generated code or its contract changes. The code and the contract are in
src/common/cpu/recompiler/, src/nx/loader/, the module compile flags in src/nx/runtime/driver/toolchain.c,
src/common/cpu/aarch64/cpu.h, cpu_helpers.c and the files under src/common/cpu/aarch64/helpers/. Other runtime
changes usually need only a runtime rebuild. The runtime checks only SNX_ABI. Thus, after an ABI change that kept
SNX_ABI, an older module still loads and runs the old code. A bump of SNX_ABI makes the runtime refuse every
module from before the bump.
A recompile of a large game takes several minutes. A successful recompile removes its temporary objects. --keep-ir
keeps the generated code (LLVM IR) in <outdir>/<id>/<id>_build/ for inspection.
build/scarletnx -r work/<Game> # runtime and game module, both with Clang, into games/<id>/
scarletnx -r uses the LLVM backend, which is the only backend. The backend does not keep its objects in a cache.
Each recompile builds every unit again.
llvm_qemu_differential checks against qemu-aarch64 a change to the profile of the decoder
(src/common/cpu/aarch64/decode/a64.profile) or to an emitter. The emitters are
src/common/cpu/recompiler/llvm/ir_int.c, ir_ldst.c, ir_fpu.c and ir_vec*.c, which the project ported from
QEMU. Run it with ctest -R llvm_qemu_differential. The --sweep option of the binary takes more words and states.
A difference that is not in tests/llvm_qemu_expected.txt makes the test fail. A listed difference that no longer
occurs also makes it fail.
To add or bump a third-party package
Add a CPMAddPackage to cmake/dependencies.cmake with the URL of the release archive and its URL_HASH SHA256=.
The URL names a tag, or a commit for projects without tags. Get the hash from the digest of the release asset
(gh api repos/OWNER/REPO/releases/tags/TAG --jq '.assets[].digest'), or download the archive one time. Never trust
the next configure for the hash. Then:
- For projects without CMake (FFmpeg) or with a CMake too old for CMake 4, prefer
DOWNLOAD_ONLYand our own targets. - For the other projects, prefer
EXCLUDE_FROM_ALL YES. Do not useSYSTEM. A system copy of the headers that an-Iin CFLAGS finds would win over the-isystemheaders. SDL3 on Windows is the one exception, because there is no system copy for it to collide with. - Put
snx_quiet(target...)on the targets that the package builds.
OPTIONS are cache variables of the whole build, not only of that package. Then add the license to the table in
docs/CREDITS.md and to the install (see below). Give the version in the commit message. CI caches .cache/cpm on the
hash of the file.
The AppImage
The appimage job of CI installs the build into /usr in the Arch container of pkgforge. Then it runs
scripts/make-appimage.sh. This script bundles scarletnx with quick-sharun. It also checks that a copy with the
name Probe.AppImage looks for Probe_code.so next to itself. The release job replaces the nightly prerelease
with it on every push to main that passed every other job.
On this machine, quick-sharun works from the libraries of the host and needs patchelf on PATH. It bundles the full
Mesa. Thus the local AppImage is several times bigger than the AppImage of CI, which uses the debloated packages of
pkgforge. Inside an AppImage, argv[0] points into its mount. Thus code that finds files next to the runtime must use
g_exedir, which main.c takes from $APPIMAGE.
cmake --install puts the license files in share/licenses/scarletnx. First is LICENSE. Then there is a folder
for each library or ported code in the runtime or the driver. The license files for code without its own license file
come from dist/licenses/, for example Noto Sans and the default Miis of Ryujinx.
The AppImage copies that folder and adds the licenses of the Arch packages that it bundles. Their list is in
share/licenses/bundled-packages.txt. The CI artifacts are the install tree. A new package needs a new
install(...) line for its license in CMakeLists.txt.
Never delegate builds to subagents
Reviewer and investigator agents read and grep. Only the main session builds and runs, one job at a time. Two builds
at the same time fight over build/ and the deploy. A build during a test run changes how fast the test runs.