Hotel Mario Static Recomp

Hotel Mario Static Recomp is a native Windows port of Hotel Mario for the Philips CD-i with quicksaves, run-ahead, and sprite interpolation.

README

Hotel Mario — static recompilation for Windows

A native Windows port of Hotel Mario (Philips CD-i, 1994). The game's 68000 code is translated ahead of time into C and compiled with MSVC. A runtime provides the parts of the CD-i the game expects: the CD-RTOS (OS-9) kernel, the CD file manager, the MCD212 video decoder (shown through Direct3D 11 at any resolution) and the ADPCM audio processor.

It is built from the CD-i static recompilation template that came out of the Link: The Faces of Evil port, extended for what Hotel Mario needs (see What this port added).

Building

Requirements: Windows 10+, Visual Studio 2022 (C compiler), CMake 3.19+ and Python 3.8+. No third-party libraries are needed.

  1. Put the disc image in rom/ (Hotel Mario (USA).bin + .cue, raw 2352-byte sectors). The image is not included.
  2. Run build.bat. It extracts the program modules from the disc, recompiles them into gen/, builds with MSVC and copies two executables to the project root.
  3. Run one of them. Each finds the image in rom/ next to the executable, or takes the path to the .cue/.bin as an argument.
ExecutablePicture
hotelmario.exeThe original picture: sharp, smooth, CRT or nearest scaling. Sprite interpolation is off by default (F6 turns it on).
hotelmario_hd.exeAdds the Cartoon HD+ (default) and Cartoon HD upscalers, and sprite interpolation is on.

