From f9a26978dd9a343221e1744409ae135e6fab0df3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 17 Jul 2026 10:38:53 +0300 Subject: [PATCH] fix(render): install the GPU error handler once per device, throttled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The uncaptured-error handler from #404 was armed in `Pipeline::new`, which `MultiPipeline::ensure_len` runs again for every pane added — so the slot was re-registered on every new pane. Move it to `MultiPipeline::new`, which runs once per device; if iced ever rebuilds the device it builds a new `MultiPipeline` too, so the fresh device stays covered — where a `Once` guard would have left it on wgpu's panicking default. Throttle the message. The handler slot is per-device and single, so it also catches iced's own draw errors and is the only thing that can report them — it has to stay loud. But a validation error normally repeats on every frame, and an unthrottled print floods stderr at refresh rate and buries the first, most useful message. Log the 1st, 2nd, 4th, 8th … occurrence instead: the error is never hidden, the running count shows it is ongoing, and the output stays finite. Co-Authored-By: Claude Opus 4.8 --- src/scene/pipeline/mod.rs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 5f30e81f8..52898f6bc 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -281,15 +281,6 @@ pub struct Pipeline { impl Pipeline { pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { - // wgpu's default uncaptured-error handler panics, and inside iced's - // main-thread redraw that panic aborts the whole process (#358). A - // validation error on a pathological drawing must not take the app - // (and the user's unsaved work) down with it — log it and keep the - // session alive; the affected frame renders incompletely at worst. - device.on_uncaptured_error(std::sync::Arc::new(|e: wgpu::Error| { - eprintln!("[gpu] uncaptured wgpu error (frame degraded): {e}"); - })); - // ── Shared frame uniform buffer (view_proj etc.) ─────────────────── let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("viewer.uniform_buffer"), @@ -2910,8 +2901,37 @@ impl MultiPipeline { } } +/// Send wgpu's uncaptured validation errors to stderr instead of the default +/// handler, which panics — and inside iced's main-thread redraw that panic +/// aborts the process, taking the user's unsaved work with it (#358). A bad +/// frame must degrade, not end the session. +/// +/// The handler slot is per-device and single: this also catches iced's own draw +/// errors, and nothing else can report them, so it must stay loud enough to be +/// noticed. A validation error normally repeats on every frame, though, so a +/// plain print would flood stderr at refresh rate and bury the first (most +/// useful) message. Log the 1st, 2nd, 4th, 8th … occurrence instead: the error +/// is never hidden, the count shows it is ongoing, and the output stays finite. +/// +/// Installed from `MultiPipeline::new`, which runs once per device — NOT from +/// `Pipeline::new`, which runs again for every pane `ensure_len` adds. If iced +/// ever rebuilds the device it builds a new `MultiPipeline` too, so the fresh +/// device is covered (and the throttle restarts with it). +fn install_gpu_error_handler(device: &wgpu::Device) { + use std::sync::atomic::{AtomicUsize, Ordering}; + static SEEN: AtomicUsize = AtomicUsize::new(0); + SEEN.store(0, Ordering::Relaxed); + device.on_uncaptured_error(std::sync::Arc::new(|e: wgpu::Error| { + let n = SEEN.fetch_add(1, Ordering::Relaxed) + 1; + if n.is_power_of_two() { + eprintln!("[gpu] uncaptured wgpu error #{n} (frame degraded, session kept alive): {e}"); + } + })); +} + impl iced::widget::shader::Pipeline for MultiPipeline { fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { + install_gpu_error_handler(device); Self { inners: vec![Pipeline::new(device, queue, format)], format,