Skip to content

Native wayland preview window#1397

Closed
popcornmix wants to merge 2 commits into
raspberrypi:mainfrom
popcornmix:qtgl-native-wayland
Closed

Native wayland preview window#1397
popcornmix wants to merge 2 commits into
raspberrypi:mainfrom
popcornmix:qtgl-native-wayland

Conversation

@popcornmix

Copy link
Copy Markdown

This is largely claude generated, based on the discussion about whether a native wayland preview window was possible.
More for testing/discussion than for merging.

The first commit adds native wayland support, and the second allows a direct mode, that avoid a GPU blit.
The numbers:

        1920x1080   QTGL    (XWayland)  59.5 fps  gl 15.33 ms/f  client render 1.64 ms/f
                    QTGL_WL (FBO)       80.0 fps  gl 11.39 ms/f  client render 3.58 ms/f
                    QTGL_WL_DIRECT      90.4 fps  gl  7.42 ms/f  client render 1.02 ms/f
        3840x2160   QTGL    (XWayland)  30.1 fps  gl 31.16 ms/f  client render 5.73 ms/f
                    QTGL_WL (FBO)       42.2 fps  gl 25.05 ms/f  client render 13.70 ms/f
                    QTGL_WL_DIRECT      80.0 fps  gl 12.07 ms/f  client render 3.64 ms/f 

Consider this draft. It works for my simple test app (e.g. examples/preview_qtgl_wayland.py).
I don't really know whether there are other use cases that will not work.

In this version, the new paths are opt-in, so it shouldn't break any existing code.
It would be possible to make the wayland/x11 switch automatic, but obviously will change behaviour for many.

QtGlPreview (QGlPicamera2) renders raw EGL straight onto the widget's
native window: WA_PaintOnScreen plus eglCreateWindowSurface(winId()).
That contract only holds on X11, where winId() is an X window. On Wayland
eglCreateWindowSurface needs a wl_egl_window built from the window's
wl_surface, and no Qt binding exposes that per-window surface to Python
(Qt6 dropped the public per-window native interface; only
QNativeInterface::QWaylandApplication remains). So under a Wayland
compositor QtGlPreview can only run through XWayland, which adds a
per-frame texture-format/copy pass in the compositor.

Add an alternative preview, Preview.QTGL_WL (QtGlPreviewWayland, widget
QGlPicamera2Wl), that renders through Qt's own OpenGL context via
QOpenGLWidget. Qt creates and owns the platform surface (the
wl_egl_window on Wayland, the GLX/EGL drawable on X11), so we never
touch wl_surface and there is no platform-specific code. The only thing
the zero-copy path needs is an EGLDisplay to import the camera dmabuf on,
and inside paintGL() Qt's context is current, so eglGetCurrentDisplay()
returns the right display on either platform. The dmabuf ->
EGLImage(EGL_LINUX_DMA_BUF_EXT) -> GL_OES_EGL_image_external sampling is
reused unchanged.

Because it uses Qt's context, the same widget works on X11 too, but the
existing Preview.QTGL is left as the default so nothing changes for
current users; QTGL_WL is purely additive and opt-in.

Enabling it:

    from picamera2 import Picamera2, Preview
    picam2 = Picamera2()
    picam2.start_preview(Preview.QTGL_WL)
    picam2.configure(picam2.create_preview_configuration())
    picam2.start()

See examples/preview_qtgl_wayland.py.

Performance (Pi 4, IMX219, labwc, Mesa 25.0.7 / V3D; GPU-busy time from
DRM fdinfo, summed across the preview client and the compositor, over a
20s window):

    1920x1080   QTGL (XWayland)   59.8 fps   gl 15.29 ms/frame   labwc tfu 2.77 ms/frame
                QTGL_WL (Wayland) 80.9 fps   gl 11.38 ms/frame   labwc tfu 0
    3840x2160   QTGL (XWayland)   30.1 fps   gl 31.16 ms/frame   labwc tfu 9.31 ms/frame
                QTGL_WL (Wayland) 42.3 fps   gl 24.95 ms/frame   labwc tfu 0

