fix(render): install the GPU error handler once per device, throttled

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-17 10:38:53 +03:00
1 parent 75752abd03
commit f9a26978dd
1 file changed
+29 -9
+29 -9
View File
@@ -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,