Xenon 2

How it works · write-up

Reliable Windows Hatari build and launch

xenondoc/HATARI_BUILD_RUN.MD · 13 KB · updated 2026-10-06

Use xenon_tools\hatari_dev.ps1 for command-line builds and isolated Hatari runs. It mirrors the checked-in MinGW CMake presets and the Visual Studio launch profile instead of depending on the environment inherited by a particular terminal.

Desktop SDL3 recordings can encode directly to MP4 with -RecordFormat mp4 -RecordEncoder x264 (or qsv/nvenc). Campaign validation uses --record-format mp4 --record-encoder qsv. PNG AVI remains the default. See EXTERNAL_FFMPEG_RECORDING.MD for commands, audio synchronization, graceful stopping and validation results.

What went wrong in earlier runs

The MinGW Hatari executable does not contain or sit beside all of its runtime DLLs. It imports SDL3, Capstone, libpng, libarchive, zlib, PortMidi, Readline and the MinGW threading runtime from C:\msys64\ucrt64\bin. Visual Studio explicitly prepends that directory (and C:\msys64\usr\bin) to the child PATH. A plain Start-Process from the Codex PowerShell did not, so Windows' loader terminated Hatari before its control socket opened. Changing build directories or retrying the same executable could not fix that.

The same shadowing breaks the compiler, not just the launch, and it looks like a compiler bug rather than an environment one. gcc invoked from a terminal whose PATH has Strawberry Perl or Git's mingw64 ahead of C:\msys64\ucrt64\bin exits 1 with no diagnostic at all on src/autopilot/*.c -- but only at -O1 and above, so the same file compiles at -O0 and the failure looks optimizer-specific. cc1.exe is resolving libisl-23.dll, libmpfr-6.dll and libmpc-3.dll from those other toolchains (ldd on cc1.exe shows where each one comes from), and dies with STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) once an optimization pass calls into one of them. hatari_dev.ps1 build passes the same normalized PATH to CMake and Ninja that it passes to Hatari, so it is immune; a bare cmake --build out\build\mingw-debug from an arbitrary shell is not.

The Visual Studio launch profile also uses the repository root as Hatari's working directory. Relative snapshot and configuration arguments therefore resolve under the repository. Atlas and TOS assets are a separate concern: BIN2DATADIR is . for this build, so Hatari loads them from the executable directory. CMake's hatari-runtime-assets target stages them there on every build. A run can have the correct DLL path and still render incorrectly if those staged files are absent or stale.

Finally, a snapshot needed at startup should be passed with --memstate. Do not start Hatari, pause it through the Xenon protocol, and then request a load: snapshot restoration completes at a safe emulation-loop point, so pausing first can prevent the request from reaching that point.

Script behavior

The script:

  • configures mingw-debug by default, or mingw-release with -BuildType Release, then builds only target hatari;
  • launches the selected output's src\hatari.exe with the repository root as its working directory;
  • supplies snapshots as absolute --memstate paths;
  • constructs one normalized child PATH, beginning with the two MSYS2 directories, so duplicate Path/PATH entries cannot shadow the required DLL directory;
  • checks every physical imported DLL before launch (Windows API-set contract names are virtual);
  • verifies that TOS and the packed/shared/per-level atlas banks have been staged beside Hatari;
  • refuses to overwrite an executable which is currently running;
  • refuses an occupied control port, preventing an isolated test from accidentally using an existing Hatari instance; and
  • waits until the Xenon control socket is actually listening, reporting an early loader/startup failure immediately.

The script defaults to control port 6902 for isolated development runs. The usual manually started instance can continue to use 6802.

Debug uses -g -O0; Release uses -O3 -DNDEBUG. Their default output trees are out/build/mingw-debug and out/build/mingw-release, respectively. A custom -BuildDirectory preserves the selected -BuildType; the directory name does not determine optimization. The resident C autopilot is optimized in both.

