Shantae Recompiled
Shantae
OriginGame Boy Color
TechnologyStatic recompilation (native C/C++)
Available forWindows, Linux
View official projectAbout the project
Shantae es una serie de videojuegos de plataformas desarrollados por WayForward. La heroína epónima de la serie, una medio genio, fue creada por Erin Bozon, mientras que los juegos fueron creados a partir de este personaje por su esposo Matt Bozon. La serie consta de seis juegos: Shantae (2002), Shantae: Risky's Revenge (2010), Shantae and the Pirate's Curse (2014), Shantae: Half-Genie Hero (2016), Shantae and the Seven Sirens (2019) y Shantae Advance: Risky Revolution (2025).
Official project information
Shantae (GBC) static recompilation
Shantae (USA) recompiled to native code with gbrecompiled. Boots as a Game Boy Advance by default, so the GBA Enhanced extras are on (title badge, the Bandit Town Tinkerbat secret), while the palette loader is kept on the original GBC colors. On top of that: an expanded world view (an aspect ratio of your choice, or Adaptive to fill the whole screen), no slowdown, reduced input lag, smoother movement (running, whipping and crawling without losing momentum), a button for each transformation with a HUD of the Dance Menu's icons, a map screen, a day and night clock, rewind, and RetroArch shader presets through librashader.
Download
Builds for Windows (x64, x86, ARM64) and Linux (x86_64, aarch64; a tarball or an AppImage) are on
the Releases page. Extract one
and run shantae.exe / shantae; the launcher asks for the ROM on the first start.
No ROM is included. You need your own Shantae (USA) Game Boy Color ROM, the No-Intro dump:
SHA-256 1b92e22d5510c51bab97d23074e4aad7464d93eb15f7596ef7da0a5efa27a19d (CRC32 E994B59B).
The launcher reads "ROM verified" under the box art and enables PLAY only for that file. Any other
file reads "ROM not recognized" and does not start: bad dumps, other games, and Shantae's other
releases (World, Switch, Limited Run Games), which are different ROMs and not supported yet. The
code was recompiled from that exact ROM. Translations and other patches for it are applied by the
launcher (ROM patches); a ROM file that already has one of the patches the
build was made with applied is verified too.
Layout
| Path | What |
|---|---|
gbrecompiled/ | Recompiler and runtime (submodule: fork of mstan/gbrecompiled, branch shantae) |
recomp-ui/ | Launcher and in-game menu (submodule: fork of RetroPortingToolKit/recomp-ui, branch shantae) |
shantae.toml | Recompiler config: far-call / inline-argument routines, scan banks, palette override site |
shantae.annotations | Generated candidate entry points (automatically refreshed by tools/build.sh) |
dispatch_misses.toml | Runtime-harvested entry points (Tier-0), auto-ingested by the recompiler |
extras.c | Shantae game hooks: hardware mode (GBA Enhanced on/off), palette override, shantae.ini settings |
expanded_view.c, expanded_background.inc | Expanded world compositor, live background objects, and wider object activation |
object_slots.c | With the expanded view, the object table grown from 32 slots to 157 (past DFFF and at A000 in bank 3) and the collision node pool from 12 to one per slot (towns keep the original 32 and 12) |
ram_native.c | Native translations of writable JP vectors and the copied DMA routine |
moveset.c | "Smoother movement": the whip slide and cancel, the run and air speed from B, the whip and the dance as she lands, and the faster crawl, as hooks in Shantae's movement routines (docs/moveset.md) |
forms.c | "Smoother movement" for the transformations: the monkey's and tinkerbat's attack slide, run and air speed, faster climbs, the harpy's speed and flaps through her talons, the charges, the elephant's turn, landing and stomp, the spider's run and shot, webs taken with A, the tinkerbat's box (docs/moveset.md) |
dance.c | "Easier dancing": the dance's steps entered like a code, each with its pose, and the blink once a transformation's silhouette ends and after turning back (docs/dance.md) |
transform.c | "Transformation buttons": a button for each form and the heal, switching her in the tick she is in control, where the form fits |
hud.c | The buttons' HUD (the Dance Menu's icons, decoded from the ROM) and the day and night clock (the status bar's numerals) |
map.c | The map screen: the world laid out as on vgmaps' World Map, each area's terrain from its collision, markers from the spawn records, explored cells kept with each save file, and the Zombie Caravan at its stop |
launcher_options.c | Built-in GBA, expanded-view and gameplay features on the launcher's Mods page |
extras_ui.cpp | In-game Shantae settings, including expanded-view size and the movement options |
game_build.cmake | Adds game hooks, UI, and regression targets to the generated project |
generated/ | Recompiler output (not committed) — never edit; regenerate |
roms/shantae.gbc | Stock ROM (CRC32 E994B59B): you supply it; never committed |
patches/ | ROM patches made for Shantae that the build compiles (translations, fixes): you supply them too; patches/README.md lists them, patches/patches.toml names them for players |
tools/build.sh | Rebuild everything (below) |
tools/gen_annotations.py, tools/inline_args.py | Derive shantae.annotations and the [[inline_call]] entries from the ROM |
tools/audit_whole_rom.py, tools/whole_rom_check.c | Exhaustive bank/address audit and native-instruction differential checks |
tools/audit_ram_coverage.py, tools/ram_native_check.c | ROM writer evidence, RAM disassembly, and direct RAM program checks |
tools/audit_native_coverage.py | Check discovered native script targets and banked inline returns against emitted metadata |
tools/test_annotations.py, tools/native_dispatch_check.c | Discovery regressions and compiled-dispatch differential checks |
tools/expanded_view_check.c, tools/object_slots_check.c | Expanded-view compositor checks; the grown object table through the game's own routines |
tools/check_towns.py | Every town with the expanded view against the original, frame for frame, from a debug-grid state |
tools/check_totem.py | The labyrinth's totem puzzle with the expanded view: the orb, then the key, from a saved state |
tools/check_budgets.py | Spawners that share a count of what they have made, with the expanded view against the original, from a cold boot |
tools/check_crow.py | Sky's crow at the desert labyrinth with the expanded view: its dialogue opens the door, and it is there however Shantae comes back, from a saved state |
tools/check_eyes.py | The third labyrinth's eye puzzle with the expanded view: the eye settles where its jar was, the socket takes it, the statue gives its key, and another puzzle in view is left alone, from a saved state |
tools/check_pictures.py | The fourth labyrinth's picture puzzles and key doors with the expanded view against the original, pixel for pixel: each picture and door shows its own state, the puzzle is solved and its key taken, from a saved state |
tools/make_states.py | The saved states the next three checks play, made from a cold boot with WayForward's debug mode (the title code, Start Debug Game with every dance learned, a scene of the debug grid, the debug flight) into logs/states/ |
tools/check_ui_fit.py | The world above the bottom UI in every scene of the debug grid, in a hidden window and three setups: the floor above the bottom UI, a dialogue box's rows of play kept above it |
tools/check_moveset.py | Shantae's moves with "Smoother movement" beside the original, frame for frame: the whip slide and cancel, air speed, a whip that lands or slides off a ledge, B and Select as she lands from a standing jump, the crawl, the crouch whip under a low ceiling; and the feature off against the previous release, from a saved state |
tools/check_forms.py | The transformations with "Smoother movement" beside the original: the monkey's and tinkerbat's slide (from standing and walking), cancel and air speed, the harpy's talons in her run and while flapping, the tinkerbat's box in the ice tower's hidden passage and the pipe maze, the pillar, the charges, the elephant's turn, landing and stomp, climbing with B, the spider's run and shot, webs, and the spider's shot in the expanded view; Transformations off against the previous release, from four saved states |
tools/check_dance.py | "Easier dancing": every dance of the ROM's table on its last step, skipped and restarted steps, the original rhythm, a slider against the new monkey (the blink once its silhouette ends) and against Shantae turned back, with and without the blink, and every transformation's blink; the feature off against the previous release, from a saved state |
tools/check_transform.py | The transformation buttons: every form by its button as by its dance, walking and jumping, mid-jump, a one-tile corridor, shops and scenes, forms not learned, the heal, beside quick steps, and the HUD's layouts, from saved states |
tools/check_hud.py | The HUD and the clock in a hidden window with no preset, an LCD dot matrix, crt-geom and a koko-aio bezel; no HUD and no switching on a town's street |
tools/map_extract.py | The map's research side: every room load, map and spawn record in the ROM |
tools/trace_writes.c | Which instructions write a memory range, from a saved state (research only) |
tools/check_rom_gate.py | Only the ROM the build was recompiled from starts: the launcher's "ROM verified" line and PLAY, and the runtime's own check, for the ROM, bad dumps made from it, and optionally another game |
tools/check_patches.py | Every patch in patches/ on the stock ROM: every instruction position it changes against the reference interpreter (whole_rom_check patches), recognized with all of them compiled, the native code against the interpreter on the patched ROM from a cold boot and from the first town's shop, the debug-grid route, scene 3C's text and the shop pictured; with the dance floor fix, the dance floor at night as a player gets there and a round's end against the interpreter; the patched ROM file accepted; a code patch the build was not made with stops with its message, a data patch runs, a size-changing one is refused; the launcher's line for each (docs/rom-patches.md) |
tools/check_dance_floor.py | The dance floor on the stock game: from a cold boot, the Zombie Caravan's gates, the warp dance, Scuttle Town at night, the Dance Parlor, a round to its end; it must return to the owner, and the round's end must match the interpreter (The dance floor) |
tools/check_leave.py | Leaving from the in-game menus: the first confirmed Return to Launcher ends the game and opens the launcher, Quit ends it and opens nothing, from the Escape menu, the settings window and the debug server |
tools/build_ghidraboy.py, tools/ghidraboy-ghidra12.patch | Rebuild/install the GhidraBoy extension, ported to Ghidra 12 by the patch (instructions in the script) |
tools/ghidra_listing.py, tools/ghidra/Listing.java | Ghidra's disassembly of ROM ranges (6:4AA7:4B19) from the command line: headless, read-only, far-call aware; imports the ROM into logs/ghidra on first use, and with --patch the ROM with an IPS/BPS patch applied |
tools/build_librashader.sh | Build librashader.dll (OpenGL runtime; x64, or x86/arm64 when named) and stage the slang-shaders presets |
third_party/librashader/ | Its output: the x64 DLL, windows-<arch>/librashader.dll, shaders/, and patches/ applied to the librashader source; linux-<arch>/librashader.so from tools/build_linux.sh |
tools/build_windows.sh, tools/windows/ | Windows releases for x64, x86 and ARM64 (below): pinned msys2 libraries, CMake toolchain, DLL staging, packaging, checks |
third_party/windows/ | The pinned msys2 packages and the x86/ARM64 sysroots unpacked from them (tools/windows/bootstrap.sh) |
tools/build_linux.sh, tools/linux/ | Linux releases for x86_64 and aarch64 (below): toolchain bootstrap, CMake toolchain, packaging, checks |
LICENSE, THIRD-PARTY-LICENSES.md | This project's license (PolyForm Noncommercial 1.0.0) and the components the releases carry |
Build and run
The recompiler and the Windows build run on Windows with MSYS2
installed at C:\msys64, plus Git and Python 3.11+:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-SDL2 mingw-w64-x86_64-angleproject
git clone --recursive https://github.com/vibecodekun/shantaerecomp.git
cd shantaerecomp
mkdir -p roms && cp /path/to/your/Shantae.gbc roms/shantae.gbc
bash tools/build.sh # recompiler -> generated/ -> generated/build/shantae.exe
Already cloned without --recursive? Run git submodule update --init.
The generated project builds for size (MinSizeRel, the recompiled ROM code at
-O1); game_build.cmake builds the runtime and this game's modules
(expanded_view.c, object_slots.c, moveset.c, forms.c, dance.c, dialogue.c,
transform.c, extras.c, ram_native.c) at -O2,
since the PPU, APU, timers and the expanded-view compositor run every frame.
The ROM code keeps its own level, so this does not recompile it.
The build also runs a 120-frame startup smoke test through the actual game loop.
Run it independently with python tools/check_startup.py; --rom, --exe, and
--frames select another ROM path, binary, or duration. It uses temporary settings
and saves, checks that the frame limit was reached, and fails on native execution
errors or interpreter fallback. Its log is logs/startup-smoke.log.
Only the ROM the build was recompiled from starts. The recompiler embeds its SHA-256, the
generated main() hands it to the launcher before the launcher opens (the line under the box
art and the PLAY button follow it), and the runtime checks the file again at boot, which is what
stops a wrong ROM when "Skip launcher on boot" is on (on Windows a "Wrong ROM" box, then the file
picker). python tools/check_rom_gate.py checks both with the ROM, a one-bit bad dump and an
underdump made from it; --other adds another game's ROM.
The menus' Return to Launcher and Quit end the game at the confirming press. The runtime notes
the request in a flag that gb_platform_poll_events() reads; it used to push an SDL_QUIT, which
the menu's hold caught and pushed back every frame without the main loop ever seeing it (SDL_PollEvent
stops at the poll sentinel queued before the push), so the game ran on, unpaced, behind the open
menu until a second leave. The relaunch registered by Return to Launcher also now checks the
last leave asked for, so a Quit after it no longer opens the launcher.
python tools/check_leave.py leaves through the Escape menu, the settings window and the debug
server, and fails on v0.1.9.
Run generated/build/shantae.exe. In the launcher, Mods holds Shantae's options:
-
GBA Enhanced mode (on by default): boot as a Game Boy Advance, so the title shows "GBA Enhanced!" and the Bandit Town Tinkerbat secret is available. Off = plain Game Boy Color.
-
Colors: Original GBC colors (default) or GBA brightened colors.
-
Remove slowdown (on by default): the original drops to half speed whenever a frame's logic does not finish before VBlank, and the wider expanded views, which keep more enemies active, do so often (state1 at 426×240 lagged 86 of 240 frames). With this on, the frame waits at the end of line 143 until the game has finished it: PPU, timers, audio and serial stand still while only the CPU runs, so music and screen timing are unchanged. Every frame loop ends at 00:0851, which sets FF8F for the VBlank handler, so FF8F still clear at line 143 means an unfinished frame. A wait is capped at two frames of CPU time; after hitting the cap (a screen load, not lag) it resumes only once a frame finishes unaided. The mechanism is
gb_frame_hold_hookin the runtime (gbrt.h). Off keeps the original slowdown. The Esc → Shantae checkbox applies it immediately. -
Reduce input lag (on by default): sprites appear one frame sooner, and Shantae's moves start on the tick they are pressed. With the runtime's Preemptive Frames at 1 (below), a press shows on the very next picture: hold A or B in frame advance and the jump or whip shows on advance 1 (the original game: 4 and 3). Two changes:
- Sprites. The VBlank handler streams each tick's sprite graphics (C4C0) to
VRAM with an HBlank DMA (00:0A7D), finished only by the end of the next picture,
so 01:667C points the OAM DMA page FF81 at the previous tick's buffer (D700/D800).
The scroll is held back to match: each tick starts by committing the camera the
previous tick drew its sprites with (00:26F6 → 03:722F). With this on, hooks on
the
[[imm_override]]sites 00:0A50 and 00:0A7D copy the buffer the tick just wrote (FFD9), upload its graphics with a general DMA, and move the copied sprites by the camera's last step (FFE1/FFE3) onto the committed scroll, the camera the expanded view's surround uses. The committed scroll's camera is the scroll less the background offset (C9D2/C9D4) it was built with, noted at 03:730E after both camera routines' build: bosses move the offset later in the tick (Risky's ship bobs it), and the value at the VBlank could leave the copy a pixel off. FF81 is restored after the copy, so game state is unchanged; the camera still follows a frame behind, as in the original. - Moves. Every loop runs each object's movement routine (00:0C45) and then
each object's script (00:1305). The player's idle routine turns a press into a
script branch, and the script picks the new movement routine (jump 06:5520,
walk, whip, ...), which then first ran in the next tick. When the player's
script (slot at CA13) has picked a new routine, hooks on 00:130B and 00:1384 and
the runtime's
gb_step_hookrun it once right after the script, with the registers and banks it interrupts saved on the game's stack (so a save state taken meanwhile is safe). The whole move then runs a tick earlier, on the same path.
The Esc → Shantae checkbox applies it immediately.
- Sprites. The VBlank handler streams each tick's sprite graphics (C4C0) to
VRAM with an HBlank DMA (00:0A7D), finished only by the end of the next picture,
so 01:667C points the OAM DMA page FF81 at the previous tick's buffer (D700/D800).
The scroll is held back to match: each tick starts by committing the camera the
previous tick drew its sprites with (00:26F6 → 03:722F). With this on, hooks on
the
-
Smoother movement (on by default): Shantae's base form keeps her momentum. In the original, B is both the whip and the run: a B press on the ground is always a whip, which stops her for its 24 frames, and the run only starts once B has been held for 15; a jump keeps the speed she left the ground with, so a standing jump is at walking speed whatever is held; a whip begun in the air and landed finishes on the spot; a B or Select pressed as she lands from a standing jump, or in the 24 frames after, is lost (her landing takes A, a direction or Down, but no B or Select); the crawl moves half a pixel a frame, standing up into a run when B is held; and in a corridor too low to stand in, B while crawling is lost (only a crouch held still whips there). With this on, holding B runs at once, B whips and Select starts a dance from the frame she lands, B whips while crawling under a low ceiling as anywhere else, and three options choose the rest (details and addresses in docs/moveset.md):
- Whip on the move: what B does while a direction is held, standing or crouched. Slide (default): the whip comes out and hits as usual while she keeps moving, at running speed with B held and walking speed without, turning with the D-pad; the run follows without a frame at rest, a whip that lands carries on along the ground, and one that slides off a ledge carries on in the air. Cancel: B with a direction is no whip at all, she runs (or crawls on) at once, and a whip in progress ends when B and a direction are held; stand still to whip. Original: she stops for the whole whip.
- Air speed: Hold B for running speed (default): two pixels a frame while B is held and one when it is released, jumping, falling or whipping in the air. Original: the speed she left the ground with.
- Crawl: Hold B for walking speed (default): she stays down and crawls a pixel a frame, twice as fast, with the animation at twice the rate. Original: half a pixel, and B stands her up into a run.
- Transformations: Like Shantae (default): the monkey and the tinkerbat run at once with B held, their claw and sword follow Whip on the move whenever B is pressed, standing or walking (in the original they stop for 16 and 13 frames, even mid-run), and their jumps follow Air speed; the monkey's air claw keeps the jump's speed. Climbing, walls and webs, is twice as fast with B held. The harpy keeps her speed when her talons end (the original stops her for a frame and starts the run again from nothing), and her run goes on gaining speed through a talon with the direction held (the original holds the speed she had, so a talon early in the run left her crawling); A still flaps during a talon, with the flap's sound, while the talon plays out (the original drops those presses and she sinks), and on the frame she touches the ground (the original lost A there, and again on the next frame). The tinkerbat has the monkey's box, so she climbs and walks into the 16-pixel gaps and pipes the monkey can (her own is 20 pixels tall, the monkey's 14). The elephant's and the tinkerbat's charges can be pressed again, at full speed, or jumped out of with A, and go the way the D-pad is held, from a charge too; the tinkerbat charges with Down and B from her walk, run and landing, so she can break what she pushes into. The elephant charges (or jumps) from her turn and charges from her landing, and from the frame after a stomp's hit she can walk, charge or jump (the original holds her for 40 frames). Turning the other way, she walks off at once while her turn plays (the original stands her for its ten frames, and from a walk one more). The spider runs with B held, at two pixels a frame (in the air too with Air speed), and keeps moving through her shot with Whip on the move. On webs, the spider and the tinkerbat jump normally in front of one and take hold with A in the air; A on a web jumps off it, and A again takes hold (the original takes hold as the jump starts and ignores A on the web). They stay on a web up to its edges until A (the original jumps or drops them off at an edge). Original: they move as in the game.
Off plays as the original, frame for frame. The Esc → Shantae controls apply at once.
-
Easier dancing (on by default): after Select, the dance's steps are entered like a code (details and addresses in docs/dance.md):
- Dance steps: Quick (default): every press is a step at once and shows its own pose. A press that goes on to no dance she knows is skipped, Down starts over (every dance begins with it), and the transformation, healing or warp begins on the last step. Original: one step every eight beats, in rhythm; a missed or doubled beat breaks the dance, and it begins a pose after the last step.
- After transforming: Blink (default): after an animal transformation she stays safe through the silhouette, as in the original, and then flashes and cannot be hurt for two more seconds from the moment the silhouette ends, as after a hit; the same after turning back into Shantae, from the moment she can move again. Original: she is safe only until the new form's silhouette ends, or until she can move after turning back, with nothing to show it, and an enemy nearby hits her at once.
Off plays as the original, frame for frame. The Esc → Shantae controls apply at once.
-
Text speed (10, instant, by default; 0 is the original): hold B, or press it again and again, while dialogue prints and it prints faster, speed n prints n + 1 letters a frame (a box scrolls as much sooner) and 10 the whole text at once, every box of it up to the next wait for a new B. The game's own text loop does it: with B in use its waits are cut short. A new B turns the box as before, and A is left to the game (it backs out of shop menus, which the A of earlier releases' Instant text got in the way of). Shops go as quickly: with B in use the pause after "Yes" is passed over and the price comes off the gems 10 a frame instead of 10 every eighth frame, so buying the same item again and again takes a fifth of a second a time instead of almost two. Scenes that counted on the text taking its time are fixed where they broke: in Risky's hold, and again where she takes the Twinkle, her script could lose a signal sent while she was still laughing if the lines went by fast, and she laughed for good with Shantae frozen. Her signals now count, so both scenes play through at any speed. A freed genie's "Thank you for freeing me!" could end before the genie had started to wait for it: the Dance Menu then opened without her dance (with the expanded view showing its graphics as they loaded), and she danced afterwards and left the screen white until Shantae danced. Her wait now keeps that signal, so she dances first, then the flash and the menu, at any speed. At 0 everything plays as the original.
-
Transformation buttons (on by default): a button for each form whose dance she has learned (monkey, elephant, spider, harpy, tinkerbat) turns her into it at once, standing, walking or in the air, and the same button turns her back; a sixth heals as the heal dance does (10 gems a heart). A form switches only where it fits and while she is in your hands, never in a shop's menu or a scene, nor on a town's street, where the original cannot dance either. Bind them in the in-game settings' Controls (keys 1-6; X, Y, LB and RB on a controller). Select still dances. The Buttons HUD shows the Dance Menu's own icons in a corner of the picture (top left by default; never in the black beside it): the form she is framed in yellow, dances not learned and a heal that would heal nothing dark. Its layout (one to three columns, or a row) and size are options; it is not shown on a town's street.
-
Map (on by default): M (the right stick's click on a controller) opens a map with the game paused: the world as cells laid out as on vgmaps' World Map, and the area she is in drawn as its terrain from the ROM; A zooms, the pad moves it, Select shows the world from a labyrinth, B closes it. Heart holders, fireflies (at night), warp squids, talismans, save statues, caves and labyrinth doors are marked, dimmed once taken. It fills in as she explores, kept with each save file and save state (or the whole map from the start), and can mark enemies. The Zombie Caravan is drawn at its stop at night.
-
Day and night clock (top right by default): the sun or the moon and the minutes and seconds to the next nightfall or daybreak, in the status bar's own numerals and colors, dimmed while time stands still.
-
Zombie Caravan stays (off by default): later in the story the caravan moves on each night to one of four borders (Mens Desert and Naga Wasteland, Rain Storm and Snail Wasteland, the Ghost and Archer Forests, Eagle Wasteland and Mud Bog), picked the first time she crosses one of them that night. On, it stops every night between the Ghost and Archer Forests, where it first stops.
The HUD, the clock and the map are drawn inside the picture whatever its shape, and a shader preset shades each on its own. At the picture's pixels (the clock's and the map's default size) they are on the picture's pixel grid, so an LCD's dots or a CRT's scanlines fall on them as on the game; with a preset that draws the whole window (a bezel) they are drawn into the picture.
-
Expanded view (off by default): a larger world view with the original pixel scale and the status bar at the bottom, 256×240 (NES size) unless changed. Enable it on the Mods page, then launch. Its options set the size: Adaptive fills the whole screen or window, whatever its shape (Height is then the least height); an Aspect ratio preset (Game Boy 10:9, NES 16:15, 4:3, 16:10, 16:9, 21:9, 32:9) sets the width for the current height. Zoom then picks the height: Original is 144 tall like the original screen, so the game is as large as in the original and the view only adds to the sides (16:9 is then 256×144; a custom shape takes the screen's), and Default is 240 tall. Width and Height (160–8192 and 144–8192) adjust one pixel at a time; Height keeps a preset's shape, and any other size is shown as Custom. Esc → Shantae → Original height is the in-game Zoom: Original. The title screen and menus always show the original picture, so the zoom shows once play starts. Rooms smaller than the view zoom in (Room zoom): Fill the screen (default) until the room fills the view's own shape on screen (an NES 16:15 view stays 16:15 between bars), Whole room as far as no side that fits is cut (a large area stays as it is), or Off; the room's floor stays above the bottom UI, and towns fill the screen above their name panel. Bottom UI size sets the status bar, dialogue boxes and towns' name panels: 1x (the default) with the view's own pixels, 2x–8x larger, or Auto as large as in the original picture at any view size, the same size on screen when a room zooms in; other sizes are drawn through the shader at their own pixel size, with the world kept above them. World: Fits above it (default) keeps the whole room above the bottom UI, sky to floor, the ground the original's status bar hides included: while a dialogue box is up the world zooms out to make room for it (into any black around the picture first), and a status bar larger than 1x gets its room in large areas too, in rooms of every size; Under it, as original lays the bottom UI over the world's bottom. The in-game Esc → Shantae section has the same controls, and a size changed there applies at once; turning the view on or off takes effect on the next launch.
Preemptive Frames (Esc → Advanced → Display; 1 by default for Shantae) is the
runtime's port of RetroArch's preemptive frames (runahead.c, preempt_run).
A ring keeps the state from before each of the last N frames; when the input
changes, the oldest is loaded and those frames run again with the new input,
silently, before the next frame is shown. The game sees each press N frames
sooner and is otherwise unchanged. One frame is the delay every Game Boy game
has (a tick's result shows in the picture after the next VBlank), and all that
Reduce input lag leaves; more than that skips the first frames of a move.
Loading a state empties the ring; while paused, loading runs N frames to
refill it, as RetroArch runs one. Saved as emulation.preemptive_frames in
runtime_prefs.ini.
Settings are saved to shantae.ini next to the exe. expanded_view=1,
view_width=256 and view_height=240 select the NES-size view; remove_slowdown=0
brings the slowdown back and reduce_input_lag=0 the original input timing;
smooth_moves=0 the original moves, with whip_moving (0 original, 1 slide, 2 cancel),
air_speed_b, fast_crawl and smooth_forms its options; easy_dance=0 the original
dance, with quick_steps and transform_invincible its options; text_speed=0
the original text, 1-10 faster (instant_text=0 from earlier releases reads as 0);
ui_scale the bottom UI's size (0 Auto, 1 the default, up to 16) and ui_fit=0 the bottom UI covering the world (dialogue_lift=0 from earlier
releases reads the same). Menus retain their original
centered layout; the dialogue box sits at the bottom of the wide picture like the
status bar, at the bottom UI's size, so talking never zooms in and out.
Towns are shown wide: their street wraps at 640 pixels like their camera, and the
townsfolk, who are always about, walk in from either side, with the building-name
panel at the bottom and the street right on it, as in the original (Room zoom fills
the screen above the panel with the town, past the original's zoom when the panel
is smaller than the original's). They keep the original object activation and 32-slot
object table (their doors are found by the low byte of the distance, and copies of
the first doors past the seam stand in for them), so every door, label and entrance
is where the original has it. tools/check_towns.py checks all five (Scuttle Town,
Water Town, Oasis Town, the Zombie Caravan and Bandit Town, the debug grid's N row)
against the original: walking and running round each town plays frame for frame
the same, with the original picture where the wide one has it, and every door
shows its label, opens on the same frame and leads to the same room (and, walking
back out, to the same spot). Rooms whose
camera is pinned to one screen, such as the first boss's arena, keep the
original picture and activation too. Rooms narrower or shorter than the view
(Risky's ship in the opening) are centered. This is experimental: the
saved-state regressions, the towns and the opening area have been checked, not
a full playthrough.
The expanded view draws the ROM world map plus the game's current background objects and metasprites, positioned with the scroll the game actually committed to the hardware (so slowdown cannot shift the surround against the native picture). Sprites use the camera without the background-only offset that bosses drawn in the background add to the scroll. It uses world coordinates so moving/collapsed scenery does not leave stale tiles when the camera reverses. In rooms shown expanded, enemies activate and remain active across the larger area; their behavior can therefore begin earlier than in the original game. To hold them, the game's object table has 157 slots instead of 32 while the view is on, towns aside (as many as 16-bit addresses leave room for; the water tower's rooms at 1920×1080 want about 130), and the collision pool that platforms and hazards take from has a node for every slot instead of 12. Encounters that start the moment they exist (the water tower's mini-boss) still wait for the original distance; NPCs, their houses and other set pieces appear with the view. The labyrinth's totem puzzles keep one list of stones for every totem, and the view woke a second totem whose stones took the list over, so matching the stones never brought the orb (or its key); a totem's pedestal now checks its own stones. Spawners that share a count of what they have made (the swamp creatures in one level, and three kinds of enemy spawner) counted the ones the view kept far behind Shantae, so fewer appeared near her; they now count what the original would still have around her. The view woke objects farther out than it kept them, so some were let go the frame after they appeared. Sky's crow, which the desert labyrinth's door brings out as the door appears, went while the door stayed, and without its dialogue the door never opened. Objects are now kept as far past where they appear as the original keeps them, and a save state taken with the crow missing gets it back when loaded with the view on. The third labyrinth's eye puzzles (whip a jar, knock the eye that comes out into its statue's socket) went wrong three ways. The eye moves for up to seven frames before it has its own collision box, with whatever box the last object in its memory slot left; with the view that was often a tall enemy's, which reached into the floor and pushed the eye up to 53 pixels sideways, into the wall and out of the room, differently on every try. The puzzles also share their lists of open sockets and of which eye is out, and the view has several puzzles awake at once, so the room's socket never took the eye and one puzzle's eye could be written into another's jar. And the eye was let go 160 pixels off the original screen while its broken jar stayed. The eye now has its own box from the start, each puzzle reads its own eyes and sockets, and the eye is kept as far as the view keeps its jar. A save state taken after an eye was lost that way, with its jar left empty, has the jar full again when loaded with the view on. The spider's shot lives half a second, about as far as the original screen reaches ahead of her, so with the view it vanished in mid-air; it now flies until it leaves the picture. The fourth labyrinth's picture puzzles (four quarters, each turned by a whip until the picture is whole) draw their pieces into one set of tiles that every picture in the level uses, and its key doors do the same with their shut and open looks; the third labyrinth's doors too. The original only ever has one picture or door on screen. The view has several awake, all drawing into those tiles, so every picture showed a mix of two puzzles' pieces and they all seemed to turn at once: the picture in front of Shantae could not be told from its solution, and an opened door could look like a wall beside a shut one. Now only the one the original would have awake draws into the tiles, and the view draws every picture and door from its own pieces. Camera/physics reads and the native PPU remain unchanged. See expanded-view implementation and checks.
ROM patches (IPS/BPS)
Translations, fixes and hacks made as IPS or BPS patches for Shantae (USA) can be played. On the launcher's Mods page, ROM PATCH → Browse... picks the patch file; the line under it says at once what it does here, and PLAY starts the game with it. The patch is applied to the ROM in memory every time the game starts, so the ROM file itself is left as it is, and "Skip launcher on boot" keeps it; Clear or the checkbox turns it off. Picking a ROM file that already has one of the patches below applied works too. In-game saves are shared with and without a patch; save states only load with the patch they were made with.
Every instruction of this build was compiled from the stock ROM, so a patch that changes code
needs that code compiled as well. The build does it for the patches in patches/ (bring your
own, like the ROM: patches/README.md lists the files): each instruction they
change is compiled from its patched bytes, and at boot the game runs those instead, with no
interpreter. These are compiled into the releases:
- French translation by Anthiflo (v1.2), with the dance floor fix
- Italian translation
- Polish translation by PieniondzPS (v2)
- Brazilian Portuguese translation by Sliter and ajkmetiuk (v1), with the dance floor fix, and the same with GBA Enhanced mode and GBC palettes too
- Dance floor hang fix by Griever; GBA Enhanced mode on any Game Boy Color (Polar-Star); GBC palettes by Drenn (v1.1). All three are in this build already: the dance floor fix without a patch (below), the other two as options (GBA Enhanced mode, Colors).
Any other patch works if it changes only the game's data (text, graphics, levels): the launcher says so in green. If it changes code this build wasn't made with, the launcher says where, in amber, and the game stops with a message if it ever reaches that code. Patches that change the ROM's size are refused. How it works, what each patch above changes, and the checks: docs/rom-patches.md.
The dance floor
The Dance Parlor's floor (Scuttle Town at night) no longer hangs at the end of a round. Once a
round is over, the round's end makes an object (script 12:63C4) in object slot 14 without placing
it, so it starts where the slot's last object was left (D718-D71C). On a game started on cleared
WRAM, as here and on emulators, that is 0,0, and the game stayed on "NOT ENOUGH GEMS!" (or the
winnings) for good. Griever's dance floor hang fix sets D719-D71C to 8, 8, 8, 2 in 12:5725, which
the round's controller (script 12:5713) calls as the round ends; extras.c does the same as that
op dispatches, so the round ends and Shantae is back with the owner, with or without that patch. python tools/check_dance_floor.py plays there the way a player does, from
the Zombie Caravan's gates and the warp dance at night, and checks the round's end against the
interpreter.
Shader presets (librashader)
RetroArch .slangp presets (CRT, LCD, handheld, scalers, Mega Bezel...) run through
librashader. Set it up once:
bash tools/build_librashader.sh # needs Rust (cargo); ~2 min the first time
bash tools/build.sh # or just ninja -C generated/build
The script clones librashader 0.12.0 into third_party/librashader/checkout
the first time (LIBRASHADER_SRC points it at a checkout of your own instead),
exports that checkout without modifying it, applies
third_party/librashader/patches/, builds the OpenGL runtime only, and copies
the checkout's slang-shaders collection. The build then stages librashader.dll
and shaders/ next to shantae.exe. Without them the game runs as before.
The DLL links the C and C++ runtimes in (crt-static), so players need no
Visual C++ Redistributable. bash tools/build_librashader.sh x86 arm64 builds
the other Windows arches' DLLs (tools/build_windows.sh does this itself).
In game: Esc → Shader presets, or the settings menu's Shader Presets
(librashader) section. Pick a preset (the filter box takes words such as
crt or handheld gbc), tune its parameters, and Reset All to go back.
Choice and parameters are saved in runtime_prefs.ini
(shader.slang, shader.slang_param.*). Preset draws the whole window is
for bezel presets such as Mega Bezel. Shader folder points the picker at
another collection, e.g. RetroArch's shaders_slang. GBRECOMP_SHADER_PRESET
overrides the preset for one run. Mega Bezel opens with a few seconds of static
and its logo; its When to Show Intro parameter turns that off.
Edit Preset (a button beside the picker, and the list at the end of the
section) changes how a preset is set up, the way RetroArch's Shader menu does:
it holds the passes of whichever preset was picked, and you can set each pass's
filter, scale, wrap mode, framebuffer formats, frame count mod and alias,
reorder and remove passes, add single .slang passes, append or prepend whole
presets, or Start Empty to build one from nothing. Changes apply a moment
after they are made (or with Apply), through the same background compile as
any preset, as an edited copy (shader_presets/.builder.slangp), so the
preset's own file is left as it is; the picker lists that copy as
"<preset>, edited (not saved)" until another edit replaces it, and Undo
Changes goes back to the preset. Save Preset writes the chain, with its
parameters as tuned, to shader_presets/<name>.slangp beside the other state
(the name starts as the edited preset's; a folder's own presets are never
overwritten, your saved ones can be); saved presets are listed first in the
picker and saved as shader.slang=saved:<name>.slangp. Delete Preset
(beside the picker while one of yours is on) and Delete All My Presets
remove them after asking, to the Recycle Bin on Windows (elsewhere for good);
nothing in the shader folder is ever deleted or written. The editor
reads presets as librashader does (gbrecompiled/runtime/src/slang_preset.cpp:
#reference order, first-wins pass settings, last-wins parameters), so a
rewritten stock preset parses the same as the original: checked against
librashader's own parse for 2514 of the 2515 readable stock presets (the other
uses Mega Bezel's $PRESET$ path wildcard).
To see a preset while tuning it, Game Dimming (how dark the game gets behind
a menu; 0 leaves it untouched) and Menu Opacity (the menus' own backgrounds)
are in the settings menu's Playback group and Esc → Display
(ui.menu_dim, ui.menu_opacity). Menus hold the game while
open (Pause in Menu, ui.pause_in_menu), and no key or button reaches the
game or its shortcuts while one is open, typing into a filter included.
How it works under ANGLE (see gbrecompiled/runtime/src/librashader_chain.cpp):
- The context is OpenGL ES 3.1. Presets compile to GLSL ES 3.10, which is what arrays of arrays (crt-geom and others) need.
- A preset that has not loaded before is first compiled in a hidden child
process while the game keeps running. Microsoft's HLSL optimizer recurses
until the stack overflows on the two stock
vectorscalepresets, which kills the process. After such a crash the child runs again with the optimizer off (the runtime routes ANGLE'sD3DCompilethrough itself for this), and a preset that works that way is always loaded that way; vectorscale does, and the menu says so. Presets that fail anyway are refused and the previous look stays. Results are inlibrashader_probe.txt. - Compiled programs are cached in
shader_cache/. The largest Mega Bezel preset compiles for about a minute the first time and loads in under 2 s afterwards.
third_party/librashader/patches/ makes librashader's GLSL ES output work
under ANGLE:
| Patch | Fixes |
|---|---|
| 0001 | History copies used a blit ANGLE rejects; presets reading previous frames read black |
| 0002 | Integer varyings need flat on the vertex side in GLSL ES (crt-yah, ntsc-blastem) |
| 0003 | ANGLE's D3D translator cannot write global arrays-of-arrays constants; they are assigned in main() instead (crt-hyllian, crt-sony-megatron, crt-nobody, dithering) |
| 0004 | Quoted preset values end at the closing quote, as in RetroArch ("../..//x", "true"") |
| 0005 | RelaxedPrecision is dropped, working around a SPIRV-Cross GLSL ES bug (simple-crt) |
A sweep of the stock collection (every non-bezel preset plus every 25th Mega
Bezel preset, 1095 in all) has 1075 working: 1069 render in its first frames,
the two vectorscale presets work through the optimizer-off fallback, three Mega Bezel
screen-only presets are dark only during their intro, and 120hz-safe-BFI
draws black frames by design. The other 20 are broken in the collection itself
or need an HDR display: presets that point at files
that were moved (glow_trails, ray_traced_curvature, royale-curve-append,
crt-sony-megatron-v2-gba-gbi, two phosphorlut presets), the nedi
bilateral-variant presets (they use the vhs pass without its play
texture), scanline-classic's HDR10/wide-gamut presets (they read EnableHDR,
MaxNits and PaperWhiteNits, which librashader does not provide; its HDR
values are HDRMode and BrightnessNits), and test/feedback-noncausal,
which is invalid on purpose.
Windows (x64, x86 and ARM64)
tools/build_windows.sh builds and packages the Windows releases. x64 is the
generated/build that tools/build.sh makes (brought up to date first); the
other two build from the same generated/ C:
bash tools/build_windows.sh # all three -> dist/windows/ (or name x64, x86, arm64)
bash tools/windows/check.sh # startup smoke + differential checks
- x86 (32-bit) builds in
generated/build-x86with msys2's mingw32 GCC, the same compiler and runtime family as x64 (pacman -S mingw-w64-i686-gcc). Floating point is SSE2, as on x64 (-msse2 -mfpmath=sse), and the exe is large-address-aware (4 GB of address space on 64-bit Windows). - ARM64 builds in
generated/build-arm64with mingw64's clang and lld cross-compiling toaarch64-w64-mingw32against msys2's clangarm64 runtime (libc++, compiler-rt, UCRT); msys2's own ARM64 compilers only run on ARM64. - SDL2, ANGLE and the ARM64 runtime come from msys2 packages pinned by SHA-256
in
tools/windows/bootstrap.sh(their msys2 signatures were checked when they were pinned), unpacked intothird_party/windows/($SHANTAE_WINDOWS_CACHE, no spaces); nothing is installed into msys2. x64 and ARM64 have the same ANGLE (2.1.r25748); x86 has msys2's last 32-bit ANGLE package, 2.1.r21358. librashader.dllistools/build_librashader.sh <arch>'s, through Rust's MSVC targets (Visual Studio's x86 and ARM64 C++ tools).tools/windows/dlls.shfollows the imports of the exe and oflibrashader.dll: each build dir gets the DLLs it needs beside the exe, the zip carries exactly those, and the build fails if one is missing or is another arch's.dist/windows/shantae-recomp-<version>-windows-<arch>.zipis a folder withshantae.exe, its DLLs,assets/,shaders/andtools/windows/README-windows.txt;<version>is$SHANTAE_VERSION, or the date.tools/windows/check.shchecks every build withtools/windows/check_imports.py(each module is the arch's, and every function imported from a DLL beside the exe is exported by it), then runstools/check_startup.pyand the 3000-frame differential check on each build this PC can run: x64 and x86 on an x64 PC; ARM64 needs an ARM64 PC.- On 32-bit x86, librashader calls its GL loader
extern "system"(stdcall) while its C header'slibra_gl_loader_thas no convention, so the runtime declares its loader__stdcall; with a cdecl loader every preset crashed its probe. x64 and ARM64 have one calling convention, so they never saw this.
Linux (x86_64 and aarch64)
tools/build_linux.sh builds the Linux releases from generated/. The generated
C is the same on every platform, so the recompiler only runs on Windows: run
tools/build.sh first, then, in WSL (Ubuntu) or on any Debian/Ubuntu machine:
wsl bash tools/build_linux.sh # both arches -> dist/linux/ (x86_64 or aarch64 for one)
wsl bash tools/linux/check.sh # startup smoke + differential checks
The first run fetches its toolchain into ~/.cache/shantae-linux
(tools/linux/bootstrap.sh, $SHANTAE_LINUX_CACHE moves it; nothing needs
root): Zig as the C/C++ cross compiler, CMake and Ninja, each arch's Ubuntu 24.04
X11/Wayland/audio/GL headers and libraries through an unprivileged apt, SDL2
built static, Rust and cargo-zigbuild for librashader, Debian 10's libraries for
the checks, and appimagetool. After that a build takes about 2.5 minutes per
arch; the sources are rsynced to the Linux file system and build incrementally.
- The binaries target glibc 2.28 (Zig's glibc stubs), so they run on Debian
10+, Ubuntu 20.04+, RHEL 8+, Fedora, Arch, SteamOS, Raspberry Pi OS 64-bit
and so on. SDL2 and the C++ runtime (libc++) are linked in; at start they
load only glibc and
libGL.so.1. X11, Wayland (with libdecor), KMSDRM, PipeWire, PulseAudio, ALSA, udev and D-Bus are opened at run time when the system has them. Plaincharis signed on aarch64 too (-fsigned-char), as on x86.build_linux.shfails a binary that needs a newer glibc symbol. librashader.sois built fromthird_party/librashader/src(exported and patched bytools/build_librashader.sh) with cargo-zigbuild intothird_party/librashader/linux-<arch>/, and the game finds it beside itself through its RUNPATH ($ORIGIN).dist/linux/shantae-recomp-<version>-linux-<arch>.tar.gzis a folder that runs from anywhere writable and keeps its state inside it;Shantae_Recomp-<version>-<arch>.AppImagekeeps its state beside the .AppImage (gb_host_paths).<version>is$SHANTAE_VERSION, or the date. The tarball carriestools/linux/README-linux.txt;tools/linux/shantae.pngis the AppImage icon (a placeholder: recomp-ui's brand mark).tools/linux/check.shrunstools/check_startup.pyon the host's glibc and on Debian 10's glibc 2.28 (aarch64 through qemu-user), and the 3000-frame differential check (--quick: 300 frames under qemu).
What the runtime does differently on Linux: a shader preset's probe child is
this executable forked and exec'd again (/proc/self/exe; there is no HLSL
optimizer to retry without), and deleted presets go to the freedesktop.org
Trash. On every platform, a machine with no working sound output now runs the
game silently rather than failing at SDL_Init, and imgui.ini is kept in the
state folder. The launcher only forces SDL's EGL path on Windows (ANGLE needs
it; on Linux it would also need libGLESv2.so.2).
After changing the recompiler or runtime, run the direct coverage and differential checks below. Gameplay replays can test a particular regression; they are not the method for discovering missing ROM entries or expanding coverage.
Whole-ROM native coverage
exhaustive_rom = true generates a native entry at every byte of all 256 ROM
banks, including bank zero mapped at $4000, cross-window operands, and HALT-bug
fetch variants. Existing analyzed functions remain the fast path; other ROM
addresses dispatch to ahead-of-time compiled instruction bodies with constant
operands. There is no runtime opcode decoder or interpreter call in that ROM path.
The older scan_banks list controls only function discovery/optimization now.
The build audits all 8,421,376 normal/HALT-bug address slots, verifies shared
instruction signatures against the ROM, and rejects generated interpreter calls.
Listings are generated/shantae_exhaustive_bank_<window>.asm: window 0 is fixed
bank zero; window N+1 is switchable bank N. These intentionally disassemble at
every byte, including overlapping interpretations of data. They establish
conservative executable coverage, not a manually annotated code/data map.
Run the full checks with the MinGW toolchain on PATH:
python tools/test_annotations.py
python tools/audit_whole_rom.py
python tools/audit_ram_coverage.py
python tools/audit_native_coverage.py --targets logs/native-targets.txt --report logs/native-coverage.json
cmake -G Ninja -S generated -B generated/build
ninja -C generated/build whole_rom_check ram_native_check native_dispatch_check
generated/build/whole_rom_check.exe roms/shantae.gbc variants
generated/build/whole_rom_check.exe roms/shantae.gbc production
generated/build/whole_rom_check.exe roms/shantae.gbc patches patches/*/*.ips
generated/build/ram_native_check.exe roms/shantae.gbc
generated/build/native_dispatch_check.exe roms/shantae.gbc logs/native-targets.txt
Both whole-ROM test modes audit every mapped slot and compare representatives of
all used native instruction bodies against the reference interpreter, with memory
comparison, both flag outcomes, and every cross-window CB opcode. production
uses the real game dispatcher; variants exercises the exhaustive entries
directly. Optional all compares every legal normal/HALT-bug address individually
and takes substantially longer. patches takes the ROM patches the build was compiled
with (ROM patches) and runs every instruction position each one
changes, through the game's dispatcher on the patched ROM, from both flag states:
1,060,826 comparisons for the eight. Tests use private contexts and do not load or save
battery data. The interpreter remains available as an explicit testing reference.
An analyzed entry the analysis emitted no instruction at (20 of them, all in banks' unused
space, such as 0B:79FF) runs the instruction at its PC through the exhaustive path, one step.
It used to return without running anything, which only patches could see: two translations
put bytes there.
The identified RAM programs have explicit native translations: 116 patched JP
vectors (including both WRAM object pools) and six DMA instruction entries.
generated/shantae_ram.asm lists them. The RAM check executes their actual ROM
initializers and compares all 65,536 JP destinations, every slot across all ROM
banks, and DMA operands/timing against the independent interpreter decoder.
With the expanded view the object pool in bank 3 runs on to 157 slots, past
DFFF and at A000, and the collision node pool in bank 7 on past its 12 at E000;
their vectors go through the same translation (object_slots_check).
Generic runtime RAM opcode helpers are disabled in production dispatch.
Unexpected executable RAM or illegal opcodes stop with a diagnostic;
they do not silently enter the interpreter. This is a guard, not proof that all
possible writable-memory programs have been statically classified. Whole-ROM
coverage does not establish a bug-free full playthrough.
See the whole-ROM coverage report for implementation, validation and limitations. The earlier coverage investigation records the original script/callback gaps and their regressions.
License
This project's own code is licensed under the PolyForm Noncommercial License 1.0.0. The submodules and the libraries the releases carry keep their own licenses; see THIRD-PARTY-LICENSES.md.
Shantae is a trademark of WayForward Technologies. This project contains no ROM and is not affiliated with or endorsed by WayForward, Capcom, or Nintendo.
