From 6747d7125ed6bc28099572ab529155af03ae06c2 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:13:16 +0300 Subject: [PATCH] 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 --- src/scene/pipeline/hatch_gpu/texture.rs | 13 ++-- src/scene/pipeline/image_gpu.rs | 38 +++++------ src/scene/pipeline/mesh_gpu.rs | 45 +++++++------ src/scene/pipeline/mod.rs | 37 ++++++----- src/scene/pipeline/viewcube.rs | 85 ++++++++++++++----------- src/scene/pipeline/wipeout_gpu.rs | 71 +++++++++++++-------- src/scene/pipeline/wire_gpu.rs | 18 ++++-- src/scene/view/render.rs | 3 +- 8 files changed, 181 insertions(+), 129 deletions(-) diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index 7ad49c703..16846e7be 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -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"), diff --git a/src/scene/pipeline/image_gpu.rs b/src/scene/pipeline/image_gpu.rs index 5a239e4ca..284a207f1 100644 --- a/src/scene/pipeline/image_gpu.rs +++ b/src/scene/pipeline/image_gpu.rs @@ -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(¶ms), - usage: wgpu::BufferUsages::UNIFORM, - }, + let params_buf = Arc::new(super::gpu_upload::upload_buffer( + device, + queue, + "image.params", + std::slice::from_ref(¶ms), + 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), diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 36bfa856c..3a4f84250 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -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::() 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(¶ms), - usage: wgpu::BufferUsages::UNIFORM, - }); + let params_buffer = super::gpu_upload::upload_buffer( + device, + queue, + "mesh.material.params", + std::slice::from_ref(¶ms), + 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::())).max(1); - let stubs = MeshBatchStubs::new(device); + let stubs = MeshBatchStubs::new(device, queue); let mut chunks = Vec::new(); let mut verts: Vec = Vec::new(); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index a27331615..b3ade3daf 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -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 = 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( diff --git a/src/scene/pipeline/viewcube.rs b/src/scene/pipeline/viewcube.rs index e6cbbb64c..782f10a1e 100644 --- a/src/scene/pipeline/viewcube.rs +++ b/src/scene/pipeline/viewcube.rs @@ -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::() 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, diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 02b15f221..73105d784 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -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 { @@ -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, diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index a23eb6faf..215854b77 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -676,7 +676,6 @@ impl BlockWireGpu { color_override: Option<[f32; 4]>, const_bgl: &wgpu::BindGroupLayout, ) -> Vec { - 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"), diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 8feda949b..3326543f4 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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,