From 543942d08555710339173a8062613049bf58d672 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 14:52:28 -0400 Subject: [PATCH] Never crash: frame-pool panic handler and ui-egui without expect() ui-egui without expect()/unreachable!(): the frame pool is optional (rayon's global pool if the OS refuses threads) and gets a panic_handler, because a panic in a rayon spawn() job aborts the whole process by default; render-queue progress, the text-edit anchor and the GPU viewer texture are bound where they're checked; the settings panel's Bool arm no longer panics. Denies the panic lints in the crate. Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 ++++ crates/ui-egui/src/frames.rs | 20 ++++++++++++++++---- crates/ui-egui/src/lib.rs | 2 ++ crates/ui-egui/src/panels/render_queue.rs | 3 +-- crates/ui-egui/src/panels/settings.rs | 3 ++- crates/ui-egui/src/panels/viewer_text.rs | 3 +-- crates/ui-egui/src/panels/viewer_tools.rs | 2 +- 7 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/ui-egui/src/frames.rs b/crates/ui-egui/src/frames.rs index 19839189..005c9d40 100644 --- a/crates/ui-egui/src/frames.rs +++ b/crates/ui-egui/src/frames.rs @@ -232,7 +232,8 @@ pub struct Frames { /// Pending jobs, picked by priority when a pool thread frees up (the viewer's frame first). queue: Arc>, #[cfg(not(target_arch = "wasm32"))] - pool: rayon::ThreadPool, + /// None if the OS refused the threads: jobs then go to rayon's global pool. + pool: Option, ctx: Option, /// Render time of the last viewer (urgent) frame in ms, for the Info panel / perf readout. pub last_ms: Arc>, @@ -251,7 +252,14 @@ impl Default for Frames { inflight: Arc::new(Mutex::new(HashSet::new())), queue: Arc::new(Mutex::new(Queue::default())), #[cfg(not(target_arch = "wasm32"))] - pool: rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("ec-frame-{i}")).build().expect("frame pool"), + // A panicking job would abort the whole process (rayon's default for `spawn`): log it + // and lose only that frame (the panic hook has logged the message). + pool: rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .thread_name(|i| format!("ec-frame-{i}")) + .panic_handler(|_| log::error!("a frame render panicked; that frame was skipped")) + .build() + .ok(), ctx: None, last_ms: Arc::new(Mutex::new(0.0)), content_keys: Arc::default(), @@ -405,9 +413,13 @@ impl Frames { if self.remote.is_none() { let w = self.worker(); // One spawn per job; each spawn renders whichever queued job matters most right now. - self.pool.spawn(move || { + let job = move || { w.run_next(); - }); + }; + match &self.pool { + Some(pool) => pool.spawn(job), + None => rayon::spawn(job), + } } #[cfg(target_arch = "wasm32")] if let Some(ctx) = &self.ctx { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3060eabe..24a9be93 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -5,6 +5,8 @@ //! Everything goes through `effectcraft_engine::Session::execute` (or `menus::invoke` for //! frontend-only commands) so every gesture is also available to agents. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + pub mod audio; pub mod automation; pub mod bench; diff --git a/crates/ui-egui/src/panels/render_queue.rs b/crates/ui-egui/src/panels/render_queue.rs index cf6c0c71..a5c190f7 100644 --- a/crates/ui-egui/src/panels/render_queue.rs +++ b/crates/ui-egui/src/panels/render_queue.rs @@ -570,8 +570,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { lp.text(pos2(cx_num + 2.0, row.center().y), Align2::LEFT_CENTER, n.to_string(), Tokens::ui(12.0), t.text_dim); let name = comp_item.map(|i| i.name.clone()).unwrap_or_else(|| "(missing composition)".into()); lp.text(pos2(cx_name + 2.0, row.center().y), Align2::LEFT_CENTER, &name, Tokens::ui(12.0), t.text); - let status = if rendering && progress.as_ref().and_then(|p| p.current) == Some(it.id) { - let pr = progress.as_ref().expect("progress"); + let status = if rendering && let Some(pr) = progress.as_ref().filter(|p| p.current == Some(it.id)) { format!("Rendering {:.0}%", if pr.total > 0 { pr.done as f64 * 100.0 / pr.total as f64 } else { 0.0 }) } else { it.status.label().to_string() diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index d116c7ca..1abceb56 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -176,7 +176,8 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value } r } - Kind::Bool => unreachable!(), + // Checkboxes are drawn above; nothing to edit here. + Kind::Bool => ui.label(""), }; resp = Some(r); }); diff --git a/crates/ui-egui/src/panels/viewer_text.rs b/crates/ui-egui/src/panels/viewer_text.rs index 80ee1209..dd0a28a5 100644 --- a/crates/ui-egui/src/panels/viewer_text.rs +++ b/crates/ui-egui/src/panels/viewer_text.rs @@ -193,8 +193,7 @@ pub fn hook( handled = true; } else if e.contains(map, pos) { let ci = e.hit(map, pos); - let sel = editing.as_ref().expect("editing"); - let anchor = if mods.shift { sel.anchor } else { ci }; + let anchor = if mods.shift { editing.as_ref().map_or(ci, |sel| sel.anchor) } else { ci }; exec(app, "text.setSelection", json!({"anchor": anchor, "caret": ci})); typing_key(ui, true); drag = Some(Drag::Select { layer: e.layer.id }); diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index e0219dfb..534e501d 100644 --- a/crates/ui-egui/src/panels/viewer_tools.rs +++ b/crates/ui-egui/src/panels/viewer_tools.rs @@ -380,7 +380,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter: // read it back first (asynchronously in the browser: from the next frame on). let gpu = matches!((&app.viewer_shown, &app.viewer_native), (Some(s), Some(n)) if s.0 == n.0 && s.1 == n.1 && n.1.comp == cid.0); let rect = if gpu { - let id = app.viewer_shown.map(|s| s.0).expect("shown"); + let Some(id) = app.viewer_shown.map(|s| s.0) else { return }; if plain || app.viewer_pixels().is_none() { if !plain { ctx.request_repaint();