video: drive capture runs without a window or event loop#268
Merged
Conversation
--screenshot-after and --dump-frames runs used to route through the windowed App with an invisible window, so winit's event-loop setup still registered with the host display server. On hosts without one (SSH sessions, sandboxes that deny window-server access) NSApplication aborts in _RegisterApplication on macOS 15 and blocks forever on macOS 26, so a nominally headless run could never complete. Give App a run_headless() loop that never touches winit: the scheduled capture and input flags are armed and fired by helpers shared verbatim with the windowed about_to_wait path (arm_scheduled_events, fire_scheduled_events, fire_auto_save_state, fire_auto_shot, and an event-loop-free dump_frame_if_due), so a capture run produces byte-identical output either way. main() routes to it whenever a capture flag is present and --control-gui is not. One behaviour change: a machine that halts before the scheduled captures complete now fails the run with an error instead of idling forever, which is what the invisible-window path did. Fixes #267
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes headless capture runs (--screenshot-after, --dump-frames) so they can complete without creating a winit window/event loop (and therefore without registering with the host display server), addressing macOS crashes/hangs in sandboxed/SSH environments (Fixes #267).
Changes:
- Added
App::run_headless()to drive scheduled capture runs entirely without winit/window/display-server access. - Refactored scheduled-event arming/firing into shared helpers so windowed and windowless capture paths execute the same logic.
- Routed CLI capture runs to the new windowless loop (unless
--control-guiis used), and updated headless docs + added tests covering the new path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/video/window.rs | Introduces run_headless() and extracts shared scheduled-event/capture logic to support windowless capture runs. |
| src/video/window/tests.rs | Adds tests validating windowless screenshot, frame dump, scheduled input + recording flush, and no-capture error behavior. |
| src/main.rs | Routes capture flags to run_headless() when --control-gui isn’t requested. |
| docs/guide/headless.md | Updates headless documentation to note capture runs don’t require a display server. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Check the full 8-byte PNG signature with starts_with so a truncated screenshot fails the assertion instead of panicking on the slice, and stop the headless-guide intro claiming no run opens a window: only capture runs skip the display server, and the paragraph that says so now carries that detail alone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #267. The crash there was not AROS-related:
--screenshot-afterruns routed through the windowedAppwith an invisible window, so winit's event-loop setup still registered with the host display server before emulation began. On hosts without window-server access (the reporter launched Copperline from a sandboxed agent session; SSH sessions hit the same wall) NSApplication aborts inside_RegisterApplicationon macOS 15, and blocks forever on macOS 26 -- so a nominally headless run could never complete.What changed
App::run_headless()drives--screenshot-after/--dump-framesruns with no winit event loop, window, or display connection.resumed()intoarm_scheduled_events(), and the firing blocks moved out ofabout_to_wait()intofire_scheduled_events()/fire_auto_save_state()/fire_auto_shot();dump_frame_if_due()returns an exit flag instead of taking the event loop. Both loops call the same helpers, so captures are byte-identical by construction.main()routes to the windowless loop when a capture flag is present and--control-guiis not.--save-state-afteralone keeps the windowed session (a state save is a capture along the way, not a run-ender);--benchmark-until/--gdb/--controlalready had windowless paths.docs/guide/headless.mdnotes capture runs need no display server.Verification
--record-inputflushing, and the no-captures error; clippy and fmt clean; builds with and without default features.--save-state-after,--dump-frames, and--load-stateresume all verified through the new path at the CLI level.