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>
The flag added for macOS is honored on Linux too, so a test suite or a CI machine can drive `update`/`draw` and read the framebuffer back with no window, no focus moved, and — the part X11 and Wayland cannot give — no display server running at all. EGL can hand out a context without a window system: `EGL_DEFAULT_DISPLAY` plus a pbuffer surface. `native::linux_headless` does that and nothing else. It runs before `Platform::linux_backend` is consulted, because on a machine with neither an X11 nor a Wayland socket both of those backends fail at connect time, and it keeps the shape of the macOS path: - an RGBA8 + depth24/stencil8 framebuffer object of `window_width` x `window_height` is bound before the event handler is built, so `GlContext::new` adopts it as the default framebuffer and default passes, `screen_size()`, render textures and readback all work unchanged. The pbuffer exists only because `eglMakeCurrent` needs a surface; nothing is drawn to it and it is never swapped; - `update`/`draw` run at ~60 Hz with no event queue, `set_window_size` re-specifies the renderbuffers in place, and the run quits when the handler orders it (or requests it and does not cancel). The dpi scale is 1 and `sample_count` is ignored, as on macOS. The windowed X11 and Wayland backends are untouched; `egl.rs` gains only the pbuffer constants and the EGL null handles. Needs a software EGL to run against where there is no GPU: Mesa's `libegl-mesa0` and `libgl1-mesa-dri` are enough, and no X11 or Wayland package is required.
`eglInitialize` failed on a CI executor with no display server, so every
headless run died before drawing:
headless EGL backend failed: InitializeFailed
`EGL_DEFAULT_DISPLAY` is the wrong question to ask on a stock Linux image.
The driver reads it as "the X11 display", hands back a non-null `EGLDisplay`,
and initialisation then fails because no X server is listening — which looks
like a broken driver rather than the absent window system it is.
`EGL_MESA_platform_surfaceless` names a platform that is not a window system
at all, which is exactly the situation. `open_display` asks for that first,
through `eglGetPlatformDisplayEXT`, and falls back to `EGL_DEFAULT_DISPLAY`
for a driver that does not offer the extension. The chosen route is printed,
so a log says which platform answered rather than leaving a future Mesa
change to be guessed at.
`eglGetPlatformDisplayEXT` is resolved through `eglGetProcAddress` rather
than declared in `LibEgl`, because `declare_module!` unwraps every symbol it
names: declaring an extension there would make a driver without it fail to
load libEGL at all, instead of falling back.
Also stop requiring 24-bit depth and 8-bit stencil of the pbuffer config.
Nothing is ever drawn to the pbuffer — the depth and stencil that get used
are the offscreen framebuffer's own renderbuffer — so those constraints only
narrowed the candidate set, and a driver offering no pbuffer config with 24/8
would have failed for a buffer this run does not touch.
A headless run is normally a child process whose stdout and stderr some test is reading as the run's result. An unconditional line naming the EGL platform is indistinguishable there from what the run was meant to print, and it displaced the real assertion output of a failing scripted test. Gate it on MINIQUAD_HEADLESS_VERBOSE.
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.
Problem
startcreates a window unconditionally on every backend. A test suite or a CImachine that wants to drive
update/drawand read the framebuffer back hasno way to ask for anything else — so tests that draw take the screen and the
focus on a developer's machine, and on a CI executor with no display attached
they fail for the environment rather than for the code.
Working around it outside miniquad means a virtual X server (Xvfb plus a
software Mesa), which is heavier, only helps on Linux, and exercises the
windowed path rather than the offscreen one such a run actually wants.
Solution
Conf::headless(defaultfalse, so nothing changes unless it is asked for).When set, the backend creates no window and draws into an offscreen framebuffer
of
window_widthxwindow_heightat a dpi scale of 1.updateanddrawrun at roughly 60 Hz, no input or window event ever arrives, and
window::screen_size(), render passes and framebuffer readback all behave asusual — the framebuffer object is bound before the event handler is built, so
GlContext::newadopts it as the default framebuffer and nothing aboveminiquad has to know.
Two commits, one per platform, sharing that shape:
macOS, with
AppleGfxApi::OpenGl: anNSOpenGLContextwith no drawableis made current (Apple discards drawing to its framebuffer 0, but framebuffer
objects work). No
NSApplication,NSWindowor view is created, so nothingappears in the Dock and no focus moves.
Linux: an EGL pbuffer context in a new
native::linux_headless. Thisneeds no display server at all — neither
DISPLAYnorWAYLAND_DISPLAYhas to be set, and no X11 or Wayland package has to beinstalled. It is dispatched before
Platform::linux_backendis consulted,because on such a machine both the X11 and the Wayland backends fail at
connect time.
The display comes from
EGL_MESA_platform_surfacelessviaeglGetPlatformDisplayEXT, falling back toEGL_DEFAULT_DISPLAY. Thatorder matters and was found the hard way:
EGL_DEFAULT_DISPLAYon a stockLinux image resolves to the X11 display, returns a non-null
EGLDisplay,and then
eglInitializefails because no X server is listening — whichreads like a broken driver rather than the absent window system it is.
eglGetPlatformDisplayEXTis resolved througheglGetProcAddressratherthan declared in
LibEgl, sincedeclare_module!unwraps every symbol itnames and a driver without the extension would then fail to load libEGL at
all instead of falling back.
It does need an EGL implementation to render with; on a machine with no GPU,
Mesa's software stack (
libegl1,libegl-mesa0,libgles2,libgl1-mesa-dri) is enough.sample_countis ignored for a headless run and the dpi scale is 1. Otherbackends ignore the flag and open a window as before. The windowed X11 and
Wayland paths are untouched;
egl.rsgains only the pbuffer constants and theEGL null handles.
Testing
examples/headless.rsdraws the triangle example into the offscreenframebuffer, reads a pixel from the middle of the triangle and one from a
corner, checks both and exits — so it is a real assertion about what was
rendered, not just that the process started.
cimg/rust, no GPU, no display server)against Mesa 23.2.1's software EGL, running a downstream suite of scripted
UI runs, layout dumps and screenshot comparisons against blessed snapshots.
The executor log confirms the platform that answered:
miniquad: headless EGL on EGL_PLATFORM_SURFACELESS_MESA. That line isbehind
MINIQUAD_HEADLESS_VERBOSE, because a headless run's output isusually some test's captured result and an unconditional line there
displaces what the run was meant to print.
Opened as a draft: happy to split the two commits, rename anything, or adjust
where the Linux dispatch sits if you would rather it lived inside
linux_x11/linux_waylandthan beside them.