The native path eliminates the compositor's XWayland texture-format
(tfu) pass entirely; that cost scales with buffer size (~2.8 ms/frame at
1080p, ~9.3 ms/frame at 4K). At the same GPU saturation that freed
budget becomes more delivered frames (+35% at 1080p, +40% at 4K).

Trade-off: QOpenGLWidget renders into an FBO that Qt then composites into
the window (one extra blit) rather than rendering directly to the
surface; this shows up as higher client-side render cost, most visibly at
4K, but is outweighed by dropping the compositor format pass.

Implementation notes:
- Requests a GLES context (samplerExternalOES / GL_OES_EGL_image_external
  live in GLES).
- Camera frames arrive on the GUI thread via the existing QSocketNotifier;
  render_request() stashes the request and calls update(), and the GL work
  happens in paintGL() with Qt's context current.
- Live resize is handled (resizeGL repaints; the viewport, including
  aspect-ratio letterboxing, is recomputed from the live widget size every
  repaint).

Signed-off-by: Dom Cobley <popcornmix@gmail.com>
…L_WL_DIRECT)

Preview.QTGL_WL uses a QOpenGLWidget, which always renders into an
offscreen FBO that Qt then composites into the window - one extra
full-frame GPU blit per frame. Add Preview.QTGL_WL_DIRECT, which uses a
QOpenGLWindow embedded with QWidget.createWindowContainer(): the
QOpenGLWindow owns its own native (sub)surface and presents directly via
eglSwapBuffers, so there is no in-process blit. This restores the
directness of the original X11 QGlPicamera2 (WA_PaintOnScreen) while
staying native Wayland - on Wayland Qt backs the window with a
wl_egl_window, on X11 with an X drawable. The zero-copy dmabuf ->
EGLImage -> GL_OES_EGL_image_external path is unchanged.

Rendering is done synchronously in render_request() (makeCurrent ->
repaint -> swapBuffers), exactly like the original QGlPicamera2. Deferred
update()/requestUpdate() does not work here: it is throttled to Wayland
frame callbacks, which an embedded subsurface does not reliably receive
(observed ~1 paint/s), so the preview would starve. A QOpenGLWindow, unlike
a QOpenGLWidget, lets us drive its context from outside paintGL, which makes
the synchronous path possible.

Both previews are kept. QTGL_WL_DIRECT is the fastest option;
QTGL_WL remains for embedders that need it, because container/native
windows always stack above sibling widgets and cannot be clipped by
non-rectangular masks. For a viewfinder that fills its area this is fine
(overlays are drawn inside this GL context); apps that float Qt widgets
over the preview should use QTGL_WL.

Enabling it:

    from picamera2 import Picamera2, Preview
    picam2 = Picamera2()
    picam2.start_preview(Preview.QTGL_WL_DIRECT)
    picam2.configure(picam2.create_preview_configuration())
    picam2.start()

See examples/preview_qtgl_wayland_direct.py.

Performance (Pi 4, IMX219, labwc, Mesa 25.0.7 / V3D; GPU-busy time from
DRM fdinfo summed across the preview client and the compositor; the
client-side render/frame column isolates the blit cost; 20s window):

    1920x1080   QTGL    (XWayland)  59.5 fps  gl 15.33 ms/f  client render 1.64 ms/f
                QTGL_WL (FBO)       80.0 fps  gl 11.39 ms/f  client render 3.58 ms/f
                QTGL_WL_DIRECT      90.4 fps  gl  7.42 ms/f  client render 1.02 ms/f
    3840x2160   QTGL    (XWayland)  30.1 fps  gl 31.16 ms/f  client render 5.73 ms/f
                QTGL_WL (FBO)       42.2 fps  gl 25.05 ms/f  client render 13.70 ms/f
                QTGL_WL_DIRECT      80.0 fps  gl 12.07 ms/f  client render 3.64 ms/f

Removing the blit cuts the preview client's per-frame GPU render cost from
3.58 to 1.02 ms at 1080p and from 13.70 to 3.64 ms at 4K (a ~2.6 / ~10
ms/frame saving), and roughly doubles 4K throughput versus XWayland
(30 -> 80 fps).

Signed-off-by: Dom Cobley <popcornmix@gmail.com>
@davidplowman

Copy link
Copy Markdown
Collaborator

Closing this as I've taken the changes and incorporated them here: #1398

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.

2 participants