Xenon 2

How it works · write-up

Desktop FFmpeg recording

xenondoc/EXTERNAL_FFMPEG_RECORDING.MD · 8 KB · updated 2026-10-06

The desktop SDL3 build can encode both authentic and Sprite Stream captures directly to H.264/AAC MP4. PNG AVI remains the default. FFmpeg is an external executable, found on PATH or selected explicitly; Hatari does not link FFmpeg libraries. The browser and SDL2 builds do not launch an encoder.

Commands

Use the official wrapper for builds. This command works in cmd.exe, launches a visible emulator, records both MP4s and events, and retains periodic checkpoints:

python xenon_tools\validate_campaign.py native-run mp4-demo --build --build-type Release --resume assets\xenonplay.sav --record-format mp4 --record-encoder qsv --record-quality 18 --sprite-scale 4 --hd-sprites --high-fps --fast-forward --frames 600 --checkpoint-interval 150

Use --record-encoder x264 for the software encoder. nvenc selects Nvidia's encoder when the installed FFmpeg and driver support it. Quality is 0–51, with lower values preserving more detail. The controls are x264 CRF, QSV global quality and NVENC CQ; equal numbers do not imply equal output quality across encoders. Use --ffmpeg-path "C:\encoder tools\ffmpeg.exe" to select an executable explicitly. Remove --fast-forward for normal-speed play. --frames counts game updates; videos contain every accepted VBL capture and play at normal emulated speed.

Equivalent startup options in the official PowerShell wrapper are -RecordFormat mp4 -RecordEncoder qsv -RecordQuality 18 -FfmpegPath ffmpeg. They configure the recorder without starting it. The interactive Xenon recording button then creates .x2events, -original.mp4 and -sprite.mp4 together. The direct Hatari options are --record-format, --record-encoder, --record-quality and --ffmpeg-path. Existing --avirecord, --avi-file, --sprite-avirecord and --sprite-avi-file switches still start/select the captures.

These encoder settings are startup settings: launch a new instance through native-run rather than passing them with --connect. The existing TCP avi-start/avi-stop commands remain usable on an instance launched in MP4 mode, with .mp4 filenames. No per-frame Python controller is involved.

Capture and lifecycle

  • src/record_ffmpeg.c owns the child process and a minimal streaming Matroska writer. Raw BGR24 video and PCM16 audio share one stdin pipe. There is no temporary video file, PNG compression or separate feeder queue. Output is MP4; Matroska is only the transport into FFmpeg.
  • The existing bounded capture queues provide backpressure. Slow encoding slows emulation instead of dropping accepted captures. Conversion buffers are allocated once per recording. Sprite recording still uses the existing asynchronous GPU readback and VBL/audio pairing.
  • Video timestamps use the emulated rational frame rate; audio timestamps use sample counts. Host time and fast-forward speed do not affect playback timestamps. The ST produces mono AAC; later stereo machines retain stereo.
  • Normal stop drains GPU readbacks and queued packets, closes encoder stdin, waits for FFmpeg to flush codecs and finalize the MP4, and checks its exit code. Repeated start/stop and starting again after encoder failure are supported.
  • A pipe watchdog terminates only this recorder's encoder after 30 seconds with no write progress. Finalization also has a 30-second timeout. Encoder failure is reported through the recording control API and makes campaign validation fail.
  • Each output has .vbl and .ffmpeg.log sidecars. The video frame index maps to the recorded VBL just as with AVI. Lazy GPU startup can make the sprite file begin a few VBLs later than the authentic file; use the sidecars for comparisons.
  • The campaign manifest includes format, encoder, quality, executable and all command-line arguments. original_video/sprite_video are container-independent; older original_avi/sprite_avi keys remain aliases for existing tooling. Event recording and authoritative web replay decoding are unchanged.

MP4 needs graceful finalization. Closing the emulator normally, stopping through the control API, or the validation script's stop request finalizes it. Killing Hatari or FFmpeg can leave an incomplete MP4. The finalized file uses fast-start metadata and can be played or uploaded without a separate conversion step.

The streaming header follows the Matroska element specification. Process launch uses SDL's argv process API, so neither paths nor options are interpreted by a shell.

Validation, 2026-10-05

Tests used the official Release build at out/build/codex-validate with visible resident-C gameplay and paired captures. Recordings and audit JSON are under work/ffmpeg-validation (ignored scratch artifacts).

Recording Result
ffmpeg-qsv-20261005-r575.validation 2,409 frames per stream, 1280×800 sprite and 640×400 authentic, exact 491827/8192 FPS, mono AAC, identical decoded audio. Three periodic checkpoints. 40.125 seconds of video encoded during 11.2 seconds of fast-forward gameplay; sprite MP4 13.8 MB.
ffmpeg-x264-20261005-r576.validation Normal-speed HD recording; both streams decode, every indexed frame retained, image orientation verified. Audio/video durations differ by less than 0.02 ms. Sprite startup is three VBLs later than authentic capture.
restart-0, restart-1 Restarted recording twice in the same paused/resumed instance; all indexed frames decode, real mono audio, repeated stop succeeds.
killed-*, recovered-* Deliberately terminated one encoder belonging to the test instance. Stop reported failure in 0.15 seconds, and recording again in the same instance succeeded.
avi-regression-20261005-r577.validation Default PNG AVI remains valid: 416 indexed video/audio chunks per stream, mono PCM, every paired audio chunk matches and both RIFF/OpenDML audits pass.
ffmpeg-missing-20261005-r578.validation Nonexistent executable is rejected without hanging; campaign result is failed rather than recording_finalized.
ffmpeg-final-20261005-r579.validation Final build, FFmpeg executable and output paths containing spaces, QSV HD fast-forward: 677 frames per stream, identical decoded mono audio, duration difference 0.014 ms, clean finalization. This folder is under work/ffmpeg validation spaces.

The first longer test exposed a pre-existing audio loss: Sound_Update_VBL reset the playback ring before recording the generated samples. Every reset (including checkpoint pause/resume) discarded a VBL of PCM. Recording now happens before that reset. This fixes both AVI and MP4 without changing game decisions. AAC can pad its tail by one codec block; the auditor distinguishes this from growing audio loss. Across 601 common game frames before and after the audio fix, recorded input, scroll, shield, lives and score are identical.

This machine's Intel UHD 770 supports the tested QSV encoder. Its installed FFmpeg requires NVENC API 13.1 while the current RTX 4090 driver exposes 13.0; NVENC cannot run with that combination. x264 and QSV are available fallbacks. No driver changes were made.

Audit a completed pair with:

python xenon_tools\audit_mp4_recording.py path\run-sprite.mp4 --original path\run-original.mp4 --dimensions 1280 800 --output audit.json

The auditor checks decoded frame counts against VBL tags, gaps/duplicates, dimensions, real audio, H.264/AAC codecs, duration drift and complete decoding. It reports decoded audio hashes for comparison; hashes may differ when the two captures start at different VBLs. Unit tests cover missing frames, index gaps, discarded audio and acceptable AAC padding.

Focused regression tests (32 passed):

cd xenon_tools
python -m unittest test_campaign test_hatari_avi test_sprite_avi_audit test_mp4_recording_audit

Campaign recordings must include the final shop dialogue and return to playable Level 1, rather than stopping when the ending-message byte first appears. The native validator now uses that endpoint. The focused 44-second MP4 continuation, its media audits and correction to the earlier full-run claims are documented in FINAL_SHOP_VALIDATION_20261005.MD.