In the Codex sandbox, MSYS2 Ninja may stall with no child compiler and no output. On 2026-09-02 the same documented build2 command completed in a few seconds outside that sandbox. Request the scoped build escalation once in this situation; do not start concurrent Ninja processes against the same directory or keep switching build trees. Wait for the actual build completion before launching.

Recipes

From D:\src\hatari:

# Validate the existing executable, DLL resolution, and staged assets without starting anything.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 check

# Reconfigure and build exactly as the Visual Studio mingw-debug preset does.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 build

# Optimized emulator, using a separate output tree.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 build -BuildType Release

# Build, then launch the reproducible initial Xenon state.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 build-run `
  -Snapshot assets\xenonplay.sav -ControlPort 6902

# Run an existing build from a later checkpoint without rebuilding.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 run `
  -Snapshot xenon_tools\run_logs\level2-start-0822-2.sav -ControlPort 6903

# Visible resident-C campaign; --build also rebuilds the native replay DLL.
python xenon_tools/validate_campaign.py native-run optimized-campaign --build-type Release `
  --build --fast-forward --resume assets/xenonplay.sav --checkpoint-interval 150

# Full campaign observation: keep reporting after lost lives or temporary stalls.
# Game over, persistent stalls and completion still end recording gracefully.
python xenon_tools/validate_campaign.py native-run full-campaign --build-type Release `
  --build --fast-forward --resume assets/xenonplay.sav --checkpoint-interval 150 `
  --continue-after-life-loss --continue-after-stall

# Agent/background test: suppress the Hatari and --wincon windows.
powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 run `
  -Snapshot assets\xenonplay.sav -ControlPort 6904 -Hidden

Additional Hatari options can be repeated with -HatariArgument. Use -Wait when the invoking terminal should remain attached until Hatari exits. -NoSnapshot starts without restoring state, and -NoWinConsole omits the VS profile's --wincon option.

Sprite AVI resolution and sound

--xenon-sprite-scale 4 creates a 1280×800 drawable area, four pixels per original 320×200 pixel in each direction. It preserves the 4× HD atlas detail. The default is still scale 2 (640×400); scales 1 through 8 are supported. This changes the Sprite Stream window and capture resolution, not game coordinates, autopilot decisions, or the original AVI's size. Choose the scale before recording; resizing the window during recording stops the sprite AVI.

The official launcher exposes -SpriteScale 4, and both campaign scripts expose --sprite-scale 4. For example:

powershell -ExecutionPolicy Bypass -File xenon_tools\hatari_dev.ps1 run -BuildType Release -HighFps -HdSprites -SpriteScale 4
python xenon_tools/validate_campaign.py native-run hd-campaign --build-type Release --build --fast-forward --high-fps --hd-sprites --sprite-scale 4 --checkpoint-interval 150

Both AVIs now contain the game's mixed 16-bit PCM at Hatari's configured sample rate: one channel for ST/Mega ST, two for machines with stereo sound. ST mono stores one of the identical mixer channels, halving audio payload size without changing the sound. The sprite AVI previously contained a synthetic silent audio stream. It now retains a small VBL audio history and pairs each GPU download with that VBL's sound before handing the detached frame/audio pair to the encoder. Audio is also captured when only --sprite-avirecord true is enabled. Stopping a recorder and starting it again creates fresh audio history. No audio history is allocated while sprite recording is disabled.

--run-vbls now uses normal shutdown, so AVI workers and GPU readbacks finish before their files close. The former immediate exit(0) left incomplete headers. The campaign validator continues to finalize both AVIs through the control API.

After a recording has finalized, verify its dimensions, audible PCM, and parity with the original AVI using:

python xenon_tools/audit_sprite_avi.py path/to/sprite.avi --original path/to/original.avi --size 1280 800

The audit decodes the first and last images, checks RIFF/OpenDML video and audio indexes, requires one PCM chunk per frame, and compares overlapping audio by VBL. The original AVI tags are one VBL higher because video.c increments its counter between sprite capture and authentic capture/audio generation.

