shantaerecomp
shantaerecomp is a community PC port of Shantae in the Quiver Launcher catalog.
- Type: PC port
- Game: Shantae
- Runs on Windows and Linux
- Latest release v0.1.4, 2026-09-27
- Source: https://github.com/vibecodekun/shantaerecomp
README
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, 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, CRC32 E994B59B;
the launcher verifies it.
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 |
launcher_options.c | Built-in GBA and expanded-view features on the launcher's Mods page |
extras_ui.cpp | In-game Shantae settings, including expanded-view size |
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 |
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/build_ghidraboy.py, tools/ghidraboy-ghidra12.patch | Rebuild/install the GhidraBoy extension, ported to Ghidra 12 by the patch (instructions in the script) |
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, 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.
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
-
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, and Width and Height (160–8192 and 144–8192) adjust one pixel at a time; any other size is shown as Custom. Rooms smaller than the view zoom in to fill it (Room zoom). 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. Menus and dialogue retain their original centered layout.
Towns keep the original picture, object activation and 32-slot object table
(their building-name panel covers the bottom, their camera wraps at 640 pixels,
and their doors are found by the low byte of the distance), 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, 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. Camera/physics reads and the native PPU remain unchanged. See expanded-view implementation and checks.
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
", 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 no