From dc99d6677ceac1bd9977c9a9c814aeb945a89db5 Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 3 Oct 2026 20:26:00 +0200 Subject: [PATCH] remove layer usage to reduce vram consumption --- Drawers/Windows.qml | 63 +-- Modules/Settings/Common/WallpaperCropper.qml | 2 - Modules/Wallpaper/Wallpaper.qml | 1 + Plugins/ZShell/Blobs/CMakeLists.txt | 4 + Plugins/ZShell/Blobs/blobgroup2.cpp | 382 +++++++++++++++++++ Plugins/ZShell/Blobs/blobgroup2.hpp | 128 +++++++ Plugins/ZShell/Blobs/blobinvertedrect2.cpp | 44 +++ Plugins/ZShell/Blobs/blobinvertedrect2.hpp | 58 +++ Plugins/ZShell/Blobs/blobmaterial.cpp | 9 +- Plugins/ZShell/Blobs/blobmaterial.hpp | 5 + Plugins/ZShell/Blobs/blobrect.cpp | 119 +++--- Plugins/ZShell/Blobs/blobrect.hpp | 7 + Plugins/ZShell/Blobs/blobrect2.cpp | 353 +++++++++++++++++ Plugins/ZShell/Blobs/blobrect2.hpp | 167 ++++++++ Plugins/ZShell/Blobs/blobshape.cpp | 6 +- Plugins/ZShell/Blobs/blobshape2.cpp | 67 ++++ Plugins/ZShell/Blobs/blobshape2.hpp | 76 ++++ Plugins/ZShell/Blobs/shaders/blob.frag | 56 ++- Plugins/ZShell/Internal/wallpaperimage.cpp | 4 +- shell.qml | 18 +- 20 files changed, 1455 insertions(+), 114 deletions(-) create mode 100644 Plugins/ZShell/Blobs/blobgroup2.cpp create mode 100644 Plugins/ZShell/Blobs/blobgroup2.hpp create mode 100644 Plugins/ZShell/Blobs/blobinvertedrect2.cpp create mode 100644 Plugins/ZShell/Blobs/blobinvertedrect2.hpp create mode 100644 Plugins/ZShell/Blobs/blobrect2.cpp create mode 100644 Plugins/ZShell/Blobs/blobrect2.hpp create mode 100644 Plugins/ZShell/Blobs/blobshape2.cpp create mode 100644 Plugins/ZShell/Blobs/blobshape2.hpp diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 2121c27..84b53e5 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -52,7 +52,7 @@ CustomWindow { property var root: Quickshell.shellDir readonly property ScreenState screenState: ShellState.forScreen(screen) readonly property real sdfBorderOffset: 2 * fsTransitionProg - readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg)) + readonly property real shadowOpacity: 0.5 * (1 - (bar.isHovered ? 0 : fsTransitionProg)) property color surfaceColor: Colors.tPalette.m3surface WlrLayershell.exclusionMode: ExclusionMode.Ignore @@ -145,7 +145,6 @@ CustomWindow { HyprlandFocusGrab { id: focusGrab - windows: [root] active: { const s = root.screenState; const c = Config; @@ -162,6 +161,7 @@ CustomWindow { return true; return false; } + windows: [root] onCleared: { root.screenState.launcher = false; @@ -187,23 +187,19 @@ CustomWindow { id: surface anchors.fill: parent - layer.enabled: true opacity: root.surfaceColor.a - layer.effect: MultiEffect { - blurMax: 32 - shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity)) - shadowEnabled: true - } - - BlobGroup { + BlobGroup2 { id: blobGroup + anchors.fill: parent color: root.surfaceColor + shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity)) + shadowRadius: 18 smoothing: Config.bar.smoothing } - BlobInvertedRect { + BlobInvertedRect2 { anchors.fill: parent anchors.margins: -50 borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset @@ -299,13 +295,15 @@ CustomWindow { PanelBg { id: resourcesBg + property real extraExtent: 0.2 + deformAmount: 0.05 - implicitHeight: panels.resources.height - implicitWidth: panels.resources.width + implicitHeight: panels.resources.height * (1 + extraExtent) + implicitWidth: panels.resources.width * (1 + extraExtent) panel: panels.resourcesWrapper radius: Tokens.rounding.largeIncreased + Tokens.padding.small - x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) - y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) + x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) - panels.resources.width * extraExtent + y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) - panels.resources.height * extraExtent } PanelBg { @@ -335,9 +333,15 @@ CustomWindow { PanelBg { id: clipboardBg + property real extraExtent: 0.2 + deformAmount: 0.03 + implicitHeight: panels.clipboard.height * (1 + extraExtent) + implicitWidth: panels.clipboard.width * (1 + extraExtent) panel: panels.clipboard radius: 29 + x: panels.clipboard.x + geometry.insetLeft(root.borderThickness) - panels.clipboard.width * extraExtent + y: panels.clipboard.y + geometry.insetTop(root.borderThickness) } } @@ -345,34 +349,8 @@ CustomWindow { id: drawing anchors.fill: parent - layer.enabled: true screenState: root.screenState z: 2 - - layer.effect: MultiEffect { - maskEnabled: true - maskInverted: true - maskSource: maskSource - } - } - - Item { - id: maskSource - - anchors.fill: parent - layer.enabled: true - visible: false - - CustomRect { - readonly property int extraWidth: radius - - color: "white" - implicitHeight: panels.drawing.height - implicitWidth: panels.drawing.width + extraWidth - radius: drawingBg.radius - x: -extraWidth + root.borderThickness + panels.drawing.x - y: panels.drawing.y + bar.implicitHeight - } } Interactions2 { @@ -452,10 +430,11 @@ CustomWindow { } } - component PanelBg: BlobRect { + component PanelBg: BlobRect2 { property real deformAmount: 0.15 required property Item panel + damping: 28.3 deformScale: (deformAmount * Config.appearance.deform.scale) / 10000 group: blobGroup implicitHeight: panel.height diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index 70418ee..393b3d9 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -235,8 +235,6 @@ Item { implicitWidth: Math.round(fitW) radius: Tokens.rounding.largeIncreased - onAspectChanged: console.log(aspect) - Loader { active: opacity > 0 anchors.centerIn: parent diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index 9de4551..02fe54b 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -21,6 +21,7 @@ Loader { WlrLayershell.namespace: "ZShell-Wallpaper" color: "transparent" screen: modelData + surfaceFormat.opaque: true anchors { bottom: true diff --git a/Plugins/ZShell/Blobs/CMakeLists.txt b/Plugins/ZShell/Blobs/CMakeLists.txt index a51b194..32798f5 100644 --- a/Plugins/ZShell/Blobs/CMakeLists.txt +++ b/Plugins/ZShell/Blobs/CMakeLists.txt @@ -5,6 +5,10 @@ qml_module(ZShell-blobs blobshape.cpp blobrect.cpp blobinvertedrect.cpp + blobgroup2.cpp + blobshape2.cpp + blobrect2.cpp + blobinvertedrect2.cpp blobmaterial.cpp LIBRARIES Qt::Quick diff --git a/Plugins/ZShell/Blobs/blobgroup2.cpp b/Plugins/ZShell/Blobs/blobgroup2.cpp new file mode 100644 index 0000000..219d48c --- /dev/null +++ b/Plugins/ZShell/Blobs/blobgroup2.cpp @@ -0,0 +1,382 @@ +#include "blobgroup2.hpp" +#include "blobinvertedrect2.hpp" +#include "blobshape2.hpp" + +#include +#include + +#include +#include +#include + +static float cpuSdBox( + float px, float py, float cx, float cy, float hw, float hh) { + const float dx = std::abs(px - cx) - hw; + const float dy = std::abs(py - cy) - hh; + const float mdx = std::max(dx, 0.0f); + const float mdy = std::max(dy, 0.0f); + return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); +} + +static float cpuSmoothstep(float edge0, float edge1, float x) { + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +static float cornerFillFactor(float sd, float smoothFactor) { + const float outside = cpuSmoothstep(0.0f, smoothFactor, sd); + const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd); + return std::max(outside, inside); +} + +BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +BlobGroup2::~BlobGroup2() { + for (auto* shape : std::as_const(m_shapes)) + shape->m_group = nullptr; + if (m_invertedRect) + static_cast(m_invertedRect)->m_group = nullptr; +} + +void BlobGroup2::setSmoothing(qreal s) { + if (qFuzzyCompare(m_smoothing, s)) return; + m_smoothing = s; + emit smoothingChanged(); + markDirty(); +} + +void BlobGroup2::setColor(const QColor& c) { + if (m_color == c) return; + m_color = c; + emit colorChanged(); + markDirty(); +} + +void BlobGroup2::setCornerFill(bool e) { + if (m_cornerFill == e) return; + m_cornerFill = e; + emit cornerFillChanged(); + markDirty(); +} + +void BlobGroup2::setShadowColor(const QColor& c) { + if (m_shadowColor == c) return; + m_shadowColor = c; + emit shadowColorChanged(); + markDirty(); +} + +void BlobGroup2::setShadowRadius(qreal r) { + if (qFuzzyCompare(m_shadowRadius, r)) return; + m_shadowRadius = r; + emit shadowRadiusChanged(); + markDirty(); +} + +void BlobGroup2::addShape(BlobShape2* shape) { + if (!shape || m_shapes.contains(shape)) return; + m_shapes.append(shape); + markDirty(); +} + +void BlobGroup2::removeShape(BlobShape2* shape) { + m_shapes.removeOne(shape); + markDirty(); +} + +void BlobGroup2::setInvertedRect(BlobInvertedRect2* rect) { + if (m_invertedRect == rect) return; + m_invertedRect = rect; + markDirty(); +} + +void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) { + if (m_invertedRect != rect) return; + m_invertedRect = nullptr; + markDirty(); +} + +void BlobGroup2::markDirty() { + m_physicsUpdated = false; + m_dataBuilt = false; + // Physics runs during polish, before the render pass. The panel content is + // transformed by deformMatrix from QML, so the matrix has to be final for this + // frame; building the data in updatePaintNode would let the transform lag one + // frame behind the material. + polish(); + update(); +} + +void BlobGroup2::updatePolish() { + // The group is the only thing that draws, so it also drives the shapes' + // physics. + ensureDataBuilt(); +} + +void BlobGroup2::ensurePhysicsUpdated() { + if (m_physicsUpdated) return; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} + +void BlobGroup2::ensureDataBuilt() { + if (m_dataBuilt) return; + ensurePhysicsUpdated(); + + // Physics can request the next frame through markDirty(); the data is + // refreshed here anyway, so the flag is set after it runs. + m_dataBuilt = true; + m_data.rects.clear(); + m_bounds = QRectF(); + + const float smoothFactor = static_cast(m_smoothing); + // The same reach the shader uses to decide whether a rect can contribute, plus + // the shadow, so every pixel the shader can give a value to is inside the node. + const double extent = static_cast(smoothFactor) * 1.5 + + static_cast(m_shadowRadius); + + // Group order, no pruning: the shader sees this list for every pixel. + // entryShapes maps a shared entry back to the shape it came from. + QVector entryShapes; + + for (auto* shape : std::as_const(m_shapes)) { + if (shape->width() <= 0 || shape->height() <= 0) continue; + if (static_cast(m_data.rects.size()) >= BlobGroupData::kMaxRects) + continue; + + const QPointF scene = shape->mapToScene(QPointF(0, 0)); + const QMatrix4x4& dm = shape->m_deformMatrix; + + BlobRectData r; + r.cx = static_cast(scene.x() + shape->width() / 2.0); + r.cy = static_cast(scene.y() + shape->height() / 2.0); + r.hw = static_cast(shape->width()) * 0.5f; + r.hh = static_cast(shape->height()) * 0.5f; + shape->cornerRadii(r.radius); + r.offsetX = dm(0, 3); + r.offsetY = dm(1, 3); + + const float a = dm(0, 0), b = dm(1, 0); + const float c = dm(0, 1), e = dm(1, 1); + const float det = a * e - c * b; + const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + r.invDeform[0] = e * invDet; + r.invDeform[1] = -b * invDet; + r.invDeform[2] = -c * invDet; + r.invDeform[3] = a * invDet; + + const float halfTr = 0.5f * (a + e); + const float halfDiff = 0.5f * (a - e); + r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); + + r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; + r.screenHalfY = std::abs(b) * r.hw + std::abs(e) * r.hh; + + m_data.rects.append(r); + m_bounds = m_bounds.united(QRectF( + static_cast(r.cx) + static_cast(r.offsetX) - + static_cast(r.screenHalfX) - extent, + static_cast(r.cy) + static_cast(r.offsetY) - + static_cast(r.screenHalfY) - extent, + 2.0 * (static_cast(r.screenHalfX) + extent), + 2.0 * (static_cast(r.screenHalfY) + extent))); + + entryShapes.append(shape); + } + + const int count = static_cast(m_data.rects.size()); + + // Exclusion gates only the smooth blend. + for (int i = 0; i < count; ++i) { + int mask = 0; + BlobShape2* const si = entryShapes[i]; + for (int j = 0; j < count; ++j) { + if (j == i) continue; + BlobShape2* const sj = entryShapes[j]; + if (si->isExcluded(sj) || sj->isExcluded(si)) mask |= (1 << j); + } + m_data.rects[i].excludeMask = mask; + } + + constexpr float minR = 2.0f; + const bool cornerFill = m_cornerFill; + + for (int i = 0; i < count; ++i) { + auto& ri = m_data.rects[i]; + BlobShape2* const si = entryShapes[i]; + float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; + + const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; + const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; + const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; + const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; + + if (cornerFill) { + for (int j = 0; j < count; ++j) { + if (j == i) continue; + if (ri.excludeMask & (1 << j)) continue; + BlobShape2* const sj = entryShapes[j]; + if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) + continue; + const auto& rj = m_data.rects[j]; + fTr = std::min( + fTr, + cornerFillFactor( + cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh), + smoothFactor)); + fBr = std::min( + fBr, + cornerFillFactor( + cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh), + smoothFactor)); + fBl = std::min( + fBl, + cornerFillFactor( + cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh), + smoothFactor)); + fTl = std::min( + fTl, + cornerFillFactor( + cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh), + smoothFactor)); + } + } + + ri.radius[0] = std::max(ri.radius[0] * fTr, minR); + ri.radius[1] = std::max(ri.radius[1] * fBr, minR); + ri.radius[2] = std::max(ri.radius[2] * fBl, minR); + ri.radius[3] = std::max(ri.radius[3] * fTl, minR); + } + + // Inverted rect. The frame term is evaluated for every pixel in the bounds, + // so the group flag cannot be gated per shape. + if (!m_invertedRect) return; + + auto* inv = static_cast(m_invertedRect); + if (inv->width() <= 0 || inv->height() <= 0) return; + + const QPointF invScene = inv->mapToScene(QPointF(0, 0)); + const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = + static_cast(invScene.y() + inv->height() / 2.0); + const float outerHW = static_cast(inv->width()) * 0.5f; + const float outerHH = static_cast(inv->height()) * 0.5f; + + const auto* invRect = m_invertedRect; + const float innerCX = + outerCX + static_cast( + (invRect->borderLeft() - invRect->borderRight()) / 2.0); + const float innerCY = + outerCY + static_cast( + (invRect->borderTop() - invRect->borderBottom()) / 2.0); + const float innerHW = + outerHW - static_cast( + (invRect->borderLeft() + invRect->borderRight()) / 2.0); + const float innerHH = + outerHH - static_cast( + (invRect->borderTop() + invRect->borderBottom()) / 2.0); + + m_data.hasInverted = true; + m_data.invertedRadius = static_cast(invRect->radius()); + + m_data.invertedOuter[0] = outerCX; + m_data.invertedOuter[1] = outerCY; + m_data.invertedOuter[2] = outerHW; + m_data.invertedOuter[3] = outerHH; + + m_data.invertedInner[0] = innerCX; + m_data.invertedInner[1] = innerCY; + m_data.invertedInner[2] = innerHW; + m_data.invertedInner[3] = innerHH; + + m_bounds = m_bounds.united(QRectF( + static_cast(outerCX) - static_cast(outerHW) - extent, + static_cast(outerCY) - static_cast(outerHH) - extent, + 2.0 * (static_cast(outerHW) + extent), + 2.0 * (static_cast(outerHH) + extent))); +} + +void BlobGroup2::fillMaterial(BlobMaterial* material) { + material->m_paddedX = static_cast(m_bounds.x()); + material->m_paddedY = static_cast(m_bounds.y()); + material->m_paddedW = static_cast(m_bounds.width()); + material->m_paddedH = static_cast(m_bounds.height()); + + material->m_smoothFactor = static_cast(m_smoothing); + // The single group draw has no owner, so the shader skips the ownership test. + material->m_myIndex = -2; + material->m_color = m_color; + material->m_hasInverted = m_data.hasInverted ? 1 : 0; + material->m_invertedRadius = data().invertedRadius; + memcpy( + material->m_invertedOuter, + data().invertedOuter, + sizeof(data().invertedOuter)); + memcpy( + material->m_invertedInner, + data().invertedInner, + sizeof(data().invertedInner)); + + material->m_shadowRadius = static_cast(m_shadowRadius); + material->m_shadowColor[0] = static_cast(m_shadowColor.redF()); + material->m_shadowColor[1] = static_cast(m_shadowColor.greenF()); + material->m_shadowColor[2] = static_cast(m_shadowColor.blueF()); + material->m_shadowColor[3] = static_cast(m_shadowColor.alphaF()); + + const int count = + static_cast(qMin(data().rects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = data().rects[i]; +} + +QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + // Data is built during polish. This only covers a render pass that happens + // without one. + ensureDataBuilt(); + + if (m_data.rects.isEmpty() && !m_data.hasInverted) { + delete oldNode; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode; + + auto* geometry = + new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // The shader evaluates the field at the scene position a vertex maps to, so + // the group item itself must not be transformed. The vertex positions are the + // bounds in this item's frame; the material carries the same rect in scene + // coordinates. + auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); + const QPointF p0 = mapFromScene(m_bounds.topLeft()); + const QPointF p1 = mapFromScene(m_bounds.bottomRight()); + + v[0].set(static_cast(p0.x()), static_cast(p0.y()), 0.0f, 0.0f); + v[1].set(static_cast(p1.x()), static_cast(p0.y()), 1.0f, 0.0f); + v[2].set(static_cast(p0.x()), static_cast(p1.y()), 0.0f, 1.0f); + v[3].set(static_cast(p1.x()), static_cast(p1.y()), 1.0f, 1.0f); + + node->markDirty(QSGNode::DirtyGeometry); + + fillMaterial(static_cast(node->material())); + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} diff --git a/Plugins/ZShell/Blobs/blobgroup2.hpp b/Plugins/ZShell/Blobs/blobgroup2.hpp new file mode 100644 index 0000000..9f922e6 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobgroup2.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include "blobmaterial.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +class BlobShape2; +class BlobInvertedRect2; + +// One shared valuation for a whole group, drawn by one node. +// +// The shapes here are data only: they say where the material should be. The group +// draws a single quad over everything the shader can evaluate, so a pixel cannot +// be evaluated by one shape and missed by another, and no second draw can +// disagree with the first. That is what makes overlapping shapes read as one +// surface - one value, one writer, one alpha per pixel. +struct BlobGroupData { + // The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit + // mask. The shared list is capped at 16, in group order, so the cap is the + // same list for every pixel. + static constexpr int kMaxRects = 16; + + QVector rects; + bool hasInverted = false; + float invertedRadius = 0; + float invertedOuter[4] = {}; + float invertedInner[4] = {}; +}; + +class BlobGroup2 : public QQuickItem { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY( + qreal smoothing READ smoothing WRITE setSmoothing NOTIFY + smoothingChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + Q_PROPERTY( + bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY + cornerFillChanged) + Q_PROPERTY( + QColor shadowColor READ shadowColor WRITE setShadowColor NOTIFY + shadowColorChanged) + Q_PROPERTY( + qreal shadowRadius READ shadowRadius WRITE setShadowRadius NOTIFY + shadowRadiusChanged) + + friend class BlobShape2; + + public: + explicit BlobGroup2(QQuickItem* parent = nullptr); + ~BlobGroup2() override; + + [[nodiscard]] qreal smoothing() const { return m_smoothing; } + + void setSmoothing(qreal s); + + [[nodiscard]] QColor color() const { return m_color; } + + void setColor(const QColor& c); + + [[nodiscard]] bool cornerFill() const { return m_cornerFill; } + + void setCornerFill(bool e); + + [[nodiscard]] QColor shadowColor() const { return m_shadowColor; } + + void setShadowColor(const QColor& c); + + [[nodiscard]] qreal shadowRadius() const { return m_shadowRadius; } + + void setShadowRadius(qreal r); + + void addShape(BlobShape2* shape); + void removeShape(BlobShape2* shape); + + void setInvertedRect(BlobInvertedRect2* rect); + void clearInvertedRect(BlobInvertedRect2* rect); + + [[nodiscard]] const QList& shapes() const { return m_shapes; } + + [[nodiscard]] BlobInvertedRect2* invertedRect() const { + return m_invertedRect; + } + + [[nodiscard]] const BlobGroupData& data() const { return m_data; } + + // One draw per group, so a change to any shape changes the value for every + // pixel. Refresh the whole group. + void markDirty(); + + signals: + void smoothingChanged(); + void colorChanged(); + void cornerFillChanged(); + void shadowColorChanged(); + void shadowRadiusChanged(); + + protected: + void updatePolish() override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + private: + void ensurePhysicsUpdated(); + void ensureDataBuilt(); + void fillMaterial(BlobMaterial* material); + + qreal m_smoothing = 32.0; + QColor m_color{0x44, 0x88, 0xff}; + bool m_cornerFill = true; + QColor m_shadowColor{0, 0, 0, 128}; + qreal m_shadowRadius = 0.0; + QList m_shapes; + BlobInvertedRect2* m_invertedRect = nullptr; + bool m_physicsUpdated = false; + bool m_dataBuilt = false; + BlobGroupData m_data; + + // Scene-space union of what the shader can evaluate for any shape. The node + // covers exactly this, so gaps are not possible. + QRectF m_bounds; +}; diff --git a/Plugins/ZShell/Blobs/blobinvertedrect2.cpp b/Plugins/ZShell/Blobs/blobinvertedrect2.cpp new file mode 100644 index 0000000..2972cf2 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobinvertedrect2.cpp @@ -0,0 +1,44 @@ +#include "blobinvertedrect2.hpp" +#include "blobgroup2.hpp" + +BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {} + +BlobInvertedRect2::~BlobInvertedRect2() { + if (m_group) m_group->clearInvertedRect(this); +} + +void BlobInvertedRect2::setBorderLeft(qreal v) { + if (qFuzzyCompare(m_borderLeft, v)) return; + m_borderLeft = v; + emit borderLeftChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setBorderRight(qreal v) { + if (qFuzzyCompare(m_borderRight, v)) return; + m_borderRight = v; + emit borderRightChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setBorderTop(qreal v) { + if (qFuzzyCompare(m_borderTop, v)) return; + m_borderTop = v; + emit borderTopChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setBorderBottom(qreal v) { + if (qFuzzyCompare(m_borderBottom, v)) return; + m_borderBottom = v; + emit borderBottomChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::registerWithGroup() { + if (m_group) m_group->setInvertedRect(this); +} + +void BlobInvertedRect2::unregisterFromGroup() { + if (m_group) m_group->clearInvertedRect(this); +} diff --git a/Plugins/ZShell/Blobs/blobinvertedrect2.hpp b/Plugins/ZShell/Blobs/blobinvertedrect2.hpp new file mode 100644 index 0000000..4b7fb85 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobinvertedrect2.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "blobshape2.hpp" + +#include + +class BlobInvertedRect2 : public BlobShape2 { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY( + qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY + borderLeftChanged) + Q_PROPERTY( + qreal borderRight READ borderRight WRITE setBorderRight NOTIFY + borderRightChanged) + Q_PROPERTY( + qreal borderTop READ borderTop WRITE setBorderTop NOTIFY + borderTopChanged) + Q_PROPERTY( + qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY + borderBottomChanged) + + public: + explicit BlobInvertedRect2(QQuickItem* parent = nullptr); + ~BlobInvertedRect2() override; + + [[nodiscard]] qreal borderLeft() const { return m_borderLeft; } + + void setBorderLeft(qreal v); + + [[nodiscard]] qreal borderRight() const { return m_borderRight; } + + void setBorderRight(qreal v); + + [[nodiscard]] qreal borderTop() const { return m_borderTop; } + + void setBorderTop(qreal v); + + [[nodiscard]] qreal borderBottom() const { return m_borderBottom; } + + void setBorderBottom(qreal v); + + signals: + void borderLeftChanged(); + void borderRightChanged(); + void borderTopChanged(); + void borderBottomChanged(); + + protected: + void registerWithGroup() override; + void unregisterFromGroup() override; + + private: + qreal m_borderLeft = 0; + qreal m_borderRight = 0; + qreal m_borderTop = 0; + qreal m_borderBottom = 0; +}; diff --git a/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp index cdd5cd7..528d617 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.cpp +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData( Q_UNUSED(oldMaterial); auto* mat = static_cast(newMaterial); QByteArray* buf = state.uniformData(); - Q_ASSERT(buf->size() >= 1440); + Q_ASSERT(buf->size() >= 1472); if (state.isMatrixDirty()) { const QMatrix4x4 m = state.combinedMatrix(); @@ -56,7 +56,7 @@ bool BlobMaterialShader::updateUniformData( // Rect count (offset 88) memcpy(buf->data() + 88, &mat->m_rectCount, 4); - // My index (offset 92) + // My index (offset 92). -2 skips the ownership test in the shader. memcpy(buf->data() + 92, &mat->m_myIndex, 4); // Color as vec4 (offset 96, 16 bytes) @@ -100,5 +100,10 @@ bool BlobMaterialShader::updateUniformData( memcpy(buf->data() + base + 64, r.radius, 16); } + // Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned, + // so it lands at 1456. + memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); + memcpy(buf->data() + 1456, mat->m_shadowColor, 16); + return true; } diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp index 4a30448..19536a5 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.hpp +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -32,12 +32,17 @@ class BlobMaterial : public QSGMaterial { float m_paddedH = 0; float m_smoothFactor = 32.0f; int m_rectCount = 0; + // -2 means no owner test: the single group draw writes every pixel it covers. int m_myIndex = -2; QColor m_color{0x44, 0x88, 0xff}; int m_hasInverted = 0; float m_invertedRadius = 0; float m_invertedOuter[4] = {}; float m_invertedInner[4] = {}; + // Phase 2: shadow around the material edges. Matches the uniform block, which + // puts these after rectData. + float m_shadowRadius = 0.0f; + float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; BlobRectData m_rects[16] = {}; }; diff --git a/Plugins/ZShell/Blobs/blobrect.cpp b/Plugins/ZShell/Blobs/blobrect.cpp index f7dc72c..3100604 100644 --- a/Plugins/ZShell/Blobs/blobrect.cpp +++ b/Plugins/ZShell/Blobs/blobrect.cpp @@ -5,6 +5,10 @@ #include #include +namespace { +constexpr float k_retireMs = 120.0f; +} + BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {} BlobRect::~BlobRect() { @@ -12,41 +16,75 @@ BlobRect::~BlobRect() { } void BlobRect::updatePolish() { - if (m_physicsActive) { - float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + - std::abs(m_dm11 - 1.0f); - float totalVel = - std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + BlobShape::updatePolish(); - if (totalDelta < 0.004f && totalVel < 0.05f) { + if (!m_physicsActive) return; + + if (m_retiring) { + const float t = std::clamp( + static_cast(m_retireElapsed.elapsed()) / k_retireMs, + 0.0f, + 1.0f); + const float s = t * t * (3.0f - 2.0f * t); + + if (t >= 1.0f) { m_dm00 = 1.0f; m_dm01 = 0.0f; m_dm11 = 1.0f; - m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_deformMatrix = QMatrix4x4(); - emit rawDeformMatrixChanged(); - updateCenteredDeformMatrix(); + m_retiring = false; m_physicsActive = false; - - if (m_group) { - QMetaObject::invokeMethod( - this, - [this]() { - if (m_group) m_group->markDirty(); - }, - Qt::QueuedConnection); - } } else { - QMetaObject::invokeMethod( - this, - [this]() { - if (m_physicsActive && m_group) m_group->markDirty(); - }, - Qt::QueuedConnection); + m_dm00 = m_retire00 + (1.0f - m_retire00) * s; + m_dm01 = m_retire01 - m_retire01 * s; + m_dm11 = m_retire11 + (1.0f - m_retire11) * s; + m_deformMatrix = QMatrix4x4( + m_dm00, + m_dm01, + 0, + 0, + m_dm01, + m_dm11, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1); } + + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + + if (!m_physicsActive) return; + } else { + const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f); + const float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire(); } - BlobShape::updatePolish(); + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) m_group->markDirty(); + }, + Qt::QueuedConnection); +} + +void BlobRect::beginRetire() { + m_retire00 = m_dm00; + m_retire01 = m_dm01; + m_retire11 = m_dm11; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_retireElapsed.start(); + m_retiring = true; } void BlobRect::updatePhysics() { @@ -74,6 +112,13 @@ void BlobRect::updatePhysics() { const float speed = std::sqrt(velX * velX + velY * velY); + if (m_retiring) { + if (speed > 5.0f) + m_retiring = false; + else + return; + } + if (!m_physicsActive) { if (speed < 5.0f) return; m_physicsActive = true; @@ -297,6 +342,8 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty* prop) { } void BlobRect::checkAtRest(float speed) { + if (m_retiring) return; + constexpr float kEpsilon = 0.002f; const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && @@ -304,25 +351,5 @@ void BlobRect::checkAtRest(float speed) { std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; - if (atRest) { - m_dm00 = 1.0f; - m_dm01 = 0.0f; - m_dm11 = 1.0f; - m_dmVel00 = 0.0f; - m_dmVel01 = 0.0f; - m_dmVel11 = 0.0f; - m_deformMatrix = QMatrix4x4(); - emit rawDeformMatrixChanged(); - updateCenteredDeformMatrix(); - m_physicsActive = false; - - if (m_group) { - QMetaObject::invokeMethod( - this, - [this]() { - if (m_group) m_group->markDirty(); - }, - Qt::QueuedConnection); - } - } + if (atRest) beginRetire(); } diff --git a/Plugins/ZShell/Blobs/blobrect.hpp b/Plugins/ZShell/Blobs/blobrect.hpp index 51a6157..68df1c3 100644 --- a/Plugins/ZShell/Blobs/blobrect.hpp +++ b/Plugins/ZShell/Blobs/blobrect.hpp @@ -108,6 +108,7 @@ class BlobRect : public BlobShape { private: void checkAtRest(float speed); + void beginRetire(); // Physics state QPointF m_prevScenePos; @@ -115,6 +116,12 @@ class BlobRect : public BlobShape { bool m_physicsActive = false; bool m_hasPrevPos = false; + bool m_retiring = false; + QElapsedTimer m_retireElapsed; + float m_retire00 = 1.0f; + float m_retire01 = 0.0f; + float m_retire11 = 1.0f; + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, // m11) Rest state is identity: m00=1, m01=0, m11=1 float m_dm00 = 1.0f; diff --git a/Plugins/ZShell/Blobs/blobrect2.cpp b/Plugins/ZShell/Blobs/blobrect2.cpp new file mode 100644 index 0000000..3fdfa28 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobrect2.cpp @@ -0,0 +1,353 @@ +#include "blobrect2.hpp" +#include "blobgroup2.hpp" + +#include +#include + +namespace { +constexpr float k_retireMs = 120.0f; +} // namespace + +BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {} + +BlobRect2::~BlobRect2() { + if (m_group) m_group->removeShape(this); +} + +void BlobRect2::finishAtIdentity() { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_deformMatrix = QMatrix4x4(); + m_retiring = false; + m_physicsActive = false; + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); +} + +void BlobRect2::beginRetire() { + // The settle check would otherwise re-enter this every frame and restart the + // curve timer, so the curve never reaches t = 1. + if (m_retiring) return; + + m_retire00 = m_dm00; + m_retire01 = m_dm01; + m_retire11 = m_dm11; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_retireElapsed.start(); + m_retiring = true; +} + +void BlobRect2::updatePhysics() { + // Shapes are data only, so the group drives physics here. + if (m_retiring) { + const float t = std::clamp( + static_cast(m_retireElapsed.elapsed()) / k_retireMs, + 0.0f, + 1.0f); + const float s = t * t * (3.0f - 2.0f * t); + + if (t >= 1.0f) { + finishAtIdentity(); + return; + } + + m_dm00 = m_retire00 + (1.0f - m_retire00) * s; + m_dm01 = m_retire01 - m_retire01 * s; + m_dm11 = m_retire11 + (1.0f - m_retire11) * s; + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + + if (!m_physicsActive) return; + } + + // No "not active" guard here: an inactive shape still has to be evaluated so it + // can see the panel start moving and become active. The frame loop is only kept + // alive while active, and it is requested before anything can return early — + // requesting it at the end lets a shape stop being evaluated with the matrix + // still off identity, which is what left the panel contents blurry. + if (m_physicsActive && m_group) m_group->markDirty(); + + const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + + if (!m_hasPrevPos) { + m_prevScenePos = scenePos; + m_elapsed.start(); + m_hasPrevPos = true; + return; + } + + const float dt = static_cast(m_elapsed.restart()) / 1000.0f; + if (dt > 0.1f || dt < 0.001f) { + m_prevScenePos = scenePos; + if (m_physicsActive) checkAtRest(0.0f); + return; + } + + const float velX = + static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = + static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + m_prevScenePos = scenePos; + + const float speed = std::sqrt(velX * velX + velY * velY); + + if (m_retiring) { + if (speed > 5.0f) + m_retiring = false; + else + return; + } + + if (!m_physicsActive) { + if (speed < 5.0f) return; + m_physicsActive = true; + } + + const float kStretchFactor = static_cast(m_deformScale); + constexpr float kMaxStretch = 0.35f; + + float target00 = 1.0f; + float target01 = 0.0f; + float target11 = 1.0f; + + if (speed > 5.0f) { + const float targetStretch = + 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetCompress = 1.0f / targetStretch; + + const float cosA = velX / speed; + const float sinA = velY / speed; + const float cos2 = cosA * cosA; + const float sin2 = sinA * sinA; + const float cs = cosA * sinA; + + target00 = targetStretch * cos2 + targetCompress * sin2; + target01 = (targetStretch - targetCompress) * cs; + target11 = targetStretch * sin2 + targetCompress * cos2; + } + + const float kStiffness = static_cast(m_stiffness); + const float kDamping = static_cast(m_damping); + const float invDamp = 1.0f / (1.0f + kDamping * dt); + + m_dmVel00 = (m_dmVel00 - kStiffness * (m_dm00 - target00) * dt) * invDamp; + m_dm00 += m_dmVel00 * dt; + + m_dmVel01 = (m_dmVel01 - kStiffness * (m_dm01 - target01) * dt) * invDamp; + m_dm01 += m_dmVel01 * dt; + + m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp; + m_dm11 += m_dmVel11 * dt; + + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + + // Start the retire as soon as the matrix is close to identity, as in v1. Waiting + // for every term to fall below kEpsilon keeps the matrix just off identity for + // longer, and the panel content is transformed by it, which reads as blur on + // text. + const float totalDelta = + std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + const float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + // The residual is already inside the rest epsilon, so the eased curve would only + // be aborted or restarted every frame by the panel creeping. Snap instead. + if (totalDelta < 0.004f && totalVel < 0.05f) { + finishAtIdentity(); + return; + } + + checkAtRest(speed); +} + +void BlobRect2::setTopLeftRadius(qreal r) { + if (!qFuzzyCompare(m_topLeftRadius, r)) { + m_topLeftRadius = r; + emit topLeftRadiusChanged(); + if (m_group) m_group->markDirty(); + } +} + +void BlobRect2::setTopRightRadius(qreal r) { + if (!qFuzzyCompare(m_topRightRadius, r)) { + m_topRightRadius = r; + emit topRightRadiusChanged(); + if (m_group) m_group->markDirty(); + } +} + +void BlobRect2::setBottomLeftRadius(qreal r) { + if (!qFuzzyCompare(m_bottomLeftRadius, r)) { + m_bottomLeftRadius = r; + emit bottomLeftRadiusChanged(); + if (m_group) m_group->markDirty(); + } +} + +void BlobRect2::setBottomRightRadius(qreal r) { + if (!qFuzzyCompare(m_bottomRightRadius, r)) { + m_bottomRightRadius = r; + emit bottomRightRadiusChanged(); + if (m_group) m_group->markDirty(); + } +} + +void BlobRect2::cornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto base = std::min(static_cast(m_radius), maxR); + out[0] = std::min( + m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, + maxR); + out[1] = std::min( + m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) + : base, + maxR); + out[2] = std::min( + m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, + maxR); + out[3] = std::min( + m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, + maxR); +} + +bool BlobRect2::isExcluded(const BlobShape2* other) const { + for (const auto& ptr : m_exclude) { + if (ptr == other) return true; + } + return false; +} + +bool BlobRect2::isCornerExcluded(const BlobShape2* other) const { + for (const auto& ptr : m_excludeCorners) { + if (ptr == other) return true; + } + return false; +} + +QQmlListProperty BlobRect2::exclude() { + return QQmlListProperty( + this, + nullptr, + &excludeAppend, + &excludeCount, + &excludeAt, + &excludeClear, + &excludeReplace, + &excludeRemoveLast); +} + +QQmlListProperty BlobRect2::excludeCorners() { + return QQmlListProperty( + this, + nullptr, + &excludeCornersAppend, + &excludeCornersCount, + &excludeCornersAt, + &excludeCornersClear, + &excludeCornersReplace, + &excludeCornersRemoveLast); +} + +void BlobRect2::excludeAppend( + QQmlListProperty* prop, BlobRect2* rect) { + auto* self = static_cast(prop->object); + self->m_exclude.append(rect); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeChanged(); +} + +qsizetype BlobRect2::excludeCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_exclude.size(); +} + +BlobRect2* BlobRect2::excludeAt( + QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_exclude.at(index); +} + +void BlobRect2::excludeClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) return; + self->m_exclude.clear(); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect2::excludeReplace( + QQmlListProperty* prop, qsizetype index, BlobRect2* rect) { + auto* self = static_cast(prop->object); + self->m_exclude[index] = rect; + if (self->m_group) self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect2::excludeRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) return; + self->m_exclude.removeLast(); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect2::excludeCornersAppend( + QQmlListProperty* prop, BlobRect2* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners.append(rect); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +qsizetype BlobRect2::excludeCornersCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.size(); +} + +BlobRect2* BlobRect2::excludeCornersAt( + QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.at(index); +} + +void BlobRect2::excludeCornersClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) return; + self->m_excludeCorners.clear(); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect2::excludeCornersReplace( + QQmlListProperty* prop, qsizetype index, BlobRect2* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners[index] = rect; + if (self->m_group) self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect2::excludeCornersRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) return; + self->m_excludeCorners.removeLast(); + if (self->m_group) self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect2::checkAtRest(float speed) { + if (m_retiring) return; + + constexpr float kEpsilon = 0.002f; + const bool atRest = + std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && + speed < 5.0f; + + if (atRest) beginRetire(); +} diff --git a/Plugins/ZShell/Blobs/blobrect2.hpp b/Plugins/ZShell/Blobs/blobrect2.hpp new file mode 100644 index 0000000..800b37b --- /dev/null +++ b/Plugins/ZShell/Blobs/blobrect2.hpp @@ -0,0 +1,167 @@ +#pragma once + +#include "blobshape2.hpp" + +#include +#include +#include +#include + +class BlobRect2 : public BlobShape2 { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY( + qreal stiffness READ stiffness WRITE setStiffness NOTIFY + stiffnessChanged) + Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY( + qreal deformScale READ deformScale WRITE setDeformScale NOTIFY + deformScaleChanged) + Q_PROPERTY( + QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY( + QQmlListProperty excludeCorners READ excludeCorners NOTIFY + excludeCornersChanged) + Q_PROPERTY( + qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY + topLeftRadiusChanged) + Q_PROPERTY( + qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY + topRightRadiusChanged) + Q_PROPERTY( + qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius + NOTIFY bottomLeftRadiusChanged) + Q_PROPERTY( + qreal bottomRightRadius READ bottomRightRadius WRITE + setBottomRightRadius NOTIFY bottomRightRadiusChanged) + + public: + explicit BlobRect2(QQuickItem* parent = nullptr); + ~BlobRect2() override; + + [[nodiscard]] qreal stiffness() const { return m_stiffness; } + + void setStiffness(qreal s) { + if (!qFuzzyCompare(m_stiffness, s)) { + m_stiffness = s; + emit stiffnessChanged(); + } + } + + [[nodiscard]] qreal damping() const { return m_damping; } + + void setDamping(qreal value) { + if (!qFuzzyCompare(m_damping, value)) { + m_damping = value; + emit dampingChanged(); + } + } + + [[nodiscard]] qreal deformScale() const { return m_deformScale; } + + void setDeformScale(qreal s) { + if (!qFuzzyCompare(m_deformScale, s)) { + m_deformScale = s; + emit deformScaleChanged(); + } + } + + QQmlListProperty exclude(); + QQmlListProperty excludeCorners(); + + bool isExcluded(const BlobShape2* other) const override; + bool isCornerExcluded(const BlobShape2* other) const override; + void cornerRadii(float out[4]) const override; + + [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } + + void setTopLeftRadius(qreal r); + + [[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; } + + void setTopRightRadius(qreal r); + + [[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; } + + void setBottomLeftRadius(qreal r); + + [[nodiscard]] qreal bottomRightRadius() const { + return m_bottomRightRadius; + } + + void setBottomRightRadius(qreal r); + + signals: + void stiffnessChanged(); + void dampingChanged(); + void deformScaleChanged(); + void excludeChanged(); + void excludeCornersChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); + + protected: + void updatePhysics() override; + + private: + void checkAtRest(float speed); + void beginRetire(); + void finishAtIdentity(); + + // Physics state + QPointF m_prevScenePos; + QElapsedTimer m_elapsed; + bool m_physicsActive = false; + bool m_hasPrevPos = false; + + bool m_retiring = false; + QElapsedTimer m_retireElapsed; + float m_retire00 = 1.0f; + float m_retire01 = 0.0f; + float m_retire11 = 1.0f; + + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, + // m11) Rest state is identity: m00=1, m01=0, m11=1 + float m_dm00 = 1.0f; + float m_dm01 = 0.0f; + float m_dm11 = 1.0f; + + // Spring velocities for each component + float m_dmVel00 = 0.0f; + float m_dmVel01 = 0.0f; + float m_dmVel11 = 0.0f; + + qreal m_stiffness = 200.0; + qreal m_damping = 16.0; + qreal m_deformScale = 0.0005; + + qreal m_topLeftRadius = -1; + qreal m_topRightRadius = -1; + qreal m_bottomLeftRadius = -1; + qreal m_bottomRightRadius = -1; + + QList> m_exclude; + QList> m_excludeCorners; + + static void excludeAppend( + QQmlListProperty* prop, BlobRect2* rect); + static qsizetype excludeCount(QQmlListProperty* prop); + static BlobRect2* excludeAt( + QQmlListProperty* prop, qsizetype index); + static void excludeClear(QQmlListProperty* prop); + static void excludeReplace( + QQmlListProperty* prop, qsizetype index, BlobRect2* rect); + static void excludeRemoveLast(QQmlListProperty* prop); + + static void excludeCornersAppend( + QQmlListProperty* prop, BlobRect2* rect); + static qsizetype excludeCornersCount(QQmlListProperty* prop); + static BlobRect2* excludeCornersAt( + QQmlListProperty* prop, qsizetype index); + static void excludeCornersClear(QQmlListProperty* prop); + static void excludeCornersReplace( + QQmlListProperty* prop, qsizetype index, BlobRect2* rect); + static void excludeCornersRemoveLast(QQmlListProperty* prop); +}; diff --git a/Plugins/ZShell/Blobs/blobshape.cpp b/Plugins/ZShell/Blobs/blobshape.cpp index 99d4eec..e33b2f3 100644 --- a/Plugins/ZShell/Blobs/blobshape.cpp +++ b/Plugins/ZShell/Blobs/blobshape.cpp @@ -168,8 +168,10 @@ void BlobShape::updatePolish() { if (other->width() <= 0 || other->height() <= 0) continue; - if (isExcluded(other)) continue; - + // Excluded shapes stay in the rect data. Exclusion only gates the smooth + // blend (via excludeMask below); ownership must still see every shape, + // otherwise two nodes can both believe they own a pixel and the pixel is + // blended twice. const QPointF otherScene = other->mapToScene(QPointF(0, 0)); bool include = false; diff --git a/Plugins/ZShell/Blobs/blobshape2.cpp b/Plugins/ZShell/Blobs/blobshape2.cpp new file mode 100644 index 0000000..8ce1232 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobshape2.cpp @@ -0,0 +1,67 @@ +#include "blobshape2.hpp" +#include "blobgroup2.hpp" + +#include +#include + +BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {} + +void BlobShape2::setGroup(BlobGroup2* g) { + if (m_group == g) return; + if (m_group && isComponentComplete()) unregisterFromGroup(); + m_group = g; + if (m_group && isComponentComplete()) registerWithGroup(); + emit groupChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobShape2::setRadius(qreal r) { + if (qFuzzyCompare(m_radius, r)) return; + m_radius = r; + emit radiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobShape2::componentComplete() { + QQuickItem::componentComplete(); + if (m_group) registerWithGroup(); +} + +void BlobShape2::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateCenteredDeformMatrix(); + + // One draw covers the whole group, so any shape change is a group change. + if (m_group) m_group->markDirty(); +} + +void BlobShape2::updateCenteredDeformMatrix() { + const auto cx = static_cast(width()) * 0.5f; + const auto cy = static_cast(height()) * 0.5f; + QMatrix4x4 result; + result.translate(cx, cy); + result *= m_deformMatrix; + result.translate(-cx, -cy); + if (m_centeredDeformMatrix != result) { + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); + } +} + +void BlobShape2::cornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto r = std::min(static_cast(m_radius), maxR); + out[0] = r; + out[1] = r; + out[2] = r; + out[3] = r; +} + +void BlobShape2::registerWithGroup() { + if (m_group) m_group->addShape(this); +} + +void BlobShape2::unregisterFromGroup() { + if (m_group) m_group->removeShape(this); +} diff --git a/Plugins/ZShell/Blobs/blobshape2.hpp b/Plugins/ZShell/Blobs/blobshape2.hpp new file mode 100644 index 0000000..3035082 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobshape2.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "blobgroup2.hpp" + +#include +#include + +class BlobGroup2; + +// Data-only shape. +// +// The group draws a single node for the whole group, so a shape here contributes +// only what the shader needs: position, radii, exclusion, physics. A shape does +// not create a render node, so no pixel can be evaluated by one shape and missed +// by another. +class BlobShape2 : public QQuickItem { + Q_OBJECT + Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY( + QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY( + QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY + rawDeformMatrixChanged) + + friend class BlobGroup2; + + public: + explicit BlobShape2(QQuickItem* parent = nullptr); + ~BlobShape2() override = default; + + [[nodiscard]] BlobGroup2* group() const { return m_group; } + + void setGroup(BlobGroup2* g); + + [[nodiscard]] qreal radius() const { return m_radius; } + + void setRadius(qreal r); + + [[nodiscard]] QMatrix4x4 deformMatrix() const { + return m_centeredDeformMatrix; + } + + [[nodiscard]] QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } + + signals: + void groupChanged(); + void radiusChanged(); + void deformMatrixChanged(); + void rawDeformMatrixChanged(); + + protected: + void componentComplete() override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + + virtual bool isExcluded(const BlobShape2*) const { return false; } + + virtual bool isCornerExcluded(const BlobShape2* /*other*/) const { + return false; + } + + virtual void cornerRadii(float out[4]) const; + + // Called by the group when it rebuilds the shared data, once per frame. + virtual void updatePhysics() {} + + virtual void registerWithGroup(); + virtual void unregisterFromGroup(); + void updateCenteredDeformMatrix(); + + BlobGroup2* m_group = nullptr; + qreal m_radius = 0; + QMatrix4x4 m_deformMatrix; // identity by default + QMatrix4x4 m_centeredDeformMatrix; +}; diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag index ddee3f7..1231bc6 100644 --- a/Plugins/ZShell/Blobs/shaders/blob.frag +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -19,6 +19,8 @@ layout(std140, binding = 0) uniform buf { vec4 invertedOuter; vec4 invertedInner; vec4 rectData[80]; + float shadowRadius; + vec4 shadowColor; }; float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { @@ -63,9 +65,10 @@ float smaxSharpA(float a, float b, float k) { void main() { vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); - // Phase 1: compute per-rect SDF, track owner. We can't smin yet because - // excluded pairs need to skip the smooth blend, which requires pairwise pass - // below. + // Phase 1: compute per-rect SDF, and track which rect owns the pixel for the + // per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner: + // myIndex stays -2, so the ownership test is skipped and this pass writes every + // pixel the quad covers. Excluded pairs still need the pairwise pass below. float dArr[16]; int owner = -2; float minDist = 1e10; @@ -82,8 +85,10 @@ void main() { // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; - // AABB early-out: skip rects far from this pixel - vec2 extent = sh.xy + vec2(smoothFactor * 1.5); + // AABB early-out: skip rects far from this pixel. The reach has to include the + // shadow, otherwise a rect that is only inside the shadow range would be + // skipped and its distance could not be used. + vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius); if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { dArr[i] = 1e10; @@ -251,14 +256,39 @@ void main() { } } - // Each renderer only outputs pixels it owns, but allow rendering - // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) - // myIndex == -1: inverted rect renders border-owned pixels - // myIndex >= 0: individual rect renders its owned pixels - if (owner != myIndex && mergedSdf > smoothFactor) + // A pixel this far from every rect cannot have a value, so it is not written. + // The one-unit margin keeps the outermost pixels of the quad out of the AA: their + // derivatives are undefined (the quad neighbour is outside the primitive), which + // is what drew a line along the coverage edge. + float reach = max(smoothFactor, shadowRadius); + if (mergedSdf > reach - 1.0) discard; - float fw = fwidth(mergedSdf); - float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); - fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; + // Only the owner writes a pixel, which is what makes the per-shape path look + // like one surface: qt_Opacity is applied once per pixel instead of once per + // overlapping draw. The single group draw has no owner (myIndex == -2), so the + // test is skipped and the value below is the only value a pixel can have. + if (myIndex >= -1 && owner != myIndex) + discard; + + // Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad + // boundary cannot make a wide edge. + float fw = min(fwidth(mergedSdf), 1.0); + float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + + // Phase 2: the shadow, as a distance falloff from the merged surface. The + // quadratic reaches zero with zero slope at its outer edge, so that edge needs no + // AA of its own. Only the single group draw writes a shadow: in the per-shape + // path overlapping draws would darken the same area twice. + float shadowAlpha = 0.0; + if (shadowRadius > 0.0 && myIndex == -2) { + float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0); + shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a; + } + + // Material over shadow, premultiplied. + float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha); + vec3 rgb = color.rgb * materialAlpha + + shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha); + fragColor = vec4(rgb, totalAlpha) * qt_Opacity; } diff --git a/Plugins/ZShell/Internal/wallpaperimage.cpp b/Plugins/ZShell/Internal/wallpaperimage.cpp index b6ad891..f6de9de 100644 --- a/Plugins/ZShell/Internal/wallpaperimage.cpp +++ b/Plugins/ZShell/Internal/wallpaperimage.cpp @@ -285,13 +285,15 @@ QSGNode* WallpaperImage::updatePaintNode( Q_UNUSED(oldNode); if (m_hasPending) { + qDebug() << "pending format" << m_pendingImage.format() + << double(m_pendingImage.sizeInBytes()) / 1048576.0 << "MB"; if (m_previousTexture) { delete m_previousTexture; m_previousTexture = nullptr; } m_previousTexture = m_texture; m_texture = window()->createTextureFromImage( - m_pendingImage, QQuickWindow::TextureHasAlphaChannel); + m_pendingImage, QQuickWindow::TextureIsOpaque); m_pendingImage = QImage(); m_hasPending = false; diff --git a/shell.qml b/shell.qml index 6df3f3a..f602e71 100644 --- a/shell.qml +++ b/shell.qml @@ -23,27 +23,33 @@ ShellRoot { settings.watchFiles: true - Drawers {} + Drawers { + } - Wallpaper {} + Wallpaper { + } - AreaPicker {} + AreaPicker { + } Lock { id: lock } - Shortcuts {} + Shortcuts { + } IdleMonitors { lock: lock } - Polkit {} + Polkit { + } LazyLoader { activeAsync: root.laptop - component: BatteryService {} + component: BatteryService { + } } }