# Lufia II Recompiled
A native recompilation of **Lufia II: Rise of the Sinistrals** using
[`snesrecomp`](https://github.com/mstan/snesrecomp).
The game boots from the original US ROM and supports the intro, title screen,
normal gameplay, battles, audio, input, saves and the desktop launcher. Video
output can use SDL, OpenGL 3.3 or a native Vulkan renderer.
Title screen
Mode 7 intro flyover
 Overworld |
 Town |
 Interior |
 Shop |
 Dungeon |
 Save selection |
## ROM
The ROM is not included. Use the clean, headerless US release:
```text
Size: 2621440 bytes
SHA1: A89931C1F29B161B8BE717DFAB4A4ADB54B42B84
SHA256: 7C34ECB16C10F551120ED7B86CFBC947042F479B52EE74BB3C40E92FDD192B3A
```
Place it in the repository root as `lufia2.sfc`.
## Build
### Windows
Requirements:
- Windows 10 or later
- Visual Studio 2022 with Desktop development with C++
- CMake 3.24 or later
- Python 3
Build from the command line:
```powershell
git submodule update --init --recursive
cmake --preset windows-release
cmake --build --preset windows-release
```
The executable is written to:
```text
build\windows\Release\Lufia2Recomp.exe
build\windows\Release\LUFIA2_FUNCTION_SOURCES.txt
```
`LUFIA2_FUNCTION_SOURCES.txt` lists the standalone-decomp replacements used by
that build, decomp candidates that stayed on static recomp fallback, and
consumer-owned HLE overrides.
You can also open `lufia2.sln` and build the Release configuration in Visual
Studio.
### Linux (including the Steam Deck)
Requirements: CMake 3.24 or later, Python 3, GCC or Clang, and the SDL3 build
dependencies. On Debian or Ubuntu:
```bash
sudo apt install build-essential cmake ninja-build pkg-config python3 \
libgl1-mesa-dev libegl1-mesa-dev libx11-dev libxext-dev libxrandr-dev \
libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev \
libwayland-dev wayland-protocols libdecor-0-dev libdrm-dev libgbm-dev \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev
```
```bash
git submodule update --init --recursive
cmake -S . -B build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/linux --parallel
```
The executable is written to `build/linux/Lufia2Recomp`, with `libSDL3.so.0`
staged beside it and an `$ORIGIN` runpath, so the directory can be copied to a
Steam Deck as it is. SteamOS is immutable: build elsewhere and copy it over,
keeping the target glibc no older than the build machine's.
### Build options
Everything experimental or diagnostic is off by default, so a normal build is
silent and carries no measurement code. The native patches themselves are
production code and are always built; only their instrumentation switches:
| Option | Adds |
|---|---|
| `LUFIA2_ENABLE_RUNTIME_LOG` | map, MSU, widescreen and periodic state tracing |
| `LUFIA2_ENABLE_PATCH_REPORTING` | patch counters and scheduler time every 120 boundaries |
| `LUFIA2_ENABLE_PERF_AUDIT` | the finite Vita performance audit |
| `LUFIA2_ENABLE_DECOMP` | select verified semantic replacements at runtime; OFF keeps the generated/original game-function path |
| `LUFIA2_DECOMP_ROOT` | override the required `Lufia2Decomp` checkout; otherwise use the pinned submodule |
| `LUFIA2_DECOMP_REFERENCE_ONLY` | expose the decomp library without activating native replacements |
Errors are always reported.
`Lufia2Decomp` is a required build dependency in every configuration. The host
links its shared resource-format decoder even when semantic replacements are
disabled; `LUFIA2_ENABLE_DECOMP` controls game-function source selection only.
## Run
Start `Lufia2Recomp.exe` to open the launcher, or pass a ROM directly:
```powershell
.\build\windows\Release\Lufia2Recomp.exe "D:\ROMs\lufia2.sfc"
```
On Linux it is `./Lufia2Recomp` in the build directory, same arguments.
Save data is stored in `saves\save.srm` next to the executable.
## Bug fixes
Some bugs from the original game are fixed:
- Buying a spell no longer costs extra gold after a big shopping trip. In the
original, spending 65,536 gold or more on items could make the next spell
cost 65,536 gold more than its price tag.
- Equipment names no longer leave leftover letters behind. In the original,
switching characters with L or R in the equipment or status screen could
show part of the previous name, such as "Gladiussword".
- The play time clock keeps counting after 99 hours instead of jumping back
to 00:00, and the menus show three digits for the hours. It stops at
255:59, the most an original save file can hold.
- Item drops after a battle are always announced. In the original, some
items you already carried were added without the message saying so.
- HP and MP stop at 999, the most the menus can show. In the original,
level-ups could push maximum HP past that, and a character with more
than 1,023 loaded a save with wrong HP and MP.
- Dragon eggs found in the Ancient Cave are no longer lost. In the original,
an egg could appear in a cave chest and vanished when you left the cave
or were defeated there, so all eight eggs could never be collected again.
- Being defeated in the Ancient Cave while standing on stairs restarts the
cave as it should. In the original, the stairs still took you one floor
deeper with your stats reset, which was almost always fatal.
## Repository
- `recomp/` contains the control-flow information used during recompilation.
- `src/` contains the game host and runtime code.
- `lib/snesrecomp-platform/` is the shared renderer and host submodule.
## Renderer
The launcher offers SDL accelerated, SDL software and OpenGL 3.3. OpenGL can
also use GLSL shader presets. If it cannot start, the game falls back to SDL for
that run.
A build configured with `-DLUFIA2_ENABLE_VULKAN=ON` adds a fourth entry,
`Vulkan`, and accepts `OutputMethod = Vulkan` in `config.ini`. It is a direct
Vulkan renderer: SDL still owns the window, input, audio and the surface, and
everything below the surface is Vulkan. It presents the ordinary framebuffer,
the overlays and HD Mode 7, and it implements Clean-HD as native passes rather
than by loading a `.glslp` preset, so an arbitrary `Shader =` entry stays an
OpenGL-only feature. Vulkan is not the default, and a run that cannot start it
steps down to OpenGL and then to SDL, reporting which renderer it actually
got. Building it needs a Vulkan SDK for the headers, the loader and `glslc`.
`HDMode7` in `config.ini` takes `Off` or a scale from `1x` to `8x`; the
launcher's checkbox turns that configured scale on and off rather than
replacing it. The scale only decides how finely the affine plane is sampled,
never where -- it is separate from `HDMode7Filter`, which stays off so the
pass remains pixel exact. A renderer reports which scales it can allocate, and
a request above that is announced and reduced to the largest one it offers
instead of being dropped or silently substituted.
`PresentRate = Display` in `config.ini` presents on the display's clock
instead of the guest's, so a 144 Hz monitor shows the picture 144 times a
second while the game still advances 60 times. It does not interpolate
motion yet, and it does not touch guest timing, audio or input. Mode 7 scenes
stay at guest cadence because their frame cannot be shown again without
rebuilding it. `PresentRateHz` overrides the detected refresh, which a remote
desktop session reports incorrectly.
FPS, volume, toast and rewind overlays are composited after the game and its
shaders. The default Lufia skin uses the game's logical coordinate space: one
UI texture pixel receives the same aspect, resize, fullscreen, and letterbox
transform as one SNES pixel while remaining a separate overlay.
`SNESRECOMP_UI_SCALE=0.5..3.0` applies an optional user multiplier (rounded to
whole game pixels in this mode). The game-owned skin in
`src/lufia2_overlay_ui.c` supplies a replaceable bitmap font, nine-slice panel
texture, colours and spacing; the shared presenter remains game-neutral.
On desktop, Mode 1 battle backgrounds sample their native picture and authored
side margins in one coordinate space. Heat shimmer, mosaic, colour math and
monochrome palette flashes therefore reach the margins. The battle HUD and
original sprite viewport remain centred; a separate magic tilemap can extend
across the wide screen. Individual sprite spells retain their original geometry.
`LUFIA2_BATTLE_WIDE_EFFECTS=0` restores the previous margin compositor for comparison.
An optional external panel override can be placed beside the executable as
`assets/img/lufia2_menu_panel.tga`. Put `left top right bottom tile`
source-pixel margins in the adjacent `lufia2_menu_panel.9slice`; `tile` makes
patterned edges repeat like the original SNES tilemap instead of stretching.
Missing or invalid TGA files fall through to the authentic panel reconstructed
in memory from the verified ROM. The embedded neutral skin remains the final
fallback if that defensive extraction fails. No reconstructed pixels are
written to disk and no panel image ships with the project.
`scripts/ui_asset_tool.py` crops PNG/BMP/TGA screenshots, applies integer
nearest-neighbour downscaling, losslessly compacts tiled panels, writes the
runtime TGA, and renders a tiled or stretched 9-slice preview. It requires
Pillow (`python -m pip install Pillow`). Example for the native 120x88 panel:
```powershell
python scripts/ui_asset_tool.py panel.png `
build/windows/Debug/assets/img/lufia2_menu_panel.tga `
--slice 16 16 16 16 --tile 16x16 --compact --preview 240x88
```
The 48x48 result contains both 16-pixel corner regions and one 16x16 repeat
cell. It recreates the original patterned edges without scaling them.
Rewind preview cells are copied pixel-for-pixel from the rewind core. The
Lufia skin never downsizes them again; the moving filmstrip shows three cells
in a 256-pixel 4:3 frame and four in the standard widescreen frame. Only the
surrounding tiled panel and labels are rebuilt for the available width.
Single-line status badges such as the FPS counter use a compact form of the
same panel: a four-pixel border around a tiled clean centre. This permits a
16-pixel logical height without shrinking the font or stretching the artwork.
While rewind is visible, the FPS badge shares its exact right-hand edge.
See [ISSUES.md](ISSUES.md) for the current limitations.