Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.9 KiB
Building from source
Windows 10 or later, an x86-64 CPU with AVX2, and a GPU with Vulkan 1.3 and VK_EXT_external_memory_host. Allow about
60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB afterwards.
Players who only want to play can use the installer instead: it builds the game on their machine without this toolchain.
Toolchain
Versions are the ones the build was verified with; newer patch releases are fine.
| Tool | Version | Install | Used for |
|---|---|---|---|
| MSYS2 UCRT64 | clang/lld 23.1.3 | msys2.org installer to C:\msys64, packages below |
compiling the generated C and the runtime |
| Visual Studio Build Tools | 2026, C++ workload, Windows SDK 10.0.26100 | winget Microsoft.VisualStudio.BuildTools with --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended |
the linker .NET NativeAOT calls |
| .NET SDK | 10.0 | winget Microsoft.DotNet.SDK.10 |
the shader translator (totk_xlate.exe) |
| Vulkan SDK | 1.4.363 | winget KhronosGroup.VulkanSDK |
validation layers, vulkaninfo |
| uv | 0.13 | standalone installer from astral.sh | the two Python environments |
| hactool | 1.4.0 @ 1d64a83 |
.venv\Scripts\python tools\build_hactool.py, see below |
NCA and update extraction |
MSYS2 packages, after pacman -Syuu (run it twice; the first pass updates the core and exits):
pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-{clang,lld,ninja,gcc,make,pkgconf,sdl3,opus,libvpx,zstd,vulkan-devel,\
vulkan-memory-allocator,vulkan-validation-layers,shaderc,spirv-tools} base-devel git unzip
ffmpeg from MSYS2 is a GPLv3 build and is never linked; the movies go through libvpx.
PATH:
- Add
C:\Program Files (x86)\Microsoft Visual Studio\Installerto the user PATH: NativeAOT needsvswhere.exe, or the publish fails at the link step (MSB3073). - Another
cmakeorninjaearlier on PATH (a 32-bit winlibs MinGW, for example) is picked up by accident. The build scripts call Ninja fromC:\msys64\ucrt64\binthemselves; from a shell, putC:\msys64\ucrt64\binfirst.
hactool
tools/build_hactool.py fetches SciresM/hactool at 1d64a83 into toolchain\src\hactool, applies the two patches in
tools/hactool/ and builds a static toolchain\hactool\hactool.exe with MSYS2 UCRT64 gcc (a few seconds):
portable.patch: the parts of MacNativeGames'build_hactool()that run on Windows, theMNG_TITLE_KEYSvariable and the two BKTR fixes innca.cfor the subsection table padding and the subsection layout of newer cumulative updates;unicode.patch: a manifest with<activeCodePage>UTF-8</activeCodePage>, so paths under a non-ASCII user profile reach it intact.
hactool reads -k <prod.keys>; MSYS2 shells set HOME, which hides its default key location, so pass -k there.
Build
From the repository root, in PowerShell. Every step that depends on the game version takes it as <ver>: v143 for
1.4.3 or v121 for 1.2.1. The tools default to v121 when --version is left out. Saves load on either version.
-
Python environments (once).
.venvruns the lifter and the build;.venv-py314runs the extraction scripts, which need the stdlibcompression.zstdof Python 3.14:uv venv --python 3.12 .venv uv pip install --python .venv/Scripts/python.exe -r tools/requirements.txt uv venv --python 3.14 .venv-py314 uv pip install --python .venv-py314/Scripts/python.exe -r tools/requirements-py314.txt -
Copy
config.example.tomltoconfig.local.tomland set the paths to your base game, the update and your console's keys file.config.local.tomlis ignored by git. The tools read the keys file themselves. -
Extract and verify. This writes
build\extracted\v0(1.0.0) andbuild\extracted\<ver>for each update inupdates, identified by its hash; a hash mismatch stops the run:.venv-py314\Scripts\python tools\extract.py -
Shader translator (once; no game files). This publishes
build\xlate\totk_xlate.exe:.venv\Scripts\python tools\build_xlate.py -
Lift the modules to C, write the RomFS half of the frame-rate patches, then compile and link
build\<ver>\totk.exe:.venv\Scripts\python recomp\gen.py --exefs build\extracted\<ver>\exefs --out generated\<ver> --patches recomp\patches-<ver>.txt .venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> .venv\Scripts\python tools\build_game.py --version <ver>Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed. A compiler cache in front of clang shortens it further: with
SCCACHEset to an sccache executable,build_game.pywrites it into the ninja file ahead ofcc, and every lift rewrites the generated chunks with the same contents, so a second build of the same version hits all of their objects:$env:SCCACHE = "path\to\sccache.exe" .venv\Scripts\python tools\build_game.py --version <ver>--patchesbuilds in the frame-rate, resolution and other patch sets, switchable at run time. The link copiesSDL3.dll,libiconv-2.dllandlibopus-0.dllnext to the exe. For Xbox button prompts, runmake_romfs_patches.pyonce more with--xbox-prompts: it writesbuild\<ver>\romfs-patches-xbox(controls.md, "Xbox button prompts");build_game.pycopies it next tototk.exelikeromfs-patches. -
Prepare the shaders (once per build folder and translator). It needs
refs\MacNativeGames(tools\fetch_refs.sh) for the shared shader list:.venv-py314\Scripts\python tools\prepare_shaders.py build\<ver> --version <ver>Without this step the first launch compiles shaders and pipelines during play and stutters until the caches fill. The AMD driver keeps its pipeline cache per exe path and holds a limited amount: preparing another build folder can evict this one's.
totk.exe --warm-shadersrebuilds it from the shader cache. -
Run. The game pauses while its window is out of focus, as it does on the Switch when Home is pressed;
assume_focus = 1intotk.ini, or the Game tab, keeps it running in another window.build\<ver>\totk.exe
Settings live in totk.ini next to totk.exe (totk.ini.example lists them). Saves go to
build\profile for v121 and build\profile-<ver> for other versions (build_game.py --profile changes it).
Portable folder
.venv\Scripts\python tools\package_build.py --version <ver> writes build\<ver>-portable, which runs on another of
your machines without MSYS2, Python or .NET. romfs.bin stays out unless you pass --with-romfs; otherwise put it in
romfs\ next to the exe or point TOTK_ROMFS at its folder. Run totk.exe --prepare-shaders once in the copied
folder. The folder holds code and data derived from your game: for your own machines only.
Checks
Before a commit:
python tools/check_content.py --staged: nothing game-derived is staged. CI runs the same check;git config core.hooksPath .githooksruns it before each local commit.build\<ver>\totk.exe --selftest: layout, dispatch table and host-function registry, without game files.
The lifter (recomp/) and the CPU headers (runtime/cpu.h, runtime/simd.h) are checked against Unicorn. Every lifter
change passes these, all run with .venv:
| Test | Checks | Game files |
|---|---|---|
tests\diff\lift.py |
instructions sampled from the extracted modules; --coverage lists every word the lifter rejects or traps |
yes |
tests\diff\simd.py |
the whole FP/SIMD encoding space | no |
tests\diff\atomic.py |
exclusive load/store sequences and ARMv8.1 atomics | no |
tests\diff\func.py |
whole functions as the generator emits them, loops, jump tables and calls included (calls go to a stub that checks the registers at the call) | yes |
tests\flags_check.py |
the function-local flag helpers of runtime\cpu.h against its packed ones |
no |
tests\excl_mt.py |
the exclusive monitor helpers under contention and ABA, on several threads | no |
tests\jumptables_check.py |
recomp/jumptables.py against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too |
yes |
Runtime probes, each a mode of totk.exe (the list is at the top of runtime/main.c): --probe-kernel,
--probe-vkexec, --probe-nvn-tex and --probe-shader cover most runtime changes.
Shaders: .venv-py314\Scripts\python tools\shader_census.py --version <ver> --translate extracts every Maxwell shader
to build\<ver>\shaders\ and runs it through the translator and spirv-val; failure groups go to failures.txt.
tools\xlate_smoke.py checks the translator alone, without game files.
Reverse engineering (optional)
- Ghidra 12.1.2 with Adubbz's Ghidra-Switch-Loader in
toolchain\ghidra\(JDK 21) opens the game modules (NSO). tools\fetch_refs.shclones the upstream projects this port builds on intorefs\, read-only.