fix: an out-of-memory GPU no longer aborts the process

The crash in #358 was not the out-of-memory itself. wgpu reports OOM as
recoverable and the app already installs a handler that logs and keeps
the session alive. What killed the process was what happened next:

  create_buffer_init maps the new buffer unconditionally, and mapping the
  invalid buffer an OOM device returns panics. That panic unwinds through
  iced's main-thread redraw, trips the wgpu-hal swapchain assertion in a
  destructor, and becomes a non-unwinding abort -- no chance to autosave.

Every allocation whose size scales with the drawing now goes through
src/scene/pipeline/gpu_upload.rs, which creates the buffer unmapped and
fills it with Queue::write_buffer. A failed allocation then surfaces as a
validation error on the write, which reaches install_gpu_error_handler
and degrades the frame -- exactly what that handler's comment promised:
"A bad frame must degrade, not end the session."

Converted: block-wire vertices and instances, the wire const storage
buffer, all three wire-arena allocators, face3d vertices and instances,
block-text instances and text vertices, both hatch backends, and the
silhouette buffers. mesh_gpu already allocated this way, which is why the
mesh path never produced this panic; the rest now match it.

No mapped_at_creation:true remains in the renderer. The create_buffer_init
calls left are fixed-size or bounded-small -- uniforms, the viewcube's
static geometry, per-image and per-wipeout quads -- where an allocation
failure is not a realistic outcome.

Two trade-offs worth stating:

  * write_buffer stages through a copy, which mapping at creation avoided.
    Chunking bounds that transient cost, and the measured block-wire
    upload got faster with chunking, not slower.
  * `queue` had to be threaded through the wire, face3d, text and hatch
    builders and their callers. Mechanical, but it widens the diff.

This touches every upload path, so it needs a visual check against a real
drawing before it goes upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-04 16:21:08 +03:00
1 parent 771a5b2c5c
commit d0ef752532
10 files changed
+362 -208

No files matched your search

+23 -14
View File
@@ -14,7 +14,6 @@
#![allow(dead_code)]
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
use crate::scene::text::glyph_quads::GlyphQuad;
use crate::scene::text::sdf_atlas::GlyphAtlas;
@@ -462,15 +461,17 @@ pub fn create_pipelines(
pub fn upload_block_vertices(
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<BlockTextGpu> {
let refs: Vec<&crate::scene::model::wire_model::WireModel> = wires.iter().collect();
upload_block_vertex_refs(device, &refs, depth_map, None)
upload_block_vertex_refs(device, queue, &refs, depth_map, None)
}
pub fn upload_block_vertex_refs(
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[&crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
tint: Option<[f32; 4]>,
@@ -516,7 +517,7 @@ pub fn upload_block_vertex_refs(
} else {
source.text_verts.as_slice()
};
let Some(vertex_buffer) = upload_vertices(device, vertices) else {
let Some(vertex_buffer) = upload_vertices(device, queue, vertices) else {
continue;
};
let instances: Vec<BlockTextInstance> = group
@@ -544,11 +545,13 @@ pub fn upload_block_vertex_refs(
let max_instances =
super::gpu_budget::max_elements::<BlockTextInstance>(device);
for chunk in instances.chunks(max_instances) {
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_text.instances"),
contents: bytemuck::cast_slice(chunk),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"block_text.instances",
chunk,
wgpu::BufferUsages::VERTEX,
);
out.push(BlockTextGpu {
vertex_buffer: vertex_buffer.clone(),
instance_buffer,
@@ -561,15 +564,21 @@ pub fn upload_block_vertex_refs(
}
/// Upload a finished vertex list; `None` if empty.
pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option<wgpu::Buffer> {
pub fn upload_vertices(
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[TextVertex],
) -> Option<wgpu::Buffer> {
if verts.is_empty() {
return None;
}
Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("text.vbuf"),
contents: bytemuck::cast_slice(verts),
usage: wgpu::BufferUsages::VERTEX,
}))
Some(super::gpu_upload::upload_buffer(
device,
queue,
"text.vbuf",
verts,
wgpu::BufferUsages::VERTEX,
))
}
// ── Tests ─────────────────────────────────────────────────────────────────