Files
OneCAD-Draft/src/scene/pipeline/wipeout_gpu.rs
T
PanosandClaude Opus 5 6747d7125e 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 <noreply@anthropic.com>
2026-09-04 16:21:08 +03:00

568 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Wipeout GPU buffers — the legacy per-primitive fill renderer, now used ONLY
// for wipeout masks (solid fills drawn after wires to hide them). Real hatch
// fills go through the capability-selected `hatch_gpu` renderer.
//
// It retains a general pattern/gradient capability (mode 0/2 below) and the
// `MAX_FAMILIES = 16` cap, but wipeouts are always solid (mode 1, zero pattern
// families), so the cap never truncates anything in practice. The family code
// is kept only as a latent non-batched fallback; do NOT read `MAX_FAMILIES` as
// a limit on visible hatch fills.
//
// Group 1 bindings per fill:
// binding 0 — HatchUniformData fill and plane parameters
// binding 1 — BoundaryData (16384 bytes) boundary polygon vertices
// binding 2 — FamilyBatchData (1296 bytes) up to 16 line families + 128 dash values (unused for wipeouts)
use crate::scene::model::hatch_model::{
FillPlane, HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS,
};
use iced::wgpu;
// ── Constants ──────────────────────────────────────────────────────────────
pub const MAX_FAMILIES: usize = 16;
pub const MAX_DASHES: usize = 128;
// ── Vertex ────────────────────────────────────────────────────────────────
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchVertex {
pub pos: [f32; 3],
pub _pad: f32,
}
impl HatchVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<HatchVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WipeoutPlacement {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: f32,
}
impl WipeoutPlacement {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: 12, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
}
// ── Uniform structs ───────────────────────────────────────────────────────
/// Per-fill parameters (binding 0).
///
/// `mode` encoding:
/// 0 → Pattern (families in FamilyBatchData)
/// 1 → Solid
/// 2 → Gradient (grad_cos/sin/min/range used)
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchUniformData {
pub color: [f32; 4], // 0: primary RGBA / gradient start
pub color2: [f32; 4], // 16: gradient end color
pub mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
pub vertex_count: u32, // 36: boundary vertex count
pub angle_offset: f32, // 40: pattern rotation (radians)
pub scale: f32, // 44: pattern scale multiplier
pub grad_cos: f32, // 48: gradient direction cos
pub grad_sin: f32, // 52: gradient direction sin
pub grad_min: f32, // 56: gradient proj_min
pub grad_range: f32, // 60: gradient proj_range
pub origin: [f32; 2], // 64: reserved pattern origin
pub origin_low: [f32; 2], // 72: reserved low residual
pub draw_depth: f32, // 80: signed (-1,1) draw-order bias
pub _pad: [f32; 3], // 84..96
pub plane_origin_high: [f32; 4],
pub plane_origin_low: [f32; 4],
pub plane_x: [f32; 4],
pub plane_y: [f32; 4],
}
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BoundaryData {
pub verts: [[f32; 4]; MAX_HATCH_BOUNDARY_VERTS], // world XY in .xy, .zw unused
}
/// One line family packed for the shader (48 bytes).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineFamilyGpu {
pub cos_a: f32, // cos(angle_rad)
pub sin_a: f32, // sin(angle_rad)
pub x0: f32, // family origin x
pub y0: f32, // family origin y
pub dx: f32, // step vector x
pub dy: f32, // step vector y
pub perp_step: f32, // -dx*sin_a + dy*cos_a (perpendicular spacing)
pub along_step: f32, // dx*cos_a + dy*sin_a (along-line phase shift per step)
pub line_width: f32, // |perp_step| * 0.08
pub period: f32, // sum of |dash values| (0 = solid)
pub n_dashes: u32, // number of dash entries (0 = solid)
pub dash_off: u32, // index into FamilyBatchData::dash_values
}
/// All line families + dash values for one hatch (binding 2).
/// Total size: 16×48 + 32×16 + 4×4 = 768 + 512 + 16 = 1296 bytes.
/// dash_values is [f32; 4]×32 so each element is vec4<f32> (16-byte stride, valid in uniform).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct FamilyBatchData {
pub families: [LineFamilyGpu; MAX_FAMILIES], // 768 bytes
pub dash_values: [[f32; 4]; 32], // 512 bytes (128 f32s as 32×vec4)
pub n_families: u32, // 4 bytes
pub _pad: [u32; 3], // 12 bytes
} // 1296 bytes total
// ── Per-hatch GPU handle ───────────────────────────────────────────────────
pub struct WipeoutGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub instance_count: u32,
pub bind_group: wgpu::BindGroup,
/// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
/// boundary polygon. Used by the per-frame LOD pass to skip hatches
/// whose entire footprint projects to less than ~2 px (Phase 3.3).
pub world_aabb: [f32; 4],
_uniform_buf: wgpu::Buffer,
_boundary_buf: wgpu::Buffer,
_family_buf: wgpu::Buffer,
}
impl WipeoutGpu {
pub fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
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 {
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Gradient { angle_deg, color2, .. } => {
let r = angle_deg.to_radians();
(2u32, *color2, r.cos(), r.sin())
}
};
// ── Bounding box ─────────────────────────────────────────────────
let plane = model.fill_plane.unwrap_or(FillPlane {
origin: [model.world_origin[0], model.world_origin[1], 0.0],
x_axis: [1.0, 0.0, 0.0],
y_axis: [0.0, 1.0, 0.0],
});
let gpu_boundary = model
.fill_plane_boundary
.as_deref()
.unwrap_or(model.boundary.as_ref());
let (mut min_x, mut max_x, mut min_y, mut max_y) = (
f32::INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
);
for &[x, y] in gpu_boundary {
if !x.is_finite() || !y.is_finite() {
continue;
}
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
let max_spacing = match &model.pattern {
HatchPattern::Pattern(families) => families
.iter()
.map(|f| perp_spacing(f).abs())
.fold(0.0f32, f32::max),
_ => 5.0,
};
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
let pad = if mode == 1 {
0.0
} else {
(diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0)
};
let (x0, x1, y0, y1) = (
min_x - pad,
max_x + pad,
min_y - pad,
max_y + pad,
);
let quad = [
HatchVertex {
pos: [x0, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y1, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x0, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y1, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x0, y1, 0.0],
_pad: 0.0,
},
];
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 {
let projs: Vec<f32> = gpu_boundary.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| x * grad_cos + y * grad_sin)
.collect();
if projs.is_empty() {
(0.0, 1.0)
} else {
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
(proj_min, (proj_max - proj_min).max(1e-4))
}
} else {
(0.0, 1.0)
};
// ── HatchUniformData ─────────────────────────────────────────────
let n = gpu_boundary.len().min(MAX_HATCH_BOUNDARY_VERTS);
let plane_origin_high = plane.origin.map(|value| value as f32);
let plane_origin_low: [f32; 3] = std::array::from_fn(|index| {
(plane.origin[index] - plane_origin_high[index] as f64) as f32
});
let uniform_data = HatchUniformData {
color: model.color,
color2,
mode,
vertex_count: n as u32,
angle_offset: model.angle_offset,
scale: model.scale,
grad_cos,
grad_sin,
grad_min,
grad_range,
origin: [0.0; 2],
origin_low: [0.0; 2],
draw_depth: 0.0,
_pad: [0.0; 3],
plane_origin_high: [
plane_origin_high[0],
plane_origin_high[1],
plane_origin_high[2],
0.0,
],
plane_origin_low: [
plane_origin_low[0],
plane_origin_low[1],
plane_origin_low[2],
0.0,
],
plane_x: [
plane.x_axis[0] as f32,
plane.x_axis[1] as f32,
plane.x_axis[2] as f32,
0.0,
],
plane_y: [
plane.y_axis[0] as f32,
plane.y_axis[1] as f32,
plane.y_axis[2] as f32,
0.0,
],
};
// ── BoundaryData (in snapped-local space) ────────────────────────
let mut boundary_data = BoundaryData {
verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS],
};
for (i, &[x, y]) in gpu_boundary.iter()
.take(MAX_HATCH_BOUNDARY_VERTS)
.enumerate()
{
if x.is_finite() && y.is_finite() {
boundary_data.verts[i] = [x, y, 0.0, 0.0];
} else {
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
boundary_data.verts[i] = [s, s, 0.0, 0.0];
}
}
// ── FamilyBatchData ───────────────────────────────────────────────
let family_batch = build_family_batch(&model.pattern);
// ── GPU buffers ───────────────────────────────────────────────────
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"),
layout: bgl1,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: _uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: _boundary_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: _family_buf.as_entire_binding(),
},
],
});
// Boundary vertices are stored as f32 offsets from
// `model.world_origin` (so f32 keeps precision at UTM-scale
// WCS). The pipeline-level frustum cull (Phase 2.3
// `aabb_offscreen`) projects through `view_proj`, which expects
// local-space (world_offset-subtracted) coordinates — the same
// space as `world_origin`. Add the origin back so `world_aabb`
// is in that absolute local space rather than relative to the
// anchor. (Sub-pixel LOD via `aabb_below_pixel` is unaffected:
// it measures a projected diagonal, invariant under translation.)
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placements: Vec<WipeoutPlacement> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = std::array::from_fn(|index| translation[index] - base[index]);
let high = delta.map(|value| value as f32);
WipeoutPlacement {
translation: high,
translation_low: std::array::from_fn(|index| {
(delta[index] - high[index] as f64) as f32
}),
draw_depth: model.draw_depth,
_pad: 0.0,
}
})
.collect();
let instance_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"wipeout.instances",
&placements,
wgpu::BufferUsages::VERTEX,
);
let mut world_bounds = [
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for &[x, y] in model.boundary.iter() {
if x.is_finite() && y.is_finite() {
world_bounds[0] = world_bounds[0].min(x);
world_bounds[1] = world_bounds[1].min(y);
world_bounds[2] = world_bounds[2].max(x);
world_bounds[3] = world_bounds[3].max(y);
}
}
let tilted = plane.origin[2].abs() > f64::EPSILON
|| plane.x_axis[2].abs() > f64::EPSILON
|| plane.y_axis[2].abs() > f64::EPSILON;
let world_aabb = if tilted {
[f32::NAN; 4]
} else if world_bounds[0].is_finite() && world_bounds[1].is_finite() {
let ox = model.world_origin[0] as f32;
let oy = model.world_origin[1] as f32;
let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for placement in &placements {
aabb[0] = aabb[0].min(world_bounds[0] + ox + placement.translation[0]);
aabb[1] = aabb[1].min(world_bounds[1] + oy + placement.translation[1]);
aabb[2] = aabb[2].max(world_bounds[2] + ox + placement.translation[0]);
aabb[3] = aabb[3].max(world_bounds[3] + oy + placement.translation[1]);
}
aabb
} else {
world_bounds
};
Self {
vertex_buffer,
instance_buffer,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
_boundary_buf,
_family_buf,
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
/// Perpendicular spacing between adjacent parallel lines.
/// Pattern spacing perpendicular to adjacent parallel lines.
fn perp_spacing(f: &PatFamily) -> f32 {
f.dy
}
/// Pack pattern families into the GPU batch struct.
fn build_family_batch(pattern: &HatchPattern) -> FamilyBatchData {
let mut batch = FamilyBatchData {
families: [LineFamilyGpu {
cos_a: 0.0,
sin_a: 0.0,
x0: 0.0,
y0: 0.0,
dx: 0.0,
dy: 0.0,
perp_step: 0.0,
along_step: 0.0,
line_width: 0.0,
period: 0.0,
n_dashes: 0,
dash_off: 0,
}; MAX_FAMILIES],
dash_values: [[0.0; 4]; 32],
n_families: 0,
_pad: [0; 3],
};
let HatchPattern::Pattern(families) = pattern else {
return batch;
};
let mut dash_cursor: usize = 0;
for (fi, family) in families.iter().take(MAX_FAMILIES).enumerate() {
let angle_r = family.angle_deg.to_radians();
let cos_a = angle_r.cos();
let sin_a = angle_r.sin();
// Pattern offsets are stored in the line-local frame.
let perp_step = family.dy;
let along_step = family.dx;
let line_width = 0.0_f32; // unused: shader uses screen-space derivative for 1px lines
// Dash pattern: collect up to available space.
let n_avail = MAX_DASHES.saturating_sub(dash_cursor);
let n_dashes = family.dashes.len().min(n_avail);
let period: f32 = family.dashes[..n_dashes].iter().map(|d| d.abs()).sum();
let dash_off = dash_cursor as u32;
for &d in &family.dashes[..n_dashes] {
batch.dash_values[dash_cursor / 4][dash_cursor % 4] = d;
dash_cursor += 1;
}
batch.families[fi] = LineFamilyGpu {
cos_a,
sin_a,
x0: family.x0,
y0: family.y0,
dx: family.dx,
dy: family.dy,
perp_step,
along_step,
line_width,
period: if n_dashes > 0 { period } else { 0.0 },
n_dashes: n_dashes as u32,
dash_off,
};
batch.n_families += 1;
if dash_cursor >= MAX_DASHES {
break;
}
}
batch
}