Support presenting to a native Wayland surface on Qt - #233
Conversation
<details><summary>Claude's draft</summary> On Wayland, rendercanvas put Qt on XWayland, and the code to present to a native Wayland surface was disabled with an `if False`. That code could not work: it paired the `wl_surface` from `winId()` with a display obtained from `get_alt_wayland_display()`, i.e. a fresh `wl_display_connect()`. A surface is a proxy owned by the connection that created it, so libwayland aborts with "Proxy and queue point to different wl_displays" as soon as a buffer is attached. Note that creating the surface succeeds, so this only shows when frames are actually presented. Qt can tell us what display it is connected to, and that display is the only one its surface is valid on. So: * Get the real `wl_display` from Qt (`QNativeInterface::QWaylandApplication` on Qt6, the platform native interface on Qt5). * Decide the platform from the qpa plugin that Qt actually uses, rather than from `sys.platform`: under a Wayland session Qt may still be on XWayland, in which case `winId()` is an X11 window. * Only put Qt on XWayland when the Qt lib cannot provide its display, so that those libs keep using the x11 code-path. Qt selects its platform when the application is instantiated, so this is decided at import-time. An explicit QT_QPA_PLATFORM is no longer overridden. Also release the wgpu surface when the canvas is closed, instead of leaving it to the garbage collector. On Wayland the surface is owned by the display- connection of the toolkit, so releasing it after the toolkit has torn that connection down is a use-after-free (during interpreter shutdown). Tested on Wayland with a headless weston and lavapipe: presenting to screen works, both for a top-level canvas and an embedded QRenderWidget. Only PySide6 was available to test with. The other backends are unchanged: they have no way to obtain their display, so they stay on XWayland. Resume this Claude session: ``` cd /home/mark/git/pygfx/rendercanvas claude --resume 0a03c1b5-9aa0-4d24-b370-c3799f1a1abe ``` </details>
ba7d4d1 to
61aad67
Compare
|
This is ready for review |
61aad67 to
a07ff83
Compare
<details><summary>Claude's draft</summary>
The qt backend was not tested on Linux at all: the entries in `test-backends`
all run on Windows, so the (already present) step to install xvfb never runs.
Now that Qt presents to a native Wayland surface, the Wayland and x11
code-paths need testing.
Adds a `test-qt-linux` job with three cases:
* wayland: a Wayland session (headless weston) with a Qt that can provide its
wl_display, so Qt runs on Wayland natively.
* wayland-old-qt: a Wayland session (headless weston with XWayland enabled) and
PySide6 6.9, which cannot provide its wl_display, so rendercanvas puts it on
XWayland. This exercises the real fallback target rather than a bare X server.
* x11: a system that only has x11.
Each case asserts what qpa platform Qt ends up on before running the tests,
because otherwise a case that silently falls back would still pass and test
nothing. Verified that the assertion does fail the step when the platform is
not the expected one.
Note that `QNativeInterface::QWaylandApplication` is only exposed by PySide6
since 6.10 (checked 6.5 up to 6.11), so the XWayland path is what most
installs use today, which is why it is worth a case of its own.
The jobs were checked by running the same steps in ubuntu/python docker
images. This showed that Qt's xcb plugin needs libxcb-cursor0, libxcb-icccm4,
libxcb-keysyms1 and libxkbcommon-x11-0, none of which the existing dependency
list installs. Its wayland plugin needs none of these, which is why only the
x11 cases were affected. Note that Qt's own hint ("xcb-cursor0 or
libxcb-cursor0 is needed") is printed whenever the xcb plugin fails to load,
also when the missing library is a different one.
Resume this Claude session:
```
cd /home/mark/git/pygfx/rendercanvas
claude --resume 0a03c1b5-9aa0-4d24-b370-c3799f1a1abe
```
</details>
a07ff83 to
b81ee29
Compare
|
Kind ping here |
|
I'll have a proper look this Friday, and see if we can merge this before or after #227 (which creates backwards incompatible changes for backend implementations). |
I'm not sure why your screenshot looses the close buttons but mine has them.... that is very strange. I've never seen that before. I was on Ubuntu 26.04 (sorry i updated my machines) |
|
Also, for the decorations to look more normal, apparently one needs to compile with qadwaita or something like that. a plugin that helps draw those decorations better by default. |
It's funny (or not) that stuff like this is still needed ... anyway, out of scope for rendercanvas (the qadwaita thing). |