Both have the same game features (quicksaves, run-ahead, invincibility). The HD one keeps its own settings file, so each remembers its filter. Saves (the game's NVRAM file and the quicksave slots), screenshots, settings and the log go to saves/.

Packaging and installing

Each release has a ready-made installer (HotelMarioRecomp-<version>-setup.exe) and a portable zip (HotelMarioRecomp-<version>-win64.zip). Players need only those and their own disc image; no build tools are required. The disc image itself is never included.

To make the packages yourself from a build, run:

package.bat

It runs build.bat, then writes to dist\:

FileWhat it is
HotelMarioRecomp-<version>-win64.zipPortable: unzip anywhere and run. The first start asks for the disc image.
HotelMarioRecomp-<version>-setup.exeA single-file installer, made only if Inno Setup 6 is installed (winget install JRSoftware.InnoSetup).

-SkipBuild packages the existing executables, and -NoInstaller makes only the zip.

The installer (installer\hotelmario.iss):

  • Installs without administrator rights, to C:\Games\Hotel Mario Recomp by default.
  • Asks for the Hotel Mario disc image (.cue or .bin) and checks that it is a CD-i image. The image is not copied: its path goes into config.ini in the program folder.
  • Adds Start menu shortcuts (and desktop shortcuts if you choose).
  • Uninstalling removes the program but keeps saves\ and config.ini.
  • Silent install: setup.exe /VERYSILENT /DIR="D:\Games\HM" /ROM="D:\images\Hotel Mario (USA).cue".

Portability. The program writes only to its own folder: config.ini beside the executables, and saves, settings, screenshots and logs in saves\. Nothing goes to AppData or the registry, so an install (or the unzipped folder) can be moved or copied, for example to a USB stick. The shaders are compiled into the executable. --portable is accepted (the shortcuts pass it) but changes nothing, since this is always the case.

Dependencies. The executables link the C runtime statically and use only DLLs that are part of Windows 10 and 11 (Direct3D 11 and its shader compiler, WinHTTP, XInput). Nothing else needs installing.

config.ini

[paths]
rom=D:\Games\Hotel Mario (USA).cue   ; the disc image
[update]
check=1                              ; 0: never look for new versions
skip=v1.0.1                          ; a version you chose not to install

Updates. At start (not in --headless runs, nor with --no-update-check or check=0), the program asks GitHub for the latest release of this repository (a few seconds at most; without a connection it simply starts). If the release is newer, it offers to install it:

  • Yes: update.ps1 waits for the game to close, downloads the release's portable zip, replaces the program files (the old executables are kept as *.old; config.ini and saves\ are not touched) and restarts the game. If a release has no zip, it builds that release's source with your disc image instead. That needs Visual Studio 2022 (C++), CMake and Python; without them it opens the release page and restarts the current version. The log is in saves\update.log.
  • No: skips that version.
  • Cancel: asks again next time.

To publish a new version: raise VERSION, commit and push, run package.bat, then create a GitHub release tagged v<version> with the two files from dist\ attached:

gh release create v1.0.1 dist\HotelMarioRecomp-1.0.1-setup.exe dist\HotelMarioRecomp-1.0.1-win64.zip

Controls

The CD-i thumbpad is a relative pointer with two buttons.

InputKeyboardXbox controller
MoveArrow keys (Shift = faster)Left stick / D-pad
Button 1 (jump, select)Z, Space, AA
Button 2X, Ctrl, SB, Y
Both buttonsC, EnterX, Start
KeyAction
F2Quicksave to the current slot
F3Next slot (1 to 10); the on-screen message says whether it is empty
F4Load the current slot
F5Invincibility on/off
F6Sprite interpolation on/off
F7Run-ahead: off, 1, 2 or 3 frames (default 1)
F9Aspect mode
F10Filter
F11, Alt+EnterFullscreen
F12Screenshot (saves/screenshot_NNN.bmp)
Tab (held)Turbo speed
EscQuit

Command-line options: --fullscreen, --windowed, --aspect N, --filter N, --size WxH, --gdi, --runahead N, --interpolate 0|1, --invincible, --load-slot N (start from a quicksave), --no-update-check, and --portable (accepted; the program is always portable).

Enhancements

Quicksaves. Ten slots, stored as saves/quicksave_NN.sav. A quicksave holds the complete machine: the 68000 registers, RAM, the CD-RTOS kernel, the CD play in progress, the audio processor and the video tables. Loading continues exactly where the save was made, with the music and the CD stream. You can't save during the Philips logo (it runs as a separate process). Save files work in both executables. A future change to the runtime can make older save files unusable; they are then refused with a message, and the game carries on.

Run-ahead. After each field the machine is saved, the next field(s) are emulated with the input just read, that picture is shown, and the state is restored. A press shows on screen 1 to 3 fields earlier than it otherwise would (F7). The extra emulation costs about 1x CPU per frame of run-ahead: headless gameplay runs at 345 fields per second without run-ahead, 166 with 1 and 113 with 2, against 60 needed. Run-ahead never changes the game: runs with and without it produce identical frames and audio.

Invincibility (F5). Enemies can't hurt Mario, and the open ends of floors that he would fall from become walls. The status line, timer and everything else are unchanged: running out of time still costs a life.

Sprite interpolation (F6). During play the game redraws its moving objects (enemies, items, fireballs, the status line) only every sixth field: 10 frames per second. Mario is drawn every field and already moves at 60. Interpolation shows the other objects at 60 frames per second too. While one picture is on screen the game has already drawn the next one, so each sprite moves smoothly toward its next position and reaches it exactly when the game shows that picture. Nothing is delayed, and the game's picture is shown unchanged on every sixth field. Objects that jump (teleports, new objects) are not interpolated.

Cartoon HD and Cartoon HD+ (HD executable). Two upscalers for drawn art, in addition to the original filters:

  • Cartoon HD finds the strong edges between two colours and redraws them as smooth, anti-aliased curves at screen resolution, using the original colours. Gradients and painted areas are scaled smoothly.
  • Cartoon HD+ does the same, then cleans flat areas of dithering noise, draws a thin ink line along strong edges, and adds a little saturation, for a cel-shaded look.

Both work on the game's own pixels (384 per line) at any window size, including 4K.

Status

Verified with scripted and randomized headless runs:

  • The Philips bumper (a separate program run with F$Fork) and the Fantasy Factory logo.
  • The full intro cutscene with speech and music.
  • The title menu with music, New/Restore, cutscenes and the stage cards.
  • Level 1 gameplay with music and sound effects, TIME UP, losing lives, game over and the CONTINUE? screen, and saving to NVRAM.
  • The attract-mode demo.
  • Three 30,000-field fuzz runs (random input) with no errors, in both executables.
  • Quicksave and load: 350 fields after a load are identical to the 350 after the save.
  • Invincibility: no deaths from enemies or falls in 25,000 fields of random play (18 lives lost without it).

Headless runs are bit-identical to a pure-interpreter run (--interp), as checked on 10,000 fields of boot, menus and gameplay. The instruction and ALU differential tests pass: 6,475 instruction tests and 12,600 ALU vectors.

Not yet checked: hotels after the first. They use the same stream and code paths, but nobody has played that far in this port.

The recompiler covers 110 KB of the 113 KB code section (34,045 instructions in 525 functions). The rest is constant data. The intro's intro_sub.o overlay is loaded from the disc into RAM at run time and runs in the built-in interpreter.

What this port added

The template's tools and runtime were extended as follows. Each change is written so that other CD-i games can use it.

Toolchain

  • Microware split-data programs. Hotel Mario keeps its initialized data and relocations in a separate module, cdi_hotel_data, which the C startup code loads with F$Load. That data contains a table of jmp abs.l stubs, and the game calls them with jsr N(a6). The analyzer reads <program>_data, resolves these calls statically and turns the other code relocations into function-pointer candidates. The generator emits direct native calls for the table entries, guarded by A6CHECK.
  • .stb symbol modules. The linker's symbol table, cdi_hotel.stb, gives 479 function entry points and their real names. Constant tables that carry code symbols are filtered out. Before these two changes the analyzer covered 21% of the code.

Kernel (os9.c)

  • System globals. Address 0 points to a system-globals block, and D_Ticks is kept current. Programs read the tick count directly, and the logo timeout depends on it. F$SetSys reads of D_BlkSiz and the module directory bounds now return real values.
  • F$Event: create, link, unlink, delete, wait (including relative), read, signal, pulse, set and relative set. A waiting process still takes signals: the intercept handler runs nested. The video driver's system event line_event is pulsed every field.
  • F$DatMod.
  • Modules that sit back to back in one file (cdi_bumper + cdi_bumpdata).
  • /cd@ opens the raw CD device.

CD file manager (cdfm.c)

  • SS_Abort ends the play the way CD-RTOS does: error 0x060A (E$Abort, CDFM group 6) goes into PCB_Sig, and the PCB signal is sent.

Audio (audio.c)

  • SD_Loop: a looping soundmap is read again at each pass, so data the program streams into it is heard. Hotel Mario mixes its music and sound effects this way.
  • The SM_Creat sound map types (A, B and C, mono and stereo) are mapped to ADPCM codings.
  • A mono mix through both the direct and the cross path is scaled so that it doesn't clip.

Video (video.c)

  • Plane-specific MCD212 registers can only be written by their own display channel: transparent colour, mask colour, DYUV start, mosaic, ICF, mattes and backdrop. Hotel Mario's plane B tables contain plane A register codes.

Machine snapshots (for quicksaves and run-ahead)

  • Blocking system calls (F$Sleep, Ev$Wait) no longer wait inside the host. They advance time to the next event and return their own address, and the call runs again after the scheduler has run. Every os9 instruction is a dispatch entry for this. So at each field, at top level, no 68000 state is left on the host stack, and the machine can be saved and restored there. This change is checked to be exact: runs are still bit-identical to the interpreter.
  • Each runtime module serializes its own state with one function used for both saving and loading (snapshot.c, *_snap). Snapshots include RAM up to the heap's high-water mark (about 2 MB).
  • A forked child (the Philips logo) still runs on a nested host loop; no snapshot is taken while it runs, and run-ahead passes don't start one.

Layout

PathContents
game.jsonPer-game settings: modules and their load addresses
tools/cdi_extract.pyGreen Book filesystem reader
tools/analyze.pyCode discovery: recursive descent, switch tables, A5/A6 jump tables, .stb symbols, data-module relocations
tools/gen.py68000→C generator
tools/disasm_ram.pyDisassembles code from a RAM dump, such as the intro_sub.o overlay
runtime/Dispatcher, interpreter, OS-9 kernel, CDFM, video, audio and the Windows host
runtime/snapshot.cSnapshots, quicksave slots, run-ahead, on-screen messages
runtime/game.cHotel Mario specific: invincibility, sprite interpolation
runtime/render_d3d11.cPresenter and filters, including Cartoon HD / HD+ (HD build)
test/Instruction differential tester and ALU reference vectors
docs/CDI_NOTES.mdCD-i and CD-RTOS facts worked out in the Link and Hotel Mario ports

Debugging

  • python tools/analyze.py [module index] writes a disassembly with function names to build/listing_<module>.asm.

  • Unimplemented OS calls and SetStat/GetStat codes are logged to saves/recomp.log. Fatal errors add a register dump, a stack backtrace and saves/ramdump.bin.

  • Options:

    OptionWhat it does
    --trace, --trace3Log system calls with their callers, signals, disc activity and video state changes
    --trace-funcsLog every entry into a recompiled function
    --trace-func A,B,...Log only these function entries (hex addresses)
    --headless --frames N --dump KRun without a window and write every Kth frame as a BMP (tools/bmp2png.py converts them)
    --ramdump NAt field N, write RAM to saves/ramdump_N.bin and log the display tables and per-line video state (--ramdump-count K for K fields)
    --script "field:key:dur,..."Scripted input: keys 1 2 L R U D, and hotkeys S save, O load, N next slot, I invincibility, P interpolation, A run-ahead
    --turboNo pacing (as fast as possible)
    --screenshot-at NSave the presented picture (after scaling and filtering) at field N
    --fuzz SEEDRandom input
    --wav out.wavRecord the audio
    --interpRun everything in the interpreter, as a reference for checking the translation
  • Unit tests:

    python tools/gen_tests.py
    python test/gen_alu_vectors.py
    cmake --build build\cmake --config Release --target insn_test alu_test
    build\cmake\Release\insn_test.exe "rom\Hotel Mario (USA).bin" 200
    build\cmake\Release\alu_test.exe
    

Legal

Hotel Mario is © Nintendo and Philips. The disc image is not included: the program runs the game from your own copy of the disc.