From 89eaeca2f90f907083f1de8cb9a12989fb2c1209 Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 2 Sep 2026 12:25:13 +0100 Subject: [PATCH] quickshell: bar surface is the bar column, frame moves to its own background layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bar's PanelWindow was screen-sized because the frame band and every panel drew into it. Hyprland blurs that namespace, and blur is recomputed whenever what is behind the surface changes — with a game underneath, that is every pixel every frame. Measured at 3440x1440 it cost a third of the card; the diagnostic shrink took game-running GPU 57% -> 38% and made the stutter unprovokable. So the surface is now barWidth at rest and grows to the screen only while a dropdown, the toast or the launcher is up. exclusiveZone is unchanged, so windows never reflow on resize. chromeSdf's cutout derives from screen.width rather than the surface width, so the shader is identical at both sizes and the narrow surface simply clips it. The frame band moves to its own background-layer surface, below windows and matched by no blur rule. Its shader hides (rather than the window unmapping, which would flicker) while the bar is wide, since chromeSdf then draws the frame itself and melts panels into it. Co-Authored-By: Claude Opus 5 --- settings/quickshell.nix | 93 +++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 7b0698e..83a167f 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -1474,22 +1474,31 @@ in // the grab — it self-clears and instantly closes the panel. WlrLayershell.keyboardFocus: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - // Vertical bar on the left edge. The surface still covers the - // whole screen (the frame band and every panel are drawn into - // it); only the reserved strip is the bar itself. + // Vertical bar on the left edge. anchors { top: true left: true bottom: true } - // DIAGNOSTIC — normally bar.screen.width, because the frame - // band and every panel are drawn into this one surface. - // Shrinking it to the bar column clips all of that, which - // looks broken on purpose: the point is to measure whether - // blur and the SDF shader are expensive because of this - // surface's AREA. Revert this line to restore the shell. - implicitWidth: Theme.barWidth + // True while something is drawn outside the bar column and the + // surface therefore has to span the screen. + readonly property bool surfaceWide: bar.activeDropdown !== null + || launcherPanel.visible || toastItem.visible + + // THE performance knob for this whole shell. Hyprland blurs + // this namespace, and blur behind a surface is recomputed + // whenever what is BEHIND it changes — which, with a game + // running underneath, is every pixel of the screen every + // frame. At 3440x1440 that measured ~a third of the card and + // halved framerates; at 46px it is free. So the surface is the + // bar column at rest and only grows while a panel, the toast + // or the launcher is actually on screen, where a momentary + // cost doesn't matter. + // + // exclusiveZone stays fixed at barWidth, so windows never + // reflow when this resizes. + implicitWidth: bar.surfaceWide ? bar.screen.width : Theme.barWidth exclusiveZone: Theme.barWidth color: "transparent" @@ -1977,9 +1986,9 @@ in // buries a full corner radius inside the bar column. readonly property real surfLeftX: Theme.barWidth - meltInto / 2 property vector4d cutout: Qt.vector4d( - (Theme.barWidth + bar.width - Theme.frameWidth) / 2, + (Theme.barWidth + bar.screen.width - Theme.frameWidth) / 2, bar.height / 2, - (bar.width - Theme.frameWidth - Theme.barWidth) / 2, + (bar.screen.width - Theme.frameWidth - Theme.barWidth) / 2, bar.height / 2 - Theme.frameWidth) property vector4d panel: chrome.visible ? Qt.vector4d(surfLeftX + chrome.width / 2, (panelTop + panelBottom) / 2, @@ -2044,7 +2053,7 @@ in NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } - x: Math.round((bar.width - openW) / 2) + x: Math.round((bar.screen.width - openW) / 2) y: bar.height - Theme.frameWidth - openH width: openW height: openH @@ -6802,6 +6811,64 @@ in } } + // ── Screen frame band, on its OWN background-layer surface. + // + // This used to be drawn into the bar's surface, and it is the + // reason that surface had to be screen-sized — which is what + // made blur behind it cost a third of the GPU. The frame is + // static decoration that never takes input, so it belongs + // below windows: no blur rule matches this namespace, nothing + // ever repaints it, and gaps_out keeps it visible. + // + // Hidden (the shader, not the window — mapping and unmapping a + // layer surface on every panel open would flicker) while the + // bar's own surface is wide, because chromeSdf then draws the + // frame itself and melts panels into it. Two translucent + // copies of the same band would read as a darker strip. + PanelWindow { + id: frameWin + screen: bar.screen + WlrLayershell.namespace: "quickshell-frame" + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: ExclusionMode.Ignore + anchors { top: true; left: true; right: true; bottom: true } + color: "transparent" + mask: Region {} // decoration only, never takes a click + + ShaderEffect { + // Offset past the bar column so this and the bar never + // composite on top of each other. + x: Theme.barWidth + width: frameWin.width - Theme.barWidth + height: frameWin.height + visible: !bar.surfaceWide + + // Push the cutout's left edge well off-item so only + // straight edges reach the bar column — a rounded + // corner landing here would leave a nub beside the bar. + readonly property real cutLeft: -2 * Theme.radius + readonly property real cutRight: width - Theme.frameWidth + property vector4d cutout: Qt.vector4d( + (cutLeft + cutRight) / 2, + height / 2, + (cutRight - cutLeft) / 2, + height / 2 - Theme.frameWidth) + // No panels down here — the bar's surface owns those. + property vector4d panel: Qt.vector4d(0, 0, 0, 0) + property vector4d toast: Qt.vector4d(0, 0, 0, 0) + property vector4d launcher: Qt.vector4d(0, 0, 0, 0) + property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a) + property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1) + property vector2d res: Qt.vector2d(width, height) + property real cutoutR: Theme.radius + property real panelR: Theme.radius + property real meltK: Theme.radius / 2 + property real borderW: Theme.borderWidth + fragmentShader: "file://${chromeShader}" + } + } + // ── OSD: volume / brightness level, shown inline in the bar's // empty middle — the gap between the two clusters exists for // this. Volume is reactive: Pipewire reports changes from any