aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md14
-rwxr-xr-xdesktop/modules/perf/vram.sh6
-rw-r--r--desktop/modules/sound/Osd.qml33
-rw-r--r--desktop/modules/sound/README.md5
4 files changed, 24 insertions, 34 deletions
diff --git a/AGENTS.md b/AGENTS.md
index e089254..ca862dd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -402,6 +402,20 @@ changing that component. The ones that generalise:
is not a function`, which shows only in `qs log`; a Repeater bound to it
simply builds nothing. Use `reduce((a, x) => a.concat(...), [])`. Seen
with the assistant page's cloud Prefer rows (2026-10-23).
+- **Canvas `createImageData` breaks Qt's garbage collector for good.** Qt
+ 6.11 subtracts the pixel buffer from the V4 engine's unmanaged-heap counter
+ without ever having added it, so the first GC after one is created wraps
+ the `size_t` counter below zero, and from then on every JS allocation runs
+ a full GC: a core pinned, keybinds and the drawer lagging, until a
+ restart. Reproduced in Qt's own `qml` tool with nothing but a `Canvas`
+ (17,856 of 17,856 GCs wrapped; the same canvas drawing with `fillRect`, 0
+ of 5). The volume OSD's grain did it on every show, which is why the
+ desktop shell spun after volume keys, track changes and resumes alike.
+ `QV4_GC_TIMELIMIT=0`, set earlier for a misdiagnosed "stuck incremental
+ GC", does not help. Never use `createImageData`, `getImageData` or
+ `putImageData`. Found by running each always-on unit in its own `qs` with
+ `--log-rules qt.qml.gc.allocatorStats.debug=true` and reading the
+ unmanaged counter after each GC.
- **A `Socket` whose connect failed never retries.** Quickshell 0.3.1 keeps
the failed `QLocalSocket`, so setting `connected = true` again (or toggling
it, or changing `path`) is a no-op: `setConnected` only connects when no
diff --git a/desktop/modules/perf/vram.sh b/desktop/modules/perf/vram.sh
index 8ba61fb..83db27d 100755
--- a/desktop/modules/perf/vram.sh
+++ b/desktop/modules/perf/vram.sh
@@ -17,9 +17,9 @@
# instead of scanning, for test_vram.sh.
#
# Summed here rather than in JS: a process holding thousands of DRM fds
-# makes the scan a few hundred KB, and parsing that in QML every 2 s drove
-# Qt's unmanaged-heap counter to underflow, after which the V4 engine ran a
-# GC on every allocation and the shell sat at 100% CPU (AGENTS.md).
+# makes the scan a few hundred KB, which is a lot of string churn to parse
+# in QML every 2 s. It was moved here while chasing a GC underflow that
+# turned out to be the sound OSD's createImageData (AGENTS.md), not this.
#
# Fds of a repeated client id with different values: the first one awk's
# for-in reaches counts, where Perf.js took the first in scan order. They
diff --git a/desktop/modules/sound/Osd.qml b/desktop/modules/sound/Osd.qml
index f60f5ce..e66d559 100644
--- a/desktop/modules/sound/Osd.qml
+++ b/desktop/modules/sound/Osd.qml
@@ -186,9 +186,10 @@ Scope {
color: Qt.alpha(Theme.base, 0.82)
}
- // Frost on the scrim, faked: a faint top-down sheen and fixed
- // grain. The art is opaque, so the compositor's frosting
- // cannot show here without costing the scrim its legibility.
+ // Frost on the scrim, faked: a faint top-down sheen. The art is
+ // opaque, so the compositor's frosting cannot show here
+ // without costing the scrim its legibility. No grain: see
+ // createImageData in AGENTS.md.
Rectangle {
anchors.fill: parent
radius: bgMask.radius
@@ -197,32 +198,6 @@ Scope {
GradientStop { position: 0.6; color: "transparent" }
}
}
-
- // ponytail: grain painted once with Canvas; a ShaderEffect
- // would animate or scale better but needs a compiled .qsb.
- Canvas {
- anchors.fill: parent
- layer.enabled: true
- layer.effect: MultiEffect {
- maskEnabled: true
- maskSource: bgMask
- maskThresholdMin: 0.5
- maskSpreadAtMin: 1.0
- }
- onPaint: {
- if (width <= 0 || height <= 0) return;
- const ctx = getContext("2d");
- const img = ctx.createImageData(width, height);
- for (let i = 0; i < img.data.length; i += 4) {
- const v = Math.random() * 255;
- img.data[i] = v;
- img.data[i + 1] = v;
- img.data[i + 2] = v;
- img.data[i + 3] = 14;
- }
- ctx.putImageData(img, 0, 0);
- }
- }
}
HoverHandler {
diff --git a/desktop/modules/sound/README.md b/desktop/modules/sound/README.md
index c3ada08..88aba11 100644
--- a/desktop/modules/sound/README.md
+++ b/desktop/modules/sound/README.md
@@ -60,8 +60,9 @@ When the track has art, it also fills the whole panel as a blurred background
under a dark scrim (`Theme.base` at 0.82), and the thumbnail stays sharp in
the row. The palette is dark, so the scrim is what keeps text legible on a
light cover; blur alone did not. The art is opaque, so it hides the
-compositor's frosting while it shows, so a faint sheen and grain fake it on
-the scrim. Without art the panel is unchanged.
+compositor's frosting while it shows, so a faint sheen fakes it on the
+scrim. It once had Canvas grain too, removed because `createImageData`
+underflows Qt's GC counter (AGENTS.md). Without art the panel is unchanged.
A new cover crossfades in once it has decoded, so a slow art server never
fades through black. When the track has no art, or its cover fails to load,