Skip to content
PORTSNATIVE.
HomeCatalog
Web GamesEmulatorsToolsNews
PORTSNATIVE.

A catalog for discovering ports and related projects. We link to official sources and do not host original game files.

ExploreCatalogMy list
InformationAbout usSuggest a portSupportDonationsLegal notice
© 2026 PortsNative
Back to catalog
Shantae Recompiled
PORTSNATIVE

Shantae Recompiled

NATIVE PORTPLATFORMERWINDOWSLINUXBETA

Shantae Recompiled

Shantae

OriginGame Boy Color

TechnologyStatic recompilation (native C/C++)

Available forWindows, Linux

View official project

About 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

Author's original READMEOpen on GitHub ↗

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

PathWhat
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.tomlRecompiler config: far-call / inline-argument routines, scan banks, palette override site
shantae.annotationsGenerated candidate entry points (automatically refreshed by tools/build.sh)
dispatch_misses.tomlRuntime-harvested entry points (Tier-0), auto-ingested by the recompiler
extras.cShantae game hooks: hardware mode (GBA Enhanced on/off), palette override, shantae.ini settings
expanded_view.c, expanded_background.incExpanded world compositor, live background objects, and wider object activation
object_slots.cWith 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.cNative 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.cThe buttons' HUD (the Dance Menu's icons, decoded from the ROM) and the day and night clock (the status bar's numerals)
map.cThe 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.cBuilt-in GBA, expanded-view and gameplay features on the launcher's Mods page
extras_ui.cppIn-game Shantae settings, including expanded-view size and the movement options
game_build.cmakeAdds game hooks, UI, and regression targets to the generated project
generated/Recompiler output (not committed) — never edit; regenerate
roms/shantae.gbcStock 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.shRebuild everything (below)
tools/gen_annotations.py, tools/inline_args.pyDerive shantae.annotations and the [[inline_call]] entries from the ROM
tools/audit_whole_rom.py, tools/whole_rom_check.cExhaustive bank/address audit and native-instruction differential checks
tools/audit_ram_coverage.py, tools/ram_native_check.cROM writer evidence, RAM disassembly, and direct RAM program checks
tools/audit_native_coverage.pyCheck discovered native script targets and banked inline returns against emitted metadata
tools/test_annotations.py, tools/native_dispatch_check.cDiscovery regressions and compiled-dispatch differential checks
tools/expanded_view_check.c, tools/object_slots_check.cExpanded-view compositor checks; the grown object table through the game's own routines
tools/check_towns.pyEvery town with the expanded view against the original, frame for frame, from a debug-grid state
tools/check_totem.pyThe labyrinth's totem puzzle with the expanded view: the orb, then the key, from a saved state
tools/check_budgets.pySpawners that share a count of what they have made, with the expanded view against the original, from a cold boot
tools/check_crow.pySky'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.pyThe 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.pyThe 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.pyThe 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.pyThe 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.pyShantae'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.pyThe 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.pyThe 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.pyThe 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.pyThe map's research side: every room load, map and spawn record in the ROM
tools/trace_writes.cWhich instructions write a memory range, from a saved state (research only)
tools/check_rom_gate.pyOnly 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.pyEvery 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.pyThe 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.pyLeaving 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.patchRebuild/install the GhidraBoy extension, ported to Ghidra 12 by the patch (instructions in the script)
tools/ghidra_listing.py, tools/ghidra/Listing.javaGhidra'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.shBuild 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.mdThis 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_hook in 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_hook run 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.

  • 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 vectorscale presets, which kills the process. After such a crash the child runs again with the optimizer off (the runtime routes ANGLE's D3DCompile through 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 in librashader_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:

PatchFixes
0001History copies used a blit ANGLE rejects; presets reading previous frames read black
0002Integer varyings need flat on the vertex side in GLSL ES (crt-yah, ntsc-blastem)
0003ANGLE's D3D translator cannot write global arrays-of-arrays constants; they are assigned in main() instead (crt-hyllian, crt-sony-megatron, crt-nobody, dithering)
0004Quoted preset values end at the closing quote, as in RetroArch ("../..//x", "true"")
0005RelaxedPrecision 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-x86 with 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-arm64 with mingw64's clang and lld cross-compiling to aarch64-w64-mingw32 against 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 into third_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.dll is tools/build_librashader.sh <arch>'s, through Rust's MSVC targets (Visual Studio's x86 and ARM64 C++ tools).
  • tools/windows/dlls.sh follows the imports of the exe and of librashader.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>.zip is a folder with shantae.exe, its DLLs, assets/, shaders/ and tools/windows/README-windows.txt; <version> is $SHANTAE_VERSION, or the date.
  • tools/windows/check.sh checks every build with tools/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 runs tools/check_startup.py and 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's libra_gl_loader_t has 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. Plain char is signed on aarch64 too (-fsigned-char), as on x86. build_linux.sh fails a binary that needs a newer glibc symbol.
  • librashader.so is built from third_party/librashader/src (exported and patched by tools/build_librashader.sh) with cargo-zigbuild into third_party/librashader/linux-<arch>/, and the game finds it beside itself through its RUNPATH ($ORIGIN).
  • dist/linux/shantae-recomp-<version>-linux-<arch>.tar.gz is a folder that runs from anywhere writable and keeps its state inside it; Shantae_Recomp-<version>-<arch>.AppImage keeps its state beside the .AppImage (gb_host_paths). <version> is $SHANTAE_VERSION, or the date. The tarball carries tools/linux/README-linux.txt; tools/linux/shantae.png is the AppImage icon (a placeholder: recomp-ui's brand mark).
  • tools/linux/check.sh runs tools/check_startup.py on 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.

Read official guide
CALIFICACIÓN
—
Loading votes…
Advertisement

PROJECT FILE

Project type
NATIVE PORT
Genre
Platformer
Status
beta
Original platform
Game Boy Color
Technology
Static recompilation (native C/C++)
Version
—
Updated
—

Target platforms 2

Windows
Linux
View on GitHub
Advertisement