mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 12:37:51 -06:00
Never crash: gpu and merge free of panicking calls
gpu (its render already runs under catch_unwind and falls back to the CPU): - a failed readback (device lost, out of memory) logs, marks the GPU broken and returns zeros, so crate::render discards the result and renders on the CPU, instead of panicking; - an unknown kernel name does the same instead of panic!; - finish-parameter fields are looked up as Option (debug_assert on an unknown name) instead of panic!; - the warp sampler matches (mode, warp) like the CPU pipeline; - Buf::raw keeps one documented expect (Some from construction to Drop). merge: - pyramid building (push_pull, Blender::finish), camera graph growth, bundle adjustment, focal median and alignment use let-else / filter_map instead of expect; - synth::bracket_dngs / pano_views return Result (a missing demo scene or a DNG write error is an error); lightcraft-cli synth-merge and the engine merge tests handle it. lightcraft-gpu, lightcraft-merge and lightcraft-cli now #![deny] unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in the steering PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fb8e1e93aa
commit
bd2e756b24
+21
-3
@@ -86,6 +86,9 @@ pub struct Buf {
|
||||
|
||||
impl Buf {
|
||||
pub fn raw(&self) -> &wgpu::Buffer {
|
||||
// `buf` is `Some` from construction (`Gpu::buffer`/`upload`) until `Drop` takes it back
|
||||
// for the pool, so a live `Buf` always has it.
|
||||
#[allow(clippy::expect_used)]
|
||||
&self.buf.as_ref().expect("live buffer").0
|
||||
}
|
||||
}
|
||||
@@ -442,7 +445,12 @@ impl Gpu {
|
||||
|
||||
/// Record kernel `name` with parameters `p` over buffers `bufs` (None = unused binding).
|
||||
pub fn run(&self, enc: &mut wgpu::CommandEncoder, name: &str, p: &[u32], bufs: &[Option<&Buf>], groups: [u32; 3]) {
|
||||
let k = self.kernels.get(name).unwrap_or_else(|| panic!("unknown kernel {name}"));
|
||||
let Some(k) = self.kernels.get(name) else {
|
||||
// a kernel name typo: fail this render over to the CPU instead of crashing
|
||||
log::error!("gpu: unknown kernel {name}");
|
||||
crate::mark_broken();
|
||||
return;
|
||||
};
|
||||
assert_eq!(bufs.len(), k.nbuf, "{name}: binding count");
|
||||
let params = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
@@ -479,11 +487,21 @@ impl Gpu {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
// A failed readback (device lost, out of memory) marks the GPU broken: the caller
|
||||
// (`crate::render`) then discards this render's zeroed result and renders on the CPU.
|
||||
let failed = |why: String| {
|
||||
log::error!("gpu readback failed: {why}");
|
||||
crate::mark_broken();
|
||||
vec![<T as bytemuck::Zeroable>::zeroed(); len]
|
||||
};
|
||||
match rx.recv() {
|
||||
Ok(Ok(())) => {}
|
||||
other => panic!("gpu readback failed: {other:?}"),
|
||||
other => return failed(format!("{other:?}")),
|
||||
}
|
||||
let data = slice.get_mapped_range().expect("mapped");
|
||||
let data = match slice.get_mapped_range() {
|
||||
Ok(d) => d,
|
||||
Err(e) => return failed(format!("{e:?}")),
|
||||
};
|
||||
let out: Vec<T> = bytemuck::cast_slice(&data).to_vec();
|
||||
drop(data);
|
||||
staging.unmap();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! The browser build has no GPU path yet (WebGPU device creation is asynchronous): everything here
|
||||
//! compiles to the CPU fallback on wasm32.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
+12
-10
@@ -86,15 +86,17 @@ pub fn finish_fields() -> Vec<(&'static str, usize)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn index(name: &str) -> usize {
|
||||
/// Offset of field `name`; `None` (and a debug assertion) for a name not in [`FIELDS`].
|
||||
fn index(name: &str) -> Option<usize> {
|
||||
let mut i = 0;
|
||||
for (n, len) in FIELDS {
|
||||
if *n == name {
|
||||
return i;
|
||||
return Some(i);
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
panic!("unknown finish field {name}")
|
||||
debug_assert!(false, "unknown finish field {name}");
|
||||
None
|
||||
}
|
||||
|
||||
/// A parameter block being filled by name.
|
||||
@@ -105,20 +107,20 @@ impl Block {
|
||||
Block(vec![0; FIELDS.iter().map(|f| f.1).sum()])
|
||||
}
|
||||
fn f(&mut self, name: &str, v: f32) {
|
||||
let i = index(name);
|
||||
self.0[i] = v.to_bits();
|
||||
self.u(name, v.to_bits());
|
||||
}
|
||||
fn u(&mut self, name: &str, v: u32) {
|
||||
let i = index(name);
|
||||
self.0[i] = v;
|
||||
if let Some(slot) = index(name).and_then(|i| self.0.get_mut(i)) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
fn b(&mut self, name: &str, v: bool) {
|
||||
self.u(name, v as u32);
|
||||
}
|
||||
fn fs(&mut self, name: &str, v: &[f32]) {
|
||||
let i = index(name);
|
||||
for (k, x) in v.iter().enumerate() {
|
||||
self.0[i + k] = x.to_bits();
|
||||
let Some(i) = index(name) else { return };
|
||||
for (slot, x) in self.0.iter_mut().skip(i).zip(v) {
|
||||
*slot = x.to_bits();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -402,17 +402,19 @@ fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>
|
||||
Some(d) => (Arc::new(resize(cx, &oriented, (sp.ow, sp.oh), d, 3, Filter::Mitchell)), d),
|
||||
None => (oriented, (sp.ow, sp.oh)),
|
||||
};
|
||||
match &sp.mode {
|
||||
SampleMode::Copy => base,
|
||||
SampleMode::Affine(xf) => {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let mut p = vec![bw as u32, bh as u32, w as u32, h as u32];
|
||||
p.extend(affine_bits(xf));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
SampleMode::Warp(o2t) => {
|
||||
let wp = fr.warp.as_ref().expect("warp mode has a warp");
|
||||
let mut affine = |xf: &lightcraft_geom::Affine| {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let mut p = vec![bw as u32, bh as u32, w as u32, h as u32];
|
||||
p.extend(affine_bits(xf));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
};
|
||||
match (&sp.mode, fr.warp.as_ref()) {
|
||||
(SampleMode::Copy, _) => base,
|
||||
(SampleMode::Affine(xf), _) => affine(xf),
|
||||
// `sample_plan` plans a warp only when there is one; without it the same mapping is affine
|
||||
(SampleMode::Warp(o2t), None) => affine(&(lightcraft_geom::Affine::scale(sp.sx, sp.sy) * *o2t)),
|
||||
(SampleMode::Warp(o2t), Some(wp)) => {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let p = warp_params(wp, (bw, bh), (w, h), o2t, sp.sx, sp.sy);
|
||||
cx.run("sample_warp", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
|
||||
Reference in New Issue
Block a user