Watch
1
0
Fork
You've already forked souveraine
0

quickshell: boot bloom overlay + native pipewire audio

The C splash now plays only Acts I-III and hands off at ~6.8s; a new
BootBloom layershell overlay (modules/souveraine/boot) resumes Acts IV-V
(Souvie + bloom flower) over the live compositor, clock-aligned to the boot
epoch so the animation is continuous across the process swap. It maps as
Hyprland's first surface (top-level in ShellRoot, before Config.ready),
pokes splash-signal on its first frame to release the GPU, and LockScreen
clears bootBloomActive on secure to fade it out and reveal the lock. Bloom
shader is qsb-compiled from BootBloom.frag at deploy time.

Drop services/Audio.qml from the manifest: it was a pactl mirror written
for native PulseAudio and only tracked the default sink/source. The phone
runs PipeWire now, so ii's native Pipewire Audio service (full graph,
WirePlumber-owned volume) takes over.
This commit is contained in:
Fimeg 2026-07-20 17:51:23 -04:00
commit e3cf50bf75
9 changed files with 341 additions and 17 deletions

View file

@ -45,6 +45,10 @@ Singleton {
property real superLastReleaseTime: 0
property bool wallpaperSelectorOpen: false
property bool workspaceShowNumbers: false
// Boot bloom overlay: true from shell start until the lockscreen surface is
// secure. It covers all of Hyprland's boot render, continuing the C splash's
// bloom animation. LockScreen.onSecureChanged clears it to reveal the lock.
property bool bootBloomActive: true
// In fullscreen this is the only way the dock becomes visible:
// the navigation rail swipe sets/clears it. It deliberately has no timer
// or secondary state.

View file

@ -28,7 +28,6 @@ GlobalStates.qml souveraine/GlobalStates.qml
settings-phone.qml souveraine/settings-phone.qml
panelFamilies/SouveraineFamily.qml souveraine/panelFamilies/SouveraineFamily.qml
services/Souveraine.qml souveraine/services/Souveraine.qml
services/Audio.qml souveraine/services/Audio.qml
services/Ai.qml souveraine/services/Ai.qml
services/TaskbarApps.qml souveraine/services/TaskbarApps.qml
services/GlobalFocusGrab.qml souveraine/services/GlobalFocusGrab.qml
@ -80,6 +79,10 @@ modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/L
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml
modules/souveraine/navigation/qmldir souveraine/modules/souveraine/navigation/qmldir
modules/souveraine/boot/BootBloom.qml souveraine/modules/souveraine/boot/BootBloom.qml
modules/souveraine/boot/BootBloom.frag.qsb souveraine/modules/souveraine/boot/BootBloom.frag.qsb
modules/souveraine/boot/souvie.png souveraine/modules/souveraine/boot/souvie.png
modules/souveraine/boot/qmldir souveraine/modules/souveraine/boot/qmldir
modules/ii/sessionScreen/SessionScreen.qml souveraine/modules/ii/sessionScreen/SessionScreen.qml
"
@ -88,6 +91,28 @@ PHONE_DEST="souveraine-surfaces/quickshell"
manifest_lines() { printf '%s\n' "$MANIFEST" | sed '/^[[:space:]]*$/d'; }
# Compile the boot bloom shader to .qsb (Qt RHI) if the source is newer. The
# .qsb is architecture-independent (SPIR-V + reflection), so we build it once
# here and the manifest symlink / phone rsync carries it — no qsb on the phone.
compile_shaders() {
local frag="$SRC/modules/souveraine/boot/BootBloom.frag"
local qsb_out="$SRC/modules/souveraine/boot/BootBloom.frag.qsb"
[[ -f "$frag" ]] || return 0
if [[ ! -f "$qsb_out" || "$frag" -nt "$qsb_out" ]]; then
local qsb
qsb="$(command -v qsb || echo /usr/lib/qt6/bin/qsb)"
if [[ ! -x "$qsb" ]]; then
echo "ERROR: qsb not found (need qt6-shadertools) to build BootBloom.frag.qsb" >&2
exit 1
fi
# --glsl 100es,120,150 + --hlsl/--msl targets so it runs on the Adreno
# GLES backend the phone uses; -O optimizes.
"$qsb" --glsl "100es,120,150" --hlsl 50 --msl 12 -O -o "$qsb_out" "$frag"
echo "compiled BootBloom.frag -> BootBloom.frag.qsb"
fi
}
compile_shaders
if [[ "${1:-}" == "--manifest" ]]; then
manifest_lines
exit 0

View file

@ -44,13 +44,10 @@ Scope {
KeyringStorage.fetchKeyringData();
}
}
// Poke the boot splash to fade out + hand DRM master to Hyprland.
// Fires once, the first time the lock surface goes up after boot.
// splash-signal no-ops if the splash isn't running, so re-locks are safe.
Process {
id: splashSignalProc
}
property bool splashPoked: false
// Set once, the first time the lock surface goes secure after boot, to
// dismiss the BootBloom overlay. The C splash is already gone by now (it
// released at the Act III handoff); nothing to poke here anymore.
property bool bootDismissed: false
function unlockKeyring() {
unlockKeyringProc.exec({
environment: ({
@ -114,18 +111,19 @@ Scope {
// separate so an IPC caller cannot mistake a queued lock for a secure
// surface when it is deciding whether personal content may be exposed.
// It is also the true "lockscreen is ready" event: the one moment we
// end the boot splash. The bloom has covered all of Hyprland's startup
// and now our own lock surface is mapped and on screen, so fade it
// here. Firing on the request edge (screenLocked) was too early —
// Hyprland's boot render leaked through the gap. splashPoked keeps it
// once-only so re-locks after unlock never re-fade a splash that
// isn't there.
// dismiss the boot bloom. The C splash released DRM master back at the
// Act III→IV handoff (~6.8s); the quickshell BootBloom overlay has
// covered all of Hyprland's startup since. Now our own lock surface is
// mapped and secure, so fade the bloom out to reveal it. Firing on the
// request edge (screenLocked) was too early — the lock surface wasn't
// on screen yet. bootDismissed keeps it once-only so re-locks after
// unlock never re-hide a bloom that's already gone.
onSecureChanged: {
GlobalStates.screenLockSecure = secure;
console.log("[lock] session lock secure=" + secure);
if (secure && !root.splashPoked) {
root.splashPoked = true;
splashSignalProc.exec({ command: ["splash-signal"] });
if (secure && !root.bootDismissed) {
root.bootDismissed = true;
GlobalStates.bootBloomActive = false;
}
}

View file

@ -0,0 +1,140 @@
// Souveraine boot bloom — Act IV+V, the quickshell half of the boot animation.
//
// The C splash (bootsplash/shader.frag) plays Act I-III on bare GPU: cursor
// blink, slide, and types "Hello There..." / "I'm Souveraine...", fading the
// lines out at ~6.8s. It then releases DRM master. Hyprland takes over and
// this ShaderEffect resumes the animation seamlessly — Souvie fades in and the
// bloom flower grows — over the live compositor, until the lockscreen is ready.
//
// This is the SAME flower as the C shader's flowerLayer/textLayer Souvie half,
// ported to Qt RHI (std140 UBO, qsb-compiled). The intro timeline is shifted so
// t=0 here corresponds to the C shader's t=CURSOR_SLIDE_END (3.0s): the caller
// feeds u_time already offset by the boot epoch so phase is continuous across
// the process swap. Everything is additive light on black.
//
// Qt RHI conventions vs raw GLES2:
// - qt_TexCoord0 in [0,1], origin top-left; we build the same centered,
// aspect-corrected uv the C shader derived from gl_FragCoord.
// - uniforms live in a std140 ubuf block matching ShaderEffect properties.
// - output is fragColor, premultiplied-alpha not needed (opaque black bg).
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float u_time; // seconds since FLOWER epoch start (boot-aligned)
vec2 u_resolution; // px
float u_fade; // 1.0 = full, 0.0 = black (dismiss fade)
};
#define PI 3.14159265359
// The C shader's absolute timeline (seconds from boot t0). We keep the SAME
// numbers so the phase matches: the caller passes u_time in that same clock.
#define SOUVIE_FADE vec2(7.4, 8.6)
#define FLOWER_START 8.0
#define BLOOM_DURATION 7.0
float easeOutBack(float t) { // overshoots past 1 on purpose
float c = 1.70158;
float u = t - 1.0;
return 1.0 + (c + 1.0) * u * u * u + c * u * u;
}
// Layered radial bloom on black — outer petals open first, inner follow.
// Verbatim port of flowerLayer() from bootsplash/shader.frag.
vec3 flowerLayer(vec2 p, float tF) {
vec2 c = vec2(0.0, 0.08);
vec2 q = p - c;
q.y /= 0.82;
float rho = length(q);
float th = atan(q.y, q.x);
float prog = clamp(tF / BLOOM_DURATION, 0.0, 1.0);
float bloom = easeOutBack(prog);
float gate = smoothstep(0.9, 1.0, prog);
float breathe = 1.0 + gate * 0.02 * sin(tF * 0.8);
vec3 acc = vec3(0.0);
float maxR = 0.30 * breathe;
for (int l = 0; l < 6; l++) {
float lr = (float(l) + 0.5) / 6.0;
float delay = (1.0 - lr) * 0.25;
float lb = clamp((bloom - delay) / (1.0 - delay), 0.0, 1.0);
if (lb < 0.01) continue;
float n = 7.0 + float(l) * 2.0;
float rotl = float(l) * 0.37 + gate * 0.06 * sin(tF * 0.5 + float(l));
float lobe = pow(abs(cos(n * (th - rotl) * 0.5)), 3.0 - lr * 1.5);
float d0 = maxR * max(lr, 0.12) * lb * 0.35;
float plen = maxR * max(lr, 0.12) * lb * (0.9 - lr * 0.25);
float reach = d0 + plen * (0.30 + 0.70 * lobe);
float body = smoothstep(reach, reach * 0.55, rho)
* smoothstep(d0 * 0.6, d0 + 0.01, rho);
float rim = exp(-abs(rho - reach) * 55.0) * lobe;
float shimmer = 0.88 + 0.12 * sin(tF * 2.0 + sin(th * 13.0) * 5.0
+ rho * 40.0 + float(l) * 1.7);
vec3 colL = mix(vec3(0.78, 0.12, 0.31), vec3(1.00, 0.51, 0.78), lr);
colL = mix(colL, vec3(0.71, 0.40, 0.94), 0.35 * lr);
acc += colL * (body * 0.22 + rim * 0.85) * lb * shimmer;
}
// starburst core
float coreIn = smoothstep(0.25, 0.65, bloom);
float rays = pow(abs(cos(4.0 * th + tF * 0.3)), 10.0);
float core = exp(-rho * 20.0) * 1.3 + rays * exp(-rho * 8.0) * 0.45;
acc += vec3(0.95, 0.86, 0.98) * core * coreIn;
// ring glints
if (bloom > 0.5) {
float sb = min((bloom - 0.5) / 0.4, 1.0);
float ring = exp(-abs(rho - maxR * 0.85 * sb) * 60.0);
float glint = pow(abs(cos(8.0 * th + sin(tF * 0.5) * 0.4)), 24.0);
acc += vec3(0.78, 0.24, 0.39) * ring * glint * sb * 0.8;
}
// stem
float stemIn = clamp((bloom - 0.05) / 0.5, 0.0, 1.0);
float stemTop = c.y - maxR * 0.15;
float stemLen = 0.55 * stemIn;
if (p.y < stemTop && p.y > stemTop - stemLen) {
float st = (stemTop - p.y) / 0.55;
float curveX = sin(st * PI * 0.3) * 0.045 + sin(st * PI * 0.8) * 0.015;
float d = abs(p.x - (c.x + curveX));
float shade = 0.55 + (1.0 - st) * 0.45;
acc += vec3(0.16, 0.30, 0.10) * exp(-d * 220.0) * shade;
}
return acc;
}
void main() {
float tt = u_time;
// Rebuild the C shader's centered, y-normalized uv from qt_TexCoord0.
// C: uv = (2*fragCoord - res) / res.y, origin bottom-left.
// Qt: qt_TexCoord0 origin top-left, so flip y.
vec2 frag = vec2(qt_TexCoord0.x, 1.0 - qt_TexCoord0.y) * u_resolution;
vec2 uv = (2.0 * frag - u_resolution) / u_resolution.y;
vec3 col = vec3(0.0);
float fadeT = smoothstep(FLOWER_START, FLOWER_START + 0.8, tt);
if (fadeT > 0.001)
col += flowerLayer(uv, tt - FLOWER_START) * fadeT;
// Souvie is drawn as a real PNG Image in the QML layer above this shader
// (BootBloom.qml), not baked into the atlas — so the shader is bloom-only.
fragColor = vec4(col * u_fade, 1.0);
}

View file

@ -0,0 +1,147 @@
// Souveraine boot bloom overlay — the quickshell half of the boot animation.
//
// The C splash (souveraine-splash) owns DRM master at boot and plays Act I-III
// (cursor blink/slide, types the two lines) on bare GPU, then holds. This
// overlay is Hyprland's first surface: a fullscreen layershell Overlay that
// resumes the animation (Act IV+V — Souvie + bloom flower) seamlessly, over the
// live compositor, covering ALL of Hyprland's own boot render. It:
//
// 1. maps as early as possible (top-level in ShellRoot, NOT behind the
// Config.ready LazyLoader) so nothing of Hyprland's default output shows;
// 2. on its first painted frame, pokes `splash-signal` — telling the C splash
// "I'm up, you may release DRM master now." The splash fades and exits;
// Hyprland (already waiting) grabs the CRTC; this overlay is already
// painting black+bloom over the swap, so the handoff is invisible;
// 3. drives u_time from the boot monotonic clock so the bloom phase continues
// exactly where the splash left off (no visible seam / restart);
// 4. is dismissed by LockScreen.onSecureChanged (fade out) once the real lock
// surface is up.
//
// Clock continuity: the C shader's t is CLOCK_MONOTONIC seconds since its own
// start (≈ boot). We read /proc/uptime once at map time to recover that same
// clock, so u_time here matches the splash's u_time. FLOWER_START (8.0s) is
// absolute in that clock; the bloom begins on its own regardless of when this
// overlay happens to map.
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Scope {
id: root
// Held true from boot until the lockscreen is secure. LockScreen flips it
// off (onSecureChanged) to fade the overlay out and reveal the lock.
property bool active: GlobalStates.bootBloomActive
// Monotonic seconds at the moment we captured the boot clock. u_time =
// bootBase + (now - captureWall). Filled from /proc/uptime once.
property real bootBase: 0
property bool clockReady: false
property bool splashPoked: false
// Read the boot monotonic clock once, as early as possible.
Process {
id: uptimeProc
command: ["cat", "/proc/uptime"]
running: true
stdout: StdioCollector {
onStreamFinished: {
// /proc/uptime: "<seconds-since-boot> <idle>"
const secs = parseFloat(text.trim().split(/\s+/)[0]);
if (!isNaN(secs)) {
root.bootBase = secs;
root.clockReady = true;
}
}
}
}
// Pokes the C splash to release DRM master. Fires once, on first frame.
Process { id: splashSignalProc }
// Wall-clock reference captured when the /proc/uptime read completed, so
// elapsed = frameClock.currentTime-based delta. We drive u_time off an
// Animator-free FrameAnimation for a monotonic per-frame tick.
FrameAnimation {
id: frameClock
running: root.active
// elapsedTime is seconds since this animation started running.
}
Variants {
model: root.active ? Quickshell.screens : []
PanelWindow {
id: win
required property var modelData
screen: modelData
WlrLayershell.namespace: "quickshell:bootbloom"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// No keyboard grab — we're purely visual; the lock takes input.
color: "black"
anchors { top: true; left: true; right: true; bottom: true }
// u_time in the splash's boot clock: base + seconds elapsed since
// we captured the base. Continuous with the C shader.
property real bootTime: root.bootBase + frameClock.elapsedTime
ShaderEffect {
id: bloom
anchors.fill: parent
// qsb-compiled at deploy time from BootBloom.frag
fragmentShader: Qt.resolvedUrl("BootBloom.frag.qsb")
property real u_time: win.bootTime
property vector2d u_resolution: Qt.vector2d(width, height)
property real u_fade: 1.0
// Fade out on dismiss.
Behavior on u_fade {
NumberAnimation { duration: 450; easing.type: Easing.InOutQuad }
}
}
// ShaderEffect has no per-frame signal; poke the splash once the
// shader is compiled AND the boot clock is read, one tick later, so
// the C splash only releases DRM master after we're actually able to
// paint. window.frameSwapped would be ideal but doesn't surface
// through PanelWindow — a single-shot armed Timer is the reliable
// "we're up" edge (a frame or two of overlap with the splash is
// harmless; both draw the same continuous bloom).
Timer {
interval: 32 // ~2 frames at 60Hz
repeat: false
running: bloom.status === ShaderEffect.Compiled
&& root.clockReady && !root.splashPoked
onTriggered: {
root.splashPoked = true;
splashSignalProc.exec({ command: ["splash-signal"] });
}
}
// Souvie — real PNG logo, drawn above the bloom, fading in on the
// same SOUVIE_FADE window (7.4-8.6s) the shader used for the atlas
// text. Falls back to invisible if the asset is missing.
Image {
id: souvie
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.14
width: Math.min(parent.width * 0.42, sourceSize.width)
fillMode: Image.PreserveAspectFit
source: Qt.resolvedUrl("souvie.png")
smooth: true
// fade in over 7.4-8.6s in boot clock
opacity: bloom.u_fade * Math.max(0, Math.min(1,
(win.bootTime - 7.4) / 1.2))
visible: status === Image.Ready
}
}
}
}

View file

@ -0,0 +1,2 @@
module qs.modules.souveraine.boot
BootBloom 1.0 BootBloom.qml

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -12,6 +12,7 @@
// is owned outright.
import "modules/common"
import "modules/souveraine/boot"
import "services"
import "panelFamilies"
@ -28,6 +29,13 @@ ShellRoot {
// predates it, and version-dependent types don't belong in the entry file.
ReloadPopup {}
// Boot bloom overlay. Top-level (NOT inside the Config.ready LazyLoader) so
// it maps as Hyprland's first surface, before the rest of the shell loads —
// covering all of Hyprland's boot render and continuing the C splash's
// bloom until the lockscreen is secure. Signals the splash to release DRM
// master on its first frame; LockScreen clears GlobalStates.bootBloomActive.
BootBloom {}
Component.onCompleted: {
MaterialThemeLoader.reapplyTheme()
SessiondBridge.load()