Skip to content

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Demon's Crest Recompiled

Native PC recompilation of Demon's Crest (USA), built on snesrecomp. The 65816 game code is translated to C and runs on the framework's authoritative interpreter, while the SNES hardware — PPU, APU/SPC700, DMA/HDMA and cartridge mapping — is emulated. Same model as N64Recomp and similar projects: recompile the CPU, emulate the silicon.

Play with adaptive widescreen, save states, rewind and the shared launcher, using your own legally dumped ROM.

Quick start | Features | Widescreen | Performance | Recording | Known issues | Build | Run

Demon's Crest widescreen boss fight
Widescreen gameplay
The expanded field keeps the action and the HUD in view.
Demon's Crest widescreen clock tower stage
Full-stage widescreen
Backgrounds, sprites and effects extend into the margins.

Demon's Crest Mode 7 overworld map in widescreen
Mode 7 overworld
The flying map fills the widened field, margins included.

You must legally own a copy of the game. No ROM data is distributed with this project, in the repository or in any release. The recompiled C is generated locally from your own copy and is never committed.

Status

The port boots and plays: the Capcom logo, intro, stage gameplay and boss fights all run from the generated C. The native Widescreen mod is implemented and validated through its final phase — background margins, objects and sprites, HUD anchoring, the Mode 7 overworld, the top-band flicker and save-state round-trips — with the engineering notes and validation summary in docs/PORTING_NOTES.md. See Known issues for what playtesting has since turned up, including the save-state CPU caveat.

Playtesting continues; general gameplay bugs and broader coverage are the remaining work, so treat a full-game playthrough as not yet certified.

What has been verified by playing it

These were confirmed by actually playing (and scripting) the game, not only by frame-accuracy checks:

  • Boot → play: logo, intro, title, attract and a new game all run.
  • Stage gameplay and bosses: the ice cavern, mountains and castle stages run with their bosses, HUD, crest icons and forms intact.
  • Widescreen: the widened field really renders stages, margins, HUD anchoring and the Mode 7 overworld as described below.
  • Forms: switching gargoyle forms works and each form's sprites play.
  • Scripted input: the debug server can drive the game from reset (set_controller, run_to_frame), which is how the boss and door tests in the companion port are run.
  • Recording (new): input can be recorded and replayed frame-exactly, and frames or a rolling buffer can be written to disk — see docs/RECORDING.md.
Ice cavern boss fight in widescreen
Ice cavern boss
A boss fight running from the recompiled code, HUD and crest icons included.
Aerial gargoyle form in the mountains
Form switching
The aerial form in the mountain stage, with the widened background.
Castle stage flight
Castle stage
Flight, layered backgrounds and the status bar all correct in the widened field.
First Battle of the game
Somulo Fight
First Battle of the game.

Known issues

