Zelda: Wand of Gamelon Static Recomp
Zelda: Wand of Gamelon Static Recomp is a Windows port of The Legend of Zelda: Wand of Gamelon that offers an HD renderer with new filters.
- Type: PC port
- Game: The Legend of Zelda: The Wand of Gamelon
- Runs on Windows
- By tools81
- Latest release v1.0.0, 2026-10-22
- Source: https://github.com/tools81/zelda-wand-of-gamelon-static-recomp
README
The Legend of Zelda: The Wand of Gamelon — static recompilation
This project translates the game's Philips CD-i 68000 program modules into C
ahead of time and runs them with a native Windows runtime. The runtime emulates
the CD-RTOS (OS-9) kernel, CD file manager, video decoder and ADPCM audio
processor. The boot module (cdi_zelda) and the disc's cdi_bumper module are
recompiled; the original real-time data and audio streams are read from the
disc image at runtime.
Build and run
Requirements: Windows 10+, Visual Studio 2022 with the C++ build tools (MSVC), CMake 3.19+ and Python 3.8+. No third-party libraries are needed.
- Keep your legally obtained
.binand matching.cueinrom/. - Run
build.bat. It extracts the program modules toextracted/, generates C sources ingen/, compiles the native executable and copieswand_of_gamelon.exeto this folder. - Run
wand_of_gamelon.exefor the original rendering, orwand_of_gamelon_hd.exefor the HD renderer. Either executable can also be launched with a.cueor.binpath as its argument.
The HD executable keeps its settings and NVRAM in saves_hd/, separately
from the original executable's saves/. Its F10 filter cycle includes
Cartoon HD and Cartoon HD+, which use edge-aware color interpolation
when enlarging sprite edges. This is spatial upscaling rather than temporal
per-sprite motion interpolation; both builds retain the CD-i's 60 Hz field
timing.
Controls
- Arrow keys: move the pointer; hold Shift to move faster.
- Z, Space, A, or Enter: CD-i button 1.
- X, Ctrl, S, or Enter: CD-i button 2.
- Escape: quit. F9 changes aspect mode, F10 changes the display filter, and F11 toggles fullscreen. Tab enables turbo speed.
- F5 toggles player-only invincibility, and F6 toggles run-ahead. F2 saves to the selected quicksave slot, F4 restores it, and F3 selects the next of 10 in-memory slots. Snapshots last for the current executable session.
- XInput controllers are supported; A/B map to buttons 1/2, and pressing Y opens the inventory (the game's Down + B command). The D-pad or left stick moves the pointer.
One-frame run-ahead is enabled by default to reduce input latency. If the game
repeatedly misses its 60 Hz frame deadline, it disables run-ahead automatically
to preserve smooth playback; press F6 to toggle it manually. Pass
--no-runahead to start with it disabled, or --runahead to start enabled.
The HD executable's Cartoon filters use more GPU work than the original
renderer; F10 cycles through the faster filters if needed.
Portable distribution
Build the executables, then run
powershell -ExecutionPolicy Bypass -File packaging\New-PortableZip.ps1 -Version 1.0.0
to create a portable ZIP in dist\. The ZIP contains both executables and
the launch/setup/update scripts, but never copies the rom\ folder or any
game images. Users extract it, run Install-Portable.bat to select their own
.bin or .cue, then launch with Start-Game.bat. Runtime saves and settings
are stored next to the executables, not in AppData.
Portable builds update themselves: on startup, each executable runs
Update-Portable.ps1, which checks the latest GitHub release of the repository
in update-repository.txt (default tools81/zelda-wand-of-gamelon-static-recomp,
override with -UpdateRepository owner/repository). If the release tag is newer
than version.txt and has a wand-of-gamelon-X.Y.Z-portable.zip asset, the
package is installed in place and the game restarts. Pass --no-update to skip
the check. Development builds run from the repository have no
Update-Portable.ps1 beside them and never update.
The build uses the game-specific module addresses and title in game.json.
The ROM image and extracted/build outputs are ignored by Git. The runtime's
headless, input scripting, rendering and logging options are described below.
game.json
{
"title": "My CD-i Game",
"exe_name": "mygame",
"modules": [
{"file": "auto", "base": "0x100000", "extra_entries": [], "a5_table": "auto"},
{"file": "cdi_bumper", "base": "0xE0000", "a5_table": "auto"}
]
}
- The first module is the program the player boots.
"auto"reads its name from the disc label's application identifier (extracted/_app.txt). - Other modules are programs the game loads at run time with F$Load or
F$Fork, for example the Philips logo "bumper". Each one is recompiled for its
baseaddress, and the runtime places it there. Ranges must not overlap and must stay below0x140000, where the heap starts. Program modules the game loads that are not listed still run, slowly, in the built-in interpreter. extra_entriesare code addresses the analyzer can't discover, as absolute hex values (base + offset). The analyzer already finds:- call and branch targets;
- switch tables;
- MPW A5 jump tables;
- Microware
move.l #off,dN / jsr (pc,dN.l)calls; - F$Icpt handlers;
lea/peafunction pointers that start withlinkormovem.
a5_table:"auto"detects programs built with Apple MPW-style toolchains, whose calls go throughjsr N(a5).
To see what the analyzer found, run python tools/analyze.py [module index].
It writes a disassembly with recovered function names, from MacsBug symbols if
present, to build/listing_<module>.asm.
Layout
| Path | Contents |
|---|---|
tools/cdi_extract.py | Green Book filesystem reader. Writes program files and _app.txt (--all also dumps the real-time streams). |
tools/m68k.py | MC68000 decoder |
tools/os9mod.py | OS-9 module header, idata and irefs |
tools/analyze.py | Code discovery (recursive descent plus the patterns above) |
tools/gen.py | 68000→C generator, flag liveness, dispatch tables, gen/game_config.h |
tools/config.py | game.json loader |
tools/gen_tests.py | Per-instruction differential tests |
tools/bmp2png.py | Converts frame dumps to PNG |
runtime/cpu.c | Dispatcher and run loop |
runtime/interp.c | 68000 interpreter, for RAM code and the --interp reference mode |
runtime/alu.c | Shifts, division and BCD with exact flags |
runtime/os9.c | OS-9 kernel: memory, processes (fork/wait/exit), signals, alarms, paths, NVRAM, CSD |
runtime/cdfm.c | CD file manager: reads, and real-time SS_Play with PCB/PCL/CIL |
runtime/video.c | UCM display control (FCT/LCT), MCD212-style renderer, cursor, pointer |
runtime/audio.c | ADPCM levels A/B/C, sound maps with gapless SM_Out, attenuation |
runtime/render_d3d11.c | GPU presenter: aspect modes and sharp/smooth/CRT filters |
runtime/host_win32.c | Window, input (keyboard and XInput), waveOut, pacing, scripted and fuzz input |
test/ | Instruction differential tester and ALU reference vectors |
docs/CDI_NOTES.md | CD-i facts and API codes worked out during the first port |
Debugging workflow
-
Find what fails. Unimplemented OS calls and SetStat/GetStat codes are logged to
saves/recomp.log. Fatal errors also show a 68k register dump and a stack backtrace with function names. -
Debug options:
--trace/--trace3log syscalls with callers, signals and disc activity.--headless --frames N --dump Kruns fast without a window and writes every Kth frame as a BMP.--script "field:key:dur,..."scripts input (1 2 L R U Dfor controls;S N W I Tfor save, next slot, restore, invincibility, and run-ahead).--fuzz SEEDdrives the game with random input.--wav out.wavrecords the audio.
-
Check the translation. Run the same headless session twice, once with
--interp, which executes everything in the interpreter, and compare the frame dumps and WAVs. Headless runs are deterministic: emulated time is counted in instructions, and the clock uses a fixed epoch. Any difference points to a translation bug. -
Unit-level checks.
python tools/gen_tests.pyandpython test/gen_alu_vectors.pygenerate test sources. Then build theinsn_testandalu_testtargets:cmake --build build\cmake --config Release --target insn_test alu_test build\cmake\Release\insn_test.exe rom\<image>.bin 200 build\cmake\Release\alu_test.exe -
Code that jumps into RAM is not an error. It runs in the interpreter. Jumps to undiscovered addresses inside a recompiled module are fatal. Add those addresses to
extra_entries, or teachanalyze.pythe pattern.
Things a new game may need
- More CD-RTOS calls. The runtime implements what the first game used. See
docs/CDI_NOTES.mdfor what's known and what isn't. The CD-i Full Functional Specification (Green Book, May 1994) documents the calls but gives no numeric function codes. Infer those from the game's library wrappers: look at thed2values passed withos9 I$SetStt. - UCM drawing functions (DM_/DP_/DR_/RG_ and text). These aren't implemented yet because the first game drew everything itself.
- PAL games. The CSD in
os9.c(write_csd) advertises a 525-line NTSC player and the video runs 240 lines at 60 Hz. A 625-line title needsLI="625", 280 lines (SCREEN_LINESinvideo.c) andFIELD_RATE50. - Full Motion Video (MPEG cartridge) isn't supported.