8000 macos: add Conf::headless to run without a window by 16dprice · Pull Request #666 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

macos: add Conf::headless to run without a window - #666

Open
16dprice wants to merge 1 commit into
not-fl3:masterfrom
16dprice:headless
Open

16dprice wants to merge 1 commit into
not-fl3:masterfrom
16dprice:headless

Conversation

@16dprice
@16dprice 16dprice commented Sep 13, 2026 •
Copy link
Copy Markdown

Summary

Adds Conf::headless (default false). With it set, on macOS with the OpenGL backend, miniquad::start creates no window, no view and no NSApplication. Nothing shows up on screen or in the Dock, focus stays where it was, and update/draw run at ~60 Hz into an offscreen framebuffer. window::screen_size(), default passes, render textures and pixel readback all work as before.

The motivation is testing. I have a macroquad app whose cargo test spawns a few dozen copies of the binary to dump layout and take screenshots, and every one of them opened a window and grabbed focus. There was no way to ask miniquad not to, since each backend's run creates a window unconditionally. This also makes it possible to run on a Mac with no display attached.

How it works

The macOS backend already does two things that make this small:

  • run is a hand-rolled loop rather than [NSApp run], so it can be driven without AppKit.
  • GlContext::new reads GL_FRAMEBUFFER_BINDING and treats whatever is bound as the default framebuffer.

So the headless path creates an NSOpenGLContext from the same pixel format as the windowed path (now factored into opengl_pixel_format), makes it current with no drawable, and binds an RGBA8 + depth24/stencil8 FBO of window_width x window_height before the event handler is built. Drawing to framebuffer 0 on a drawable-less context is discarded by Apple, but FBOs work fine, and since the backend adopts the FBO as its default, nothing above miniquad has to know.

The loop itself is a trimmed copy of the existing one: drain native_requests, handle the quit handshake (quit_requested_event, then quit unless cancelled), call perform_redraw, sleep out the rest of a 1/60 s frame. SetWindowSize re-specifies the renderbuffers' storage in place so the FBO id the backend holds stays valid. perform_redraw uses glFlush instead of flushBuffer and skips performClose: in this mode.

native/gl.rs was missing GL_DEPTH24_STENCIL8 and GL_DEPTH_STENCIL_ATTACHMENT, so those are added.

examples/headless.rs is the triangle example with headless: true. It draws, reads back a centre pixel and a corner pixel, asserts on both, prints ok and quits. From a terminal it exits 0 in well under a second with nothing on screen.

Limitations

  • Only macOS + AppleGfxApi::OpenGl honours the flag right now. Other backends ignore it and open a window as usual; the field exists on every platform so callers don't need a cfg. Metal panics with a clear message rather than silently opening a window. The same FBO approach should port to Linux (EGL surfaceless) and Windows (hidden window), but I haven't written those.
  • A headless run has a dpi scale of 1, and sample_count is ignored, since a multisampled default framebuffer would break glCopyTexImage2D-style readback.
  • No CVDisplayLink is created, which is what lets a display-less Mac run this.
  • Adding a field to Conf is a breaking change for anyone building it without ..Default::default(). icon and platform were both added the same way within 0.4.x, and the in-tree examples all use ..Default::default().

Test plan

  • cargo run --example headless on macOS 26 / Apple Silicon prints 320x240 framebuffer: middle [62, 64, 129, 255], corner [0, 0, 0, 255] and ok, with nothing on screen.
  • cargo check --all-targets for aarch64-apple-darwin, wasm32-unknown-unknown and aarch64-apple-ios.
  • Downstream, a macroquad + egui-macroquad app with ~90 integration tests that spawn the binary: every layout dump is byte-identical to the windowed run, screenshots read back correctly, and lsappinfo shows no app registered with the window server while a test is running.

🤖 Generated with Claude Code

A test suite or a CI machine wants to drive `update`/`draw` and read the
framebuffer back without a window appearing, stealing focus, or needing a
display at all. There was no way to ask for that: `start` on every backend
creates a window unconditionally.

`Conf::headless` (default `false`) asks for one. On macOS with the OpenGL
backend it now:

- creates no `NSApplication`, `NSWindow` or view, so nothing shows in the
  Dock and no focus moves;
- makes an `NSOpenGLContext` with no drawable current — Apple discards
  drawing to its framebuffer 0, but framebuffer objects work — and binds
  an RGBA8 + depth24/stencil8 framebuffer object of `window_width` x
  `window_height` before the event handler is built, so `GlContext::new`
  adopts it as the default framebuffer. Default passes, `screen_size()`,
  render textures and readback all work unchanged above miniquad;
- runs `update`/`draw` at ~60 Hz with no event queue, honours
  `set_window_size` by re-specifying the renderbuffers in place, and quits
  when the handler orders it (or requests it and does not cancel).

The dpi scale of a headless run is 1 and `sample_count` is ignored. Other
backends ignore the flag and open a window as before.

`examples/headless.rs` draws the triangle example into the offscreen
framebuffer, reads two pixels back, checks them and exits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@16dprice 16dprice changed the title macos: Conf::headless, a run with no window macos: add Conf::headless to run without a window Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0