fix: no GPU allocation path maps, however small the buffer

The previous commit converted the allocations that scale with the
drawing and deliberately left the small fixed-size ones -- uniforms, the
viewcube's static geometry, per-image and per-wipeout quads -- reasoning
that an allocation failure was not a realistic outcome for a few dozen
bytes.

That reasoning was wrong, and the log says so precisely. On the
reproducer the vertex buffers degraded exactly as intended:

  [gpu] uncaptured ... Out of Memory
  [gpu] uncaptured ... In Queue::write_buffer
          Buffer with 'block_wire.vertices' label is invalid
  ... errors #4 #8 #16 #32 #64 #128, session kept alive

and then the process died here:

  thread 'main' panicked at wgpu_core.rs:2253:
  Error in Buffer::get_mapped_range: Validation Error
  Caused by: Buffer with 'block_wire.const' label is invalid

'block_wire.const' is a single uniform struct. Once the device is out of
memory every allocation fails, including a 32-byte one, and
create_buffer_init maps it and panics. Size was never the relevant
property -- whether the path maps is.

So every remaining allocation goes through gpu_upload too: block-wire
const, the viewcube's seven buffers, the blit and clip-boundary buffers,
image instances/params/vertices, mesh material params and surface and the
empty index stub, the texture-hatch uniform, and all five wipeout
buffers. `grep -r create_buffer_init src` now finds only a comment, and
no mapped_at_creation:true remains outside gpu_upload's own docs.

`queue` is threaded through the last holdouts: upload_wipeouts,
upload_clip_boundary, MeshBatchStubs::new and
create_material_bind_group_from_resources.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-04 16:21:08 +03:00
1 parent 0d11421dea
commit 6747d7125e
8 files changed
+181 -129

No files matched your search

