mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 00:47:03 -06:00
GPU: running out of video memory falls back to the CPU instead of crashing (#106)
The desktop app shares egui-wgpu's device, which had no uncaptured-error handler, so wgpu's default one panicked on an out-of-memory error on every frame thread; the panicking frame stayed "in progress" forever and never rendered again. The GPU frame budget was a fixed 1 GiB. - GpuContext::new logs uncaptured errors once its pipelines are built (errors while building them still panic there, which the app catches to fall back to CPU compositing). - Gpu::within_memory runs GPU work in an out-of-memory error scope; on an error the result is discarded and uploaded layers and pooled textures are freed. - Frame workers render each frame within it; out of memory, the frame renders on the CPU and the RAM preview keeps half as many GPU frames (down to 64 MiB), since wgpu can't tell how much video memory there is. - A frame whose render panics renders again on the CPU, or is released, so the viewer asks for it again. Tests: a_frame_whose_render_panics_is_released (failed before), running_out_of_video_memory_halves_the_gpu_frame_budget and within_memory_returns_the_work_when_there_is_room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0fac647928
commit
e64076e432
+21
-10
@@ -291,6 +291,20 @@ fn storage_tex_entry(binding: u32, format: wgpu::TextureFormat) -> wgpu::BindGro
|
||||
}
|
||||
}
|
||||
|
||||
/// Log the device's uncaptured errors instead of wgpu's default panic.
|
||||
fn log_uncaptured_errors(device: &wgpu::Device) {
|
||||
device.on_uncaptured_error(Arc::new(|e| {
|
||||
// Under test a validation error (a WGSL typo invalidates every kernel) fails loudly.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::panic)] // test builds only
|
||||
{
|
||||
panic!("wgpu: {e}")
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
log::error!("wgpu: {e}")
|
||||
}));
|
||||
}
|
||||
|
||||
impl GpuContext {
|
||||
/// Build on an existing device (the desktop app shares egui-wgpu's). `Err` when the
|
||||
/// adapter cannot run the compositor (no compute shaders, e.g. WebGL2; no float storage
|
||||
@@ -469,6 +483,12 @@ impl GpuContext {
|
||||
let dummy_buf =
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("dummy"), contents: &[0u8; 16], usage: wgpu::BufferUsages::STORAGE });
|
||||
let max_dim = device.limits().max_texture_dimension_2d;
|
||||
// From here on errors are logged, not fatal: wgpu's default handler panics, and egui-wgpu
|
||||
// installs none on the device the desktop app shares. Running out of video memory on a
|
||||
// frame thread must not crash the app (#106); frames catch it with
|
||||
// [`crate::Gpu::within_memory`]. (Errors while the pipelines are built above still panic
|
||||
// on that device, which the app catches to fall back to the CPU.)
|
||||
log_uncaptured_errors(&device);
|
||||
Ok(GpuContext {
|
||||
device,
|
||||
queue,
|
||||
@@ -534,16 +554,7 @@ impl GpuContext {
|
||||
.request_device(&wgpu::DeviceDescriptor { label: Some("effectcraft gpu"), required_limits, ..Default::default() })
|
||||
.await
|
||||
.map_err(|e| format!("no device: {e}"))?;
|
||||
device.on_uncaptured_error(Arc::new(|e| {
|
||||
// Under test a validation error (a WGSL typo invalidates every kernel) fails loudly.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::panic)] // test builds only
|
||||
{
|
||||
panic!("wgpu: {e}")
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
log::error!("wgpu: {e}")
|
||||
}));
|
||||
log_uncaptured_errors(&device);
|
||||
GpuContext::new(&adapter, device, queue)
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,27 @@ impl Gpu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run GPU work `f` on this thread, catching the device running out of video memory
|
||||
/// meanwhile (#106). `Err` (why) means the result is incomplete: a texture that failed to
|
||||
/// allocate holds nothing. The uploaded layers and pooled textures are then freed, so the
|
||||
/// next render has room. The browser reports it to the device's error handler instead.
|
||||
pub fn within_memory<T>(&self, f: impl FnOnce() -> T) -> Result<T, String> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let scope = self.ctx.device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
|
||||
let out = f();
|
||||
match pollster::block_on(scope.pop()) {
|
||||
Some(e) => {
|
||||
self.ctx.clear_uploads();
|
||||
Err(e.to_string())
|
||||
}
|
||||
None => Ok(out),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Ok(f())
|
||||
}
|
||||
|
||||
/// Wait for all submitted GPU work (benchmarks).
|
||||
pub fn wait(&self) {
|
||||
let _ = self.ctx.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
|
||||
@@ -688,3 +688,11 @@ fn devices_with_webgl2_limits_are_declined() {
|
||||
let err = crate::context::GpuContext::new(&adapter, device, queue).err().expect("declined");
|
||||
assert!(err.contains("max_storage_buffers_per_shader_stage"), "{err}");
|
||||
}
|
||||
|
||||
/// GPU work inside the video memory guard returns its result when the device has room (#106).
|
||||
#[test]
|
||||
fn within_memory_returns_the_work_when_there_is_room() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let img = g.within_memory(|| g.ctx.image(64, 32)).expect("room for a 64×32 texture");
|
||||
assert_eq!((img.width, img.height), (64, 32));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user