Oracle of Ages & Oracle of Seasons - Static Recompilation
The Legend of Zelda: Oracle of Ages / Oracle of Seasons
OriginGame Boy / Game Boy Color
TechnologyStatic recompilation (native C/C++)
Available forWindows, Linux, macOS
View official projectAbout the project
The Legend of Zelda es una serie de videojuegos de acción-aventura creada por los diseñadores japoneses Shigeru Miyamoto y Takashi Tezuka, y desarrollada por Nintendo, empresa que también se encarga de su distribución internacional. Su trama por lo general describe las heroicas aventuras del joven guerrero Link, que debe enfrentarse a peligros y resolver acertijos para ayudar a la Princesa Zelda a derrotar a Ganondorf y salvar su hogar, el reino de Hyrule.
2 versions of The Legend of Zelda: Oracle of Ages / Oracle of Seasons
You're viewing: Oracle of Ages & Oracle of Seasons - Static Recompilation
Official project information
Oracle of Ages & Oracle of Seasons — Static Recompilation
Two sister games. One shared runtime. Zero emulators.
Static recompilations of The Legend of Zelda: Oracle of Ages and The Legend of Zelda: Oracle of Seasons (Game Boy Color, 2001) — the entire ROM lifted into native C code that runs directly on your CPU. No interpreter loop, no JIT, no per-instruction fetch/decode at runtime. Just the game, recompiled, as if Capcom had shipped it for Windows in 2001 instead of for a 4 MHz handheld.
The Pitch
Released the same day, the Oracle games are arguably Capcom Flagship's most ambitious Game Boy work — two parallel adventures (Ages = time-travel puzzles, Seasons = season-shifting action) connected by a passwords system that lets a finished cartridge unlock a linked sequel on the other. Same engine, different worlds. So they get the same treatment here: one monorepo, two binaries, one shared runtime.
Screenshots
| Oracle of Ages | Oracle of Seasons |
|---|---|
![]() | ![]() |
| overworld — the forest of Labrynna | overworld — Holodrum, near Horon Village |
![]() | ![]() |
| intro cinematic — the castle at sunset | intro cinematic — Link rides out |
![]() | ![]() |
| boot — Capcom Flagship logo | boot — Capcom Flagship logo |
All 160×144 framebuffer dumps from the recompiled binaries (--dump-frames),
converted to PNG. Both games now reach interactive overworld gameplay —
boot, intro cinematic, title, file select, name entry, opening cutscene, and
Link walking around with a live HUD (hearts, rupees, item slots).
These are taken straight from the PPU framebuffer. In the live SDL window the ImGui menu bar now sits in its own strip above an unobstructed game — the window is grown by the bar's height and the game is blitted below it, so the top HUD row (hearts / rupees / items) is fully visible. (Earlier the bar overlaid the stretched game and clipped that row; fixed upstream in gb-recompiled — the game image fills its strip, the bar gets its own.)
Status
| Stage | Ages | Seasons |
|---|---|---|
| ROM acquired (your own copy, gitignored) | ✅ | ✅ |
| Project scaffolded | ✅ | ✅ |
Recompiler bootstrapped (gbrecomp) | ✅ | ✅ |
| Recompiled to C (~3M lines each) | ✅ | ✅ |
| First successful native build | ✅ | ✅ |
| SDL window opens, framebuffer renders | ✅ | ✅ |
| Cartridge header recognized | ✅ | ✅ |
| Boots past publisher logos | ✅ | ✅ |
| Plays opening cinematic | ✅ | ✅ |
| Title → file select → name entry | ✅ | ✅ |
| Reaches overworld gameplay | ✅ | ✅ |
| Link responds to the controller | ✅ | ✅ |
| Writes save data to SRAM | ✅ | ✅ |
| Reaches first dungeon | ⏳ | ⏳ |
| Load existing save (Continue) | ⏳ | ⏳ |
| Linked-game passwords verified | ⏳ | ⏳ |
| Audio sounds correct (4ch, 44.1 kHz) | 🛠️ | 🛠️ |
| Full playthrough | ⏳ | ⏳ |
⚙️ Update — both games are playable into the overworld!
gbrecomptranslated both ROMs to native C on the first try (~86–89 MB of generated.ceach, ~13.5k functions, ~92k basic blocks). After three CMakeLists tweaks (path, missing runtime sources,LA_HAS_IMGUI) and one real runtime bug fix (serial-port emulation — see "Solved" below),oracle_of_ages.exe(21.7 MB) andoracle_of_seasons.exe(22.0 MB) now go all the way from the Nintendo/Capcom logos through the opening cinematic, title screen, file select, name entry, and the new-game intro into live overworld gameplay — Link walking around Labrynna / Holodrum with a working HUD (hearts, rupees, item slots), responding to the D-pad, and the game writing its save to SRAM. Driven headlessly via the--inputscript flag (frame:buttons:duration) and captured with--dump-frames.
Solved: the boot-logo stall
The first real bug, and a fun one to track down. Both games booted, rendered the Capcom Flagship logo from a genuine framebuffer, ran every interrupt handler correctly… and then froze on the logo forever. Here's the trail:
-
The logo→title transition is gated by a countdown. A 16-bit timer in WRAM (
$cbb3) is decremented once per frame; when it hits zero the boot state machine advances ($c2e7: 1→2) and sets flag$ffb9, kicking off the intro. In a reference emulator (PyBoy) the timer initializes to0x00D0and cleanly counts to zero around frame 300. -
Our timer never reached zero. Instrumenting the runtime showed the timer's high byte was
0x09instead of0x00, so it would have taken ~2500 frames — and it got reloaded before then anyway. The init code at3:4d56correctly wrote0x00D0, but a single instruction at3:4cd3(LD ($cbb4),$09) kept clobbering the high byte right back. -
That clobber is gated by
$ffba. The game only runs the clobber when$ffba != 0. In PyBoy$ffbastays0; in our build it was non-zero. -
$ffbais the serial link-cable handshake byte. The boot code probes the Game Link port: it sends0xe1/0xe0and, if it reads the same bytes back, concludes a second Game Boy is connected and enters link mode — which is what corrupted the logo timer. -
The runtime's serial emulation was the culprit. On a write to
FF02with the transfer-start bit set, the fallback handler raised the serial interrupt but never updatedFF01— so the game read back the very byte it had just sent and mistook the open bus for a real peer. On actual hardware with no link cable, an internal-clock transfer clocks in0xFFfrom the floating data line.
The fix (gbrecomp/runtime/src/gbrt.c, upstreamed to
gb-recompiled). The runtime's
serial fallback now mirrors a disconnected link cable: an internal-clock
(master) transfer clocks in 0xFF, raises the IRQ, and clears the start
bit; an external-clock (slave) transfer is left pending — with no peer
to supply the clock it never completes on hardware. That second half matters:
an interim version auto-completed slave transfers too, which (against the
runtime's newer serial_exchange path) flooded the games with spurious serial
interrupts and drove their stack down into VRAM — trading the clean logo
freeze for an outright crash. Leaving slave transfers pending fixes both
failure modes, and both Oracles sail past the logo into their intros.
🔎 How it was found:
--trace-entriesfirst pointed at the boot wait-loop; targetedfprintfprobes in the runtime exposed the$cbb3counter's bad high byte; a PyBoy reference script (pyboy.memory[...]per frame) pinned the exact divergence —$cbb3, then$ffba, then the serial register.
Known Issues / What's next
- Audio not yet validated by ear. The 4-channel APU is wired up and the 44.1 kHz stream runs, but nobody's confirmed the music/SFX actually sound correct (right notes, no glitches). Needs a listening pass.
- Load-existing-save (Continue) untested. New-game works and saves to SRAM; loading a save back through the file-select "Continue" path hasn't been exercised yet.
- Deep gameplay unproven. Verified up to walking the opening overworld. Dungeons, items, screen transitions, and a longer playthrough are next.
- Linked-game password system. The headline feature of the pair — still to be verified end-to-end across the two builds.
- RAM-resident code runs via the interpreter fallback. Both games copy a
small bank-switch trampoline into WRAM and execute it there; the recompiler
can't see RAM at build time, so those few instructions correctly fall back
to the interpreter. Working as intended, noted here so the
[INTERP] WRAM execlog lines aren't mistaken for a bug.
Updated as the work progresses — this section is the source of truth.
How It Works
This repo holds two thin game-specific wrappers (one CMakeLists.txt per game)
that share a single recompiler runtime via the gbrecomp/ submodule. The
recompiler emits oracle_of_<game>.{c,h}, _rom.c (ROM bytes as a C array),
and _main.c (entrypoint) into each game's directory.
your own ROMs (never committed)
roms/oracle-of-ages.gbc roms/oracle-of-seasons.gbc
│ │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ gbrecomp/recompiler/ (sp00nznet/gb-recompiled) │
│ SM83 → C translation, ROM-as-array, headers │
└──────────────────────────────────────────────────────┘
│ │
ages/oracle_of_ages.{c,h,_main.c,_rom.c} seasons/oracle_of_seasons.{c,h,_main.c,_rom.c}
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ ages/CMakeLists.txt │ │ seasons/CMakeLists.txt │
│ + shared gbrt │ │ + shared gbrt │
└────────────────────────┘ └────────────────────────┘
│ │
▼ ▼
oracle_of_ages.exe oracle_of_seasons.exe
The heavy lifting (SM83 CPU model, scanline PPU with full CGB color, MBC5 mapper,
APU, SDL2 platform layer, ImGui debug overlay) all lives in
sp00nznet/gb-recompiled — the
same runtime that powers the Pokémon Gold / Crystal and Link's Awakening DX recomps.
Optional — nicer function names. By default the recompiler names functions
func_<bank>_<addr>. If you analyse a ROM in Ghidra (with the GhidraBoy SM83 extension),gbrecomp/tools/ghidra/will run a headless analysis, export the function/symbol names, and feed them back so the generated C reads e.g.AdvanceLogoStateinstead offunc_03_4d23:export GHIDRA_INSTALL_DIR=/path/to/ghidra python gbrecomp/tools/ghidra/run_ghidra.py roms/oracle-of-ages.gbc -o ghidra_out python gbrecomp/tools/ghidra/build_names.py ghidra_out -o ages/names.sym ./gbrecomp/build/bin/gbrecomp.exe roms/oracle-of-ages.gbc -o ages/ --names ages/names.symSee
gbrecomp/tools/ghidra/README.mdfor details.
Building
Prerequisites
- Windows 10/11
- MSYS2 with MinGW64:
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-SDL2 - Legally-obtained Oracle of Ages and/or Oracle of Seasons ROMs (
.gbc)
One-time setup
git clone --recurse-submodules https://github.com/sp00nznet/oracle-recompiled.git
cd oracle-recompiled
If you forgot --recurse-submodules:
git submodule update --init --recursive
Generate the C output for each game
See the gb-recompiled README for the
exact recompiler invocation. Drop your ROMs into roms/ and point the tool at them:
# First, build the recompiler tool itself (only needed once)
cd gbrecomp && mkdir -p build && cd build && cmake -G Ninja .. && ninja && cd ../..
# Oracle of Ages
./gbrecomp/build/bin/gbrecomp.exe roms/oracle-of-ages.gbc -o ages/
# Oracle of Seasons
./gbrecomp/build/bin/gbrecomp.exe roms/oracle-of-seasons.gbc -o seasons/
This produces, per game, oracle_of_<game>.c (~85 MB), oracle_of_<game>_rom.c
(~6 MB ROM bytes as a C array), oracle_of_<game>.h, oracle_of_<game>_main.c,
and a fresh CMakeLists.txt. All five are gitignored — only the
hand-curated CMakeLists.txt patch (one line, fixes the runtime path for our
monorepo layout) is committed via a tracked override in this README's "Layout
caveat" below.
Layout caveat. The recompiler auto-generates a
CMakeLists.txtper game that's correct in shape but needs two tweaks for this layout, both already applied in the committed CMakeLists files (so a regeneration from a clean ROM checkout will overwrite them — re-apply if you do):
- Runtime path — auto-gen assumes the output sits at
gbrecomp/<game>/, soGBRT_DIRpoints to../../runtime. We need../gbrecomp/runtime:sed -i 's|../../runtime|../gbrecomp/runtime|' \ ages/CMakeLists.txt seasons/CMakeLists.txt- Missing runtime sources —
platform_sdl.cppin the runtime calls intomenu_gui_*,asset_viewer_*, andhwtrace_*, but auto-gen only addsgbrt.c,ppu.c,audio.c,interpreter.c, andplatform_sdl.cpp. Without the other three you get undefined-symbol link errors. Add to theadd_library(gbrt STATIC ...)block:and add${GBRT_DIR}/src/hwtrace.c ${GBRT_DIR}/src/menu_gui.cpp ${GBRT_DIR}/src/asset_viewer.cpp${GBRT_DIR}/srctotarget_include_directories.
Build
PATH="/c/msys64/mingw64/bin:$PATH"
# Ages
cmake -S ages -B ages/build -G Ninja
cmake --build ages/build # ~3M lines of C → expect a long compile
./ages/build/oracle_of_ages.exe
# Seasons
cmake -S seasons -B seasons/build -G Ninja
cmake --build seasons/build
./seasons/build/oracle_of_seasons.exe
Expected ROM Hashes
These are the (U) [C][!] revisions — the ones the recompiler is being targeted against.
| Game | SHA-1 | MD5 |
|---|---|---|
| Oracle of Ages | 880374fb978b18af4aa529e2e32f7ffb4d7dd2f4 | c4639cc61c049e5a085526bb6cac03bb |
| Oracle of Seasons | ba1268290fb2b1b70505d2d7b5825fc8a4816a4b | f2dc6c4e093e4f8c6cbea80e8dbd62cb |
Both are 1,048,576 bytes (1 MB), MBC5+RAM+BATTERY, CGB-only.
Repository Layout
.
├── README.md # this file
├── .gitmodules # gbrecomp submodule pointer
├── .gitignore # blocks ROMs and generated rom.c artifacts
├── gbrecomp/ # shared submodule → sp00nznet/gb-recompiled
├── roms/ # gitignored — drop your own ROMs here
├── ages/
│ ├── CMakeLists.txt # regenerated by gbrecomp; one-line path fix needed
│ ├── oracle_of_ages.h # generated, gitignored
│ ├── oracle_of_ages.c # generated, gitignored (~86 MB)
│ ├── oracle_of_ages_main.c # generated, gitignored — entrypoint
│ └── oracle_of_ages_rom.c # generated, gitignored (~6 MB ROM-as-C-array)
├── seasons/ # same shape as ages/, with oracle_of_seasons_*
│ └── …
└── screenshots/
A Note on the Linked-Game Passwords
The Oracles can talk to each other. A finished Seasons cart hands the player a password that, typed into a fresh Ages save, opens a continuation story — and vice versa. Two binaries, one save world. Verifying that handshake works end-to-end across the recompiled builds is a milestone in the status table above and possibly the most fun this project will produce.
Legal
This repository ships no ROM, no game code, and no game assets — nothing you could rebuild the game from:
- No ROM bytes, no decompiled sources, no extracted audio, no ripped sprite or tile sheets.
- The recompiler's output (
rom.c,rom_rom.c,rom.h) is gitignored and must be regenerated locally from your own legally obtained ROM. - Everything checked in is original glue code (MIT) or a pointer to a public
upstream (the
gbrecompsubmodule).
The only Nintendo/Capcom-owned content here is a handful of small
screenshots in screenshots/ — captured framebuffer frames used to show
progress. They remain © Nintendo / Capcom, are included for identification and
commentary (fair use), and are not game assets you could rebuild the game from.
This is a personal preservation and education project. Don't redistribute ROMs.
Credits & Acknowledgements
- Games — Nintendo / Capcom Flagship (Oracle of Ages & Oracle of Seasons, 2001)
- Recompiler — arcanite24/gb-recompiled with CGB fixes, runtime extensions, and SDL2 frontend in sp00nznet/gb-recompiled
- Reference emulators — two, with different jobs:
PyBoy is the scriptable oracle used
for debugging here (its
tools/ground-truth tracer feeds the recompiler, andpyboy.memory[addr]per-frame probing pinned the serial bug — see "Solved" above); SameBoy by LIJI32 is the accuracy reference that the runtime's--hw-tracemode diffs against scanline-by-scanline. - Frontend — Dear ImGui + SDL2
- Disassembly references (for triage only, not build inputs) — oracles-disasm by stewmath et al.
- Prior art in the sp00nznet recomp family — pokemon-gold · pokemon-crystal · LinksAwakening






