shantaerecomp

shantaerecomp is a community PC port of Shantae in the Quiver Launcher catalog.

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

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
launcher_options.cBuilt-in GBA and expanded-view features on the launcher's Mods page
extras_ui.cppIn-game Shantae settings, including expanded-view size
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
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/build_ghidraboy.py, tools/ghidraboy-ghidra12.patchRebuild/install the GhidraBoy extension, ported to Ghidra 12 by the patch (instructions in the script)
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, 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_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.

  • 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 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 no