A static recompilation of the Wii U version (USA or European) that runs natively on macOS (Apple Silicon), Linux, Windows and Android (arm64; the setup app builds the game on the phone, see Android). The game's PowerPC code is translated to C ahead of time, the Cafe OS libraries the game uses are reimplemented natively, and GX2 graphics are implemented directly on Metal (macOS) or Vulkan (all platforms), with no Cemu runtime and no GPU command emulation.
How it works and how it differs from running the game in Cemu: docs/how-it-works.md.
The changes in each update are in CHANGELOG.md and in the release notes on the Releases page.
This is an unofficial fan project. It is not affiliated with, endorsed or sponsored by Nintendo. "The Legend of Zelda", "The Wind Waker", "Wii U" and related names are trademarks of their respective owners and are used here only to describe what this software is compatible with.
This repository contains no game code, no game assets and no keys: no executable, no recompiled or disassembled game code, no textures, models, audio, shaders, screenshots or other material from the game, and no console encryption keys. It contains only the tools and the runtime written for this project (plus the third-party code listed under Credits).
To use it you need your own, legally obtained copy of the game, dumped from your own Wii U disc
and console. Everything game-specific (the extracted files, the recompiled code in build/gen/,
shader caches) is generated locally on your machine from your dump, and must not be
redistributed. The .gitignore keeps all of it out of the repository.
Releases are portable: unzip, start Wind Waker HD, choose your dump. Releases contain only this project's runtime, tools and setup: no game files, no game code and no keys. The game's code can only exist once it is built from your own dump, so the first start prepares the game once, on your computer (about two minutes); every later start launches the game directly.
-
Download the zip for your system from the Releases page and unzip it anywhere (a games folder, an external drive):
- macOS: Apple Silicon, macOS 14 or newer (Metal renderer; Vulkan through MoltenVK, included, can be picked in Graphics > Renderer)
- Windows: x86-64 (
windows-x86_64, Windows 10 version 1903 or newer, or Windows 11) or arm64 (windows-arm64, Windows 11 on ARM, e.g. Snapdragon X), a GPU with Vulkan 1.3 drivers (or Vulkan 1.1 / 1.2 drivers withVK_KHR_dynamic_rendering). The arm64 zip runs natively, and only on ARM64 (its setup says so on an x86-64 PC). On ARM, 60 fps keeps the game's speed by default (Graphics > "Keep game speed"), as on Android: where two frames per step do not fit, in-between frames are skipped instead of slowing the whole game down - Linux: x86-64 (
linux-x86_64) or arm64 (linux-aarch64, e.g. Raspberry Pi 5, Asahi Linux on Apple Silicon, other ARM boards and laptops), glibc 2.35 or newer (Ubuntu 22.04+, Debian 12+, Fedora 36+, Arch, SteamOS 3, Raspberry Pi OS 12), a GPU with Vulkan 1.3 drivers (or 1.1 / 1.2 withVK_KHR_dynamic_rendering). Take the zip that matchesuname -m(x86_64oraarch64); setup says so if it doesn't. Or take the single-file AppImage (same two architectures) instead of the zip (issue #55):chmod +xit and start it from anywhere, Steam Deck included. Nothing is unzipped and the file itself is never written to.
-
Start Wind Waker HD (
Wind Waker HD.app,Wind Waker HD.exe, orwind-waker-hd/Wind Waker HD.desktopon Linux). The first start asks for:- your disc image (
.wuxor.wud), a Cemu archive (.wua), or an already extracted game folder (withcode,contentandmeta, e.g. from dumpling or Cemu). An extracted folder is used where it is, nothing is copied; a disc image or Cemu archive is extracted into the release folder (about 1.7 GB); - for a disc image, its disc key (a
.keyfile with the image's name next to it is used automatically) and the Wii U common key (16 bytes, the same on every console; choose a key file or paste the 32 hex digits into the hidden field; acommon.keynext to the image or in the release folder is used automatically). Keys are checked before anything is extracted, never stored, and not part of any log. A Cemu archive or an extracted folder needs no keys. - A Cemu archive (Cemu's "Convert to compressed Wii U archive (.wua)") often holds the game, its update and DLC together. Setup uses the game itself, title 00050000-10143500 (USA) or 00050000-10143600 (Europe), version 0, and says so in its log; an update in the archive is not used: the port is built for the code of version 0, and the update's files belong to its newer code. The archive's checksum is verified before anything is extracted.
Then it prepares the game (extract, translate the code to C, compile with a pinned compiler) and offers to bring in a save: a Wind Waker HD
cking.savfolder (Cemu, Wii U), a GameCube.gci(converted to HD), or the saves and settings of an earlier installation or another Wind Waker HD folder (copied, never moved). The USA version (title 00050000-10143500) and the European one (00050000-10143600) are supported, version 0 of either (the disc or eShop release, without the update): before translating, setup checks the game's code (code/cking.rpx) against the SHA-256 of each and explains what to use instead when it matches neither (e.g. a game folder with an update copied over it). The European game plays in English, French, German, Italian or Spanish — its own text, chosen in the settings' Language tab. How one port serves both: docs/builds.md. - your disc image (
-
That's it: start Wind Waker HD to play. To repair, update or change the game, hold Shift while starting it (macOS, Windows) or start it with
--setup(Linux; also the "Setup" action of its menu entry).
First start, per system:
- macOS: the release is not signed by Apple, so the first time macOS says the app "cannot be
opened". macOS 14: right-click (Ctrl-click) the app, Open, Open. macOS 15 and newer: click
Done, then System Settings › Privacy & Security › Open Anyway. If Apple's Command Line Tools
(the free compiler, which also brings Python) are missing, Wind Waker HD offers Apple's installer.
Start the app inside the unzipped folder and keep it there: it needs
tools/,sdk/andportable.txtnext to it. Don't drag onlyWind Waker HD.appinto Applications; to keep the game somewhere else, move the whole folder. The folder must be writable (not a disk image or a read-only drive). If macOS asks whether Wind Waker HD may access your Downloads (or Desktop, Documents) folder, click Allow: the game is prepared in that folder. Releases up to v0.2.3 stopped with "Setup could not continue … must stay in the unpacked release folder" when started straight from the unzipped download (issue #48, macOS App Translocation); with those, runxattr -dr com.apple.quarantine <the unzipped folder>in Terminal once, then open the app again. When setup cannot continue it says what failed and what to do, and writes it todata/setup-window.log(or~/Library/Logs/Wind Waker HD setup.logwhen the folder is not writable). - Windows: the release is not code-signed, so SmartScreen may say "Windows protected your PC":
More info › Run anyway. Python comes with the release (
tools/python, the official embeddable Python). The first start downloads the compiler (llvm-mingw, 190 MB) into the release folder, SHA-256 checked, no administrator rights; at the end you can remove the compiler again (it is only needed to repair, and downloaded again then). - Linux: start
wind-waker-hd(orWind Waker HD.desktop; some desktops ask to allow launching it first). It uses your Python 3 and downloads the compiler (zig, 55 MB; the x86-64 or arm64 build matching your system) into the release folder; you can remove it at the end. - Linux (AppImage):
chmod +x WindWakerHD-*-linux-*.AppImage, then start that file (from a file manager, a terminal or Steam as a non-Steam game). It needs no unzipping and no writable folder of its own. Ubuntu 24.04 and newer have no FUSE by default: installlibfuse2t64(libfuse2on older releases), or start it with--appimage-extract-and-run. Repair or change the game with--setup. Everything it creates is listed under "Everything stays in the release folder" below.
Everything stays in the release folder (in data/): the built game, the extracted game files,
saves (data/save), settings, controls, save states and shader caches (data/user), crash logs
(data/captures), the setup log and the downloaded compiler. Nothing is written to your user folders
(Application Support, AppData, .config, Applications, Start menu) unless you tick "add a shortcut" at
the end. To remove everything, delete the folder. Starting a newer release: unzip it next to the old
one, start it, choose your game (the old folder's data/game can be used in place) and copy your saves
and settings from the old folder.
The Linux AppImage is not a portable folder (issue #55): its file is read-only, so the game, its
code and saves go to ~/.local/share/wwhd and the settings, controls, save states and caches to
~/.config/wwhd (or $XDG_DATA_HOME / $XDG_CONFIG_HOME; --data-dir FOLDER chooses another place
for the first, for example an SD card on a Steam Deck). Nothing is written beside the .AppImage; the
menu entry the setup adds starts the same game. To remove everything, delete the file and those two
folders. Portable use: create a folder named like the AppImage plus .data next to it (for example
WindWakerHD-v0.2.13-linux-x86_64.AppImage.data); the AppImage then keeps the game, its code, saves,
settings and caches in that folder instead (its share, config and cache subfolders).
The setup also runs in a terminal (the fallback): tools/Setup in Terminal.command (macOS),
tools/Setup in a console window.bat (Windows), tools/setup-in-terminal.sh (Linux). How it works
and the interface between the window and tools/installer/setup.py:
tools/installer/README.md. Scripted use: tools/installer/setup.py --help.
Source builds (below) and the Linux AppImage (without a .data folder) are not portable: they keep using the per-user folders
(~/Library/Application Support/wwhd, %LOCALAPPDATA%\WWHD for the game and saves,
~/.config/wwhd for the settings on Linux), as before.
- macOS on Apple Silicon, Linux (x86-64 or arm64, Vulkan) or Windows (x86-64 or arm64, Vulkan); the platform-specific build steps are under "Building" below
- macOS: Xcode command line tools (
xcode-select --install) - zstd for the extractor's
.wuasupport: a system one if installed (brew install zstd,apt install libzstd-dev; found through its CMake package or pkg-config), otherwise CMake downloads the pinned source (-DWWHD_BUNDLED_ZSTD=ONalways does, as release builds do) - CMake 3.20 or newer
- Python 3 with
pycryptodomefortools/wudextract.py(pip3 install pycryptodome); the nativewwhd-extractbuilt with the project (build/cmake/wwhd-extract --help) needs neither - optional:
capstone(pip3 install capstone) for the disassembler helpertools/ppcdis.py - optional, decompilation tools only:
ninjaand the requirements of the zeldaret/tww build (see below)
You also need, from your own console and disc:
- a disc image of The Wind Waker HD (USA or Europe) in
.wudor.wuxformat (or a Cemu archive,.wua:build/cmake/wwhd-extract --title 0005000010143500 extract game.wua game, with0005000010143600for the European game, no keys); - its disc key (16 bytes) in a
.keyfile next to the image, with the same base name; - the Wii U common key, either in a file
common.key(16 raw bytes or 32 hex digits) next to the image or in the current directory, or in theWIIU_COMMON_KEYenvironment variable (32 hex digits). As a text file it is one line of 32 hex digits, nothing else; a wrong or malformed common key also makes the extraction fail with a decryption error.
None of these are included or will be provided.
For the Vulkan renderer also: brew install vulkan-headers vulkan-loader molten-vk glslang (the
build needs them; the app still runs with Metal on a Mac without them). -DWWHD_RENDERER=METAL
builds a Metal-only app without any Vulkan dependency.
# 1. extract the game into game/ (game.wux with game.key next to it, plus your common key)
python3 tools/wudextract.py game.wux extract game
# -> game/code/cking.rpx, game/content/..., game/meta/...
# 2. recompile the game code to C (writes build/gen/; stays on your machine)
python3 tools/recomp/recomp.py game/code/cking.rpx build/gen
# 3. build
cmake -S . -B build/cmake && make -C build/cmake -j$(sysctl -n hw.ncpu) wwhdThe Linux build uses the Vulkan renderer with the SDL3 host (windows, input, audio). On Ubuntu 24.04:
sudo apt install clang cmake ninja-build zlib1g-dev liblz4-dev libvulkan-dev glslang-dev \
mesa-vulkan-drivers libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev \
libxfixes-dev libxkbcommon-dev libwayland-dev libasound2-dev libpulse-dev libudev-dev libdbus-1-dev
# SDL3 is not packaged in 24.04: build it from source (https://github.com/libsdl-org/SDL, release-3.2.x)
cmake -S . -B build/linux -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake --build build/linux
./build/linux/wwhd --renderer-smoke # checks the Vulkan renderer, no game files neededIf linking fails with unwinder errors (missing _Unwind_* symbols or -lunwind), your clang is set up to
use LLVM's libunwind. Configure with the GCC unwinder instead:
cmake -S . -B build/linux -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_EXE_LINKER_FLAGS="--unwindlib=libgcc"On Arch-based systems (Arch, CachyOS, Manjaro):
sudo pacman -S clang cmake ninja sdl3 vulkan-headers vulkan-icd-loader glslang shaderc python-pycryptodome
# plus the Vulkan driver for your GPU, e.g. vulkan-radeon (AMD) or vulkan-intelWii U volumes are case-insensitive and the game asks for paths in a different case than the
extracted folders (e.g. Audiores vs AudioRes); the runtime resolves such paths itself on
case-sensitive file systems. When the game asks for text (your name), a text window appears over
the game (see Entering text under Playing).
On Linux and Windows, F11 or Alt+Enter switches the focused window (TV or GamePad) to full
screen and back; the TV window's full screen (also the settings overlay's Display > Full screen) is
remembered in settings.ini and the next start begins the same way. Clicking/dragging with the left mouse button in the GamePad window uses the
touch screen. The GamePad screen has the macOS modes (settings overlay, Display): a separate
window, a picture-in-picture overlay in a corner of the TV window (corner, size and opacity
selectable; click it to touch), the automatic overlay, off, or the GamePad picture alone in the TV
window (click it to touch); Ctrl+G shows/hides it, and the choices are saved in settings.ini. Closing the TV window ends the
game; closing the GamePad window only hides it. These builds have no menu bar: the macOS menus'
options (Graphics, Display, Input, Save States) are in the settings overlay (F1, see Playing),
plus the number-key shortcuts and the environment variables below.
Settings, controls and save states live under ~/.config/wwhd (or $XDG_CONFIG_HOME/wwhd).
To check the build without the game, python3 tools/recomp/stubgen.py build/gen-stub writes
placeholder guest code and -DGEN_DIR=$PWD/build/gen-stub builds against it (the result cannot
run the game).
The Windows build uses the same Vulkan renderer and SDL3 host as Linux. Both native LLVM and
MSYS2 CLANG64 builds are supported. Choose either method below and use separate build directories
when switching toolchains. Both require the generated build/gen from the recompiler steps above.
Install LLVM (with clang and clang++), CMake, Ninja, Visual Studio's Desktop development
with C++ workload (for the Windows headers and runtime libraries), and the Vulkan SDK. Ensure
clang, clang++, cmake and ninja are on PATH, and VULKAN_SDK points to the SDK installation.
Run from PowerShell:
cmake -S . -B build/windows -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release
cmake --build build/windows
ctest --test-dir build/windows --output-on-failure
./build/windows/wwhd.exe --renderer-smokeCMake uses installed native dependency packages where available and downloads pinned source
releases of missing glslang, SDL3, zlib and LZ4 dependencies into the build directory. The first
configure therefore needs internet access. SDL3's DLL is copied next to the executable. To use
an existing zlib installation, set ZLIB_ROOT or the standard ZLIB_INCLUDE_DIR,
ZLIB_LIBRARY_RELEASE and ZLIB_LIBRARY_DEBUG cache variables.
Install MSYS2, open its CLANG64 shell, and install the toolchain and
dependencies with pacman. This method uses MSYS2 packages and does not require Visual Studio
Build Tools or the separate Vulkan SDK:
pacman -S mingw-w64-clang-x86_64-{clang,cmake,ninja,python,vulkan-headers,vulkan-loader,glslang,sdl3,lz4,zlib}
cmake -S . -B build/windows-msys2 -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release
cmake --build build/windows-msys2
ctest --test-dir build/windows-msys2 --output-on-failure
./build/windows-msys2/wwhd.exe --renderer-smoke # checks the Vulkan renderer, no game files neededRun it from the CLANG64 shell, or copy the DLLs it needs (SDL3.dll, libc++.dll,
libunwind.dll, zlib) from C:\msys64\clang64\bin next to wwhd.exe; vulkan-1.dll comes with
your GPU driver. Settings, controls and save states live in %APPDATA%\WWHD. Python for the
extraction and recompiler steps can be the MSYS2 one (pip install pycryptodome).
For either method, use Release for gameplay performance. Microsoft's cl.exe compiler is not
supported because the recompiled game requires Clang's musttail support.
The build fails with a clear message if build/gen has not been generated. The runtime checks at
startup that game/code/cking.rpx matches the recompiled code.
On an ARM64 PC (e.g. Snapdragon X), the native LLVM method works with the ARM64 LLVM
(aarch64-pc-windows-msvc; the copy in Visual Studio's C++ Clang tools component was tested) and
Visual Studio's ARM64 build tools, from an ARM64 developer prompt (vcvarsarm64.bat), plus the
ARM64 Vulkan SDK. The release is built like the x86-64 one, with the pinned llvm-mingw for ARM64
hosts (llvm-mingw-20260922-aarch64 in tools/installer/toolchains.json, target
aarch64-w64-mingw32); see the windows job of .github/workflows/release.yml.
With Visual Studio's bundled clang, add -DCMAKE_LINKER_TYPE=MSVC: its lld-link is built without
libxml2 and embeds an application manifest Windows refuses to start ("side-by-side configuration is
incorrect"); Microsoft's link.exe merges it correctly. CMake links clang's compiler-rt builtins on
the MSVC ABI (128-bit division, __udivti3, which no MSVC library provides on ARM64).
The Android port is by rhemfur (issue #23): the Vulkan renderer on
an arm64 phone through SDL3's Android activity, measured at 30–32 fps in the heaviest scenes on a
Galaxy S25 Ultra (Snapdragon 8 Elite). Releases never contain libmain.so or anything derived
from the game files, and they never will. An APK you build yourself from your own dump contains
your recompiled game, so it is for your own phone only: don't share it. (CI builds that APK only with
placeholder code, to check that it compiles.)
Development builds and releases include the setup app (…-android-arm64.apk, no game code in it):
install it, choose your game, and setup builds the game on the phone.
For players: docs/android.md explains what the Android version does, the tested phones and how to play (Português, Español).
You need:
- a phone with arm64, Android 13 or newer and Vulkan 1.3;
- a game controller (Bluetooth or USB) is best: it is the GamePad's buttons and sticks.
Without one, the 🎮 button under the view button (top left) shows on-screen controls: two
sticks, the D-pad, A B X Y, L R ZL ZR and + − (shown or hidden, remembered). They are a second
controller, so a physical one keeps working, and touches elsewhere still reach the GamePad's
touch screen. Keyboards only type text (
WWHD_ANDROID_KEYBOARD=1inenv.txtmakes them a GamePad too); - on the computer: the Android SDK (platform 36, build tools 35.0.0), NDK 30.0.16248370, JDK 17
or newer, CMake 3.20+ and Ninja, Python 3, and your own
build/gen(steps 1 and 2 under Building).
android/build_native.sh # libmain.so, libSDL3.so, libc++_shared.so -> android/app/libs
# CPU=generic (default): any arm64 phone; CPU=oryon-1: tuned for the Snapdragon 8 Elite (e.g.
# Galaxy S25), runs only there. ANDROID_SDK, NDK_VERSION, GEN_DIR, JOBS: see the script.
python3 android/make_icon.py # optional: the launcher icon from your game/meta/iconTex.tga (Pillow)
cd android && ./gradlew assembleRelease
adb install -r app/build/outputs/apk/release/app-release.apkStart the app once: it creates Android/data/org.wwhdrecomp.wwhd/files/ on the phone's storage
(reachable over USB). Copy your extracted game there as game/ (code/, content/, meta/);
save/ takes a save (user/cking.sav, the same layout as on the computer); config/wwhd/ holds
settings.ini and the shader caches; an optional env.txt takes WWHD_ options, one NAME=value
per line. Long-press the app icon to export or import the save as a zip.
In the game, the button in the top left corner switches the view: a tap cycles TV with the GamePad picture in a corner, the GamePad picture alone (Minus in the game switches to Off-TV Play, the game on the GamePad) and the TV alone; a long press switches 60 fps (its dot: green on, yellow while the phone pauses it because it is too slow or too hot). Touches on the GamePad picture touch the GamePad. The settings overlay (hold Select or press Home) has the other GamePad screen modes and the graphics options.
./build/cmake/wwhd # options: --game DIR (default game), --save DIR (default save)--renderer=metal or --renderer=vulkan (or WWHD_RENDERER_RUNTIME=metal|vulkan) overrides the
saved renderer choice for one start.
Vulkan presentation (settings overlay › Graphics › Presentation): Vsync (smooth) (FIFO, the
default), Low latency (MAILBOX: the newest frame at each refresh, no tearing) or Off (may tear)
(IMMEDIATE). Only modes the driver offers can be chosen (MoltenVK on macOS offers vsync and
immediate, no mailbox); a change applies at once and is saved with the other graphics options.
WWHD_VK_PRESENT_MODE=fifo|mailbox|immediate overrides it for one start (not saved). The log says
which mode is in use and which the driver offers ([vulkan] TV present mode fifo (available: …)).
Two windows open: the TV and the GamePad screen (map, items, menus). Click and drag in the
GamePad window to use the touch screen. Saves go to save/.
Settings overlay: press F1 in the game window, or Cmd+, on macOS (also Settings… in the app menu; most Mac keyboards send F1 only with Fn+F1 unless "Use F1, F2, etc. keys as standard function keys" is on), or hold Select / Minus for half a second, or press Home, on a controller, for an in-game menu over the picture: save states, graphics, display (full screen, remembered for the next start; picture scaling; the GamePad screen), audio (a master volume slider and the speaker setup), gameplay mods and cheats (Graphics also has the Vulkan presentation mode), controls (the same controller drawing as Input > Controls…: select a button or chip and press the key or controller input to use; also on Windows and Linux) and the console language (only the languages your game contains can be chosen; the USA game has English, French and Spanish, the European one English, French, German, Italian and Spanish; experimental: those languages from your own European or Japanese copy of the game played with the USA code, see docs/language-packs.md; fan translations into Arabic or Hebrew are drawn right to left, see docs/rtl-text.md). Mouse, keyboard (arrows, Enter, Esc) and controller (D-pad / stick, A, B; L / R switch tabs) all work. The game keeps running but gets no input while it is open; Esc, F1 or B closes it. On macOS it shows the same options as the menu bar, and both stay in sync. Shift+F1 still saves state slot 1; slot 1 is loaded from the overlay (F1 used to load it).
Entering text (the name screen): when the game asks for text — your name when you start a new
file — a text window appears under the game's own name field (also in the GamePad-only and
picture-in-picture modes; it scales down to fit, and on very small windows moves up over the field),
with a field, a character counter (the name takes up to 8 characters) and an on-screen keyboard. The
game's field shows the name in the game's font as you type. Only characters the game's font can draw
are offered: the keys and typed characters are checked against the font the name is drawn with
(CKingMsg.bffnt, read from your own game files at run time); a missing one is refused with a short
note. Type on the keyboard (any layout, accents and dead keys, input methods), Enter = OK,
Esc = Cancel, Backspace / Delete / arrows edit. With a controller: D-pad or left stick choose a
key, A types it, B deletes, X adds a space, Y is Shift (once, then Caps), L / R
switch between letters, accented letters and symbols (kana on a Japanese game), ZL / ZR move the
caret, Start or the OK key confirms. The mouse clicks keys too; on Android the system keyboard
also opens. The game gets no input while the window is open, and none after it closes until the
button that confirmed is released. WWHD_SWKBD_TEXT=<name> answers automatically (test runs); only
where the overlay can't show does the old prompt remain (the typed text in the window title on
Windows/Linux, a dialog on macOS).
Default keyboard layout:
| Keyboard | Wii U GamePad |
|---|---|
| W A S D | left stick (move) |
| arrow keys | right stick (camera) |
| K or Space | A |
| J | B |
| L | X |
| I | Y |
| Q / E | L / R |
| Left Shift | ZL (target) |
| C | ZR |
| Enter / Tab | + / − |
| H | Home |
| 1 2 3 4 | D-pad up / down / left / right |
| X / V | left / right stick click |
Input › Controls… remaps everything on a drawing of the controller: click a button, stick
direction or stick click, then press a key or a controller button / stick direction (each input
has a key, an alternate key and a controller binding); Esc cancels, right-click clears. Pressed
buttons light up and the sticks show their deflection, so you can test the mapping; a key bound
twice is marked with a warning. Changes apply immediately, also while playing. A dead zone for
controller sticks and an option to invert the camera's up/down are at the bottom, with Reset to
Defaults…. The mapping is saved to ~/Library/Application Support/WWHD/controls.json
(WWHD_CONTROLS=<file> uses another file); deleting it restores the defaults. The app's
single-key shortcuts (R, O, M, N, 6–9, P, F1–F5, F12) and Esc can't be bound. The Screenshot
key (row "Photo", F10 by default) is bound here like a button, to a key or a controller input.
The Graphics menu in the menu bar switches fixes and enhancements while playing (the TV
window title shows what is active and the current frame rate): 60 fps by frame interpolation
(6; 120 and 240 fps in the menu and the settings overlay), true 60 fps (7, experimental), internal resolution 1x / 1.5x / 2x / 3x (R
cycles; the game renders at 1280x720, 2x renders at 2560x1440), edge smoothing (FXAA, 8),
ambient-occlusion mode (O cycles), full-size occlusion depth (M), 16x anisotropic
filtering (N), the aspect ratio, the renderer (Metal or Vulkan), Take Screenshot (F10,
rebindable in Controls: the TV picture as a PNG at the internal resolution, without the settings
overlay, in ~/Library/Application Support/wwhd/screenshots; also Open Screenshots Folder and an
option to save the GamePad screen too), and a frame capture for debugging (P or fn+F12, written to captures/;
captures contain game imagery, so keep them to yourself). The Graphics choices are remembered
between launches (macOS preferences; defaults delete wwhd resets them).
True 60 (7) computes Link and the camera at 60 Hz while the game state after every 30 Hz step
stays bit-identical to the 30 fps game, except the random-number sequence, which drifts because
drawing code draws random numbers too (later drops and ambient behaviour differ like in any other
session; see docs/decomp-notes.md, "True 60 fps").
The Gameplay menu has optional changes to how the game plays, all off by default: climb any wall (with a stamina wheel; B or A lets go), a direct right-stick camera (no easing, adjustable speed), a mouse camera (click the picture to capture the pointer, Esc releases it), first person on the mouse wheel, quick doors and fast scene changes. It also has cheats: all items, the full-power Master Sword and Mirror Shield, 20 hearts / double magic / 5000 rupees, and infinite health, magic or ammo. Story cheats (all songs, Triforce shards, dungeon map/compass/boss key, a small key) can change or break story events, so use a spare save file. Cheats edit the live save data; save in game to keep them.
The Save States menu (and the Saves tab of the settings overlay) saves to one of 5 slots and
loads it back (Shift+F1–F5 save, F2–F5 load; slot 1 loads from the F1 settings overlay);
each slot shows its time and area. Slots are kept in ~/Library/Application Support/wwhd/states/
and survive restarts. By default a slot holds a portable state (slotN.wwstate, a few KB):
your progress and Link's place, no game data, safe to attach to bug reports. Loading one works
once a Quest Log is being played (it waits until then): the progress is put into the game and Link
enters the saved stage at the saved spot (on the boat if he was on it). It is not an exact snapshot:
enemies, cutscenes and other actors start fresh. A portable state can only be saved while you
control Link (not during cutscenes, dialogue or stage changes). A state from another Quest Log is
loaded into the one being played, with a notice.
Full save states (Save States menu or Saves tab, off by default, for debugging;
WWHD_FULL_SAVE_STATES=1) save the whole running game instead (slotN.bin, about 270 MB) and
restore it exactly; they contain game code and data, never share them. A full state made by an
incompatible build is refused. Loading a slot loads whichever kind it holds.
Crash Recovery (Save States menu, off by default, or WWHD_CRASH_RECOVERY=1): every 2 minutes the
game is saved into one of three automatic states (states/auto/, about 260 MB each; the save
freezes the game for about 0.1 s), and the controller input since the latest one is recorded. After a
crash, the crash log names them, and WWHD_REPLAY=<n> ./build/cmake/wwhd loads automatic state n and
plays the recorded input back to reproduce the crash. Automatic states can also be loaded from the menu.
Game controllers (Xbox, PlayStation, Switch Pro, MFi) work too; by default buttons map by
position (the bottom face button is the Wii U's B), and they can be remapped in the Controls window.
Face buttons in the settings overlay (F1 → Controls) or the Input menu switches that preset:
by position (Nintendo) is the default; by label (Xbox) makes the button named A drive the Wii
U's A instead — on an Xbox pad that means A accepts/acts and B goes back (issue #78), and the X/Y
items follow the printed labels too. Automatic reads the labels printed on the pad that was
plugged in first and follows them, so an Xbox pad plays by label and a Nintendo pad by position
without touching the setting. The choice rewrites the four face bindings only; keyboard keys
and the other inputs stay as they are, and a hand-edited face binding shows as custom (which also
leaves Automatic).
In by label (including when Automatic resolves to it), in-game dialog icons and HUD face-button
backgrounds use Xbox-style colours (green A, red B, blue X, yellow Y); changing the setting updates
cached prompts without a restart. The HUD button cluster also follows the Xbox positions: A bottom,
B right, X left, Y top. In-game shoulder prompts use R1/R2 for R/ZR and L1/L2 for L/ZL in this mode.
By position and custom keep the game's colours and positions. See in-game glyphs.
The Input menu switches whether keyboard and controllers act as the Wii U GamePad (default)
or as a Wii U Pro Controller (WWHD_PRO_CONTROLLER=1 starts in that mode); with the Pro
Controller, the GamePad window keeps its screen and touch input.
When the game asks for text (e.g. your name), the text window described above opens.
Rumble works too (SDL hosts): what the game asks its controller's motor to do is passed to the
connected controllers that have one, which includes the Pro Controller's. A host controller has a
single motor, so a GamePad rumble pattern plays as on/off (or half strength where it alternates),
and the motors stay still while the settings overlay is open, while no game window has focus and
once the app quits. Controls > Rumble in the settings overlay (F1) turns it off and is
remembered (WWHD_RUMBLE=0 starts with it off). The macOS app does not drive controller motors yet.
Gyro aiming (settings overlay → Controls → Gyro…, off by default): in first person the
game turns the camera when the GamePad moves. The port turns its virtual GamePad with a host
controller's gyro (SDL3; on the macOS app through GameController.framework), a Cemuhook (DSU) server
or the mouse (Steam Input "gyro to mouse": while the game aims, the pointer is captured and the mouse
turns the GamePad); it works in GamePad and Pro Controller mode. The axis mode (player space, yaw or
roll), sensitivity and invert per axis, a recalibrate button or key, and the Cemuhook server, port and
slot are saved; WWHD_GYRO=off|controller|cemuhook|mouse overrides the source at start. Details,
troubleshooting and what to test: docs/gyro.md.
The Display menu: full screen for the TV window (⌘F, ⌃⌘F or the green button; the
pointer hides after 2 s without movement), picture scaling (smooth, sharp, or integer scale) and
where the GamePad screen goes: a separate window (which can be put on another display, also in
full screen there), a picture-in-picture overlay in a corner of the TV picture (size, corner and
opacity selectable; click it to touch), an automatic overlay that appears for a few seconds when
the GamePad picture changes a lot (a page or menu switches; ⌘G keeps it up), off, or GamePad
only (the GamePad picture alone in the TV window, click it to touch; Minus in the game switches to
Off-TV Play).
⌘G shows/hides the GamePad screen in any mode. Window positions, full screen, these choices
and the renderer are remembered in ~/Library/Application Support/wwhd/display.plist
(delete it to reset).
Closing the TV window (its close button or ⌘W) quits the game, as on Linux and Windows; closing
the GamePad window only hides it (⌘G brings it back). While a save file is being played, closing
the TV window or ⌘Q first asks Quit Wind Waker HD?: Quit, Cancel (keep playing), or
Save State and Quit, which writes save state slot 1 (Save States menu) and then quits. On the
title screen and the file select, before a file is loaded, it quits without asking.
WWHD_QUIT_PROMPT=0 turns the question off; scripted and hidden test runs never ask.
In Gaming Mode the Steam button belongs to Steam and there is no keyboard, so keyboard shortcuts (and a menu key set to a keyboard key) can't reach the game (issue #112):
- Settings overlay: hold the View button (two squares, left of the screen) for half a second. Save states are in its first tab (Saves).
- Save-state shortcuts: in Steam, open the game's controller settings and map the back buttons (L4/R4/L5/R5) to keyboard keys: Shift+F1 saves slot 1, F2–F5 load slots 2–5, Shift+F2–F5 save them. Steam sends those keys to the game.
- Shaders are translated on first use and cached in
~/Library/Caches/wwhd/shaders.bin; later runs replay that cache at startup. - docs/performance.md covers how to profile the port, measured fixes and open performance leads.
- Useful environment variables:
WWHD_NO_AUDIO=1,WWHD_NO_GAMEPAD=1(no second window),WWHD_NO_CONTROLLERS=1(SDL builds: ignore host game controllers),WWHD_LANGUAGE=<code>(console language: 1 English, 2 French, 5 Spanish, … — the USA/Asia disc carries English, French and Spanish; a language the game doesn't contain starts in English; withWWHD_LANGUAGE_REGION=euorjpfrom a language source, docs/language-packs.md),WWHD_RTL=0/1(right-to-left text for Arabic and Hebrew packs off / forced on, docs/rtl-text.md),WWHD_DRC_MODE=window|pip|auto|off|gamepad,WWHD_FULLSCREEN=0|1(the TV window starts windowed / in full screen this time instead of as it was left; that session's full screen is not remembered),WWHD_ASPECT=16:9|window|16:10|21:9|32:9|<w:h>,WWHD_AUDIO_VOLUME=0..1(the master volume the session starts at, whatever the Audio slider saved),WWHD_AUDIO_OUTPUT=auto|tv|gamepad(the host plays the TV's sound, plus the GamePad's in Off-TV Play: auto; or only one of them),WWHD_SHADER_CACHE=<file>|0,WWHD_AO_MODE=0..2,WWHD_AO_HIRES=0|1,WWHD_ANISO=0|1,WWHD_RES_SCALE=1|1.5|2|3,WWHD_FXAA=0|1,WWHD_QUIT_PROMPT=0(macOS: quit without asking, also during a game),WWHD_INTERP=1,WWHD_INTERP_FPS=60|120|240(frame interpolation at that rate),WWHD_INTERP_PACED=0|1,WWHD_TRUE60=1(start values for the Graphics menu; they override the remembered choices);WWHD_DISPLAY_HZ=nreplaces the detected display refresh rate that 120/240 fps are capped to (0: no cap);WWHD_UNCAPPED=1starts with the debug switch "Uncapped" on (no frame limit, no vsync; the game runs faster than real time);WWHD_SHADOW_SCALE=ngives the shadow maps their own resolution factor (by default they scale with the internal resolution; since v0.2.9 both look practically the same, issue #67).=1keeps the console's 1024x1024 and uses much less GPU memory at 2x/3x, useful on weaker hardware;WWHD_STATE_DIR=<dir>stores save states elsewhere;WWHD_FULL_SAVE_STATES=0|1full or portable save states for this start;WWHD_PORTABLE_LOAD=<file.wwstate>loads that portable state (e.g. from a bug report) as soon as a Quest Log is being played;WWHD_RUMBLE=0|1(SDL builds) start value for Controls > Rumble (overrides the remembered choice);WWHD_LOG_RUMBLE=1logs the game's motor requests and what the motors do.WWHD_STRICT_MUL=0turns off the GPU's 0×anything=0 multiply rule in shaders (on by default, as in Cemu; off only for performance comparisons, it brings back e.g. the black letter in the Rito mail sorting game). - Crashes and game halts write
captures/crash-<time>.log(crash address, registers, the guest call chain, a host backtrace and the last log lines; useful for bug reports, it contains only addresses, function names, the file names of the program's modules and log text). A crash address outside the game code names its module and offset (in amdvlk64.dll+0x1A2A01): a graphics driver, or an overlay's Vulkan layer; the log lists the Vulkan layers at start ([vulkan] layers:). - Debugging aids (frame/draw dumps, traces, scheduler statistics) are documented next to their
code: grep for
WWHD_inruntime/src.
tools/savegame/gc2hd.py converts a GameCube Wind Waker save (.gci, USA GZLE01 or Japanese GZLJ01,
e.g. from a memory-card dump or Dolphin) into a Wind Waker HD save (cking.sav), all three files
with their progress, items, songs, charts and story state. It works for this port and for Cemu.
python3 tools/savegame/gc2hd.py "My Save.gci" -o converted # writes converted/cking.sav
python3 tools/savegame/hd_save_info.py converted/cking.sav # shows what is in itBack up your old cking.sav, then put the new one in save/user/ (Cemu:
mlc01/usr/save/00050000/10143500/user/80000001/ for the USA game). The Tingle Tuner becomes the
Tingle Bottle (HD's item in the same slot); Picto Box photos and HD-only statistics are not carried
over; Japanese player names become "Link". Details and options: tools/savegame/README.md.
Tested with 167 GameCube saves across the whole story (100% and Any% routes).
After building the game, setup offers Preparing shaders. It uses your own game files and a small state template supplied with the release to compile covered shaders and pipelines for your computer before you play. Preparation usually takes tens of seconds on a recent Mac; the time varies with your computer and graphics driver. The generated caches stay on your computer.
On a Mac, where Graphics → Renderer lets you choose Metal or Vulkan, setup prepares shaders for both, one after the other on the same progress bar, so the game starts prepared with either renderer. If Vulkan cannot start on your Mac, Metal is still prepared and Vulkan is skipped with a short message. Windows, Linux and Android prepare Vulkan.
You can uncheck Prepare shaders now, or use --skip-shader-prebuild with terminal setup.
On Android, Back skips the preparation screen. If preparation fails or the build has no template,
setup skips it with a short message and the game compiles shaders as needed during play.
Skipping is remembered for the same game and template, including after game runtime updates.
Use Settings → Graphics → Rebuild shader cache, then run setup and choose repair/update to prepare a fresh cache. Launching the game never starts preparation automatically. Uncovered areas can still stutter the first time you visit them. When a shader is unavailable, the renderer waits for its exact state and may repeat the last finished picture.
Caches live in the host settings folder's shader-prebuild directory, or
data/user/shader-prebuild in a portable installation. Keep generated caches and recordings local.
Local recording and preparation are available for developers and players who want to cover additional scenes. See the shader-template maintainer guide for the Windows and Linux commands and local template selection.
tools/decomp/ names WWHD functions by matching them against the
zeldaret/tww GameCube decompilation (CC0); the 60 fps features
are built on those names (tools/recomp/hooks.txt, runtime/src/interp*.cpp, runtime/src/true60*.cpp).
Findings are in docs/decomp-notes.md.
tools/verify/ is a differential test harness for a functionally verified decompilation: it runs
hand-written C++ next to the recompiled original on generated and recorded inputs and compares
return values, memory effects and call sequences (see tools/verify/README.md). The verified
source itself is derived from the game and is not part of this repository.
git clone https://github.com/zeldaret/tww tww # git-ignored reference checkout
python3 tools/decomp/match.py game/code/cking.rpx tww build/names.tsvThe assert, string and actor-profile stages only need the decompilation's sources. The
call-graph stage additionally needs the decompilation built (tww/build/GZLE01), which in turn
needs ninja and your own GameCube disc image of The Wind Waker (USA) as described in the tww
README. build/names.tsv is derived from the game and stays on your machine.
The game is playable from start to finish (players have completed it) on macOS, Windows, Linux and Android, and bugs are fixed as they are reported (see the changelog and the issues). Rendering is compared with Cemu side by side. It runs at the console's 30 fps or, with frame interpolation, at 60, 120 or 240 fps (up to the display's refresh rate); true 60 fps is experimental. Known gaps: geometry shaders and rectangle primitives are not implemented yet (not encountered in the game so far), and the macOS app does not drive controller rumble yet.
The code of this project is licensed under the Mozilla Public License 2.0 (see LICENSE).
Vendored third-party code keeps its own license: Cemu (MPL-2.0), metal-cpp (Apache-2.0), {fmt} (MIT) and Dear ImGui (MIT); see Credits. The extractor links zstd (BSD-3-Clause); releases build it from its pinned release source. The game itself is Nintendo's property and is not included.
European-game builds and the executable-derived address map are by ElFDA, contributed in PR #77. The combined integration adds the runtime address audit and regional regression scenarios. Thanks also to GreenNaugahyde and the MPL-2.0 ZeldaWWHDRecompAndroid fork: its EUR address mapping informed the desktop prototype, and the rendered mip chains and sun depth peeks (bloom, haze, the sun's glare), the GPU decoder for compressed textures, custom Adreno driver support and the run/swim speed boost were adapted from or modelled on its work.
The Android port (android/, the Android parts of the runtime, the single-screen view) is by
rhemfur, who also contributed the paced frame interpolation, the
Vulkan presentation and feedback-image work, and Linux/Windows fixes.
GPU address library, shader decompiler and a few reference structures are vendored from
Cemu (MPL-2.0, see runtime/third_party/cemu/LICENSE.txt);
tools/wudextract.py, runtime/src/espresso_fp.c and parts of the OS layer are ported from or
follow Cemu as noted in those files (in the Vulkan renderer: the vertex-format table and the
shader parser glue). Also vendored: metal-cpp
(Apache-2.0, runtime/third_party/metal-cpp/LICENSE.txt), {fmt}
(MIT, runtime/third_party/fmt/LICENSE) and Dear ImGui v1.92.9b by
Omar Cornut and contributors, for the settings overlay (MIT, runtime/third_party/imgui/LICENSE.txt).
The Cemu archive (.wua) reader in tools/wudextract/zarchive.cpp is written from the format of
ZArchive by Exzap (MIT No Attribution; nothing of it is
vendored); zstd by Meta Platforms (BSD-3-Clause) decompresses
it: release builds compile its pinned 1.5.7 release (URL + SHA-256 in cmake/Zstd.cmake) statically
into wwhd-extract and include its license in third-party-licenses/; source builds use a system
zstd when there is one.