Capture
A capture is a file the machine made. Emu198x hands you the frame its video path painted, the samples its sound chip emitted, or both muxed together — written to disk by the same headless run that produced them, with no window, no screen grabber and nobody watching.
The still image is the everyday case, and it is the one this site runs on. Every boot picture on the system matrix came out of a capture: thirty-four of them from a --screenshot flag, and the thirteen Spectrum models — the one family whose binary has no such flag — from a save_screenshot step. The ZX80 and ZX81 take the flag, so the split is not a Sinclair one. Both routes are below, and the example further down writes the NES one byte for byte.
The mental model
- A capture reads the last frame the machine emitted. Nothing is re-rendered for the file. Run the machine first: asking for a screenshot before a single frame has been painted fails with
no frame has been captured, which is the machine telling you the truth about how far it got. - You get the whole frame, border and all. The PNG is the machine's own framebuffer at its own geometry — 352 × 296 on a Spectrum, 416 × 312 on a C64. There is no cropping to the "screen area" and no aspect correction, so square pixels on disk are what the hardware clocked out.
- Two routes, one vocabulary. Most machines take a
--screenshot PATHflag for the one-shot case. Every machine takessave_screenshotas a step in a script or a tool call over MCP. The steps are the portable half. - The run is the receipt. A capture that came from a committed script can be reproduced by anyone holding the same firmware. That is the difference between a picture and evidence.
What may be published
Worth knowing before the captures pile up, because it shapes which of them you want. Software this project did not write is illustrated with stills — one to three frames, each attached to a claim the surrounding writing is making, never a gallery. Where a still cannot carry the point, because the point is a raster split or a scroll or sprite flicker, the published form is a silent animated WebP at the machine's own field rate. MP4 files and audio captures stay local: a muxed container carries a sound track, and a captured tune is a whole composition by its own author rather than an extract of anything. Capturing any of it to check your work is a separate question; this is about what goes on a public page.
What works today
All 30 machines carry the capture steps, because they live in the shared session every binary is built on. What differs is the command-line shorthand around them.
| Need | Steps | Flag |
|---|---|---|
| Still image | save_screenshot | --screenshot PATH, on every machine but the ZX Spectrum, ZX80 and ZX81 |
| Sound | start_audio_recording, stop_audio_recording, save_audio_capture, clear_audio_capture | --audio-capture PATH, on every machine but the Spectrum and the C64 |
| Moving picture | start_video_recording, stop_video_recording | None |
| Frame sequence | save_screenshot, once per frame | None |
| Did anything arrive? | query on capture.has_frame or capture.has_audio | None |
One format per medium, and the file name does not choose it. A still is a PNG: 8-bit RGBA, lossless, no palette indexing. Audio is a 16-bit PCM WAV at the machine's own rate — 48 kHz on a C64 or a Master System, 44.1 kHz on a Spectrum — with the channel count the machine emits. Video is an H.264 MP4, and the audio track is muxed in when the run captured any.
The video encode is deliberate about dither, and it took two goes to get there. Retro framebuffers carry checkerboards at single-pixel frequency, and libx264's defaults — CRF rate control over 4:2:0 chroma — average that into flat colour. The encoder now runs a constant quantiser at QP 12 over full 4:4:4 chroma, which holds the pattern and holds it equally on the first frame as on the hundredth. The earlier CRF setting drifted over the opening frames, which on a short loop reads as a flash at every restart.
What a frame comes out as
| Machine | PNG size |
|---|---|
| ZX Spectrum | 352 × 296 |
| Commodore 64 | 416 × 312 |
| Amstrad CPC | 832 × 288 |
| Sega Master System | 280 × 240 |
| NES | 256 × 240 |
| Game Boy | 160 × 144 |
Those are the emitted frames, not the advertised display modes. A Spectrum is a 256 × 192 screen inside a border the hardware also draws, and the capture keeps both. The CPC number is wide because its pixel clock is, not because anything was scaled.
Capturing a still
The shortest run that needs no firmware and no software you have to find. From a source checkout, this boots a NES on a synthetic cartridge built in this repository, runs 300 frames and writes the frame it stopped on:
cargo run --release -p emu198x-nes -- \
--rom test-data/synthetic-cartridges/nintendo-nes-logo.nes \
--frames 300 \
--screenshot boot.pngNES runtime: time=53426560 cartridge_loaded=trueboot.png is a 256 × 240 picture of the Emu198x wordmark drawn in the machine's own tiles. It is the same file the NES page shows, down to the checksum, which is the useful property: the picture and the command that made it travel together. A release binary takes the same flags.
One machine has no --screenshot flag — the Spectrum — and the step covers it. Put this in shot.json:
[
{"action":"wait_for_boot","max_frames":300},
{"action":"save_screenshot","path":"boot.png"},
{"action":"query","path":"capture.has_frame"}
]./emu198x-spectrum --script shot.jsonThis one needs the machine's firmware, since a Spectrum paints nothing without it. The report comes back on standard output:
{"kind":"wait_for_boot","frames":87,"reached":24321024,"reason":"found copyright banner on row 23","row":23}
{"kind":"query","result":{"path":"capture.has_frame","value":true}}Two steps ran and three are in the file, because save_screenshot reports nothing at all — it either writes the file or ends the run with an error. That is why the capture.has_frame query is worth the third line: it is the machine confirming there was a frame to write, in the report you keep.
A frame sequence
Moving pictures for publication start as numbered PNGs, and stay that way on disk. There is no frame-dump flag, so a sequence is one save_screenshot per frame with the machine advanced between them. Sixty frames of settling, then three consecutive fields:
[
{"action":"run_frames","frames":60},
{"action":"save_screenshot","path":"frames/0001.png"},
{"action":"run_frames","frames":1},
{"action":"save_screenshot","path":"frames/0002.png"},
{"action":"run_frames","frames":1},
{"action":"save_screenshot","path":"frames/0003.png"}
]Save that as frames.json and point a machine at a moving cartridge. The directory has to exist first — a capture writes a file and will not build a path to put it in:
mkdir frames
cargo run --release -p emu198x-sega-master-system -- \
--cart test-data/sega/synthetic-cart/master-system-raster.sms \
--script frames.jsonKeep those frames. They are the honest artefact — someone can look at what was captured instead of trusting an encoder — and the animation is built from them as a separate step, with a tool of your own. Emu198x ships no WebP encoder; the reference one from the libwebp project reads a sequence directly:
img2webp -loop 0 -d 17 -lossless frames/*.png -o clip.webp-d is the per-frame duration in whole milliseconds, and it is the number to get right. A 60Hz machine wants 17, a 50Hz machine 20. Do not halve the rate to save bytes: a scroll survives it, but flicker does not, and where the effect is the flicker — sprite multiplexing, interlace, two-frame colour mixing — a halved rate shows a reader something the hardware never did. These are small pictures from machines with a handful of colours on screen, so the saving would be tiny anyway.
Sound and video
Audio comes out as WAV, either from a flag over a whole run or from a bracketed pair of steps that captures the window between them. The flag form:
cargo run --release -p emu198x-sega-master-system -- \
--cart test-data/sega/synthetic-cart/master-system.sms \
--frames 150 \
--audio-capture tone.wavThat writes a 16-bit mono WAV at 48 kHz, and it writes silence: the synthetic cartridges paint a frame and never touch the sound chip. The capture path is what the run proves. To hear a machine you need software that plays something, which is yours to supply.
Video is one MP4 per recording, bracketed the same way. The machine has to have painted a frame before the recorder starts, because the frame is where it gets its width and height:
[
{"action":"run_frames","frames":1},
{"action":"start_video_recording","path":"clip.mp4"},
{"action":"run_frames","frames":100},
{"action":"stop_video_recording"}
]{"duration_ms":1666,"frames":100,"has_audio":true,"kind":"stop_video_recording","path":"clip.mp4"}Only the stop step reports, and it reports what landed: 100 frames, 1.666 seconds of machine time, and a muxed sound track. Every frame the machine emits between the two steps is teed into the recorder, so the clip's frame rate is the machine's field rate and not a setting.
No flag turns the sound track off, but a clear_audio_capture step immediately before the stop empties the buffer the muxer reads, and the recording finishes with "has_audio":false and one video stream. That is the whole mechanism, and it is worth knowing: a container with a sound track in it is the reason a clip stays local.
What you need that we cannot give you
Firmware and software, the same as any other way of running these machines. Emu198x ships no ROMs, no BIOS images, and no disks, tapes or cartridges; the downloads page sets out the position on that. The synthetic cartridges used above are the exception, and a narrow one: a few machines declare no firmware — the NES, Game Boy, Master System, Game Gear, SG-1000 and Atari 2600 among them — so a cartridge built in this repository is enough to reach a frame. Every other machine needs its manufacturer's ROM before there is anything to photograph.
ffmpeg, for video. The recorder streams raw frames to an ffmpeg subprocess and shells out a second time to mux audio, so a machine without it on PATH refuses the recording rather than writing a broken file. Stills and WAVs need nothing beyond the binary.
An encoder, for animation. Turning a PNG sequence into an animated WebP happens outside Emu198x, with whatever tool you prefer.
What does not work yet
- One still format, and the extension is ignored. Every screenshot is a PNG. Naming the output
shot.webpwrites a PNG calledshot.webp, which a viewer opens and a build step may not. There is no BMP, JPEG or WebP writer to ask for. - No scaling, cropping or aspect correction. Nothing offers an integer scale, a border mode, a 4:3 stretch or a palette choice. What the machine emitted is what lands, and any of that is a later step in an image tool. The Dragon is the single exception with screenshot options of its own, and they are that machine's, not a pattern.
- No frame dump. Capturing 300 frames means 600 steps in the file, generated rather than typed. A flag that writes every emitted frame to a directory would be the obvious fix and does not exist.
- The window cannot capture anything. There is no screenshot key and no record button; the controls a binary lists under
--helpare the whole set. Capture is a headless and MCP feature, so a session you drove by hand has to be re-run as a script to produce a file. - Video is MP4 or nothing. No WebM, no GIF, no animated WebP, no frame rate or bitrate control, and no switch for the sound track — dropping it means clearing the audio buffer at the right moment and reading the report to check it worked. A container built to carry sound is the only moving format the tool writes, which is the wrong shape for anything headed for a public page.
- A recording pins the session. Loading a snapshot while one is in flight is refused —
operation `restore_snapshot` is not allowed while video recording is active— as is starting a second recording, because either would jump-cut the clip. Save state before you start. - Audio has no dials. No format choice beyond WAV, no sample rate, no channel count, no mixdown. The stream arrives as the machine produced it.
- Captures do not create directories. A path whose parent is missing ends the run with
No such file or directory (os error 2)and no clue which step asked for it. - The capture flags are per-machine.
--headless --helpis the list for the machine in front of you, and reading one machine's teaches you that machine. The ZX80 and ZX81 do not even take--headlesson its own, though both run a script and both serve MCP. The steps are the part that travels.