Repository navigation
Conversation
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>
Conf::headless, a run with no window
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.<
2F1A
span class="js-resolved-thread">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
Adds
Conf::headless(defaultfalse). With it set, on macOS with the OpenGL backend,miniquad::startcreates no window, no view and noNSApplication. Nothing shows up on screen or in the Dock, focus stays where it was, andupdate/drawrun 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 testspawns 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'sruncreates 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:
runis a hand-rolled loop rather than[NSApp run], so it can be driven without AppKit.GlContext::newreadsGL_FRAMEBUFFER_BINDINGand treats whatever is bound as the default framebuffer.So the headless path creates an
NSOpenGLContextfrom the same pixel format as the windowed path (now factored intoopengl_pixel_format), makes it current with no drawable, and binds an RGBA8 + depth24/stencil8 FBO ofwindow_widthxwindow_heightbefore 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), callperform_redraw, sleep out the rest of a 1/60 s frame.SetWindowSizere-specifies the renderbuffers' storage in place so the FBO id the backend holds stays valid.perform_redrawusesglFlushinstead offlushBufferand skipsperformClose:in this mode.native/gl.rswas missingGL_DEPTH24_STENCIL8andGL_DEPTH_STENCIL_ATTACHMENT, so those are added.examples/headless.rsis the triangle example withheadless: true. It draws, reads back a centre pixel and a corner pixel, asserts on both, printsokand quits. From a terminal it exits 0 in well under a second with nothing on screen.Limitations
AppleGfxApi::OpenGlhonours 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 acfg. 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.sample_countis ignored, since a multisampled default framebuffer would breakglCopyTexImage2D-style readback.CVDisplayLinkis created, which is what lets a display-less Mac run this.Confis a breaking change for anyone building it without..Default::default().iconandplatformwere both added the same way within 0.4.x, and the in-tree examples all use..Default::default().Test plan
cargo run --example headlesson macOS 26 / Apple Silicon prints320x240 framebuffer: middle [62, 64, 129, 255], corner [0, 0, 0, 255]andok, with nothing on screen.cargo check --all-targetsforaarch64-apple-darwin,wasm32-unknown-unknownandaarch64-apple-ios.lsappinfoshows no app registered with the window server while a test is running.🤖 Generated with Claude Code