The owner hit a segfault after clicking around. Cooperative and Threaded rasterize on the scene-graph RENDER thread, and RoundedImage is instantiated in 25 files — every card, row and queue row — so list recycling creates and destroys hundreds of these while async artwork callbacks schedule repaints on them. The render thread can be mid-draw on a pixmap whose Canvas is already gone. The ambient field makes it worse by repainting 30 times a second for the whole window lifetime on that same thread. Immediate keeps paint and destruction on one thread. renderTarget: Image already meant CPU raster, so nothing looks different. Not yet confirmed on hardware: the decisive check is running the previous binary with QSG_RENDER_LOOP=basic, which gives Cooperative no separate thread to race with. If that survives the repro and the default loop does not, this is the cause. cc
126 lines
5.4 KiB
QML
126 lines
5.4 KiB
QML
// AmbientField — the app-wide dynamic background ("modo Cider", phase 14).
|
|
//
|
|
// The QML rendering of the Slint ambient WGSL scene (crates/qbz-ui/ui/
|
|
// shaders/ambient.wgsl): FOUR album-colored blobs on the shader's EXACT
|
|
// wandering orbits (10-18s, t scaled 0.75), additive-blended over a black
|
|
// base so near blobs fuse like the metaball field, plus the shader's
|
|
// vertical edge shade. Colors come from the ambient_qt album triad
|
|
// (QbzShell.ambientPrimary/Secondary/Accent, recomputed on track change —
|
|
// the field crossfades because Canvas repaints are continuous, ~0.6s of
|
|
// visible blend per the blob softness).
|
|
//
|
|
// POC-NOTEs (vs the WGSL original, ambient_qt.rs has the full list):
|
|
// - No fbm domain warp and no r²/d² metaball potential — plain radial
|
|
// gradients with 'lighter' compositing approximate the fusion.
|
|
// - No audio breathe (the POC has no FFT tap; level_smooth = 0 → the
|
|
// shader's breathe is a constant 1.0 there anyway) and no grain dither.
|
|
// - Rendered with QPainter (renderTarget Image) so it works on the
|
|
// software/offscreen path too; 30fps cap + paused when the window is not
|
|
// being shown (the spec's cost controls §6).
|
|
//
|
|
// GATING RULE (owner, 2026-07-28): freeze on NOT VISIBLE, never on lost
|
|
// focus. A tiling/mosaic desktop keeps windows visible and unfocused all the
|
|
// time — pausing on focus loss would stop the field while the user is
|
|
// looking straight at it. Minimized/hidden is the real "nobody can see it".
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
|
|
Canvas {
|
|
id: root
|
|
|
|
// The album triad (hex strings from ambient_qt).
|
|
property color primary: QbzShell.ambientPrimary
|
|
property color secondary: QbzShell.ambientSecondary
|
|
property color accent: QbzShell.ambientAccent
|
|
// Set false by the host to freeze the clock (inactive / hidden).
|
|
property bool running: true
|
|
|
|
// True unless the window is minimized or hidden — see the GATING RULE at
|
|
// the top. Focus is deliberately NOT part of this.
|
|
readonly property bool windowShowing: root.Window.window
|
|
? (root.Window.window.visibility !== Window.Minimized
|
|
&& root.Window.window.visibility !== Window.Hidden)
|
|
: true
|
|
|
|
renderTarget: Canvas.Image
|
|
// See RoundedImage.qml. This one repaints 30x/s for the whole window
|
|
// lifetime, so it is the canvas most likely to be mid-raster when another
|
|
// is torn down on the same render thread.
|
|
renderStrategy: Canvas.Immediate
|
|
|
|
// The shader clock (t = time * 0.75). Accumulated at ~30fps.
|
|
property real t: Math.random() * 40.0
|
|
|
|
Timer {
|
|
interval: 33
|
|
repeat: true
|
|
running: root.running && root.visible && root.windowShowing
|
|
onTriggered: {
|
|
root.t += interval / 1000.0 * 0.75
|
|
root.requestPaint()
|
|
}
|
|
}
|
|
|
|
onPrimaryChanged: requestPaint()
|
|
onSecondaryChanged: requestPaint()
|
|
onAccentChanged: requestPaint()
|
|
onWidthChanged: requestPaint()
|
|
onHeightChanged: requestPaint()
|
|
|
|
// One blob: radial gradient, center color at `a` alpha fading to
|
|
// transparent (the shader's wide smoothstep falloff).
|
|
function blob(ctx, cx, cy, r, c, a) {
|
|
var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r)
|
|
g.addColorStop(0.0, Qt.rgba(c.r, c.g, c.b, a))
|
|
g.addColorStop(0.55, Qt.rgba(c.r, c.g, c.b, a * 0.45))
|
|
g.addColorStop(1.0, Qt.rgba(c.r, c.g, c.b, 0.0))
|
|
ctx.fillStyle = g
|
|
ctx.fillRect(cx - r, cy - r, 2 * r, 2 * r)
|
|
}
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.reset()
|
|
var W = width
|
|
var H = height
|
|
if (W <= 0 || H <= 0) return
|
|
|
|
// Base: the shader's field->0 color (black).
|
|
ctx.fillStyle = "#000000"
|
|
ctx.fillRect(0, 0, W, H)
|
|
|
|
var t = root.t
|
|
// The shader's orbit constants (ambient.wgsl cA..cD, aspect folded
|
|
// back out — window-fraction centers are the plain expressions).
|
|
var cA = [0.32 + 0.30 * Math.sin(t * 0.40), 0.42 + 0.30 * Math.cos(t * 0.33)]
|
|
var cB = [0.66 + 0.30 * Math.sin(t * 0.35 + 2.1), 0.56 + 0.32 * Math.cos(t * 0.29 + 1.3)]
|
|
var cC = [0.50 + 0.34 * Math.cos(t * 0.31 + 4.0), 0.36 + 0.30 * Math.sin(t * 0.45 + 3.2)]
|
|
var cD = [0.46 + 0.32 * Math.sin(t * 0.27 + 5.3), 0.64 + 0.28 * Math.cos(t * 0.49 + 0.7)]
|
|
|
|
// rr = 0.34 of the field; the shader's brightness lands ~0.92 with
|
|
// the 0.42-1.12 shape spread — the alpha spread below reproduces
|
|
// that mid-to-low brightness read under the host's dim scrim.
|
|
var rr = 0.36 * Math.max(W, H)
|
|
var c4 = Qt.rgba(
|
|
(primary.r + accent.r) / 2, (primary.g + accent.g) / 2, (primary.b + accent.b) / 2, 1.0)
|
|
|
|
ctx.globalCompositeOperation = "lighter"
|
|
blob(ctx, cA[0] * W, cA[1] * H, rr, primary, 0.50)
|
|
blob(ctx, cB[0] * W, cB[1] * H, rr * 0.95, secondary, 0.46)
|
|
blob(ctx, cC[0] * W, cC[1] * H, rr * 0.88, accent, 0.44)
|
|
blob(ctx, cD[0] * W, cD[1] * H, rr * 0.82, c4, 0.42)
|
|
ctx.globalCompositeOperation = "source-over"
|
|
|
|
// Vertical shade: 16% darker at the very top/bottom edges (the
|
|
// shader's vshade — chrome sits on calmer color).
|
|
var v = ctx.createLinearGradient(0, 0, 0, H)
|
|
v.addColorStop(0.0, Qt.rgba(0, 0, 0, 0.16))
|
|
v.addColorStop(0.5, Qt.rgba(0, 0, 0, 0.0))
|
|
v.addColorStop(1.0, Qt.rgba(0, 0, 0, 0.16))
|
|
ctx.fillStyle = v
|
|
ctx.fillRect(0, 0, W, H)
|
|
}
|
|
}
|