The resolution and sound checks, including a multi-segment OpenDML capture and recorder restart, are documented in SPRITE_AVI_RESOLUTION_AUDIO_20261005.MD.

Developer memory tracing now defaults to auto: it records access rings/maps only while a developer view is visible. Sprite capture and native control hooks always run, and paired AVIs remain available. Ring-dump investigations must launch with -MemoryTrace on (or native-run --memory-trace on). off disables the bookkeeping even when a developer view is visible.

Fast-forward selects immediate GPU presentation, falling back to mailbox where supported. Normal speed restores GPU VSync. AVI capture still records every VBL; the authentic renderer's separate --vsync option is unchanged. Measurements and recording paths are in NATIVE_FAST_FORWARD_PERFORMANCE.MD.

Snapshots in the browser build

The same snapshots work in the WASM build, which matters because a lot of the rendering work is only reachable deep in a level: the Sprite Stream views and the debug hooks are the same code there, but playing to Level 2 in a browser to look at one draw procedure is not a reasonable loop.

Both development servers serve Hatari snapshots under /snapshots/<file>.sav, from directories that are not inside the served tree -- assets/ and xenon_tools/run_logs/ by default, more or different ones with --snapshot-dir (repeatable):

  • tools/run-wasm.sh (port 8732, the Visual Studio launch target) passes both defaults to tools/no-cache-http-server.py.
  • site/serve.py (used by .claude/launch.json and for previewing site/dist) takes the same --snapshot-dir option and has the same defaults.

Only a bare file name is accepted after /snapshots/, so the option cannot be used to read the rest of the disk. Nothing is copied into the build output, so a snapshot appears as soon as it is dropped into one of those directories (xenon_tools/run_logs/ is git-ignored, which is where per-run checkpoints belong).

hatari.html then takes a snapshot= URL switch naming one of those URLs:

http://localhost:8732/hatari.html?snapshot=/snapshots/l2-f10377-freshstate.sav
http://localhost:8732/hatari.html?snapshot=/snapshots/xenonplay.sav&hires=1

It fetches the file and replaces the preloaded start snapshot (the intro, xenonintro.sav, or xenonplay.sav with skipintro=1) inside the Emscripten file system before main() runs, so Hatari restores it through its own startup --memstate -- the same "restore at startup, not mid-run" rule this document states for the desktop build. Restoring mid-run instead (the AltGr+L shortcut, SHORTCUT_LOADMEM) was observed to leave a Level 2 state looping on GET READY, while the startup path restores it cleanly.

Same-origin paths only; an absolute URL is refused rather than fetched, and a failed fetch logs and falls back to the default state instead of hanging the loader. The switch is only useful against a development server -- the published site has no /snapshots/ -- so it costs nothing in production.

When experimenting with another build tree, pass -BuildDirectory build2. The wrapper explicitly selects the MSYS2 UCRT64 Ninja. It performs a fresh configure when the cache is absent or names a different Ninja, then keeps later builds incremental. This prevents a stale cache—or a Visual Studio Ninja found earlier on the parent PATH—from generating linker response files whose backslashes are consumed by GCC. The normal path remains the Visual Studio preset directory; using one deliberately prevents tests from silently running an older executable from a different tree.

Exact Visual Studio equivalent

The relevant launch settings observed in .vs\launch.vs.json are:

program: D:\src\hatari\out\build\mingw-debug\src\hatari.exe
cwd:     D:\src\hatari
PATH:    C:\msys64\ucrt64\bin;C:\msys64\usr\bin;<parent PATH>
args:    --monitor rgb --wincon --memstate <snapshot>
         --borders 0 --frameskips 0 --xenon-control-port 6802

.vs is local Visual Studio state and is not a suitable automation dependency. The checked-in script captures the same contract explicitly.