+7 -6
View File
@@ -3,7 +3,6 @@
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
/// Width (in texels) of the RGBA32F data texture. Height grows to fit; a hatch
/// with N total texels uses ceil(N / WIDTH) rows.
@@ -377,11 +376,13 @@ impl TextureHatch {
dash_off,
tex_width: width,
};
let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.texture.uniform"),
contents: bytemuck::bytes_of(&uniform_data),
usage: wgpu::BufferUsages::UNIFORM,
});
let _uniform_buf = crate::scene::pipeline::gpu_upload::upload_buffer(
device,
queue,
"hatch.texture.uniform",
std::slice::from_ref(&uniform_data),
wgpu::BufferUsages::UNIFORM,
);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("hatch.texture.bind_group1"),
+19 -19
View File
@@ -7,7 +7,6 @@
use crate::scene::model::image_model::ImageModel;
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
use std::sync::Arc;
// ── Vertex ────────────────────────────────────────────────────────────────
@@ -179,24 +178,24 @@ impl ImageGpu {
}
})
.collect();
let instance_buffer = Arc::new(device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("image.instances"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX,
},
let instance_buffer = Arc::new(super::gpu_upload::upload_buffer(
device,
queue,
"image.instances",
&instances,
wgpu::BufferUsages::VERTEX,
));
let params = ImageParams {
opacity: model.opacity.clamp(0.0, 1.0),
draw_depth: 0.0,
_pad: [0.0; 2],
};
let params_buf = Arc::new(device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("image.params"),
contents: bytemuck::bytes_of(&params),
usage: wgpu::BufferUsages::UNIFORM,
},
let params_buf = Arc::new(super::gpu_upload::upload_buffer(
device,
queue,
"image.params",
std::slice::from_ref(&params),
wgpu::BufferUsages::UNIFORM,
));
let mut output = Vec::with_capacity(x_tiles.len() * y_tiles.len());
@@ -264,12 +263,13 @@ impl ImageGpu {
},
],
});
let vertex_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image.vbuf"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let vertex_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"image.vbuf",
&verts,
wgpu::BufferUsages::VERTEX,
);
output.push(Self {
vertex_buffer,
instance_buffer: Arc::clone(&instance_buffer),
+27 -18
View File
@@ -2,7 +2,6 @@
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
// ── Vertex layout ─────────────────────────────────────────────────────────
@@ -377,13 +376,15 @@ struct MeshBatchStubs {
}
impl MeshBatchStubs {
fn new(device: &wgpu::Device) -> Self {
fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
Self {
index: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.batch.empty_ibuf"),
contents: bytemuck::cast_slice(&[0u32]),
usage: wgpu::BufferUsages::INDEX,
}),
index: super::gpu_upload::upload_buffer(
device,
queue,
"mesh.batch.empty_ibuf",
&[0u32],
wgpu::BufferUsages::INDEX,
),
vertex: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh.batch.empty_vbuf"),
size: std::mem::size_of::<MeshVertex>() as u64,
@@ -784,11 +785,13 @@ pub fn create_material_resources(
[material.color_bleed_scale, 0.0, 0.0, 0.0]
}),
};
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.material.params"),
contents: bytemuck::bytes_of(&params),
usage: wgpu::BufferUsages::UNIFORM,
});
let params_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"mesh.material.params",
std::slice::from_ref(&params),
wgpu::BufferUsages::UNIFORM,
);
MeshMaterialResources {
diffuse_view,
specular_view,
@@ -810,6 +813,7 @@ pub fn create_material_resources(
pub fn create_material_bind_group_from_resources(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
resources: &MeshMaterialResources,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
@@ -825,11 +829,13 @@ pub fn create_material_bind_group_from_resources(
advanced,
flags,
};
let surface_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.material.surface"),
contents: bytemuck::bytes_of(&surface),
usage: wgpu::BufferUsages::UNIFORM,
});
let surface_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"mesh.material.surface",
std::slice::from_ref(&surface),
wgpu::BufferUsages::UNIFORM,
);
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mesh.material.bind_group"),
layout,
@@ -912,6 +918,7 @@ pub fn create_material_bind_group(
let color = material.map_or([0.8, 0.8, 0.8, 1.0], |material| material.diffuse);
create_material_bind_group_from_resources(
device,
queue,
layout,
&resources,
material,
@@ -936,6 +943,7 @@ pub fn upload_chunk_material_bind_groups(
let resources = create_material_resources(device, queue, Some(material));
chunk.material_bind_group = Some(create_material_bind_group_from_resources(
device,
queue,
layout,
&resources,
chunk.material.as_ref(),
@@ -950,6 +958,7 @@ pub fn upload_chunk_material_bind_groups(
};
chunk.material_bind_group = Some(create_material_bind_group_from_resources(
device,
queue,
layout,
resources,
chunk.material.as_ref(),
@@ -1586,7 +1595,7 @@ pub fn build_mesh_batch_filtered(
// vertices). Bound chunks by the largest buffer produced for wire meshes.
let max_tris =
(budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
let stubs = MeshBatchStubs::new(device);
let stubs = MeshBatchStubs::new(device, queue);
let mut chunks = Vec::new();
let mut verts: Vec<MeshVertex> = Vec::new();
+22 -15
View File
@@ -16,7 +16,6 @@ pub mod wire_arena;
pub mod wire_gpu;
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
use iced::{Rectangle, Size};
pub use face3d_gpu::Face3DGpu;
@@ -2122,11 +2121,13 @@ impl Pipeline {
// UV crop uniform: [uv_offset_x, uv_offset_y, uv_scale_x, uv_scale_y]
// padded to 16 bytes (std140 vec2 alignment). Defaulted to the
// identity crop (offset 0, scale 1) for the common on-canvas case.
let blit_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("blit.uniform_buffer"),
contents: bytemuck::cast_slice(&[0.0f32, 0.0, 1.0, 1.0]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let blit_uniform_buffer = gpu_upload::upload_buffer(
device,
queue,
"blit.uniform_buffer",
&[0.0f32, 0.0, 1.0, 1.0],
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let blit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("blit.pipeline_layout"),
@@ -2969,8 +2970,7 @@ impl Pipeline {
/// so the mask pipeline stamps it straight into the stencil with `Invert`
/// (even-odd fill → interior marked, any convexity). Empty input clears the
/// boundary so the viewport renders unclipped (its render rectangle clips).
pub fn upload_clip_boundary(&mut self, device: &wgpu::Device, boundary_ndc: &[[f32; 2]]) {
use wgpu::util::DeviceExt;
pub fn upload_clip_boundary(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, boundary_ndc: &[[f32; 2]]) {
if boundary_ndc.len() < 3 {
self.clip_boundary = None;
return;
@@ -2986,11 +2986,13 @@ impl Pipeline {
self.clip_boundary = None;
return;
}
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("clip_boundary.vbuf"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let vbuf = gpu_upload::upload_buffer(
device,
queue,
"clip_boundary.vbuf",
&verts,
wgpu::BufferUsages::VERTEX,
);
self.clip_boundary = Some((vbuf, (verts.len() / 2) as u32));
}
@@ -3378,13 +3380,18 @@ impl Pipeline {
self.hatch_gpu.upload_preview(device, queue, hatches);
}
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
pub fn upload_wipeouts(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wipeouts: &[HatchModel],
) {
let renderable: Vec<HatchModel> = wipeouts
.iter()
.filter(|h| h.boundary.len() >= 3)
.cloned()
.collect();
self.gpu_wipeouts = WipeoutGpu::from_models(device, &renderable, &self.wipeout_bgl1);
self.gpu_wipeouts = WipeoutGpu::from_models(device, queue, &renderable, &self.wipeout_bgl1);
}
pub fn upload_images(
+49 -36
View File
@@ -1078,43 +1078,54 @@ pub struct ViewCubePipeline {
impl ViewCubePipeline {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
use wgpu::util::DeviceExt;
let (verts, idxs) = build_geometry();
let line_verts = surface_edge_lines(&verts, &idxs);
let (ring_verts, ring_idxs) = build_ring_geometry();
let ring_line_verts = surface_edge_lines(&ring_verts, &ring_idxs);
let cube_px = VIEWCUBE_PX;
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.vb"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.ib"),
contents: bytemuck::cast_slice(&idxs),
usage: wgpu::BufferUsages::INDEX,
});
let line_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.line_vb"),
contents: bytemuck::cast_slice(&line_verts),
usage: wgpu::BufferUsages::VERTEX,
});
let ring_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.ring_vb"),
contents: bytemuck::cast_slice(&ring_verts),
usage: wgpu::BufferUsages::VERTEX,
});
let ring_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.ring_ib"),
contents: bytemuck::cast_slice(&ring_idxs),
usage: wgpu::BufferUsages::INDEX,
});
let vertex_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"vc.vb",
&verts,
wgpu::BufferUsages::VERTEX,
);
let index_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"vc.ib",
&idxs,
wgpu::BufferUsages::INDEX,
);
let line_vertex_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"vc.line_vb",
&line_verts,
wgpu::BufferUsages::VERTEX,
);
let ring_vertex_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"vc.ring_vb",
&ring_verts,
wgpu::BufferUsages::VERTEX,
);
let ring_index_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"vc.ring_ib",
&ring_idxs,
wgpu::BufferUsages::INDEX,
);
let ring_line_vertex_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.ring_line_vb"),
contents: bytemuck::cast_slice(&ring_line_verts),
usage: wgpu::BufferUsages::VERTEX,
});
super::gpu_upload::upload_buffer(
device,
queue,
"vc.ring_line_vb",
&ring_line_verts,
wgpu::BufferUsages::VERTEX,
);
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vc.ub"),
size: std::mem::size_of::<CubeUniforms>() as u64,
@@ -1291,11 +1302,13 @@ impl ViewCubePipeline {
],
});
let composite_uniform_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vc.composite_uniform"),
contents: bytemuck::cast_slice(&[1.0f32, 1.0, 0.0, 0.0]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
super::gpu_upload::upload_buffer(
device,
queue,
"vc.composite_uniform",
&[1.0f32, 1.0, 0.0, 0.0],
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let composite_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("vc.composite_sampler"),
mag_filter: wgpu::FilterMode::Nearest,
+43 -28
View File
@@ -17,7 +17,6 @@ use crate::scene::model::hatch_model::{
FillPlane, HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS,
};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
// ── Constants ──────────────────────────────────────────────────────────────
@@ -157,6 +156,7 @@ pub struct WipeoutGpu {
impl WipeoutGpu {
pub fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
@@ -176,11 +176,16 @@ impl WipeoutGpu {
}
groups
.into_iter()
.map(|group| Self::new(device, &group, bgl1))
.map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
fn new(device: &wgpu::Device, models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout) -> Self {
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[&HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Self {
let model = models[0];
// ── Decode pattern mode ──────────────────────────────────────────
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
@@ -264,11 +269,13 @@ impl WipeoutGpu {
_pad: 0.0,
},
];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.vbuf"),
contents: bytemuck::cast_slice(&quad),
usage: wgpu::BufferUsages::VERTEX,
});
let vertex_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"hatch.vbuf",
&quad,
wgpu::BufferUsages::VERTEX,
);
// ── Gradient: projection range (in snapped-local space) ──────────
let (grad_min, grad_range) = if mode == 2 {
@@ -354,21 +361,27 @@ impl WipeoutGpu {
let family_batch = build_family_batch(&model.pattern);
// ── GPU buffers ───────────────────────────────────────────────────
let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.uniforms"),
contents: bytemuck::bytes_of(&uniform_data),
usage: wgpu::BufferUsages::UNIFORM,
});
let _boundary_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.boundary"),
contents: bytemuck::bytes_of(&boundary_data),
usage: wgpu::BufferUsages::UNIFORM,
});
let _family_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.families"),
contents: bytemuck::bytes_of(&family_batch),
usage: wgpu::BufferUsages::UNIFORM,
});
let _uniform_buf = super::gpu_upload::upload_buffer(
device,
queue,
"hatch.uniforms",
std::slice::from_ref(&uniform_data),
wgpu::BufferUsages::UNIFORM,
);
let _boundary_buf = super::gpu_upload::upload_buffer(
device,
queue,
"hatch.boundary",
std::slice::from_ref(&boundary_data),
wgpu::BufferUsages::UNIFORM,
);
let _family_buf = super::gpu_upload::upload_buffer(
device,
queue,
"hatch.families",
std::slice::from_ref(&family_batch),
wgpu::BufferUsages::UNIFORM,
);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("hatch.bind_group1"),
@@ -419,11 +432,13 @@ impl WipeoutGpu {
}
})
.collect();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("wipeout.instances"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"wipeout.instances",
&placements,
wgpu::BufferUsages::VERTEX,
);
let mut world_bounds = [
f32::INFINITY,
f32::INFINITY,
+12 -6
View File
@@ -676,7 +676,6 @@ impl BlockWireGpu {
color_override: Option<[f32; 4]>,
const_bgl: &wgpu::BindGroupLayout,
) -> Vec<Self> {
use wgpu::util::DeviceExt;
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> =
rustc_hash::FxHashMap::default();
@@ -751,11 +750,18 @@ impl BlockWireGpu {
)
})
.collect();
let const_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_wire.const"),
contents: bytemuck::bytes_of(&constant),
usage: wgpu::BufferUsages::UNIFORM,
});
// Tiny, but size is irrelevant here: once the device is out of
// memory every allocation fails, including a one-struct uniform,
// and create_buffer_init would map the invalid buffer and panic.
// This is the allocation that killed the process after the vertex
// buffers had already degraded gracefully.
let const_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"block_wire.const",
std::slice::from_ref(&constant),
wgpu::BufferUsages::UNIFORM,
);
let const_bind_group = std::sync::Arc::new(device.create_bind_group(
&wgpu::BindGroupDescriptor {
label: Some("block_wire.const.bg"),
+2 -1
View File
@@ -546,6 +546,7 @@ impl shader::Primitive for Primitive {
if wipeout_changed || fill_changed {
inner.upload_wipeouts(
device,
queue,
if fill_mode {
&vp.wipeout_hatches[..]
} else {
@@ -1117,7 +1118,7 @@ impl shader::Primitive for Primitive {
);
inner.silhouette_key = silhouette_key;
}
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
inner.upload_clip_boundary(device, queue, &vp.clip_boundary_ndc);
let hatch_lod_key = (
Arc::as_ptr(&vp.hatches) as usize,
vp.camera_generation,