491. A page under X11 keeps its window on the GPU
Date: 2026-09-30
Status
Accepted. Amends ADR-0479’s rule that a window with an embedded page stays on the CPU. Relates to ADR-0442, ADR-0445 and ADR-0046.
Context
Opening the showcase’s web tab on Linux ended the process:
Sdl3Window - "Goldberry — showcase on Linux / amd64" presents on the CPU from now on: a page is embedded in it
WebView - web-view: a page is open inside the window over 2496.0x1107.0 logical at (32.0, 232.0) (scale 1.0)
(java:9977): Gdk-WARNING **: The program 'java' received an X Window System error.
The error was 'BadDrawable (invalid Pixmap or Window parameter)'.
(Details: serial 252 error_code 9 request_code 14 (core protocol) minor_code 0)
Request 14 is GetGeometry. GDK’s X error handler exits, status 1. It
reproduced on every run.
What happened. The window was presenting through the GPU (ADR-0480). The
page was reparented into it, then Sdl3Backend moved the window to the CPU,
as ADR-0479 said a window with a page must. The next frame made the window’s
first surface. On X11 that surface is SDL’s texture framebuffer, and SDL
builds it with its OpenGL renderer. The Vulkan claim had taken the
window’s SDL_WINDOW_OPENGL flag away, so GL_CreateRenderer called
SDL_ReconfigureWindow. X11 has no ReconfigureWindow hook, so SDL fell back
to SDL_RecreateWindow: it destroyed the X window and created another with a
new id. X destroys a window’s children with it, and the page was one. GTK’s
next request about its own window was BadDrawable.
WindowIdentityTest shows it without a page: an X11 window claimed, released
and then given a surface changes id (29360179 → 29360196 here).
Why the window was on the CPU at all. ADR-0479 left it unmeasured whether the page or the swapchain shows on top. On X11 that is settled by the protocol: a child window is stacked above its parent’s contents, and the server clips the parent’s presents against it, the Vulkan swapchain’s included.
Keeping the GPU exposed two more defects that the CPU path had hidden:
- The page never left its parking place.
web-viewopens its page parked off the side of the window (ADR-0445). Its canvas painter polls the load state and brings the page over its box, and it asks for the next poll withHost.repaint(). A GPU window paints into a buffer it keeps, so its frames repaint in part, and a frame calls a painter only where it is damaged.repaintdamages nothing, so after the page opened the painter never ran again. On the CPU the surface was new after leaving the GPU, and a new buffer is a whole repaint, which started the polling. - Stale pixels where a page had been. Wherever the page covers the
parent, the parent’s presents are clipped, so the parent’s pixels there
keep what was last shown before the page arrived: the loading spinner.
When the page moves away (parked for a modal, scrolled), X exposes the
region and the parent must present again. SDL reports that as
WINDOW_EXPOSED, and it becamewindow.repaint(): no damage, so the composited present returned early and showed nothing new.
Decision
Under X11 a window keeps the GPU with a page in it. PageStacking
decides by the window system of the page’s parent handle. X11 is
AboveTheSwapchain. Cocoa and Win32 are NeedsTheCpu, with the reason
logged, until someone measures them: a Metal view added after the page
would cover it, and WebView2 over a flip-model swapchain is untested.
Sdl3Backend.createEmbeddedWebView calls stayOnTheCpu only for
NeedsTheCpu. wantsComposited no longer excludes windows with pages, since
stayOnTheCpu already keeps those on the CPU for good.
On X11 a window surface is the X server’s framebuffer.
Sdl3Backend.keepWindowsAcrossTheGpu sets SDL_FRAMEBUFFER_ACCELERATION=0
after SDL_Init, under the x11 driver and a policy that claims windows
(always, auto: Composition.claimsWindows). The native framebuffer touches
no graphics flag, so a window given back from the GPU keeps its id. This still
matters for pages under X11: auto gives a window back when its last layer
goes, and always gives one back when a present fails. An
SDL_FRAMEBUFFER_ACCELERATION already in the environment wins, and the log
warns.
A Vulkan renderer for the framebuffer (vulkan,opengl) was tried first. It
changes no flags either, but claiming a window after it segfaulted inside
libnvidia-glcore (driver 610.57.04) in SDL_ClaimWindowForGPUDevice.
web-view polls with a rebuild. WebViewState.pollAgain schedules a
zero-delay setState. A rebuild mints a new painter, and damage compares
painters by identity, so the box is damaged and the painter runs however the
window presents. It is used after opening and on every frame while the page
loads. It stops once the page has shown, so an idle application stays idle.
An expose re-presents a composited window. CompositedWindow.exposed()
marks the window stale. SdlCompositedWindow.present then draws its kept UI
texture to the swapchain even with no damage, uploading nothing, and clears
the mark once a swapchain texture was actually acquired. Sdl3Backend calls
it on WINDOW_EXPOSED, before the event’s repaint.
Consequences
- Checked on linux-x64 (XWayland under GNOME, NVIDIA 610.57.04). The web tab
opens with the window on the GPU and no X error. The page draws above the
swapchain, the spinner goes up and comes down, and the page is placed over
its box. Closing the window exits 0. With
goldberry.gpu.composite=autothe window presents on the CPU through the X framebuffer, page on top. - The CPU path on X11 loses SDL’s renderer, which waited for vertical blank.
The frame loop’s own pacer paces it. On the showcase’s animated home
screen under
autoover 600 frames: 19 and 16 late with the X framebuffer, against 22 and 29 with SDL’s renderer. Mean paint went from 8.4 ms to about 9.6 ms. - Popups share the process-wide hint. The X framebuffer’s formats for 24- and
32-bit visuals are
XRGB8888andARGB8888, both ones Goldberry paints into. A transparent popup on this path was not looked at on screen. - Tests:
PageStackingTest,CompositionTest.claimsWindows,Sdl3BackendTest.SurfaceKeepsTheWindow,WebViewPollingTest(two of its three fail withhost.repaint()put back),CompositorTest.exposed, andWindowIdentityTest(fails without the hint; it needs-Pgoldberry.gpu.videoDriver=x11where SDL would choose Wayland). - Left as found: in
:gpu:gpuTest,GpuLayerBackendTestsegfaults inVULKAN_DestroyDevicewhen it closes its device, which ends the run. With it disabled,Canvas3dGoldenTest.cubefails its golden (52% of pixels, delta 1), and under WaylandCompositorTest.givesTheWindowBackandoneDevicefail to re-claim their window. All four fail the same way without this change. An interactive showcase run that closed the window after visiting the media tabs exited with SIGABRT, which may be the first of them. It was not reproduced. - macOS and Windows keep the CPU rule. Measuring them is the way to lift it:
PageStacking.ofis the one line to change.