From b580fdafad61c3f797c707409268eb1db76c3038 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:22:19 -0600 Subject: [PATCH] fix(qbz-qt): rasterize every Canvas on the GUI thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/qbz-qt/qml/cards/PlaylistCollage.qml | 5 ++++- crates/qbz-qt/qml/shell/AmbientField.qml | 5 ++++- crates/qbz-qt/qml/theme/RoundedImage.qml | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/qbz-qt/qml/cards/PlaylistCollage.qml b/crates/qbz-qt/qml/cards/PlaylistCollage.qml index cb1436b69..f198028fe 100644 --- a/crates/qbz-qt/qml/cards/PlaylistCollage.qml +++ b/crates/qbz-qt/qml/cards/PlaylistCollage.qml @@ -135,7 +135,10 @@ Item { id: canvas anchors.fill: parent renderTarget: Canvas.Image - renderStrategy: Canvas.Cooperative + // See RoundedImage.qml: GUI-thread raster. This canvas repaints from + // libraryArtworkReady and from four async image probes, on a card the + // feed recycles. + renderStrategy: Canvas.Immediate // Dimension probes: Canvas cannot query a loaded image's intrinsic // size and the crop fit needs it (RoundedImage's pattern, one per diff --git a/crates/qbz-qt/qml/shell/AmbientField.qml b/crates/qbz-qt/qml/shell/AmbientField.qml index 5dff24c58..7391fbdc6 100644 --- a/crates/qbz-qt/qml/shell/AmbientField.qml +++ b/crates/qbz-qt/qml/shell/AmbientField.qml @@ -45,7 +45,10 @@ Canvas { : true renderTarget: Canvas.Image - renderStrategy: Canvas.Cooperative + // 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 diff --git a/crates/qbz-qt/qml/theme/RoundedImage.qml b/crates/qbz-qt/qml/theme/RoundedImage.qml index 429905ff4..bccc26eea 100644 --- a/crates/qbz-qt/qml/theme/RoundedImage.qml +++ b/crates/qbz-qt/qml/theme/RoundedImage.qml @@ -25,7 +25,13 @@ Canvas { property string fit: "crop" renderTarget: Canvas.Image - renderStrategy: Canvas.Cooperative + // GUI-thread raster. Cooperative/Threaded rasterize on the SCENE GRAPH + // RENDER THREAD, and this item is created and destroyed by list recycling + // while async artwork callbacks schedule repaints — the render thread can + // be mid-draw on a pixmap whose Canvas is already gone. Immediate keeps + // paint and destruction on one thread. renderTarget: Image already meant + // CPU raster, so this costs no visual change. + renderStrategy: Canvas.Immediate // Dimension probe: Canvas can't query the loaded image's intrinsic // size, and PreserveAspectCrop needs it. A hidden Image doubles as