How it works · write-up
Reliable Windows Hatari build and launch
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-debugby default, ormingw-releasewith-BuildType Release, then builds only targethatari; - launches the selected output's
src\hatari.exewith the repository root as its working directory; - supplies snapshots as absolute
--memstatepaths; - constructs one normalized child
PATH, beginning with the two MSYS2 directories, so duplicatePath/PATHentries 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 totools/no-cache-http-server.py.site/serve.py(used by.claude/launch.jsonand for previewingsite/dist) takes the same--snapshot-diroption 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.