Hyrule Warriors: Definitive Edition Recomp
Hyrule Warriors: Definitive Edition Recomp (HWDER) is a native Windows port of Hyrule Warriors: Definitive Edition that supports internal resolution scaling and ultrawide resolutions.
- Type: PC port
- Game: Hyrule Warriors: Definitive Edition
- Runs on Windows
- By sonsegajp
- Latest release v0.1.1, 2026-10-03
- Source: https://github.com/sonsegajp/HWDER
README
HWDER - Hyrule Warriors: Definitive Edition Recomp
A static recompilation of Hyrule Warriors: Definitive Edition (Nintendo Switch) to a native
Windows x64 executable. The game's ARM64 code is translated ahead of time to C and compiled with
clang; everything the game expects underneath it (the Horizon kernel, system services, the GPU,
audio, video decoding, the file system) is implemented by our own runtime, written for this
purpose. There is no CPU emulation, no JIT and no emulator core. The result is one hwder.exe
that reads code and assets straight out of the player's own game dump at startup.
This repository contains no game code and no game assets. The recompiled C in generated/
is produced on your machine from your own dump, and the runtime decrypts the ExeFS and RomFS out
of your own .nsp with your own prod.keys at run time. Nothing is extracted to disk.
Playing (release build)
- Download the latest zip from Releases and unpack it anywhere.
- Put your own dump of the game (
*.nsp, title0100AE00096EA000, base game) and your console'sprod.keysnext tohwder.exe. Keys are also picked up from%USERPROFILE%\.switch\prod.keysand the yuzu / Ryujinx key folders. - Run
hwder.exe.
Requirements: Windows 10/11 x64, a Vulkan 1.2 driver (NVIDIA, AMD, Intel), an AVX2 CPU, and Media Foundation (built into Windows; "N" editions need the Media Feature Pack) for the movies.
In game: F1 opens the options overlay (internal resolution scale, unlocked resolution for ultrawide, vsync, borderless, anisotropic filtering, FPS counter, 60 Hz monitor switch, and a save-file cheats tab). F11 toggles borderless fullscreen. Any XInput controller works; keyboard: WASD move, arrows camera, K/Enter = A, J/Backspace = B, I = X, L = Y, Q/E = L/R, U/O = ZL/ZR, Space = +, Tab = -, 1-4 = D-pad, Z/C = stick clicks.
Files created beside the exe: hwder_settings.ini, hwder.log (attach to bug reports),
save\ (console save layout), cache\ and pipeline_cache.bin (safe to delete).
Building from source
Toolchain: Visual Studio 2022 with the LLVM (clang-cl) component, CMake and Ninja (both ship
with VS), Python 3.10+ with cryptography (only for the extraction helper).
git clone --recursive https://github.com/sonsegajp/HWDER
cd HWDER
# 1. extract your dump once for the recompiler (exefs/ is all it needs; romfs/ is optional,
# the runtime can read it from the NSP directly)
python tools/nx_extract.py "<game>.nsp" "<prod.keys>" data --no-romfs
# 2. ARM64 -> C (all five ExeFS modules, ~5 minutes, writes generated/)
cd tools && python -m recomp.gen_main && cd ..
# 3. build (clang-cl + Ninja inside the VS developer environment)
python tools/build.py
# 4. run: drop your .nsp + prod.keys next to build/release/hwder.exe, or point at a dump
set HWDER_EXEFS=data\exefs
set HWDER_ROMFS=data\romfs
build\release\hwder.exe
# package: dist/HWDER-<date>.zip (exe, pdb, README, default settings)
python tools/make_dist.py
If the game ever jumps to an address the recompiler did not know was code, the runtime logs it,
appends it to extra_entries.txt and exits; tools/iterate.sh automates the
regenerate-rebuild-rerun loop. The current entry list already covers normal play.
How it works
1. Static recompilation (tools/recomp, tools/nso.py)
nso.py loads the five NSO modules of the ExeFS (rtld, main, subsdk0, subsdk1, sdk),
decompresses their LZ4 segments and applies the dynamic relocations, giving a fully linked
in-memory image of the process exactly as the loader on the console would have produced it.
recomp/gen.py then discovers functions in each module from several independent sources:
.eh_frame unwind entries, the entry point and init/fini arrays, exported dynamic symbols,
direct BL targets, tail-call targets, absolute code pointers found in relocated data, and
ADRP+ADD address materialisations. Inside each function it finds the intra-function branch
targets and recovers jump tables (the compiler's ADR + LDRB/LDRH + ADD + BR idiom), so
switch statements turn into real C switches rather than indirect dispatch.
recomp/a64.py and a64_simd.py translate every AArch64 instruction (base integer, FP, the
full Advanced SIMD set, the AES/SHA/CRC extensions, saturating and narrowing arithmetic) into
C that operates on a guest register file Ctx (runtime/include/hwder/cpu.h). The translator
reaches 100% of the instructions in all five modules; anything it has no pattern for would
emit a call to a runtime hook that logs and halts, which never fires in practice.
Each guest function becomes one C function f_<address>(Ctx*). Direct calls become direct C
calls. Indirect branches (BLR, BR, returns through unusual paths) go through a dense
address-to-function table built for every module's text segment, and the dispatcher uses
clang's [[clang::musttail]] so tail calls and returns cost a jump, not a growing host stack.
Functions that contain an unresolvable indirect branch are emitted as resumable: they carry a
label at every instruction and can be entered mid-function, which is how the game's hand-written
assembly and longjmp-style control flow keep working.
The generated program is split into rc_NNNN.c files of about 12k lines each so the build
parallelises. module_info.c records each module's load address and segment layout; at run
time hwder.exe maps the real NSO segments from the player's dump to those same addresses, so
the recompiled code and the original data (rodata, vtables, string tables, relocated pointers)
line up byte for byte.
2. The kernel (runtime/src/kernel)
Guest threads are real Windows threads. svcCreateThread creates a host thread whose stack
and TLS live in the guest address space, and the thread simply calls the recompiled entry
function. An svc instruction in the original code is a direct C call into svc.cpp, which
implements the Horizon system calls the game and its SDK use: memory management (SetHeapSize,
MapMemory, QueryMemory with the exact page attribute model), threads and their priorities,
core masks and the GetCurrentProcessorNumber rule the SDK's job system relies on, all the
synchronisation primitives (WaitSynchronization, ArbitrateLock/Unlock, condition variables,
WaitForAddress/SignalToAddress), events, transfer memory, sessions and IPC, and GetInfo.
Guest memory is a flat reservation at the addresses the console would use (code at 0xF_FFE0_0000
and up, a heap region, stack and TLS regions), so a guest pointer is a host pointer. There is no
page table emulation and no fault-based fastmem: the game runs on the host MMU directly.
3. Services and IPC (runtime/src/service, fs, hid, audio, video, gpu)
The SDK talks to system services over HIPC. ipc.cpp implements the real message format
(CMIF headers, descriptors for buffers, handles and domains), so the unmodified SDK code in the
sdk module marshals requests exactly as on hardware and our services unmarshal them. Services
implemented: sm, fsp-srv (RomFS storage, save data, SD card), hid (shared memory input
state fed from XInput/keyboard), appletOE/applet (focus, operation mode, performance),
vi + nvnflinger (display layers and the buffer queue), nvdrv (the GPU driver interface),
audren (the audio renderer), set, time, acc, pctl, psm, nifm, ns, mm, lm
and friends.
File system. fs/nsp.cpp reads the game straight out of the .nsp: it parses the PFS0
container, decrypts the program NCA header (AES-XTS, header_key), derives the content key
from the key area (or from the title's ticket when rights-ID crypto is used) and serves the
ExeFS partition and the RomFS section (IVFC level 5) through on-demand AES-CTR decryption with
Windows CNG. The SDK's own RomFS driver then parses that image through the IStorage
interface, as it would on the console. An extracted exefs/ + romfs/ pair works too: romfs.cpp
synthesises a RomFS image (header, hash tables, directory and file tables) over the host files
so the same driver path is used. Save data maps to save\<title>\<save id>\ on the host.
Audio. audio/audio.cpp implements the audren renderer: it parses the per-frame update
(voices, mixes, splitters, effects, sinks, wave buffers), decodes PCM16 and ADPCM voices with
pitch resampling, routes them through the mix graph into the sink and plays the result with
WASAPI in shared mode at 48 kHz.
Video. The game's cutscenes are H.264 streams fed to the console's NVDEC through nvdrv.
video/host1x.cpp and decoder.cpp reconstruct the bitstream (SPS/PPS synthesised from the
decoder registers, slices re-wrapped as Annex-B), decode it with Media Foundation in low-latency
mode, and vic composition converts the result into the surface format the game expects.
4. The GPU (runtime/src/gpu, shader)
The game renders through NVN, which on hardware is a user-space driver that writes Maxwell
(GM20B) command buffers and submits them through nvhost-gpu. Because the real NVN driver in
the sdk module is recompiled along with everything else, we never had to reverse NVN itself.
Instead gpu.cpp is a Maxwell command processor: it executes GPFIFO entries, parses pushbuffer
methods for the 3D, compute, 2D, DMA copy and inline-to-memory engines, runs the macro
interpreter for the driver's uploaded macros, keeps the full register state, and turns draws,
clears, copies and dispatches into work for the renderer.
shader/ is our SASS to SPIR-V translator. Maxwell shaders are decoded from the game's
precompiled shader binaries (decode.cpp, opcodes.inc), a control-flow graph is built
(translate.cpp), and emit_alu.cpp / emit_mem.cpp lower the ISA to SPIR-V through a small
builder (spv_builder.h) with the attribute, constant buffer, texture and image bindings
mapped from the pipeline state. program.cpp assembles a complete pipeline program and adds
the epilogue that applies our viewport scaling.
gpu/vk/vk_renderer.cpp is the Vulkan backend: pipeline state is derived from the Maxwell
registers (vertex attribute formats, blending, depth/stencil, culling with the Maxwell window
origin rule, render target formats), pipelines are created asynchronously and cached on disk,
and the draw path uses several caches (program state, texture descriptors, index ranges, vertex
buffer slices, redundant state elimination) to stay around 25 microseconds per draw.
texture_cache.cpp manages guest textures and render targets: block-linear deswizzling, format
conversion (including ASTC decoding with a disk cache), aliasing between differently sized
views of the same memory, CPU-write tracking and GPU-write ordering so that post-processing
chains read what the game last wrote. The same cache implements the internal resolution
scaler: render targets are allocated at a scaled physical size while the guest keeps seeing
its native dimensions, with viewports, scissors, blits and copies rescaled per axis. That is
also what makes ultrawide work: the targets are scaled non-uniformly to the window and the
projection is corrected in the shader epilogue, so 3D fills the screen while the 2D UI stays
correctly proportioned.
display.cpp owns the window and the swapchain, presents at a fixed 60 Hz with a precise
limiter (the game's logic is frame-locked, so presentation never changes its speed), and hosts
the Dear ImGui overlay (overlay.cpp, settings.cpp).
5. Why native, not emulated
- No instruction decoding at run time and no JIT warm-up: the whole game is C compiled with
-O2 /arch:AVX2. - Guest threads are host threads and SVCs are function calls, so synchronisation is cheap and the scheduler is the OS scheduler.
- Guest memory is host memory: no page-table lookups, no fault handling on the hot path.
- The shader translator runs ahead of time on the game's own shader packs and its output is cached, so there is no in-game stutter from shader discovery after the first run.
Repository layout
tools/recomp/ ARM64 -> C recompiler (a64.py, a64_simd.py, gen.py, gen_main.py)
tools/nso.py NSO loader / relocator
tools/nx_extract.py NSP -> exefs/ romfs/ extraction (for the recompiler's input)
tools/build.py clang-cl + Ninja build driver; make_dist.py: release zip; iterate.sh: entry-point loop
runtime/include/ Ctx (guest register file), dispatch and module interfaces used by generated code
runtime/src/ core (loader, dispatch, crash reporting), kernel/, service/, fs/, hid/, audio/, video/, gpu/, shader/
third_party/ imgui, Vulkan and SPIR-V headers (submodules)
docs/ design decisions
Environment variables (debugging)
HWDER_NSP, HWDER_KEYS, HWDER_EXEFS, HWDER_ROMFS select the game data; HWDER_LOG the
log file; HWDER_MUTE=1 silences audio; HWDER_RES_SCALE overrides the resolution scale;
HWDER_VALIDATION=1 enables Vulkan validation layers; HWDER_VK_FRAME_DUMP, HWDER_GPU_TRACE,
HWDER_IPC_TRACE, HWDER_FS_TRACE and the other HWDER_*_TRACE switches dump the respective
subsystem's activity to the log.
Status
Boots, menus, movies, Adventure/Legend mode battles, saves and audio work; gameplay runs at the game's native 60 Hz (30 Hz where the game itself drops to half rate) at native or scaled resolution. Known issues are tracked in the issue tracker.
License
MIT for everything in this repository (see LICENSE). Hyrule Warriors: Definitive Edition is
the property of Nintendo and Koei Tecmo; you need your own copy of the game.