Merge pull request #36 from storytold/pr/never-crash-ui

Never crash: frame-pool panic handler and ui-egui without expect()
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-04 15:33:38 -04:00
commit c6b8f5412f
6 files changed
+23 -10

No files matched your search

+16 -4
View File
@@ -253,7 +253,8 @@ pub struct Frames {
/// Pending jobs, picked by priority when a pool thread frees up (the viewer's frame first).
queue: Arc<Mutex<Queue>>,
#[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<rayon::ThreadPool>,
ctx: Option<egui::Context>,
/// Render time of the last viewer (urgent) frame in ms, for the Info panel / perf readout.
pub last_ms: Arc<Mutex<f64>>,
@@ -272,7 +273,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(),
@@ -440,9 +448,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 {
+2
View File
@@ -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;
+1 -2
View File
@@ -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()
+2 -1
View File
@@ -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);
});
+1 -2
View File
@@ -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 });
+1 -1
View File
@@ -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();