qt-build-utils writes the collected path verbatim as the qrc alias, and
read_dir joins with a backslash on Windows, so none of the 1469 aliases
resolved: 1106 icons, 9 fonts and 21 shaders would each fail with
"Cannot open". Normalise only when the build host is Windows, since a
backslash is a legal filename character on Unix.
Also probe qsb.exe and QT_ROOT_DIR/bin, because Path::is_file never
appends .exe, and fix the SpectrumBand hand-bake comment, whose command
lacked --hlsl 50 and the GLES variants that build.rs actually passes.
cc
Make immersive scopes responsive and derive readable colors from album artwork. Use distinct full-size bar and compact contour renderers for the seek waveform.
cx
Add a theme-aware goniometer and pitch-locked oscilloscope to the large dock and immersive focus modes.
Gate scope DSP and scenegraph updates by actual panel visibility so hidden modes remain idle, including while shader scenes cover immersive panels.
cx
TWO FINDINGS, both of which falsified something that was being assumed.
FLUIDITY IS NOT FRAME RATE. Driven at QBZ_PULSE_MS=10 the shell presented ~100
frames/s — qt-batches confirmed it, and the script now prints the pulse it ran
with so two logs cannot be confused — and the bars looked exactly as they had at
30. They would: every published frame was applied as a STEP, so the extra
presents repainted the same discrete values three times over. What the Slint
build does differently is not rate, it is `animate bar-h { 90ms }` — it moves
BETWEEN values.
So the interpolation is back, ON THE PULSE, and it costs nothing. The old
interpolator was expensive because of its private 16 ms Timer, not because it
interpolated: it presented at its own rate, unsynchronised with everything else.
Easing on the pulse presents exactly as often as stepping on the pulse did — one
repaint per period either way, only the value differs. Self-parking survives: a
converged ease writes nothing and schedules no repaint. The price is lag, and it
is the honest one — k = 0.45 is ~83% there in three pulses (~100 ms), matching
the reference's 90 ms, against 46 ms of FFT window and 77 ms of Rust smoothing
already in the chain.
The from/to/progress triple is gone with it, and its consumers moved to `cur`.
Note for the next reader: `SpectrumBand` drove all three of its modes from
`Connections` on those properties, and a Connections handler naming a property
that no longer exists is NOT a compile error — QML warns at runtime and simply
never fires. That is how the whole band went dead at once; grep for
`on<Name>Changed` when removing a property, not only for direct reads.
PLASMA WAS DRIVEN BY THE SLOWEST SIGNALS IN THE PIPELINE while the fast ones
travelled beside it unconnected. `transientAmp`, `bandsLo` and `bandsHi` were
published, parsed and stored, and never handed to the item — the shader declared
them and received zeros — so everything ran off the energy bands, which decay
x0.85 per frame (~473 ms) against the bars' x0.65 (~178 ms).
Worse, `beat` stops being a beat on dense material: it decays x0.88 per tick, so
above ~3 hits/s it never falls far enough to come back as a spike and settles
into a shallow ripple around 0.7. A splat multiplied by it becomes a steady
glow, and the busier the music the flatter the picture — the most frenetic input
produced the least motion. `beatAc` subtracts a slow density floor, so a hit
reads as a hit at any tempo.
The scene itself was rebuilt around the reference the owner filmed (Butterchurn
on a percussion-heavy track). Its recipe is three parts: one thin annulus
injected per frame that the feedback warp marches inward, so the concentric
rings ARE the last N frames of audio laid out in space; retention high enough
for that to stack (this held 0.85, where a trail died in ~15 frames and nothing
ever accumulated); and angular structure from reading the spectrum BY ANGLE.
But a CLOSED ring was wrong, and the owner named why: a perfect inner
circumference reads as a drawn shape, hard-edges the field, and at this
retention successive rings pack into one solid disc — which is what killed the
ambient feel. The annulus is gated by angle now, so a hit paints arcs the
conveyor carries inward, the circle never closes, and the gaps stay open for the
background. Noise bends, frays and thins those arcs so they are made of plasma
rather than of compass.
The worms are back at full strength with tighter cores and a contrast curve — a
raw gaussian is all shoulder and reads as haze — and their orbits now reach past
the arc band instead of being trapped inside it. Colour comes from ONE
album-derived triad, so everything used to be a shade of the same hue; a
Rodrigues hue turn splits it into worms, arcs and an ambient wash that sit apart
whatever album is playing. That wash is what stops the background being void for
the foreground to be a cut-out against. A soft limiter replaces the hard clamp:
at 0.945 retention a clamp pins the middle to white and the structure vanishes.
Release build clean, offscreen smoke green, 421/421.
cc
Port the immersive GPU scenes: Plasma, Tunnel, Aurora, Ambient, Spectral
Ribbon and Line Bed as QQuickRhiItems/ShaderEffects under a pulse-driven
ShaderSceneLayer, plus the Qt-only Tunnel Flow (Tauri Canvas2D port) fed
by the Viz16 stream. Scene rows live in the header view menu (no Off row);
g cycles the ring. A fresh immersive open defaults to Split > Lyrics
(owner ruling).
OpenGL link fix (owner smoke: Tunnel black with "uniform _22 declared as
type buf and type buf"): every ShaderEffect fragment with its own uniform
layout now ships a paired vertex stage declaring the identical block —
the shared spectrum.vert only ever linked under Vulkan.
RHI hardening: render pass descriptors on the ping-pong targets (the
Tunnel Flow segfault / Plasma dead window), mirrorVertically tied to the
backend (!== OpenGL), ribbon sub-uploads carry setSourceSize.
km
Qt Quick has no partial redraws: any dirty item presents the WHOLE
window, and on the owner's hybrid laptop each full-window present costs
~1.2% GPU flat (KWin composite + dGPU scanout; area-independent, while
the app's own render is 0.08 ms/frame). The bill was presents/s, and
the shell ran unsynchronised ~30 Hz clocks plus two leaks:
- QbzShell.pulseMs: ONE Rust-driven 33 ms edge that every continuous
animator ticks on (knob QBZ_PULSE_MS). VizSettle now stashes on
publish and applies on the pulse (its 16 ms interpolation timer is
gone); AmbientField and ImmersiveAtmosphere ride the same edge.
- VizSettle writes NOTHING while not live: the always-mounted but
invisible immersive panels were dirtying 28 bar rects per FFT
publish (named via QT_LOGGING_RULES=qt.quick.dirty=true).
- AmbientField renders inline again: ShaderEffectSource
scheduleUpdate() double-presents (minimal repro fbo_repro.qml in the
investigation folder).
- Background mode 2 (ImmersiveAtmosphere) composites its four drifting
layers + base + dim + scrim in ONE opaque ShaderEffect
(atmosphere.frag, baked by build.rs); numeric parity vs the Image
stack verified (atmo-shader-parity-check.py, delta 0.0). The
four-Image stack stays as the software/failure fallback.
Measured on the owner's machine: 93-97% -> 25-26% GPU, ~34 W -> 12-13 W,
~33 presents/s in both background modes. Full story:
qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/GPU-COST-INVESTIGATION.md
km
The 28/48/5 Rectangle delegates cost one dirty node + batch churn each
per frame on Qt's Metal path (QRhiMetal::setShaderResources per batch
is where the ~6x vs Linux lives). Each mode's Loader now instantiates
ONE ShaderEffect — one node, one draw call: heights go to scalar
uniforms (b0..bN) pushed in one JS pass per settle tick, bar/gap/
gradient geometry is computed in the fragment shader with the same
colors and 1px rounding as the Rectangle version. The pre-existing
Repeater delegates are kept verbatim as the fallback arm (software
renderer or shader-compile error — the renderer preference added in
#104 makes that arm user-reachable).
Runtime findings on Qt 6.11 (documented in the file header): inline
GLSL strings are dead (fragmentShader is a URL property), so shaders
are baked with qsb --glsl 440 --msl 12 and shipped under
qml/assets/shaders/ (auto-qrc); and the built-in default vertex stage
emits no qt_TexCoord0, so one explicit shared vertex shader is baked
alongside.
Verified: build + 204/204 tests + both QML audits; RFB (fallback arm,
all three modes live during playback); Xvfb/GL (shader arm, all three
modes, zero shader errors; QSG_VISUALIZE=batches shows one uniform
batch block for the strip).
km
The track's "ShaderEffect / ColorOverlay / MultiEffect render NOTHING on the
software path this port runs on" doctrine was measured FALSE on 2026-07-29:
qbz-qt runs on the GPU (OpenGL RHI, threaded render loop, vsync 5.56ms). The
claim came from an offscreen session, and QT_QPA_PLATFORM=offscreen forces
software by definition. Every design it justified needed re-deciding.
- RoundedImage was a Canvas (renderTarget: Canvas.Image) that rasterised with
QPainter on the CPU AND uploaded a texture per repaint, for every cover in
the app, purely to round a corner identical in every frame of that image's
life. It is now layer.enabled + MultiEffect { maskSource } — an FBO rendered
once and cached for static content. Rectangle { radius; clip: true } cannot
do this: clip in Qt Quick is a RECTANGULAR scissor and ignores radius.
maskThresholdMin/maskSpreadAtMin are 0.5/1.0, NOT the 0.0/0.0 that looks
natural: 0.0 collapses Qt's mask smoothstep to step(0, alpha) and every
pixel with any alpha snaps fully opaque — measured 0 antialiased pixels at
radius 40 against the Canvas's 292. At 0.5/1.0 it is 308, ramp [0,51,219,255].
A software machine keeps the Canvas arm (GraphicsInfo.api), and
QBZ_QT_ROUND_MODE=canvas pins it on a GPU machine where the mask misbehaves.
- VizSettle drove the spectrum from FrameAnimation, i.e. every rendered frame:
180fps on this box for data the producer publishes at TARGET_FPS = 30. Now a
16ms Timer; the wall-clock maths is unchanged.
- SpectrumBand's per-bar y was a SECOND JS binding derived from height. An
anchor does the same arithmetic in C++ with no binding at all, and each FFT
frame is materialised into a plain array once per publish instead of being
read element by element across the JS/C++ boundary. Bindings per tick: bars
28 (was 56), waveform 48 (was 96), energy 5 (was 10).
QQuickPaintedItem was investigated and rejected with evidence: cxx-qt 0.7.3
emits X(QObject* parent) : base(parent), which does not compile against
QQuickPaintedItem(QQuickItem*), paint() is pure virtual, and there is zero
precedent for a custom base class in this workspace. Qt 6 ShaderEffect takes
no GLSL, only precompiled .qsb, which would make the build depend on a Qt
tool whose path differs on macOS.
- PlaylistCollage point-sampled its mosaic in its own Canvas
(Context2D.drawImage does not filter when it scales — measured RMSE 0.000
against nearest-neighbour). It now draws through RoundedImage.
- The scaled-derivative cache had no eviction at all and the .jpg -> .png key
change had orphaned 1251 files. Boot housekeeping sweeps the orphans once and
bounds the directory at 64 MB, LRU by mtime. Verified by effect: first run
deleted 1259 files / 10.05 MB, every run since reports 0, and the directory
is 14 MB. The eviction is only survivable because RoundedImage clears
_scaled on Image.Error and re-requests.
- For You artwork patched the rails in place instead of republishing the
document, which was resetting contentY. HomeView is destroyed on every
navigation, so the patch map is re-handed on mount through
QbzHome.refreshForyouArt() — otherwise the Radio rail and the Spotlight hero
went blank for the rest of the session after one navigation, and the skeleton
pulse Timer stayed pinned ON, defeating the fix it belonged to.
- Spotlight's placeholder gated on the local path instead of the remote url, so
it flashed a glyph on a cold cache where the Slint does not.
The retired doctrine is corrected wherever it was load-bearing for a decision.
The pre-baked icon tints are deliberately NOT reopened and no drop-shadows are
added: both are visual changes, not perf fixes.
cc
The band moved at the producer's 30 Hz. Raising TARGET_FPS would have been the
obvious knob, but it lives in qbz-audio and is shared with Slint, and the
amplitudes are already EMA-smoothed in Rust to roughly 6 Hz — a faster producer
would have bought CPU and no smoothness.
The published frames are now replayed across the render frames that follow
them, which is what Slint's dock gets from its per-bar 90ms animate. A late
publish degrades to a hold instead of a jump, and the motion runs at the
display's rate rather than the data's.
Splits the strip out of the dock into SpectrumBand, so the dock file owns only
the gating and the layout arithmetic.
cc