xref: #36
got to test this on an actual linux machine soon running wayland. Curious to see this in action, but the commits look good.
Claude's notes
What this does
On Wayland, rendercanvas puts Qt on XWayland, and the code to present to a native Wayland surface is disabled with an
if False. That code could not have worked: it pairs thewl_surfacefromwinId()with a display fromget_alt_wayland_display(), i.e. a freshwl_display_connect(). Awl_surfaceis a proxy owned by the connection that created it, so libwayland aborts withProxy and queue point to different wl_displaysas soon as a buffer is attached. The existing comment ("this segfaults, so it looks like the real display object is needed?") diagnosed this correctly.Worth noting for anyone testing this: creating the surface succeeds with the wrong display, and so does
wgpuSurfaceGetCapabilities. It only aborts once frames are actually presented, so a probe has to render.Qt can say what display it is connected to, and that display is the only one its surface is valid on. So this:
wl_displayfrom Qt (QNativeInterface::QWaylandApplicationon Qt6, the platform native interface on Qt5);sys.platform, since under a Wayland session Qt may still be on XWayland, wherewinId()is an X11 window;A second bug this uncovered
WgpuContextToScreen._rc_closeunconfigured the wgpu context but never released the surface, leaving it to the garbage collector. On Wayland the surface belongs to the toolkit's display-connection, so during interpreter shutdownwgpuSurfaceReleaseran after Qt had torn that connection down — a use-after-free (gdb showed the freed map was Qt'swl_display+ 0x90). x11 never hit this because an X11 window id is just a number that outlives any connection. This was unreachable before, so it is newly-exposed rather than a regression.Which Qt versions get native Wayland
QNativeInterface.QWaylandApplicationturns out to be recent. Checked each PySide6 minor:QNativeInterfaceQWaylandApplicationSo most installs today keep the current behaviour, and this is a no-op for them. It also means the check has to be for
QWaylandApplicationspecifically:QNativeInterfacealone exists since 6.7 and would be a false positive on 6.7–6.9.Behaviour changes worth a look
QT_QPA_PLATFORMis no longer overridden. PreviouslyQT_QPA_PLATFORM=xcbwas set unconditionally under Wayland, so a user asking forwaylandwas silently overruled. Forcing still works in both directions._get_surface_ids→None→ bitmap path is now only reachable when aQApplicationalready exists at import (already_had_app_on_import) with a Qt lib that cannot provide its display. It is too late to force xcb there, so degrading to bitmap beats crashing.wgpucontext.pyis shared with glfw. Releasing the surface at close rather than at GC is correct for every backend, but it is not Qt-only code. Happy to split it out.Testing
Tested against a headless weston with lavapipe (software Vulkan), on PySide6 only, since that is all that was installed locally. Presenting to screen works for both a top-level canvas and an embedded
QRenderWidget, and the teardown crash is gone.tests/test_backend_qt.pypasses on native Wayland and on xcb. The rest of the suite is unchanged (the 3test_metafailures locally are a pre-existing git-version mismatch, identical on a stashed baseline).The second commit adds a
test-qt-linuxjob covering the three cases: Wayland with a capable Qt, Wayland with PySide6 6.9 (must fall back to XWayland), and x11-only. Each case asserts which qpa platform Qt ends up on before running the tests, because a case that silently fell back would otherwise still pass and test nothing. I checked the assertion actually fails the step when the platform is wrong.The jobs were validated by running the same steps in ubuntu/python docker images, which turned up two things worth knowing: the pip PySide6 wheel does ship
libqwayland.so, and Qt's xcb plugin needslibxcb-cursor0(Qt >= 6.5), which the existing dependency list does not install.Not addressed
glfw and wx are untouched and stay on XWayland, so they are not equivalent to Qt after this. Neither exposes the display of its connection (glfw could via
glfw.get_wayland_display(), wx has no way at all). glfw on Wayland was not testable here:glfw.init()hangs under headless weston because libdecor cannot initialise GTK, before rendercanvas is involved.