From 7a427ce1979ce7447e885c4f30129b40f3d466f5 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 17 Jan 2026 17:30:40 -0500 Subject: [PATCH 01/14] core: fix inverted inHeader condition in preprocesso --- src/core/scan.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 8ca1f51..37b0fac 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -118,7 +118,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna auto stream = QTextStream(&file); auto imports = QVector(); - bool inHeader = false; + bool inHeader = true; auto ifScopes = QVector(); bool sourceMasked = false; int lineNum = 0; @@ -177,7 +177,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna } else if (!internal && line == "//@ pragma Internal") { internal = true; } else if (line.contains('{')) { - inHeader = true; + inHeader = false; } } From 8fd0de458034174cf271fac56953d98026b291a4 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 20 Jan 2026 16:10:45 -0500 Subject: [PATCH 02/14] core/proxywindow: create window on visibility for lazily initialized windows --- src/window/proxywindow.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 3cc4378..4423547 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -288,10 +288,16 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { this->bBackerVisibility = false; this->deleteWindow(); } - } else if (this->window != nullptr) { - if (visible) this->polishItems(); - this->window->setVisible(visible); - this->bBackerVisibility = visible; + } else { + if (visible && this->window == nullptr) { + this->createWindow(); + } + + if (this->window != nullptr) { + if (visible) this->polishItems(); + this->window->setVisible(visible); + this->bBackerVisibility = visible; + } } } From 191085a8821b35680bba16ce5411fc9dbe912237 Mon Sep 17 00:00:00 2001 From: Manuel Romei Date: Sun, 18 Jan 2026 17:20:12 +0100 Subject: [PATCH 03/14] ipc: use deleteLater() in IpcServerConnection to prevent use-after-free --- src/ipc/ipc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 32d8482..40e8f0c 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -61,7 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; - delete this; + this->deleteLater(); } void IpcServerConnection::onReadyRead() { @@ -88,7 +88,7 @@ void IpcServerConnection::onReadyRead() { // async connections reparent if (dynamic_cast(this->parent()) != nullptr) { - delete this; + this->deleteLater(); } } From 1e4d804e7f3fa7465811030e8da2bf10d544426a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Jan 2026 00:23:38 -0800 Subject: [PATCH 04/14] widgets/cliprect: use layer.effect on content item over property ShaderEffectSource as a property not parented to an item does not update its sourceItem's QQuickWindow when its own is changed. This lead to use after frees and broken effects when using ClippingRectangle. --- changelog/next.md | 1 + src/widgets/ClippingRectangle.qml | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 30e998b..bccd780 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -43,6 +43,7 @@ set shell id. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. - Fixed memory leak in IPC handlers. +- Fixed ClippingRectangle related crashes. ## Packaging Changes diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 86fe601..3fc64d8 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick ///! Rectangle capable of clipping content inside its border. @@ -24,7 +26,7 @@ Item { /// Defaults to true if any corner has a non-zero radius, otherwise false. property /*bool*/alias antialiasing: rectangle.antialiasing /// The background color of the rectangle, which goes under its content. - property /*color*/alias color: shader.backgroundColor + property color color: "white" /// See @@QtQuick.Rectangle.border. property clippingRectangleBorder border /// Radius of all corners. Defaults to 0. @@ -70,19 +72,14 @@ Item { anchors.fill: parent anchors.margins: root.contentInsideBorder ? root.border.width : 0 } - } - ShaderEffect { - id: shader - anchors.fill: root - fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Rectangle rect: rectangle - property color backgroundColor: "white" - property color borderColor: root.border.color - - property ShaderEffectSource content: ShaderEffectSource { - hideSource: true - sourceItem: contentItemContainer + layer.enabled: true + layer.samplerName: "content" + layer.effect: ShaderEffect { + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Rectangle rect: rectangle + property color backgroundColor: root.color + property color borderColor: root.border.color } } } From 395a1301a83e98dafc325289630ccacda5d69607 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Fri, 6 Feb 2026 03:02:58 -0500 Subject: [PATCH 05/14] core: add hasThemeIcon mapping --- changelog/next.md | 1 + src/core/qmlglobal.cpp | 2 ++ src/core/qmlglobal.hpp | 2 ++ 3 files changed, 5 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index bccd780..583a2f4 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -24,6 +24,7 @@ set shell id. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. +- Added a way to detect if an icon is from the system icon theme or not. ## Other Changes diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 03fb818..6c26609 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -314,6 +314,8 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) return IconImageProvider::requestString(icon, "", fallback); } +bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); } + bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) { return qs::scan::env::PreprocEnv::hasVersion(major, minor, features); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 3ca70be..94b42f6 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -202,6 +202,8 @@ public: /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// icon if the requested one could not be loaded. Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); + /// Check if specified icon has an available icon in your icon theme + Q_INVOKABLE static bool hasThemeIcon(const QString& icon); /// Equivalent to `${Quickshell.configDir}/${path}` Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const; /// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity. From 4429c038377a2c59dfcab6fe2424fb2c3a99d2cd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 8 Feb 2026 20:10:11 -0800 Subject: [PATCH 06/14] widgets/cliprect: fix ShaderEffect warnings on reload layer.effect causes warnings on reload for an unknown reason which seems to be ownership or destruction time related. This commit uses an alternate strategy to create the shader which does not show this warning. --- src/widgets/ClippingRectangle.qml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 3fc64d8..749b331 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -66,20 +66,22 @@ Item { Item { id: contentItemContainer anchors.fill: root + layer.enabled: true + visible: false Item { id: contentItem anchors.fill: parent anchors.margins: root.contentInsideBorder ? root.border.width : 0 } + } - layer.enabled: true - layer.samplerName: "content" - layer.effect: ShaderEffect { - fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Rectangle rect: rectangle - property color backgroundColor: root.color - property color borderColor: root.border.color - } + ShaderEffect { + anchors.fill: contentItemContainer + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Item content: contentItemContainer + property Rectangle rect: rectangle + property color backgroundColor: root.color + property color borderColor: root.border.color } } From dacfa9de829ac7cb173825f593236bf2c21f637e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 9 Feb 2026 19:14:36 -0800 Subject: [PATCH 07/14] widgets/cliprect: use ShaderEffectSource to propagate mouse events --- src/widgets/ClippingRectangle.qml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 749b331..604f346 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -26,7 +26,7 @@ Item { /// Defaults to true if any corner has a non-zero radius, otherwise false. property /*bool*/alias antialiasing: rectangle.antialiasing /// The background color of the rectangle, which goes under its content. - property color color: "white" + property /*color*/alias color: shader.backgroundColor /// See @@QtQuick.Rectangle.border. property clippingRectangleBorder border /// Radius of all corners. Defaults to 0. @@ -66,8 +66,6 @@ Item { Item { id: contentItemContainer anchors.fill: root - layer.enabled: true - visible: false Item { id: contentItem @@ -76,12 +74,19 @@ Item { } } + ShaderEffectSource { + id: shaderSource + hideSource: true + sourceItem: contentItemContainer + } + ShaderEffect { - anchors.fill: contentItemContainer + id: shader + anchors.fill: root fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Item content: contentItemContainer property Rectangle rect: rectangle - property color backgroundColor: root.color + property color backgroundColor: "white" property color borderColor: root.border.color + property ShaderEffectSource content: shaderSource } } From afbc5ffd4e846515ae5efeb41580eb25171faa52 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 15 Feb 2026 23:23:34 +0700 Subject: [PATCH 08/14] services/pipewire: use node volume control when device missing Some outputs which present a pipewire device object do not present routes, instead expecting volume to be set on the associated pipewire node. --- changelog/next.md | 1 + src/services/pipewire/device.cpp | 4 ++++ src/services/pipewire/device.hpp | 5 ++++- src/services/pipewire/node.cpp | 34 +++++++++++++++++++++----------- src/services/pipewire/node.hpp | 5 ++++- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 583a2f4..66f87c1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed volume control breaking with bluez streams and potentially others. +- Fixed volume control breaking for devices without route definitions. - Fixed escape sequence handling in desktop entries. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index e3bc967..61079a1 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -141,6 +141,10 @@ bool PwDevice::tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps return true; } +bool PwDevice::hasRouteDevice(qint32 routeDevice) const { + return this->routeDeviceIndexes.contains(routeDevice); +} + void PwDevice::polled() { // It is far more likely that the list content has not come in yet than it having no entries, // and there isn't a way to check in the case that there *aren't* actually any entries. diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 22af699..cd61709 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -12,13 +12,15 @@ #include #include "core.hpp" -#include "node.hpp" #include "registry.hpp" namespace qs::service::pipewire { class PwDevice; +// Forward declare to avoid circular dependency with node.hpp +struct PwVolumeProps; + class PwDevice: public PwBindable { Q_OBJECT; @@ -33,6 +35,7 @@ public: [[nodiscard]] bool waitingForDevice() const; [[nodiscard]] bool tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps); + [[nodiscard]] bool hasRouteDevice(qint32 routeDevice) const; signals: void deviceReady(); diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index b6f0529..075a7ec 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -429,6 +429,10 @@ void PwNodeBoundAudio::setMuted(bool muted) { } float PwNodeBoundAudio::averageVolume() const { + if (this->mVolumes.isEmpty()) { + return 0.0f; + } + float total = 0; for (auto volume: this->mVolumes) { @@ -572,22 +576,28 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); const auto* volumeStepProp = spa_pod_find_prop(param, nullptr, SPA_PROP_volumeStep); - const auto* volumes = reinterpret_cast(&volumesProp->value); - const auto* channels = reinterpret_cast(&channelsProp->value); - - spa_pod* iter = nullptr; - SPA_POD_ARRAY_FOREACH(volumes, iter) { - // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. - auto linear = *reinterpret_cast(iter); - auto visual = std::cbrt(linear); - props.volumes.push_back(visual); + if (volumesProp) { + const auto* volumes = reinterpret_cast(&volumesProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(volumes, iter) { + // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. + auto linear = *reinterpret_cast(iter); + auto visual = std::cbrt(linear); + props.volumes.push_back(visual); + } } - SPA_POD_ARRAY_FOREACH(channels, iter) { - props.channels.push_back(*reinterpret_cast(iter)); + if (channelsProp) { + const auto* channels = reinterpret_cast(&channelsProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(channels, iter) { + props.channels.push_back(*reinterpret_cast(iter)); + } } - spa_pod_get_bool(&muteProp->value, &props.mute); + if (muteProp) { + spa_pod_get_bool(&muteProp->value, &props.mute); + } if (volumeStepProp) { spa_pod_get_float(&volumeStepProp->value, &props.volumeStep); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index fdec72d..efc819c 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -15,6 +15,7 @@ #include #include "core.hpp" +#include "device.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -249,7 +250,9 @@ public: bool proAudio = false; [[nodiscard]] bool shouldUseDevice() const { - return this->device && !this->proAudio && this->routeDevice != -1; + if (!this->device || this->proAudio || this->routeDevice == -1) return false; + // Only use device control if the device actually has route indexes for this routeDevice + return this->device->hasRouteDevice(this->routeDevice); } signals: From e7cd1e9982426fdcc617910597ab3d8f71346e4f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Feb 2026 21:11:45 -0800 Subject: [PATCH 09/14] core: add env and isEnvSet functions to pragma context --- src/core/scanenv.cpp | 9 +++++++++ src/core/scanenv.hpp | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp index b8c514c..047f472 100644 --- a/src/core/scanenv.cpp +++ b/src/core/scanenv.cpp @@ -1,6 +1,7 @@ #include "scanenv.hpp" #include +#include #include "build.hpp" @@ -19,4 +20,12 @@ bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) { return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor; } +QString PreprocEnv::env(const QString& variable) { + return qEnvironmentVariable(variable.toStdString().c_str()); +} + +bool PreprocEnv::isEnvSet(const QString& variable) { + return qEnvironmentVariableIsSet(variable.toStdString().c_str()); +} + } // namespace qs::scan::env diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp index 0abde2e..c1c6814 100644 --- a/src/core/scanenv.hpp +++ b/src/core/scanenv.hpp @@ -12,6 +12,9 @@ class PreprocEnv: public QObject { public: Q_INVOKABLE static bool hasVersion(int major, int minor, const QStringList& features = QStringList()); + + Q_INVOKABLE static QString env(const QString& variable); + Q_INVOKABLE static bool isEnvSet(const QString& variable); }; } // namespace qs::scan::env From 158db16b931d04b43ec84748ee49390ea9f7c3f8 Mon Sep 17 00:00:00 2001 From: Bryan Paradis Date: Wed, 18 Feb 2026 07:36:21 -0800 Subject: [PATCH 10/14] wayland: check screen isPlaceholder and if wl_output is null Fixes crashes on disconnected monitors --- changelog/next.md | 1 + src/wayland/session_lock/surface.cpp | 2 +- src/wayland/wlr_layershell/surface.cpp | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 66f87c1..042a6ea 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -46,6 +46,7 @@ set shell id. - Fixed asynchronous loaders not working before window creation. - Fixed memory leak in IPC handlers. - Fixed ClippingRectangle related crashes. +- Fixed crashes when monitors are unplugged. ## Packaging Changes diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index 6ec4eb6..c73f459 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -28,7 +28,7 @@ QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWayla wl_output* output = nullptr; // NOLINT (include) auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { qFatal() << "Session lock screen does not corrospond to a real screen. Force closing window"; diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 3c71ff9..4a5015e 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -143,11 +143,11 @@ LayerSurface::LayerSurface(LayerShellIntegration* shell, QtWaylandClient::QWayla auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { qWarning() - << "Layershell screen does not corrospond to a real screen. Letting the compositor pick."; + << "Layershell screen does not correspond to a real screen. Letting the compositor pick."; } } From a99519c3adbc9eb9a80b32cde7264e9f147e3416 Mon Sep 17 00:00:00 2001 From: reakjra Date: Mon, 9 Feb 2026 18:20:14 +0100 Subject: [PATCH 11/14] wayland/screencopy: support dmabufs in vulkan mode --- .github/workflows/build.yml | 1 + BUILD.md | 1 + changelog/next.md | 2 + default.nix | 3 +- src/wayland/buffer/CMakeLists.txt | 5 +- src/wayland/buffer/dmabuf.cpp | 319 ++++++++++++++++++++++++++++++ src/wayland/buffer/dmabuf.hpp | 36 ++++ src/window/proxywindow.cpp | 10 + 8 files changed, 375 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66c3691..8d19f58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: wayland-protocols \ wayland \ libdrm \ + vulkan-headers \ libxcb \ libpipewire \ cli11 \ diff --git a/BUILD.md b/BUILD.md index fdea27e..c9459b5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -146,6 +146,7 @@ To disable: `-DSCREENCOPY=OFF` Dependencies: - `libdrm` - `libgbm` +- `vulkan-headers` (build-time) Specific protocols can also be disabled: - `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] diff --git a/changelog/next.md b/changelog/next.md index 042a6ea..7180d53 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -25,6 +25,7 @@ set shell id. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. - Added a way to detect if an icon is from the system icon theme or not. +- Added vulkan support to screencopy. ## Other Changes @@ -51,3 +52,4 @@ set shell id. ## Packaging Changes `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. +`vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). diff --git a/default.nix b/default.nix index 0b6f303..7783774 100644 --- a/default.nix +++ b/default.nix @@ -19,6 +19,7 @@ xorg, libdrm, libgbm ? null, + vulkan-headers, pipewire, pam, polkit, @@ -77,7 +78,7 @@ ++ lib.optional withJemalloc jemalloc ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ wayland wayland-protocols ] - ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] + ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt index f80c53a..a8f2d2d 100644 --- a/src/wayland/buffer/CMakeLists.txt +++ b/src/wayland/buffer/CMakeLists.txt @@ -1,6 +1,8 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl) +find_package(VulkanHeaders REQUIRED) + qt_add_library(quickshell-wayland-buffer STATIC manager.cpp dmabuf.cpp @@ -10,9 +12,10 @@ qt_add_library(quickshell-wayland-buffer STATIC wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") target_link_libraries(quickshell-wayland-buffer PRIVATE - Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::QuickPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client PkgConfig::dmabuf-deps wlp-linux-dmabuf + Vulkan::Headers ) qs_pch(quickshell-wayland-buffer SET large) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index e51a1d0..89c9108 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,17 @@ #include #include #include +#include #include +#include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -48,6 +54,25 @@ QS_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg) LinuxDmabufManager* MANAGER = nullptr; // NOLINT +VkFormat drmFormatToVkFormat(uint32_t drmFormat) { + // NOLINTBEGIN(bugprone-branch-clone): XRGB/ARGB intentionally map to the same VK format + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_XRGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_ABGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_XBGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_ARGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_XRGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_ABGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_XBGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_ABGR16161616F: return VK_FORMAT_R16G16B16A16_SFLOAT; + case DRM_FORMAT_RGB565: return VK_FORMAT_R5G6B5_UNORM_PACK16; + case DRM_FORMAT_BGR565: return VK_FORMAT_B5G6R5_UNORM_PACK16; + default: return VK_FORMAT_UNDEFINED; + } + // NOLINTEND(bugprone-branch-clone) +} + } // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { @@ -532,6 +557,15 @@ bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { } WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + if (ri && ri->graphicsApi() == QSGRendererInterface::Vulkan) { + return this->createQsgTextureVulkan(window); + } + + return this->createQsgTextureGl(window); +} + +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureGl(QQuickWindow* window) const { static auto* glEGLImageTargetTexture2DOES = []() { auto* fn = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES") @@ -662,6 +696,291 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { return tex; } +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + auto* vkInst = window->vulkanInstance(); + + if (!vkInst) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: no QVulkanInstance."; + return nullptr; + } + + auto* vkDevicePtr = + static_cast(ri->getResource(window, QSGRendererInterface::DeviceResource)); + auto* vkPhysDevicePtr = static_cast( + ri->getResource(window, QSGRendererInterface::PhysicalDeviceResource) + ); + + if (!vkDevicePtr || !vkPhysDevicePtr) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: could not get Vulkan device."; + return nullptr; + } + + VkDevice device = *vkDevicePtr; + VkPhysicalDevice physDevice = *vkPhysDevicePtr; + + auto* devFuncs = vkInst->deviceFunctions(device); + auto* instFuncs = vkInst->functions(); + + if (!devFuncs || !instFuncs) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "could not get Vulkan functions."; + return nullptr; + } + + auto getMemoryFdPropertiesKHR = reinterpret_cast( + instFuncs->vkGetDeviceProcAddr(device, "vkGetMemoryFdPropertiesKHR") + ); + + if (!getMemoryFdPropertiesKHR) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "vkGetMemoryFdPropertiesKHR not available. " + "Missing VK_KHR_external_memory_fd extension."; + return nullptr; + } + + const VkFormat vkFormat = drmFormatToVkFormat(this->format); + if (vkFormat == VK_FORMAT_UNDEFINED) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: unsupported DRM format" + << FourCCStr(this->format); + return nullptr; + } + + if (this->planeCount > 4) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: too many planes" + << this->planeCount; + return nullptr; + } + + std::array planeLayouts = {}; + for (int i = 0; i < this->planeCount; ++i) { + planeLayouts[i].offset = this->planes[i].offset; // NOLINT + planeLayouts[i].rowPitch = this->planes[i].stride; // NOLINT + planeLayouts[i].size = 0; + planeLayouts[i].arrayPitch = 0; + planeLayouts[i].depthPitch = 0; + } + + const bool useModifier = this->modifier != DRM_FORMAT_MOD_INVALID; + + VkExternalMemoryImageCreateInfo externalInfo = {}; + externalInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + VkImageDrmFormatModifierExplicitCreateInfoEXT modifierInfo = {}; + modifierInfo.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT; + modifierInfo.drmFormatModifier = this->modifier; + modifierInfo.drmFormatModifierPlaneCount = static_cast(this->planeCount); + modifierInfo.pPlaneLayouts = planeLayouts.data(); + + if (useModifier) { + externalInfo.pNext = &modifierInfo; + } + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.pNext = &externalInfo; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = vkFormat; + imageInfo.extent = {.width = this->width, .height = this->height, .depth = 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = useModifier ? VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT : VK_IMAGE_TILING_LINEAR; + imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage image = VK_NULL_HANDLE; + VkResult result = devFuncs->vkCreateImage(device, &imageInfo, nullptr, &image); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create VkImage for DMA-BUF import, result:" << result; + return nullptr; + } + + VkDeviceMemory memory = VK_NULL_HANDLE; + + // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT + // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. + const int dupFd = dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (dupFd < 0) { + qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + { + VkMemoryRequirements memReqs = {}; + devFuncs->vkGetImageMemoryRequirements(device, image, &memReqs); + + VkMemoryFdPropertiesKHR fdProps = {}; + fdProps.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; + + result = getMemoryFdPropertiesKHR( + device, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + dupFd, + &fdProps + ); + + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkGetMemoryFdPropertiesKHR failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + const uint32_t memTypeBits = memReqs.memoryTypeBits & fdProps.memoryTypeBits; + + VkPhysicalDeviceMemoryProperties memProps = {}; + instFuncs->vkGetPhysicalDeviceMemoryProperties(physDevice, &memProps); + + uint32_t memTypeIndex = UINT32_MAX; + for (uint32_t j = 0; j < memProps.memoryTypeCount; ++j) { + if (memTypeBits & (1u << j)) { + memTypeIndex = j; + break; + } + } + + if (memTypeIndex == UINT32_MAX) { + close(dupFd); + qCWarning(logDmabuf) << "No compatible memory type for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + VkImportMemoryFdInfoKHR importInfo = {}; + importInfo.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; + importInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + importInfo.fd = dupFd; + + VkMemoryDedicatedAllocateInfo dedicatedInfo = {}; + dedicatedInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + dedicatedInfo.image = image; + dedicatedInfo.pNext = &importInfo; + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.pNext = &dedicatedInfo; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = memTypeIndex; + + result = devFuncs->vkAllocateMemory(device, &allocInfo, nullptr, &memory); + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkAllocateMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + result = devFuncs->vkBindImageMemory(device, image, memory, 0); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "vkBindImageMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + } + + { + // acquire the DMA-BUF from the foreign (compositor) queue and transition + // to shader-read layout. oldLayout must be GENERAL (not UNDEFINED) to + // preserve the DMA-BUF contents written by the external producer. Hopefully. + window->beginExternalCommands(); + + auto* cmdBufPtr = static_cast( + ri->getResource(window, QSGRendererInterface::CommandListResource) + ); + + if (cmdBufPtr && *cmdBufPtr) { + VkCommandBuffer cmdBuf = *cmdBufPtr; + + // find the graphics queue family index for the ownrship transfer. + uint32_t graphicsQueueFamily = 0; + uint32_t queueFamilyCount = 0; + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( + physDevice, &queueFamilyCount, nullptr + ); + std::vector queueFamilies(queueFamilyCount); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( + physDevice, &queueFamilyCount, queueFamilies.data() + ); + for (uint32_t i = 0; i < queueFamilyCount; ++i) { + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphicsQueueFamily = i; + break; + } + } + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.dstQueueFamilyIndex = graphicsQueueFamily; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + devFuncs->vkCmdPipelineBarrier( + cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, + 0, + nullptr, + 0, + nullptr, + 1, + &barrier + ); + } + + window->endExternalCommands(); + + auto* qsgTexture = QQuickWindowPrivate::get(window)->createTextureFromNativeTexture( + reinterpret_cast(image), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + static_cast(vkFormat), + QSize(static_cast(this->width), static_cast(this->height)), + {} + ); + + auto* tex = new WlDmaBufferVulkanQSGTexture( + devFuncs, + device, + image, + memory, + qsgTexture + ); + qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; + return tex; + } + +cleanup_fail: + if (image != VK_NULL_HANDLE) { + devFuncs->vkDestroyImage(device, image, nullptr); + } + if (memory != VK_NULL_HANDLE) { + devFuncs->vkFreeMemory(device, memory, nullptr); + } + return nullptr; +} + +WlDmaBufferVulkanQSGTexture::~WlDmaBufferVulkanQSGTexture() { + delete this->qsgTexture; + + if (this->image != VK_NULL_HANDLE) { + this->devFuncs->vkDestroyImage(this->device, this->image, nullptr); + } + + if (this->memory != VK_NULL_HANDLE) { + this->devFuncs->vkFreeMemory(this->device, this->memory, nullptr); + } + + qCDebug(logDmabuf) << "WlDmaBufferVulkanQSGTexture" << this << "destroyed."; +} + WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { auto* context = QOpenGLContext::currentContext(); auto* display = context->nativeInterface()->display(); diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp index 1e4ef1a..ffe5d02 100644 --- a/src/wayland/buffer/dmabuf.hpp +++ b/src/wayland/buffer/dmabuf.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,9 +13,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -114,6 +117,36 @@ private: friend class WlDmaBuffer; }; +class WlDmaBufferVulkanQSGTexture: public WlBufferQSGTexture { +public: + ~WlDmaBufferVulkanQSGTexture() override; + Q_DISABLE_COPY_MOVE(WlDmaBufferVulkanQSGTexture); + + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; } + +private: + WlDmaBufferVulkanQSGTexture( + QVulkanDeviceFunctions* devFuncs, + VkDevice device, + VkImage image, + VkDeviceMemory memory, + QSGTexture* qsgTexture + ) + : devFuncs(devFuncs) + , device(device) + , image(image) + , memory(memory) + , qsgTexture(qsgTexture) {} + + QVulkanDeviceFunctions* devFuncs = nullptr; + VkDevice device = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + QSGTexture* qsgTexture = nullptr; + + friend class WlDmaBuffer; +}; + class WlDmaBuffer: public WlBuffer { public: ~WlDmaBuffer() override; @@ -151,6 +184,9 @@ private: friend class LinuxDmabufManager; friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); + + [[nodiscard]] WlBufferQSGTexture* createQsgTextureGl(QQuickWindow* window) const; + [[nodiscard]] WlBufferQSGTexture* createQsgTextureVulkan(QQuickWindow* window) const; }; QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 4423547..b4f79da 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -147,6 +148,15 @@ void ProxyWindowBase::ensureQWindow() { this->window = nullptr; // createQQuickWindow may indirectly reference this->window this->window = this->createQQuickWindow(); this->window->setFormat(format); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } void ProxyWindowBase::createWindow() { From 2cf57f43d5f2a5b139d1f1702c83e126e17f27f8 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Feb 2026 12:14:36 -0500 Subject: [PATCH 12/14] core/proxywindow: expose updatesEnabled property --- src/window/proxywindow.cpp | 14 ++++++++++++++ src/window/proxywindow.hpp | 6 ++++++ src/window/windowinterface.cpp | 4 ++++ src/window/windowinterface.hpp | 10 ++++++++++ 4 files changed, 34 insertions(+) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index b4f79da..62126bd 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -223,6 +223,7 @@ void ProxyWindowBase::completeWindow() { this->trySetHeight(this->implicitHeight()); this->setColor(this->mColor); this->updateMask(); + QQuickWindowPrivate::get(this->window)->updatesEnabled = this->mUpdatesEnabled; // notify initial / post-connection geometry emit this->xChanged(); @@ -479,6 +480,19 @@ void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { emit this->surfaceFormatChanged(); } +bool ProxyWindowBase::updatesEnabled() const { return this->mUpdatesEnabled; } + +void ProxyWindowBase::setUpdatesEnabled(bool updatesEnabled) { + if (updatesEnabled == this->mUpdatesEnabled) return; + this->mUpdatesEnabled = updatesEnabled; + + if (this->window != nullptr) { + QQuickWindowPrivate::get(this->window)->updatesEnabled = updatesEnabled; + } + + emit this->updatesEnabledChanged(); +} + qreal ProxyWindowBase::devicePixelRatio() const { if (this->window != nullptr) return this->window->devicePixelRatio(); if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio(); diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 86d66f8..aec821e 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -57,6 +57,7 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged); Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -140,6 +141,9 @@ public: [[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; } void setSurfaceFormat(QsSurfaceFormat format); + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled); + [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT [[nodiscard]] QQmlListProperty data(); @@ -163,6 +167,7 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); void polished(); protected slots: @@ -187,6 +192,7 @@ protected: ProxyWindowContentItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; + bool mUpdatesEnabled = true; QsSurfaceFormat qsSurfaceFormat; QSurfaceFormat mSurfaceFormat; diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index 8917f12..e41afc2 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -127,6 +127,9 @@ void WindowInterface::setMask(PendingRegion* mask) const { this->proxyWindow()-> QsSurfaceFormat WindowInterface::surfaceFormat() const { return this->proxyWindow()->surfaceFormat(); }; void WindowInterface::setSurfaceFormat(QsSurfaceFormat format) const { this->proxyWindow()->setSurfaceFormat(format); }; +bool WindowInterface::updatesEnabled() const { return this->proxyWindow()->updatesEnabled(); }; +void WindowInterface::setUpdatesEnabled(bool updatesEnabled) const { this->proxyWindow()->setUpdatesEnabled(updatesEnabled); }; + QQmlListProperty WindowInterface::data() const { return this->proxyWindow()->data(); }; // clang-format on @@ -148,6 +151,7 @@ void WindowInterface::connectSignals() const { QObject::connect(window, &ProxyWindowBase::colorChanged, this, &WindowInterface::colorChanged); QObject::connect(window, &ProxyWindowBase::maskChanged, this, &WindowInterface::maskChanged); QObject::connect(window, &ProxyWindowBase::surfaceFormatChanged, this, &WindowInterface::surfaceFormatChanged); + QObject::connect(window, &ProxyWindowBase::updatesEnabledChanged, this, &WindowInterface::updatesEnabledChanged); // clang-format on } diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index 9e917b9..6f3db20 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -143,6 +143,12 @@ class WindowInterface: public Reloadable { /// /// > [!NOTE] The surface format cannot be changed after the window is created. Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + /// If the window should receive render updates. Defaults to true. + /// + /// When set to false, the window will not re-render in response to animations + /// or other visual updates from other windows. This is useful for static windows + /// such as wallpapers that do not need to update frequently, saving GPU cycles. + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -231,6 +237,9 @@ public: [[nodiscard]] QsSurfaceFormat surfaceFormat() const; void setSurfaceFormat(QsSurfaceFormat format) const; + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled) const; + [[nodiscard]] QQmlListProperty data() const; static QsWindowAttached* qmlAttachedProperties(QObject* object); @@ -258,6 +267,7 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); protected: void connectSignals() const; From c3c3e2ca251a430dbe1b2d46ab0af4e5ca82c7e8 Mon Sep 17 00:00:00 2001 From: reakjra Date: Mon, 23 Feb 2026 18:28:01 +0100 Subject: [PATCH 13/14] wayland/screencopy: pin XRGB alpha to 1 in vulkan mode While EGL handles this internally, vulkan's alpha channel behavior is undefined when rendering depending on the driver. Notably intel does not treat it as 1.0. --- src/wayland/buffer/CMakeLists.txt | 2 +- src/wayland/buffer/dmabuf.cpp | 65 +++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt index a8f2d2d..15818fc 100644 --- a/src/wayland/buffer/CMakeLists.txt +++ b/src/wayland/buffer/CMakeLists.txt @@ -12,7 +12,7 @@ qt_add_library(quickshell-wayland-buffer STATIC wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") target_link_libraries(quickshell-wayland-buffer PRIVATE - Qt::Quick Qt::QuickPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::QuickPrivate Qt::GuiPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client PkgConfig::dmabuf-deps wlp-linux-dmabuf Vulkan::Headers diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 89c9108..7d17884 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,19 @@ VkFormat drmFormatToVkFormat(uint32_t drmFormat) { // NOLINTEND(bugprone-branch-clone) } +bool drmFormatHasAlpha(uint32_t drmFormat) { + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_ABGR8888: + case DRM_FORMAT_ARGB2101010: + case DRM_FORMAT_ABGR2101010: + case DRM_FORMAT_ABGR16161616F: + return true; + default: + return false; + } +} + } // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { @@ -106,25 +120,27 @@ GbmDeviceHandle::~GbmDeviceHandle() { } } -// This will definitely backfire later +// Prefer ARGB over XRGB: XRGB has undefined alpha bytes which cause +// transparency artifacts on Vulkan (notably Intel GPUs) since Vulkan +// doesn't auto-fill alpha=1.0 for X formats like EGL does. void LinuxDmabufFormatSelection::ensureSorted() { if (this->sorted) return; auto beginIter = this->formats.begin(); - auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { - return format.first == DRM_FORMAT_XRGB8888; - }); - - if (xrgbIter != this->formats.end()) { - std::swap(*beginIter, *xrgbIter); - ++beginIter; - } - auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { return format.first == DRM_FORMAT_ARGB8888; }); - if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); + if (argbIter != this->formats.end()) { + std::swap(*beginIter, *argbIter); + ++beginIter; + } + + auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_XRGB8888; + }); + + if (xrgbIter != this->formats.end()) std::swap(*beginIter, *xrgbIter); this->sorted = true; } @@ -946,6 +962,33 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co {} ); + // For opaque DRM formats (XRGB, XBGR, etc.), the alpha bytes are underfined. + // EGL silently forces alpha=1.0 for these, but Vulkan doesn't. Replace Qt's + // default identity-swizzle VkImageView with one that maps alpha to ONE. + if (!drmFormatHasAlpha(this->format)) { + auto* vkTexture = static_cast(qsgTexture->rhiTexture()); // NOLINT + + devFuncs->vkDestroyImageView(device, vkTexture->imageView, nullptr); + + VkImageViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = vkFormat; + viewInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.a = VK_COMPONENT_SWIZZLE_ONE; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + result = devFuncs->vkCreateImageView(device, &viewInfo, nullptr, &vkTexture->imageView); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create alpha-swizzled VkImageView, result:" << result; + } + } + auto* tex = new WlDmaBufferVulkanQSGTexture( devFuncs, device, From 8e8f27a22aa3fe8a0470dd19ebbd87b1b9d72ed0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 23 Feb 2026 23:03:48 -0800 Subject: [PATCH 14/14] wayland/screencopy: enable vulkan dmabuf support on session locks Also reformat dmabuf --- src/wayland/buffer/dmabuf.cpp | 27 ++++++++++----------------- src/wayland/session_lock.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 7d17884..ed9dbeb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -80,10 +80,8 @@ bool drmFormatHasAlpha(uint32_t drmFormat) { case DRM_FORMAT_ABGR8888: case DRM_FORMAT_ARGB2101010: case DRM_FORMAT_ABGR2101010: - case DRM_FORMAT_ABGR16161616F: - return true; - default: - return false; + case DRM_FORMAT_ABGR16161616F: return true; + default: return false; } } @@ -818,7 +816,8 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. - const int dupFd = dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const int dupFd = + dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) if (dupFd < 0) { qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; goto cleanup_fail; // NOLINT @@ -909,12 +908,12 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // find the graphics queue family index for the ownrship transfer. uint32_t graphicsQueueFamily = 0; uint32_t queueFamilyCount = 0; - instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, nullptr - ); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, nullptr); std::vector queueFamilies(queueFamilyCount); instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, queueFamilies.data() + physDevice, + &queueFamilyCount, + queueFamilies.data() ); for (uint32_t i = 0; i < queueFamilyCount; ++i) { if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { @@ -989,13 +988,7 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co } } - auto* tex = new WlDmaBufferVulkanQSGTexture( - devFuncs, - device, - image, - memory, - qsgTexture - ); + auto* tex = new WlDmaBufferVulkanQSGTexture(devFuncs, device, image, memory, qsgTexture); qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; return tex; } diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index d5a3e53..2ebe3fd 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,15 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) { if (this->window == nullptr) { this->window = new QQuickWindow(); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } this->mContentItem->setParentItem(this->window->contentItem());