Found while using the runner as a reference for a native port. None of them stop normal play, but they matter to anyone scripting or capturing the game.

  1. Save states made by older builds lack the execution chunk. The current build saves and restores the 65816 execution state too (the v5 chunk in src/game_rtl.c), so a state loads in a fresh process and plays — verified with in-stage states. A state written by a build from before that chunk (the framework also ships snes_saveload, which serialises the legacy Cpu, not the runtime's CpuState) has no execution state to restore: it falls back to the guest's boot-time scheduler context, so a resumed boss fight from such a legacy state restarts the scene.
  2. screenshot / dump_frame_raw capture the current frame, and frames only advance through run_to_frame. A paused runner that is asked for a screenshot without advancing returns the same (stale) picture, which looks like a drifting capture. Pair every capture with a frame advance.
  3. run_to_frame returns before the target frame is reached. Clients that read memory immediately after the command see the old frame; poll frame until it reports the target.
  4. Widescreen margins: the level geometry stops where the engine stopped staging. Demon's Crest streams its BG1 tilemaps just in time for the native 256-pixel view and only keeps 0–48 px of world ahead of the camera, so a widened margin can ask for columns that still hold the previous page — at the end of the city stage that put the entrance gate (shop area, EXIT sign) on the exit side. The port measures the staging (the tilemap lives in VRAM and the engine is its only writer): when most of a margin is unstaged, BG1 is clamped to the authentic 256 columns for that frame and the margin shows the layers below it — the parallax city and backdrop, which are real content. The city itself is not a bug: it is BG2 and the 4:3 frame already shows it at its edge. SNESRECOMP_WS_MARGIN_CLAMP=0 disables the clamp for A/B captures; SNESRECOMP_WS_EDGE=1 opts into an experimental continuation that repeats the last staged tile instead (off by default — it replaces real content). See docs/PORTING_NOTES.md.
  5. Boss sprite composition is only correct in the runner. The native port built from these captures had to reassemble boss OAM parts itself and got them wrong for a while; that is a port problem, recorded here so the distinction is clear when comparing screenshots.

Features

Available Feature What it adds
☑ Adaptive widescreen Native widened field with adaptive, 16:9, 21:9 and 32:9 views. Disabled by default; with it off the game renders at the original 256-pixel width.
☑ Save states Ten slots (F1–F10 load, Shift+F1–Shift+F10 save) plus the save-state browser (F11). The current build restores the 65816 execution state (v5 chunk), so states load in a fresh process and play; states written by builds that predate the chunk resume at the boot-time scheduler context — see Known issues.
☑ Rewind Rewind history (F12) over the shared framework's snapshot ring.
☑ Recording Alt+R starts/stops a take: one BMP per presented frame plus a replayable input_log.jsonl, in captures/rec_<time>/. Needs no debug server; the protocol can additionally replay the pad stream frame-exactly or keep a rolling capture. See docs/RECORDING.md.
☑ Launcher recomp-ui ROM picker with a verification badge, display/audio/input settings and the Mods screen.
☑ ROM verification The ROM is checked against the project digests before anything boots; a mismatched dump is refused instead of mis-executing.
☑ Authentic 4:3 The original 256-pixel field and layer layout remain the default when the widescreen mod is off.

Widescreen

Enable Widescreen in the launcher's Mods screen and pick a view: Adaptive, 16:9 (342 px), 21:9 (448 px) or 32:9 (682 px). The mod is off by default.

What it does:

  • renders the game directly into the widened field (native_widescreen), so the scrolling backgrounds continue into symmetric margins;
  • gates widening to stage gameplay ($8D in 1..0x7F, or the Mode 7 overworld map with its alternate-HUD variant): menus, cutscenes, password and transitional screens stay pillarboxed;
  • widens the object-loop cull and the OAM emitter so enemies and effects just outside the native view keep updating and appear in the margins, and widens the attack projectile's on-screen test so shots fly across the whole field;
  • anchors the BG3 status bar to the widened left edge and leaves the freed columns transparent, showing the scenery where the bar used to sit; BG3 is widened only on the bar's scanlines, so a dialogue box on the same layer keeps its native columns;
  • keeps the Start/status menu pillarboxed ($00FB marks it): it is a full-screen native UI, and widening it would show the stage tilemap in the margins;
  • drops the game's forced-blank upload cover from the raster replay in every scene (a field that stays blank keeps its black), which removes the black band that flickered at the top and lets HDMA tables start at the top of the field, keeping window effects aligned with their targets;
  • keeps dialogue boxes and edge windows intact: the fixed-color box cannot leak a strip into the top-left corner, and a parked one-pixel layer window moves with the widened edge instead of cutting a one-pixel seam at the margin boundary;
  • opens the Mode 7 overworld map to the widened field; the terrain continues into the margins with no renderer change.

The generated-code hooks are applied by tools/dc_apply_widescreen_hooks.py, an idempotent injector run by tools/regen.sh and by the build. At margin 0 every injected expression evaluates to the authentic constant, so the object and sprite hooks add nothing to the 4:3 path.

The engine stages its BG1 tilemaps for the 256-pixel view and keeps only 0–48 px of world ahead of the camera, so the margins can hold the previous page where the engine has not restaged them. The port measures which columns are staged and, when most of a margin is not, clamps BG1 to the authentic 256 columns for that frame: the margin then shows the layers below the level geometry — the parallax city and the backdrop — instead of a foreign gate. Ordinary frames are untouched, and nothing is invented; at 21:9/32:9 this is what keeps the extreme edges showing real background. See docs/PORTING_NOTES.md.

Diagnostic kill-switches for A/B captures: SNESRECOMP_WS_OAM=0 (object/sprite widening), SNESRECOMP_WS_HUD=0 (status-bar anchoring), SNESRECOMP_WS_NOBLANK=0 (upload-cover policy), SNESRECOMP_WS_CULL=<px> (cap the margin object window; 0 = authentic cull) and SNESRECOMP_WS_EXTRA=<px> (fixed margin).

Quick start (build from source)

  1. Clone with the framework and launcher submodules:

    git clone --recurse-submodules https://github.com/magnopaz/DemonsCrestSNESRecomp.git
    cd DemonsCrestSNESRecomp
  2. Have your own Demon's Crest (USA) ROM available. Keeping it outside the repository is the better habit; SNESRECOMP_ROM sets the path once for a shell.

  3. Generate the recompiled C from the ROM:

    bash tools/regen.sh --rom "/path/to/Demon's Crest (USA).sfc"
  4. Configure and build:

    cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
    cmake --build build -j
  5. Run:

    ./build/DemonsCrestSNESRecomp

    The launcher asks for the ROM on the first run and caches the choice.

On Windows with the MinGW toolchain, tools/play.ps1 wraps regeneration, configuration, build and launch in one step.

Build

The default desktop backend is SDL3 (fetched and built from source when no package is installed). To build against an installed SDL2 development package instead — for example the MinGW one at M:\SDL on Windows:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DSNESRECOMP_SDL_BACKEND=SDL2 \
  -DSDL2_DIR="M:/SDL/SDL2-2.32.6/x86_64-w64-mingw32/lib/cmake/SDL2"

tools/play.ps1 automates regen + configure + build + launch for the Windows MinGW toolchain (CMake, Ninja, GCC, Rust GNU, SDL2 under M:\SDL). It refuses to build while a game instance is running (a forced kill would end that session with no exit report); pass -KillRunning to terminate it explicitly.

The ROM does not have to live in the repository — SNESRECOMP_ROM sets the path for a shell. Without it, tools/regen.sh looks for Demon's Crest (USA).sfc at the repo root, then for the path the setup wizard recorded in rom.cfg; .gitignore blocks both from ever being committed.

tools/regen.sh verifies the ROM, generates src/gen/*.c, and re-syncs recomp/funcs.h. Re-run it whenever you change anything under recomp/.

The desktop build defaults to the framework's execution policy off — the faithful LLE floor is what runs, by design, until an optimized path clears the framework's promotion gates (snesrecomp/docs/PERFORMANCE.md, recomp-ai-rules/OPTIMIZATION.md). A release that has promoted paths sets -DSNESRECOMP_EXECUTION_DEFAULT=on; SNESRECOMP_EXECUTION_MODE and SNESRECOMP_FORCE_FLOOR=1 override it at runtime.

On Windows the MinGW link prints .rsrc merge failure: multiple non-default manifests: binutils merges the UTF-8 manifest snesrecomp/runner/windows/snesrecomp_utf8.manifest with the linker's default one. The explicit manifest is embedded (the built executable contains activeCodePage=UTF-8) and the host reports its code page at startup, so this is link noise, not a missing manifest.

Tests

The port's invariants run headlessly against the built executable:

cmake --build build -j
cd build && ctest --output-on-failure

ctest -L unit needs no ROM (the generated-code injector: synthetic anchors, idempotency, drift rejection, plus --check against src/gen). ctest -L integration boots the game, checks that a widened field keeps its authentic center bit-identical to 4:3, that two runs are deterministic, and — with a save state — that the far-right staging repair touches only the columns the engine has not staged. Integration tests report SKIP when no ROM is available and the repair test also wants SNESRECOMP_TEST_SAVE. See docs/TESTING.md.

Run

./build/DemonsCrestSNESRecomp
# or name the ROM and skip the launcher
./build/DemonsCrestSNESRecomp "/path/to/Demon's Crest (USA).sfc"

With no ROM on the command line the build opens the recomp-ui launcher: a ROM picker with a verification badge, display/audio/input settings and the Mods screen. Your choice is cached in rom.cfg beside the executable, so the next launch opens on it and "Skip launcher on boot" makes it immediate. If the launcher is not built in (--no-recomp-ui), the host falls back to a native file picker.

Either way the ROM is checked against the digests below before anything boots — a dump that could not have produced this build is refused at the door rather than mis-executing ten frames in.

During play: F11 opens the save-state browser and F12 opens rewind; both pause gameplay while you choose. Alt+R starts/stops a recording (frames plus a replayable input log, saved under captures/). Hotkeys, controller mappings and pixel/aspect options live in the launcher; Alt+R can be moved with Record = <key> in config.ini's [KeyMap].

Recording

The debug server can capture a session and reproduce it, which is how the screenshots above and the port's reference captures are made:

# keep the last ~5 seconds in memory and write them when the window closes
SNESREC_RING_DIR=captures/somulo ./build/DemonsCrestSNESRecomp "Demon's Crest (USA).sfc"

# or, with SNESRECOMP_DEBUG_PORT set, from a debug client
record_input_start
... play the fight ...
record_input_stop captures/fight.jsonl
replay_input captures/fight.jsonl      # reproduce it frame-exactly
record_frames captures/somulo 600 1     # one BMP per frame
python tools/bmp_sequence.py captures/somulo --gif somulo.gif

Input is sampled where the runner reads the pad, so file frame N is the input frame N ran with. See docs/RECORDING.md for every command and its limits.

Environment switches: SNESREC_AUTORECORD=1 starts a take on the first guest frame (so it is replayable from boot), SNESREC_RECORD_DIR picks the take directory, SNESREC_RING_DIR / SNESREC_RING=1 arm the rolling buffer, and SNESREC_INPUT_ONLY=1 records the input log without per-frame BMPs — the last one is a small local patch to the framework host (see docs/RECORDING.md).

ROM identity

File Demon's Crest (USA).sfc
Publisher Capcom
Developer Capcom
Year 1994
Mapping lorom
Region USA (North America)
Coprocessor none
CRC32 e8236ad2
SHA-256 18d40a807d5f88c5b6a1ad849eec7e0f189225d9a1586037c850f6680b5844de

tools/regen.sh refuses to run against anything else, so a mismatched revision fails immediately instead of producing subtly wrong output.

How the recompilation works

The 65816 CPU code from the ROM is statically translated to C — every function the analysis can prove is a real generated C function in src/gen/. Execution is LLE-first: an authoritative 65816 interpreter is the correctness floor, and the statically compiled bodies are exact, proven materializations on top of it — anything the static pass cannot prove keeps running through the interpreter, loudly. The rest of the SNES is not recompiled: PPU rendering, the APU/SPC700, DMA/HDMA, hardware register I/O and bank mapping run through snesrecomp's runner implementations.

Rules for contributors:

  1. Never edit src/gen/ by hand and never synthesise a result to get past uncovered code. Fix the config or the framework and regenerate.
  2. Name routines in recomp/symbols.toml, then re-run tools/regen.sh. Set emit = true to promote a routine into ahead-of-time analysis; leave it false to keep it interpreted. Only proven variants become AOT code. See recomp/README.md.
  3. Deal with unresolved indirect dispatch targets before anything else — they are the reason a port diverges.

Performance

A recompilation removes the cost of interpreting the 65816; it does not make the console free. This port keeps the framework's LLE-first execution model (see How the recompilation works), so every frame still pays for three different things: guest code execution (generated bodies plus everything the analysis left on the interpreter), the emulated PPU and APU/SPC700, and the host's composition and presentation. The measurements below attribute a frame to those buckets, and explain why a mature SNES emulator can still use less CPU on the same machine — and why that is the expected shape of this port rather than a defect.

How to reproduce

The runner carries its own attribution, so no rebuild is needed:

cd build

# throughput config: unpaced, audio off, default PPU tier, SDL's own presenter.
# Keep it in its own file and re-create it before every A/B run -- the host
# writes the active config back on exit.
cat > perf-config.ini <<'EOF'
[General]
DisableFrameDelay = 1
Autosave = 0

[Graphics]
WindowScale = 3
Renderer = auto
NewRenderer = 1

[Sound]
EnableAudio = 0
EOF

printf 'loadstate 0\nwait 900\nquit\n' > perf-script.txt
cp saves/lvl_0_murkland-colosseum.sav saves/save0.sav

SDL_VIDEODRIVER=offscreen SDL_AUDIODRIVER=dummy \
SNESRECOMP_FRAME_TIMING="$PWD/perf-timing.csv" \
./DemonsCrestSNESRecomp --no-launcher --config perf-config.ini \
    --script perf-script.txt "/path/to/Demon's Crest (USA).sfc"

(The working directory also needs mods/preloaded for the widescreen variant; the build directory already has it.)

SNESRECOMP_FRAME_TIMING writes one row per presented frame (guest, raster_ms, acquire_ms, compose_ms, present_ms, trace_ms, hook_ms, events_ms, wait_ms); SNESRECOMP_HOST_PROFILE=1 prints the same stage means at exit. Three practical traps: the host rewrites the active config.ini on exit, so a variant whose axis lives in that file silently reverts unless you re-create it per run; Renderer = opengl (the native GL presenter) cannot initialise headless — use auto or software; and SDL_VIDEODRIVER=dummy has no render driver at all (the host stops at renderer init FAILED), so headless means offscreen.

Where a frame goes

Sample run: Raspberry Pi 4 (aarch64, Ubuntu 24.04, 4× Cortex-A72), Release build, SDL2 backend, no window, from the gameplay save state lvl_0_murkland-colosseum.sav, 902 frames, pacing disabled, 4:3, audio off. Means over the 892 frames that follow the state load.

Stage ms/frame Share What it is
guest 8.36 75% running the machine: 65816 execution (generated + interpreted), APU stepping, bus and cycle accounting
raster-capture 1.51 13% drawing the frame from PPU/VRAM state and capturing it for presentation
compose 0.26 2% extra composition (widescreen/blend staging)
present 1.02 9% texture upload and present
acquire / events / others 0.05 0% surface and event pumping
total 11.20 100% ≈ 89 fps uncapped, one core saturated

Two things about that table are easy to misread:

  • guest is not "the PPU". It is the simulated machine minus the draw. The pixel work sits in raster-capture, and it is cheap here: re-running the same scene with SNESRECOMP_LAYER_MASK=0 (which keeps no layer at all, so no tile or sprite pixel is fetched or written) drops that stage from 1.51 to 1.00 ms/frame and the whole frame by only 4% — 89.3 → 93.0 fps. This title is bound by executing code, the APU and the bus, not by drawing.
  • present is where the presentation backend shows up, and it is the only stage whose cost depends on how the frame reaches the window.

What each option costs

Same scene, same host, one axis changed at a time:

Variant ms/frame fps Delta Where it lands
baseline (above) 11.20 89.3 — —
audio enabled (32 kHz stereo) 11.31 88.4 +1% guest +0.1, present flat
widescreen 16:9 11.73 85.2 +5% raster-capture +0.19, compose +0.12, present +0.22
widescreen + audio 11.79 84.8 +6% additive, still small
NewRenderer = 0 (reference PPU) 17.07 58.6 +53% raster-capture 1.51 → 7.30 (4.9×)
Renderer = software 20.03 49.9 +79% present 1.02 → 9.77 (9.5×)
SNESRECOMP_LAYER_MASK=0 (no layer drawn) 10.75 93.0 −4% raster-capture 1.51 → 1.00
WindowScale = 1 instead of 3 11.23 89.0 0% the 3× scale is free in this path

So the two expensive switches are the ones users can flip in the launcher, not the enhancement: the software presentation backend costs almost as much as simulating the whole console, and the legacy PPU tier costs more than everything else combined. Widescreen and audio are in the noise by comparison.

Why this is the expected shape

  • Only the CPU was recompiled. PPU rendering, APU/SPC700 and DSP synthesis, DMA/HDMA and hardware register I/O are emulated by the framework. That work is architecturally the same work an emulator does; a recompilation has no way to remove it. On this title it is ~25% of the frame (raster-capture, compose, present), and inside guest the APU and bus accounting.
  • The guest tier is LLE-first by design. The interpreter is the correctness floor and the scheduler/main loop runs on it; generated bodies are entered through the dispatch table wherever the analysis proved a variant, and anything indirect or unproven stays interpreted. Proving more of the game (entries in recomp/symbols.toml, resolved indirect targets) moves time from interpreted to native execution — that is the one lever a recompilation actually owns, and it is deliberately gated on proof rather than on speed.
  • The interpreter boundary is not free. Every native↔interp transfer re-establishes the guest stack contract, settles DRAM refresh/cycle debt and re-checks width variants. A byte-for-byte-faithful 65816 running a scheduler in this shape costs more than an emulator's tight computed-goto loop that owns the whole machine and can ignore the boundary.
  • A mature emulator is very hard to beat at its own game. Snes9x has decades of tuning on exactly this model: per-scanline rendering straight into the texture with no staging step, no widescreen shadow to maintain, no diagnostic or state rings, no recompilation ABI, and no reason to keep LLE paths around for correctness investigations. This port pays for all of those, and it also renders through a composition + upload stage so that enhancement policies can exist at all.
  • This title is a light one. ~89 fps uncapped on a 4-core ARM SBC means the frame is ~11 ms on a CPU that is several times slower per thread than a modern desktop. The port is nowhere near CPU-bound at 60 fps.

Reading CPU usage while playing

Task-manager CPU percentage at 60 fps is a poor metric: the loop is paced, the audio device and the graphics driver own threads of their own, and time spent waiting for vsync is indistinguishable from time spent emulating. A paced run of the same scene here took 15.05 s wall for 902 frames (59.9 fps) but 11.98 s of CPU — the frame cost is unchanged, the remainder is pacing slack. When a number needs to mean something, use the stage CSV above (or SNESRECOMP_HOST_PROFILE=1) rather than the percentage.

Headroom that already exists

Nothing above is a promise that the port is as fast as it can get. The framework's own burn-down (snesrecomp/docs/PERFORMANCE.md) records the levers it measures and retains, and HLE_DEFAULTS.md records the one that has already been qualified on another title: composing each frame in cached host memory and uploading it once — LLE → HLE frame service — took Mega Man X from 341 to 890 uncapped fps with 67% less process CPU on Windows x64. This port pins the framework's execution policy to the faithful floor (off) and keeps the LLE frame service on non-Windows targets, so those gains are available but not taken: promoting them needs the framework's correctness gates plus a gameplay pass, exactly like any other optimisation in this tree.

Layout

Path What lives there
recomp/ Analysis input: bank*.cfg, symbols.toml, generated funcs.h
rom_identity.txt ROM digests — read by the build, tools/regen.sh and CI
src/ Host code you own: main.c, game_rtl.c, widescreen policy
src/gen/ Generated C. Never committed — regenerate locally
docs/ PORTING_NOTES.md — verified state, fixes and roadmap
mods/ Preloaded mod catalog (the widescreen package)
tools/ regen.sh, play.ps1 and the widescreen code injector
snesrecomp/ Framework submodule (owns lib/recomp-net, lib/retcomm-rbengine)
recomp-ui/ Shared launcher UI submodule
scripts/ package_release.sh — player-facing zip
framework_pins.txt Exact framework commits this project was scaffolded against

Reporting problems

Open an issue with the build, the enabled mods and the steps to reproduce the problem. For gameplay bugs, attach the save state (build/saves/saveN.sav) and describe the inputs that trigger it. If the game closes unexpectedly, also attach last_run_report.prev.json (the previous run's report, preserved on the next launch) and any crash_report_*.json / crash_minidump_*.dmp from the build directory.

github.com/magnopaz/DemonsCrestSNESRecomp/issues

License

This project's own source is under the MIT license in LICENSE. The framework and launcher carry their own terms — see snesrecomp/LICENSE, snesrecomp/THIRD_PARTY_ATTRIBUTION.md and recomp-ui/LICENSE. Neither covers the game data, which is not distributed here.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages