forked from 1b-agent/opencadstudio-upstream
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:
1 parent
771a5b2c5c
commit
d0ef752532
10 files changed
+362
-208
No files matched your search
@@ -28,34 +28,20 @@
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use iced::wgpu;
|
||||
|
||||
/// Allocate a VERTEX buffer with `mapped_at_creation` and write `data` directly
|
||||
/// into the mapped slice. Skips the intermediate staging copy that
|
||||
/// `create_buffer_init` performs and avoids holding a second `Vec` worth of
|
||||
/// memory during upload — meaningful on cold open where wire buffers can run
|
||||
/// into the hundreds of MB.
|
||||
fn instance_buffer_mapped<T: bytemuck::Pod>(
|
||||
/// Allocate a VERTEX buffer and fill it through the queue.
|
||||
///
|
||||
/// This used to map at creation to skip the staging copy. Mapping panics on
|
||||
/// the invalid buffer an out-of-memory device returns, and that panic aborts
|
||||
/// the process from inside iced's redraw, so the staging copy is the price of
|
||||
/// a session that survives a failed allocation. Chunking keeps the transient
|
||||
/// staging cost bounded. See `super::gpu_upload`.
|
||||
fn instance_buffer<T: bytemuck::Pod>(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
data: &[T],
|
||||
) -> wgpu::Buffer {
|
||||
let bytes: &[u8] = bytemuck::cast_slice(data);
|
||||
// wgpu rejects size-0 buffers; the renderer already guards `instance_count`
|
||||
// before issuing a draw, so a placeholder allocation is fine here.
|
||||
let size = bytes.len().max(std::mem::size_of::<T>()) as u64;
|
||||
let buf = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some(label),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
mapped_at_creation: true,
|
||||
});
|
||||
{
|
||||
let mut view = buf.slice(..).get_mapped_range_mut();
|
||||
if !bytes.is_empty() {
|
||||
view.slice(..bytes.len()).copy_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
buf.unmap();
|
||||
buf
|
||||
super::gpu_upload::upload_buffer(device, queue, label, data, wgpu::BufferUsages::VERTEX)
|
||||
}
|
||||
|
||||
// ── Instance layout ───────────────────────────────────────────────────────
|
||||
@@ -684,6 +670,7 @@ pub(crate) fn wire_draw_depth(
|
||||
impl BlockWireGpu {
|
||||
pub fn from_wires(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[&WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
color_override: Option<[f32; 4]>,
|
||||
@@ -753,11 +740,13 @@ impl BlockWireGpu {
|
||||
.chunks(max_verts)
|
||||
.map(|chunk| {
|
||||
(
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("block_wire.vertices"),
|
||||
contents: bytemuck::cast_slice(chunk),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}),
|
||||
super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
"block_wire.vertices",
|
||||
chunk,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
),
|
||||
chunk.len() as u32,
|
||||
)
|
||||
})
|
||||
@@ -812,7 +801,7 @@ impl BlockWireGpu {
|
||||
.chunks(max_instances)
|
||||
.map(|chunk| {
|
||||
(
|
||||
instance_buffer_mapped(device, "block_wire.instances", chunk),
|
||||
instance_buffer(device, queue, "block_wire.instances", chunk),
|
||||
chunk.len() as u32,
|
||||
)
|
||||
})
|
||||
@@ -840,18 +829,20 @@ impl BlockWireGpu {
|
||||
/// each chunk.
|
||||
fn build_const_bind_group(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
consts: &[WireConst],
|
||||
) -> std::sync::Arc<wgpu::BindGroup> {
|
||||
use wgpu::util::DeviceExt;
|
||||
// wgpu rejects zero-sized buffers; pad with one zeroed record when empty.
|
||||
let one = [<WireConst as bytemuck::Zeroable>::zeroed()];
|
||||
let data: &[WireConst] = if consts.is_empty() { &one } else { consts };
|
||||
let buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("wire_const.buf"),
|
||||
contents: bytemuck::cast_slice(data),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
});
|
||||
let buf = super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
"wire_const.buf",
|
||||
data,
|
||||
wgpu::BufferUsages::STORAGE,
|
||||
);
|
||||
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("wire_const.bg"),
|
||||
layout: bgl,
|
||||
@@ -869,6 +860,7 @@ impl WireGpu {
|
||||
/// of a large selected polyline or block before packing the overlay.
|
||||
pub fn from_highlight_refs(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[&WireModel],
|
||||
color: [f32; 4],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
@@ -895,12 +887,13 @@ impl WireGpu {
|
||||
if instances.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let bind_group = build_const_bind_group(device, const_bgl, &consts);
|
||||
let bind_group = build_const_bind_group(device, queue, const_bgl, &consts);
|
||||
return instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| Self {
|
||||
instance_buffer: instance_buffer_mapped(
|
||||
instance_buffer: instance_buffer(
|
||||
device,
|
||||
queue,
|
||||
"wire.highlight.ibuf",
|
||||
chunk,
|
||||
),
|
||||
@@ -928,8 +921,9 @@ impl WireGpu {
|
||||
instances
|
||||
.chunks(MAX_PACKED_INSTANCES)
|
||||
.map(|chunk| Self {
|
||||
instance_buffer: instance_buffer_mapped(
|
||||
instance_buffer: instance_buffer(
|
||||
device,
|
||||
queue,
|
||||
"wire.highlight.compat.ibuf",
|
||||
chunk,
|
||||
),
|
||||
@@ -947,6 +941,7 @@ impl WireGpu {
|
||||
/// uses chunked resident buffers.
|
||||
pub fn from_run_refs(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[&WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
mesh_edge: bool,
|
||||
@@ -975,8 +970,9 @@ impl WireGpu {
|
||||
return instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| Self {
|
||||
instance_buffer: instance_buffer_mapped(
|
||||
instance_buffer: instance_buffer(
|
||||
device,
|
||||
queue,
|
||||
"wire.run.hybrid.compat.ibuf",
|
||||
chunk,
|
||||
),
|
||||
@@ -1012,11 +1008,11 @@ impl WireGpu {
|
||||
if instances.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let bind_group = build_const_bind_group(device, const_bgl, &consts);
|
||||
let bind_group = build_const_bind_group(device, queue, const_bgl, &consts);
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| Self {
|
||||
instance_buffer: instance_buffer_mapped(device, "wire.run.hybrid.ibuf", chunk),
|
||||
instance_buffer: instance_buffer(device, queue, "wire.run.hybrid.ibuf", chunk),
|
||||
first_instance: 0,
|
||||
instance_count: chunk.len() as u32,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
@@ -1037,6 +1033,7 @@ impl WireGpu {
|
||||
/// image for transparent / coincident wires.
|
||||
pub fn from_run(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
mesh_edge: bool,
|
||||
@@ -1071,11 +1068,11 @@ impl WireGpu {
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let bg = build_const_bind_group(device, const_bgl, &consts);
|
||||
let bg = build_const_bind_group(device, queue, const_bgl, &consts);
|
||||
return instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
let buf = instance_buffer(device, queue, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
first_instance: 0,
|
||||
@@ -1108,7 +1105,7 @@ impl WireGpu {
|
||||
instances
|
||||
.chunks(MAX_PACKED_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.compat.ibuf", chunk);
|
||||
let buf = instance_buffer(device, queue, "wire.run.compat.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
first_instance: 0,
|
||||
@@ -1125,6 +1122,7 @@ impl WireGpu {
|
||||
/// per-instance.
|
||||
pub fn from_batch(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||
@@ -1158,13 +1156,13 @@ impl WireGpu {
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let bg = build_const_bind_group(device, const_bgl, &consts);
|
||||
let bg = build_const_bind_group(device, queue, const_bgl, &consts);
|
||||
return instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let label = format!("wire.batch.ibuf.{i}");
|
||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
let instance_buffer = instance_buffer(device, queue, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
first_instance: 0,
|
||||
@@ -1191,7 +1189,7 @@ impl WireGpu {
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let label = format!("wire.batch.compat.ibuf.{i}");
|
||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
let instance_buffer = instance_buffer(device, queue, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
first_instance: 0,
|
||||
|
||||
Reference in new issue
Block a user