perf(qt): bring the immersive onto the pulse (83% -> 25% GPU)
- AlbumReactivePanel: drop the eight 100ms Behaviors on 30 Hz data (a retriggered Behavior animates at display rate and every animation frame is a full-window present), and read the settle — not the publishing bridge list — in the energy formulas (the .length guard re-evaluated the visual bindings on every FFT publish). - LyricsSyncEngine: the fast cadence rides the pulse; the 250 ms idle re-check keeps its own timer. - EqualizerBars: the phase accumulates on the pulse and the 'active' gate is host-provided (invisible subtrees still dirty the window). Measured on the owner's machine, immersive reactive panel open: 132 -> ~33 presents/s, 83% -> ~25% GPU. km
This commit is contained in:
4 files changed
+89
-39
No files matched your search
@@ -13,10 +13,12 @@
|
||||
// player clearance(132) — keep it in sync with the paddings below (:106-112).
|
||||
//
|
||||
// FFT CONSUMPTION (trap 6): energy arrives through ONE VizSettle on
|
||||
// QbzViz.energy — NEVER a Repeater.model on a QbzViz list. The visual
|
||||
// settle is then reproduced by the 100ms eases on width/height/opacity/blur
|
||||
// (:138-139,:160), exactly as the Slint panel reproduces the Svelte EMA with
|
||||
// its 100ms `animate`s.
|
||||
// QbzViz.energy — NEVER a Repeater.model on a QbzViz list. The scalars apply
|
||||
// DIRECTLY at the pulse rate: the 100ms eases the Slint panel uses to
|
||||
// reproduce the Svelte EMA (:138-139,:160) were dropped 2026-08-13 — a
|
||||
// Behavior retriggered at 30 Hz animates at display rate, and every
|
||||
// animation frame is a whole-window present (measured: 132 presents/s, 83%
|
||||
// GPU, immersive open). The Rust EMA already provides the smoothing.
|
||||
//
|
||||
// SPEAKER-CONE PULSE: the art grows from its CENTER via animated
|
||||
// width/height; x/y are PLAIN bindings ((parent - size)/2), NEVER animated —
|
||||
@@ -51,25 +53,34 @@ Item {
|
||||
live: root.visible && QbzImmersive.open
|
||||
}
|
||||
readonly property real globalEnergy: {
|
||||
if (QbzViz.energy.length < 5)
|
||||
// Read the SETTLE, never QbzViz.energy: a binding that reads the
|
||||
// bridge list re-evaluates on every 30 Hz publish — OFF the pulse —
|
||||
// and this one feeds geometry (artScale/glow), so each publish was a
|
||||
// whole-window present of its own (measured: the immersive ran at
|
||||
// 57 presents/s with 1 ms-apart pairs, 2026-08-13).
|
||||
if (energySettle.to.length < 5)
|
||||
return 0.0
|
||||
return (energySettle.at(0) + energySettle.at(1) + energySettle.at(2)
|
||||
+ energySettle.at(3) + energySettle.at(4)) / 5.0
|
||||
}
|
||||
readonly property real bassEnergy: {
|
||||
if (QbzViz.energy.length < 2)
|
||||
if (energySettle.to.length < 2)
|
||||
return 0.0
|
||||
return (energySettle.at(0) + energySettle.at(1)) / 2.0
|
||||
}
|
||||
|
||||
// --- Reactive formulas (:62-66) ----------------------------------------
|
||||
// NO 100ms Behaviors here (2026-08-13): a Behavior retriggered by 30 Hz
|
||||
// data animates at DISPLAY rate, and every animation frame is a
|
||||
// whole-window present — measured with the immersive open: ~132
|
||||
// presents/s and 83% GPU, versus ~33/s when these apply directly. The
|
||||
// Rust EMA (attack 0.7 / decay 0.65) already band-limits the stream to
|
||||
// ~6 Hz, so the values step at the pulse rate and still read smooth.
|
||||
// The Slint `animate 100ms` is unaffordable in Qt Quick — its ease only
|
||||
// repaints the rect; ours presents the window.
|
||||
readonly property real artScale: 1.0 + root.globalEnergy * 0.25
|
||||
// NOT readonly: the 100ms Behavior below must attach (a Behavior on a
|
||||
// readonly property is a load-time "Invalid property assignment").
|
||||
property real glowSpread: 15.0 + root.bassEnergy * 200.0
|
||||
Behavior on glowSpread { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
property real glowOpacity: Math.min(0.1 + root.globalEnergy * 1.5, 1.0)
|
||||
Behavior on glowOpacity { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
|
||||
// --- Base art box (:84-92) ---------------------------------------------
|
||||
// Native source resolution — cap the RESTING art so the cover is never
|
||||
@@ -113,6 +124,11 @@ Item {
|
||||
height: root.baseArt * 1.25
|
||||
|
||||
// Static black art shadow 32/#00000080/8 (:157-159).
|
||||
// NO 100ms width/height Behaviors (2026-08-13) — a Behavior
|
||||
// retriggered by 30 Hz data animates at display rate, and
|
||||
// each animation frame is a whole-window present. The art
|
||||
// pulse steps at the pulse rate; see the note by the
|
||||
// reactive formulas above.
|
||||
Rectangle {
|
||||
width: root.artSize
|
||||
height: root.artSize
|
||||
@@ -126,13 +142,12 @@ Item {
|
||||
blurMax: 32
|
||||
blur: 1.0
|
||||
}
|
||||
Behavior on width { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
Behavior on height { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
}
|
||||
|
||||
// Glow halo (:123-140): same size/position as the art, glow
|
||||
// color + reactive opacity, blurred. x/y NOT animated (see
|
||||
// the header's speaker-cone note).
|
||||
// the header's speaker-cone note). No size Behaviors either
|
||||
// (see the reactive-formulas note).
|
||||
Rectangle {
|
||||
width: root.artSize
|
||||
height: root.artSize
|
||||
@@ -147,19 +162,16 @@ Item {
|
||||
blurMax: 64
|
||||
blur: Math.min(1, root.glowSpread / 64)
|
||||
}
|
||||
Behavior on width { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
Behavior on height { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
}
|
||||
|
||||
// Artwork (:147-170). Grows from center via animated
|
||||
// width/height; x/y plain bindings (lockstep, zero drift).
|
||||
// Artwork (:147-170). Grows from center via width/height
|
||||
// bindings applied at the pulse rate; x/y plain bindings
|
||||
// (lockstep, zero drift). No Behaviors (see above).
|
||||
Item {
|
||||
width: root.artSize
|
||||
height: root.artSize
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
Behavior on width { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
Behavior on height { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
|
||||
@@ -9,19 +9,22 @@
|
||||
// 0 / 0.25 / 0.125 / 0.375 (the Svelte animation-delays 0.0/0.2/0.1/0.3 of
|
||||
// the 0.8s period, as fractions).
|
||||
//
|
||||
// The Slint loop is pull-based off animation-tick(); the Qt twin is ONE
|
||||
// 800ms linear NumberAnimation on `t` (0->1), so the whole equalizer is a
|
||||
// single animation node. The wave is the Slint formula verbatim:
|
||||
// The Slint loop is pull-based off animation-tick(); the Qt twin accumulates
|
||||
// `t` on the shared shell pulse (see the note at the property), so the whole
|
||||
// equalizer ticks at the shell's 30 Hz instead of the display rate. The wave
|
||||
// is the Slint formula verbatim:
|
||||
// 0.3 + 0.7 * (0.5 - 0.5*cos((t+phase mod 1) * 2π))
|
||||
// (Svelte keyframes 0/100% -> 0.3, 50% -> 1.0). The loop runs only while
|
||||
// the item is visible — every mount site gates visibility on npPlaying
|
||||
// (ImmersiveTrackInfo.slint:85,:100), so a paused player animates nothing.
|
||||
// (Svelte keyframes 0/100% -> 0.3, 50% -> 1.0). The loop runs only while the
|
||||
// item is visible AND the immersive is open — every mount site gates
|
||||
// visibility on npPlaying (ImmersiveTrackInfo.slint:85,:100), so a paused
|
||||
// player animates nothing.
|
||||
// Reduce-motion: the seam EXISTS now — `QbzShell.reduceMotion`, built by the
|
||||
// cortinilla-parity contract (§5.g). This surface is simply not a consumer
|
||||
// yet; wiring it is a one-liner (throttle the tick when it is true) and its
|
||||
// own change, not a side effect of someone else's commit.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -34,14 +37,32 @@ Item {
|
||||
implicitWidth: 18
|
||||
implicitHeight: 14
|
||||
|
||||
// Loop phase 0..1, 800ms (ImmersiveTrackInfo.slint:32).
|
||||
// Loop phase 0..1, 800ms (ImmersiveTrackInfo.slint:32). Advanced on the
|
||||
// SHELL PULSE (QbzShell.pulseMs), not a NumberAnimation: an 800ms
|
||||
// infinite animation is serviced by the animation driver at DISPLAY rate
|
||||
// (180 Hz on the owner's panel) and every update is a whole-window
|
||||
// present. The wave's period is 800 ms, so the 30 Hz pulse samples it
|
||||
// ~24 times per loop — indistinguishable. Riding the pulse also means
|
||||
// ZERO extra presents: the bars move in the same frame as everything
|
||||
// else the pulse moves.
|
||||
//
|
||||
// The `active` gate belongs to the HOST, because this component mounts
|
||||
// in always-alive-but-invisible trees (the immersive overlay) where a
|
||||
// local `visible` check alone keeps ticking with the overlay closed —
|
||||
// and invisible geometry changes still dirty the whole window (the
|
||||
// 2026-08-13 SpectrumPanel finding). ImmersiveTrackMeta passes
|
||||
// `QbzImmersive.open`; the track row passes its playing-row predicate.
|
||||
// `_tickMs` must match settings_qt::shell_pulse_ms (default 33) — a
|
||||
// mismatch only mis-speeds the loop, never the present rate.
|
||||
property bool active: true
|
||||
property real t: 0
|
||||
NumberAnimation on t {
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 800
|
||||
loops: Animation.Infinite
|
||||
running: root.visible
|
||||
readonly property int _tickMs: 33
|
||||
Connections {
|
||||
target: QbzShell
|
||||
function onPulseMsChanged() {
|
||||
if (root.active && root.visible)
|
||||
root.t = (root.t + root._tickMs / 800.0) % 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// Slint wave() verbatim (:34-36).
|
||||
|
||||
@@ -43,6 +43,9 @@ Column {
|
||||
height: 14
|
||||
EqualizerBars {
|
||||
tint: root.equalizerTint
|
||||
// The gate lives at the mount (EqualizerBars.qml explains
|
||||
// why): this host sits inside the always-mounted ImmersiveView.
|
||||
active: QbzImmersive.open
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
// the same `qbz_lyrics::sync` pure functions, over the same Rust-held parsed
|
||||
// document, so native Qobuz wsync word stamps drive the fill).
|
||||
//
|
||||
// Cadence is ADAPTIVE, 1:1 with the Slint engine:
|
||||
// - ACTIVE 33ms (~30Hz) while the gate is open (surface shown + doc synced
|
||||
// + ready + effectively playing);
|
||||
// - IDLE 250ms otherwise — the tick then only re-checks the gate.
|
||||
// Every tick recomputes from the ABSOLUTE position, which makes seeks
|
||||
// Cadence is ADAPTIVE:
|
||||
// - ACTIVE: the shell pulse (QbzShell.pulseMs, ~30 Hz) while the gate is
|
||||
// open and playback runs — NOT a private 33 ms Timer: a second 33 ms
|
||||
// clock beats against the pulse and doubles the present rate (measured
|
||||
// with the immersive open, 2026-08-13: 57 presents/s with 1 ms-apart
|
||||
// pairs = the pulse and this timer landing back-to-back).
|
||||
// - IDLE 250ms Timer otherwise — the tick then only re-checks the gate.
|
||||
// Every pass recomputes from the ABSOLUTE position, which makes seeks
|
||||
// inherently safe: `fillAnimMs` is zeroed across discontinuities (line change
|
||||
// or backward move) so the fill first-paints exactly at the reported
|
||||
// progress, and smoothed (45ms, just above the tick) while continuous.
|
||||
@@ -50,11 +53,22 @@ Item {
|
||||
width: 0
|
||||
height: 0
|
||||
|
||||
// The ACTIVE cadence rides the shared pulse — one frame per period for
|
||||
// the whole shell, lyrics fill included.
|
||||
Connections {
|
||||
target: QbzShell
|
||||
function onPulseMsChanged() {
|
||||
if (engine.gateOpen && engine._fast)
|
||||
engine.pump(true)
|
||||
}
|
||||
}
|
||||
// The IDLE cadence keeps its own slow timer: with nothing playing it
|
||||
// only re-checks the gate, and 4/s of no-write ticks cost no frames.
|
||||
Timer {
|
||||
id: tick
|
||||
interval: engine._fast ? 33 : 250
|
||||
interval: 250
|
||||
repeat: true
|
||||
running: engine.gateOpen
|
||||
running: engine.gateOpen && !engine._fast
|
||||
onTriggered: engine.pump(true)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user