style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+83 -47
View File
@@ -229,7 +229,14 @@ pub fn extract_circle_instance_from_geom_indexed(
axis_x,
axis_y,
radius,
} => (center, axis_x, axis_y, radius, 0.0f32, std::f32::consts::TAU),
} => (
center,
axis_x,
axis_y,
radius,
0.0f32,
std::f32::consts::TAU,
),
crate::scene::model::wire_model::TangentGeom::Arc {
center,
axis_x,
@@ -247,14 +254,29 @@ pub fn extract_circle_instance_from_geom_indexed(
),
_ => return None,
};
if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() || radius > 1e6 {
if radius <= 0.0
|| !radius.is_finite()
|| !start_angle.is_finite()
|| !end_angle.is_finite()
|| radius > 1e6
{
return None;
}
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
let hw = wire.line_weight_px * 0.5;
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
let pat0 = [
wire.pattern[0],
wire.pattern[1],
wire.pattern[2],
wire.pattern[3],
];
let pat1 = [
wire.pattern[4],
wire.pattern[5],
wire.pattern[6],
wire.pattern[7],
];
let (start_width, end_width) = if !wire.taper_widths.is_empty() {
if wire.taper_widths.len() >= 2 && wire.tangent_geoms.len() == 1 {
@@ -419,12 +441,13 @@ mod tests {
#[test]
fn extract_circle_instance_planar_success() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [100.0, 200.0, 300.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 50.0,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [100.0, 200.0, 300.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 50.0,
});
wire.line_weight_px = 2.0;
wire.color = [1.0, 0.0, 0.0, 1.0];
@@ -441,14 +464,15 @@ mod tests {
#[test]
fn extract_circle_instance_arc_success() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [10.0, 20.0, 30.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 25.0,
start_angle: 0.5,
end_angle: 2.5,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [10.0, 20.0, 30.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 25.0,
start_angle: 0.5,
end_angle: 2.5,
});
wire.line_weight_px = 3.0;
wire.color = [0.0, 1.0, 0.0, 1.0];
@@ -464,12 +488,13 @@ mod tests {
#[test]
fn extract_circle_instance_rejects_non_planar_or_complex() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
// 1. With fill_tris (e.g. 3D solid or hatch)
wire.fill_tris = vec![[0.0; 3]];
assert!(extract_circle_instance(&wire, 0.0).is_none());
@@ -496,14 +521,15 @@ mod tests {
#[test]
fn extract_circle_instance_constant_wide_arc() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [100.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 40.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [100.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 40.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
wire.world_width = 12.0;
wire.color = [1.0, 0.5, 0.0, 1.0];
@@ -516,14 +542,15 @@ mod tests {
#[test]
fn extract_circle_instance_tapered_arc() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.2,
end_angle: 1.8,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.2,
end_angle: 1.8,
});
wire.world_width = 20.0;
wire.taper_widths = vec![4.0, 20.0];
wire.color = [0.2, 0.8, 1.0, 1.0];
@@ -537,15 +564,17 @@ mod tests {
#[test]
fn extract_circle_instance_accepts_pick_tris() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Circle {
center: [5.0, 5.0, 0.0],
radius: 15.0,
});
wire.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Circle {
center: [5.0, 5.0, 0.0],
radius: 15.0,
});
wire.world_width = 6.0;
wire.pick_tris = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
wire.pick_tris_low = vec![[0.0; 3]; 3];
let insts = extract_circle_instances(&wire, 0.0).expect("wide curve with pick_tris should extract");
let insts =
extract_circle_instances(&wire, 0.0).expect("wide curve with pick_tris should extract");
assert_eq!(insts.len(), 1);
assert_eq!(insts[0].start_width, 6.0);
assert_eq!(insts[0].params[3], 6.0);
@@ -572,11 +601,18 @@ mod tests {
None,
false,
);
assert_eq!(wires.len(), 2, "donut should split into 2 analytical arc wires");
assert_eq!(
wires.len(),
2,
"donut should split into 2 analytical arc wires"
);
for w in &wires {
assert_eq!(w.world_width, 20.0);
assert_eq!(w.tangent_geoms.len(), 1);
assert!(matches!(w.tangent_geoms[0], crate::scene::model::wire_model::TangentGeom::Arc { .. }));
assert!(matches!(
w.tangent_geoms[0],
crate::scene::model::wire_model::TangentGeom::Arc { .. }
));
}
let depth_map = rustc_hash::FxHashMap::default();
+34 -16
View File
@@ -107,8 +107,8 @@ fn color_at(bytes: &[u8], ndc_x: f32) -> [u32; 3] {
#[test]
#[ignore = "requires a GPU adapter"]
fn wipeout_respects_close_block_draw_order() {
use codec::{entities::Wipeout, types::Vector2, EntityType};
use crate::scene::{model::wire_model::TangentGeom, Scene};
use codec::{entities::Wipeout, types::Vector2, EntityType};
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&Default::default())).expect("GPU adapter");
@@ -151,40 +151,58 @@ fn wipeout_respects_close_block_draw_order() {
let wires: Vec<_> = [(-0.3, 1), (0.3, 2)]
.into_iter()
.map(|(x, id)| {
let mut wire = WireModel::solid(
id.to_string(),
Vec::new(),
[1.; 4],
false,
);
let mut wire = WireModel::solid(id.to_string(), Vec::new(), [1.; 4], false);
let center = [x as f64 - 0.1, 0., 0.5];
let axis_x = [1., 0., 0.];
let axis_y = [0., 1., 0.];
match kind {
"circle" => wire.tangent_geoms.push(TangentGeom::PlanarCircle {
center, axis_x, axis_y, radius: 0.1,
center,
axis_x,
axis_y,
radius: 0.1,
}),
"arc" => wire.tangent_geoms.push(TangentGeom::Arc {
center, axis_x, axis_y, radius: 0.1,
center,
axis_x,
axis_y,
radius: 0.1,
start_angle: -std::f64::consts::FRAC_PI_2,
end_angle: std::f64::consts::FRAC_PI_2,
}),
"ellipse" => wire.tangent_geoms.push(TangentGeom::PlanarEllipse {
center, major_axis: [0.1, 0., 0.], normal: [0., 0., 1.],
minor_axis_ratio: 0.6, start_param: 0., end_param: std::f64::consts::TAU,
center,
major_axis: [0.1, 0., 0.],
normal: [0., 0., 1.],
minor_axis_ratio: 0.6,
start_param: 0.,
end_param: std::f64::consts::TAU,
}),
_ => wire.points = vec![[x, -0.65, 0.5], [x, 0.65, 0.5]],
}
wire
})
.collect();
pipeline.gpu_circles = std::sync::Arc::new(pipeline.upload_circles(&device, &queue, &wires, &depths));
pipeline.gpu_ellipses = std::sync::Arc::new(pipeline.upload_ellipses(&device, &queue, &wires, &depths));
pipeline.gpu_circles =
std::sync::Arc::new(pipeline.upload_circles(&device, &queue, &wires, &depths));
pipeline.gpu_ellipses =
std::sync::Arc::new(pipeline.upload_ellipses(&device, &queue, &wires, &depths));
let bytes = pixels(&device, &queue, &mut pipeline, &uniforms, &wires, &depths);
assert_eq!(color_at(&bytes, -0.3), [0; 3], "mask must cover earlier {kind}");
assert_ne!(color_at(&bytes, 0.3), [0; 3], "mask erased later {kind}, packed={packed}");
assert_eq!(
color_at(&bytes, -0.3),
[0; 3],
"mask must cover earlier {kind}"
);
assert_ne!(
color_at(&bytes, 0.3),
[0; 3],
"mask erased later {kind}, packed={packed}"
);
}
assert!(block_on(validation.pop()).is_none(), "GPU validation failed");
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
}
@@ -34,7 +34,6 @@ impl DeviceCapabilities {
pub fn supports_batched_hatch(self) -> bool {
self.max_storage_buffers_per_shader_stage >= Self::HATCH_STORAGE_BINDINGS
}
}
#[cfg(test)]
+63 -30
View File
@@ -212,31 +212,53 @@ pub fn extract_ellipse_instance_from_geom(
minor_axis_ratio,
start_param,
end_param,
} = *geom else {
} = *geom
else {
return None;
};
let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2];
let major_len_sq = major_axis[0] * major_axis[0]
+ major_axis[1] * major_axis[1]
+ major_axis[2] * major_axis[2];
let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
if major_len_sq <= 1e-12 || !major_len_sq.is_finite() || major_len_sq > 1e12
|| norm_len_sq <= 1e-12 || !norm_len_sq.is_finite()
|| minor_axis_ratio <= 1e-6 || !minor_axis_ratio.is_finite()
|| !start_param.is_finite() || !end_param.is_finite()
if major_len_sq <= 1e-12
|| !major_len_sq.is_finite()
|| major_len_sq > 1e12
|| norm_len_sq <= 1e-12
|| !norm_len_sq.is_finite()
|| minor_axis_ratio <= 1e-6
|| !minor_axis_ratio.is_finite()
|| !start_param.is_finite()
|| !end_param.is_finite()
{
return None;
}
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
let hw = wire.line_weight_px * 0.5;
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
let pat0 = [
wire.pattern[0],
wire.pattern[1],
wire.pattern[2],
wire.pattern[3],
];
let pat1 = [
wire.pattern[4],
wire.pattern[5],
wire.pattern[6],
wire.pattern[7],
];
Some(EllipseInstance {
center_high: ch,
_pad0: 0.0,
center_low: cl,
minor_axis_ratio: minor_axis_ratio.min(1.0) as f32,
major_axis: [major_axis[0] as f32, major_axis[1] as f32, major_axis[2] as f32],
major_axis: [
major_axis[0] as f32,
major_axis[1] as f32,
major_axis[2] as f32,
],
start_param: start_param as f32,
normal: [normal[0] as f32, normal[1] as f32, normal[2] as f32],
end_param: end_param as f32,
@@ -279,12 +301,19 @@ pub fn can_extract_ellipse_instances(wire: &crate::scene::WireModel) -> bool {
end_param,
..
} => {
let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2];
let major_len_sq = major_axis[0] * major_axis[0]
+ major_axis[1] * major_axis[1]
+ major_axis[2] * major_axis[2];
let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
major_len_sq > 1e-12 && major_len_sq.is_finite() && major_len_sq <= 1e12
&& norm_len_sq > 1e-12 && norm_len_sq.is_finite()
&& minor_axis_ratio > 1e-6 && minor_axis_ratio.is_finite()
&& start_param.is_finite() && end_param.is_finite()
major_len_sq > 1e-12
&& major_len_sq.is_finite()
&& major_len_sq <= 1e12
&& norm_len_sq > 1e-12
&& norm_len_sq.is_finite()
&& minor_axis_ratio > 1e-6
&& minor_axis_ratio.is_finite()
&& start_param.is_finite()
&& end_param.is_finite()
}
_ => false,
})
@@ -366,14 +395,16 @@ mod tests {
#[test]
fn extract_ellipse_instance_success() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [100.0, 200.0, 300.0],
major_axis: [50.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: 0.0,
end_param: std::f64::consts::TAU,
});
wire.tangent_geoms.push(
crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [100.0, 200.0, 300.0],
major_axis: [50.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: 0.0,
end_param: std::f64::consts::TAU,
},
);
wire.line_weight_px = 2.0;
wire.color = [1.0, 0.5, 0.0, 1.0];
@@ -391,14 +422,16 @@ mod tests {
#[test]
fn extract_elliptical_arc_instance_success() {
let mut wire = crate::scene::WireModel::default();
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [0.0, 0.0, 0.0],
major_axis: [10.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.8,
start_param: 0.5,
end_param: 2.5,
});
wire.tangent_geoms.push(
crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [0.0, 0.0, 0.0],
major_axis: [10.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.8,
start_param: 0.5,
end_param: 2.5,
},
);
wire.line_weight_px = 1.0;
wire.color = [0.0, 1.0, 0.0, 1.0];
+30 -26
View File
@@ -28,11 +28,7 @@ pub(crate) fn planar_solid_faces_view(wire: &WireModel, view_dir: glam::Vec3) ->
let view = view_dir.as_dvec3().normalize_or_zero();
let point = |index: usize| {
let high = wire.fill_tris[index];
let low = wire
.fill_tris_low
.get(index)
.copied()
.unwrap_or([0.0; 3]);
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
glam::DVec3::new(
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
@@ -48,10 +44,7 @@ pub(crate) fn planar_solid_faces_view(wire: &WireModel, view_dir: glam::Vec3) ->
})
}
pub(crate) fn planar_solid_visibility_key(
wires: &[WireModel],
view_dir: glam::Vec3,
) -> u64 {
pub(crate) fn planar_solid_visibility_key(wires: &[WireModel], view_dir: glam::Vec3) -> u64 {
let mut key = 0xcbf29ce484222325_u64;
for wire in wires
.iter()
@@ -138,9 +131,21 @@ pub struct Face3DInstance {
impl Face3DInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation_low) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(Face3DInstance, translation) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(Face3DInstance, translation_low) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(Face3DInstance, draw_depth) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -194,8 +199,7 @@ impl Face3DGpu {
view_dir: glam::Vec3,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Self {
let depth_of =
|w: &WireModel| -> f32 { super::wire_gpu::wire_draw_depth(w, depth_map) };
let depth_of = |w: &WireModel| -> f32 { super::wire_gpu::wire_draw_depth(w, depth_map) };
let mut verts_3d: Vec<Face3DVertex> = Vec::with_capacity(face3d_wires.len() * 6);
let mut verts_2d: Vec<Face3DVertex> = Vec::new();
@@ -212,11 +216,7 @@ impl Face3DGpu {
position,
color: fill_color,
draw_depth: 0.0,
position_low: wire
.fill_tris_low
.get(index)
.copied()
.unwrap_or([0.0; 3]),
position_low: wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]),
});
}
}
@@ -288,7 +288,11 @@ impl Face3DGpu {
zmin = zmin.min(p[2]);
zmax = zmax.max(p[2]);
}
let depth = if zmax - zmin > 1e-4 { 0.0 } else { depth_of(wire) };
let depth = if zmax - zmin > 1e-4 {
0.0
} else {
depth_of(wire)
};
for (i, &position) in wire.fill_tris.iter().enumerate() {
verts_2d.push(Face3DVertex {
position,
@@ -359,7 +363,11 @@ fn upload_block_chunks(
continue;
};
let [r, g, b, a] = source.color;
let color = if is_3d { [r * 0.45, g * 0.45, b * 0.45, a] } else { source.color };
let color = if is_3d {
[r * 0.45, g * 0.45, b * 0.45, a]
} else {
source.color
};
let vertices: Vec<Face3DVertex> = source
.fill_tris
.iter()
@@ -368,11 +376,7 @@ fn upload_block_chunks(
position,
color,
draw_depth: 0.0,
position_low: source
.fill_tris_low
.get(index)
.copied()
.unwrap_or([0.0; 3]),
position_low: source.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]),
})
.collect();
if vertices.is_empty() {
+144 -66
View File
@@ -68,28 +68,44 @@ fn block_edits_preserve_cache_coordinates_and_arena_partition() {
let first = block(1, 10.0);
let second = block(2, 20.0);
let depth = rustc_hash::FxHashMap::from_iter([(1, [0.1, 0.01]), (2, [0.2, 0.01])]);
let original = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
let unchanged = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
let original = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&depth,
&mut block_geometry,
);
let unchanged = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&depth,
&mut block_geometry,
);
assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id());
assert_eq!(original[0].instance_count, 2);
let removed = pipeline.upload_block_wires(&device, &queue, &[&second], &depth, &mut block_geometry);
let removed =
pipeline.upload_block_wires(&device, &queue, &[&second], &depth, &mut block_geometry);
assert_ne!(
original[0].geometry_id(),
removed[0].geometry_id(),
"removing the base instance must replace vertices baked at its old position"
);
let moved = block(2, 30.0);
let moved_gpu = pipeline.upload_block_wires(&device, &queue, &[&moved], &depth, &mut block_geometry);
let moved_gpu =
pipeline.upload_block_wires(&device, &queue, &[&moved], &depth, &mut block_geometry);
assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id());
let restored = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
let restored = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&depth,
&mut block_geometry,
);
assert_ne!(moved_gpu[0].geometry_id(), restored[0].geometry_id());
assert_eq!(restored[0].instance_count, 2);
assert_eq!(
block_geometry.len(),
1,
"discard obsolete geometry"
);
assert_eq!(block_geometry.len(), 1, "discard obsolete geometry");
let line = WireModel {
render_instance: None,
@@ -462,12 +478,14 @@ fn test_selected_circle_arc_ellipse_highlight_overlay() {
let mut circle_wire = WireModel::default();
circle_wire.name = "circle_wire".into();
circle_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
circle_wire
.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
let handle = codec::Handle::new(100);
let mut selected_handles = rustc_hash::FxHashSet::default();
@@ -548,8 +566,18 @@ fn test_selected_circle_arc_ellipse_highlight_overlay() {
&mut encoder,
&target,
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
[0.0, 0.0, 0.0, 1.0],
false,
false,
@@ -575,14 +603,16 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
// 1. Wide circular arc with pick triangles
let mut wide_arc = WireModel::default();
wide_arc.name = "wide_arc".into();
wide_arc.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
wide_arc
.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
wide_arc.world_width = 10.0;
wide_arc.color = [1.0, 0.2, 0.2, 1.0];
wide_arc.pick_tris = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
@@ -591,14 +621,16 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
// 2. Tapered circular arc
let mut tapered_arc = WireModel::default();
tapered_arc.name = "tapered_arc".into();
tapered_arc.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [150.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 40.0,
start_angle: 0.2,
end_angle: 2.8,
});
tapered_arc
.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [150.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 40.0,
start_angle: 0.2,
end_angle: 2.8,
});
tapered_arc.world_width = 16.0;
tapered_arc.taper_widths = vec![2.0, 16.0];
tapered_arc.color = [0.2, 0.8, 1.0, 1.0];
@@ -637,8 +669,18 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
&mut encoder,
&target,
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
[0.0, 0.0, 0.0, 1.0],
false,
false,
@@ -674,22 +716,26 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
// Donut circular arc segments
let mut donut = WireModel::default();
donut.name = "donut".into();
donut.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
donut.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: std::f64::consts::PI,
end_angle: std::f64::consts::TAU,
});
donut
.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: 0.0,
end_angle: std::f64::consts::PI,
});
donut
.tangent_geoms
.push(crate::scene::model::wire_model::TangentGeom::Arc {
center: [50.0, 50.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 30.0,
start_angle: std::f64::consts::PI,
end_angle: std::f64::consts::TAU,
});
donut.world_width = 12.0;
donut.color = [0.2, 0.9, 0.3, 1.0];
@@ -743,8 +789,18 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
&mut encoder,
&target,
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
[0.0, 0.0, 0.0, 1.0],
false,
false,
@@ -770,14 +826,16 @@ fn test_selected_ellipse_overlay() {
let mut ellipse_wire = WireModel::default();
ellipse_wire.name = "ellipse_wire".into();
ellipse_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [0.0, 0.0, 0.0],
major_axis: [10.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: 0.0,
end_param: std::f64::consts::TAU,
});
ellipse_wire.tangent_geoms.push(
crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center: [0.0, 0.0, 0.0],
major_axis: [10.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: 0.0,
end_param: std::f64::consts::TAU,
},
);
let handle = codec::Handle::new(200);
let mut selected_handles = rustc_hash::FxHashSet::default();
@@ -859,8 +917,18 @@ fn test_selected_ellipse_overlay() {
&mut encoder,
&target,
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
[0.0, 0.0, 0.0, 1.0],
false,
false,
@@ -931,8 +999,18 @@ fn test_pline_arc_switch_preview_and_render() {
&mut encoder,
&target,
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
iced::Rectangle {
x: 0,
y: 0,
width: 512,
height: 512,
},
[0.0, 0.0, 0.0, 1.0],
false,
false,
+18 -22
View File
@@ -84,9 +84,15 @@ impl HatchGpu {
})),
});
let vertex_layouts = if uses_storage {
vec![storage::HatchVertex::layout(), storage::HatchPlacement::layout()]
vec![
storage::HatchVertex::layout(),
storage::HatchPlacement::layout(),
]
} else {
vec![texture::vertex_layout(), texture::TextureHatchPlacement::layout()]
vec![
texture::vertex_layout(),
texture::TextureHatchPlacement::layout(),
]
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(if uses_storage {
@@ -180,7 +186,8 @@ impl HatchGpu {
.filter(|hatch| hatch.boundary.len() >= 3)
.cloned()
.collect();
*resident = StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable);
*resident =
StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { resident, .. } => {
let renderable: Vec<HatchModel> = hatches
@@ -188,12 +195,8 @@ impl HatchGpu {
.filter(|hatch| hatch.boundary.len() >= 3)
.cloned()
.collect();
*resident = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
*resident =
TextureHatch::from_models(device, queue, &renderable, &self.bind_group_layout);
}
}
}
@@ -211,7 +214,8 @@ impl HatchGpu {
.filter(|hatch| hatch.boundary.len() >= 3)
.cloned()
.collect();
*preview = StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable);
*preview =
StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { preview, .. } => {
let renderable: Vec<HatchModel> = hatches
@@ -219,12 +223,8 @@ impl HatchGpu {
.filter(|hatch| hatch.boundary.len() >= 3)
.cloned()
.collect();
*preview = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
*preview =
TextureHatch::from_models(device, queue, &renderable, &self.bind_group_layout);
}
}
}
@@ -236,8 +236,7 @@ impl HatchGpu {
queue: &wgpu::Queue,
mut is_visible: impl FnMut([f32; 4]) -> bool,
) -> usize {
let HatchBackend::Storage { resident, .. } = &mut self.backend
else {
let HatchBackend::Storage { resident, .. } = &mut self.backend else {
return 0;
};
let mut updated = 0;
@@ -287,10 +286,7 @@ impl HatchGpu {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
pass.set_index_buffer(
hatch.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.set_index_buffer(hatch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..hatch.index_count, 0, 0..hatch.instance_count);
}
}
+78 -37
View File
@@ -30,9 +30,9 @@ pub struct HatchInstance {
pub family_count: u32, // 100
/// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z
/// bias in the vertex shader so this fill orders against other types.
pub draw_depth: f32, // 104
pub draw_depth: f32, // 104
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
pub grad_kind: u32, // 108
pub grad_kind: u32, // 108
}
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
@@ -43,7 +43,10 @@ const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
fn split_origin_ds(o: [f64; 2]) -> ([f32; 2], [f32; 2]) {
let hx = o[0] as f32;
let hy = o[1] as f32;
([hx, hy], [(o[0] - hx as f64) as f32, (o[1] - hy as f64) as f32])
(
[hx, hy],
[(o[0] - hx as f64) as f32, (o[1] - hy as f64) as f32],
)
}
/// Mirrors the per-family struct used by the existing per-hatch shader,
@@ -53,18 +56,18 @@ fn split_origin_ds(o: [f64; 2]) -> ([f32; 2], [f32; 2]) {
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineFamilyGpu {
pub cos_a: f32, // 0
pub sin_a: f32, // 4
pub x0: f32, // 8
pub y0: f32, // 12
pub dx: f32, // 16
pub dy: f32, // 20
pub perp_step: f32, // 24
pub along_step: f32, // 28
pub line_width: f32, // 32
pub period: f32, // 36
pub n_dashes: u32, // 40
pub dash_offset: u32, // 44
pub cos_a: f32, // 0
pub sin_a: f32, // 4
pub x0: f32, // 8
pub y0: f32, // 12
pub dx: f32, // 16
pub dy: f32, // 20
pub perp_step: f32, // 24
pub along_step: f32, // 28
pub line_width: f32, // 32
pub period: f32, // 36
pub n_dashes: u32, // 40
pub dash_offset: u32, // 44
}
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
@@ -113,10 +116,26 @@ impl HatchPlacement {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: 20, shader_location: 5, format: wgpu::VertexFormat::Uint32 },
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 8,
shader_location: 3,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 16,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 20,
shader_location: 5,
format: wgpu::VertexFormat::Uint32,
},
],
}
}
@@ -137,18 +156,23 @@ pub(super) struct StorageHatchBatch {
// dropping them would invalidate it, but the bind group is the
// only direct consumer. Keep them as fields to keep ownership in
// one place; `#[allow(dead_code)]` silences the read-never warning.
#[allow(dead_code)] pub instance_buffer: wgpu::Buffer,
#[allow(dead_code)] pub family_buffer: wgpu::Buffer,
#[allow(dead_code)] pub dash_buffer: wgpu::Buffer,
#[allow(dead_code)]
pub instance_buffer: wgpu::Buffer,
#[allow(dead_code)]
pub family_buffer: wgpu::Buffer,
#[allow(dead_code)]
pub dash_buffer: wgpu::Buffer,
/// Per-instance visibility flag (1=draw, 0=skip). Stored in its
/// own small storage buffer so per-frame updates don't have to
/// touch the large `instance_buffer`. Vertex shader reads
/// `visibility[instance_index]` — when 0 it emits an out-of-NDC
/// clip position so the GPU clips the primitive before the
/// fragment stage runs.
#[allow(dead_code)] pub visibility_buffer: wgpu::Buffer,
#[allow(dead_code)]
pub visibility_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
#[allow(dead_code)] pub instance_count: u32,
#[allow(dead_code)]
pub instance_count: u32,
/// CPU mirror — `update_visibility` re-uploads this whole slice
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
/// per pan tick. Far cheaper than touching the instance data.
@@ -168,7 +192,10 @@ fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] {
let (families, dashes) = match &hatch.pattern {
HatchPattern::Pattern(families) => (
families.len() as u64,
families.iter().map(|family| family.dashes.len() as u64).sum(),
families
.iter()
.map(|family| family.dashes.len() as u64)
.sum(),
),
_ => (0, 0),
};
@@ -234,7 +261,6 @@ impl StorageHatchBatch {
bgl: &wgpu::BindGroupLayout,
hatches: &[HatchModel],
) -> Option<Self> {
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
let mut families: Vec<LineFamilyGpu> = Vec::new();
let mut dashes: Vec<f32> = Vec::new();
@@ -332,10 +358,18 @@ impl StorageHatchBatch {
let mut max_y = f32::NEG_INFINITY;
for &[x, y] in h.boundary.iter() {
if x.is_finite() && y.is_finite() {
if x < min_x { min_x = x; }
if y < min_y { min_y = y; }
if x > max_x { max_x = x; }
if y > max_y { max_y = y; }
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
}
if !min_x.is_finite() {
@@ -435,7 +469,12 @@ impl StorageHatchBatch {
if end > 0 {
draws.push((0..end, 0..1));
}
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
let mut union = [
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for inst in instances.iter().take(unique_source_count) {
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
union[1] = union[1].min(inst.aabb[1] + inst.world_origin[1]);
@@ -576,12 +615,14 @@ impl StorageHatchBatch {
});
let source_aabbs = instances
.iter()
.map(|inst| [
inst.aabb[0] + inst.world_origin[0],
inst.aabb[1] + inst.world_origin[1],
inst.aabb[2] + inst.world_origin[0],
inst.aabb[3] + inst.world_origin[1],
])
.map(|inst| {
[
inst.aabb[0] + inst.world_origin[0],
inst.aabb[1] + inst.world_origin[1],
inst.aabb[2] + inst.world_origin[0],
inst.aabb[3] + inst.world_origin[1],
]
})
.collect();
Some(Self {
+42 -9
View File
@@ -44,9 +44,21 @@ impl TextureHatchPlacement {
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::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 8,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 16,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
},
],
}
}
@@ -164,7 +176,13 @@ impl TextureHatch {
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, kind, invert, .. } => {
HatchPattern::Gradient {
angle_deg,
color2,
kind,
invert,
..
} => {
let gk = (kind.shader_kind() | if *invert { 16 } else { 0 }) << 8;
if kind.radial() {
// Radial: centre is the local origin; grad_cos/sin unused.
@@ -177,8 +195,12 @@ impl TextureHatch {
};
// ── Bounding box ─────────────────────────────────────────────────
let (mut min_x, mut max_x, mut min_y, mut max_y) =
(f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::NEG_INFINITY);
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 model.boundary.iter() {
if !x.is_finite() || !y.is_finite() {
continue;
@@ -196,7 +218,10 @@ impl TextureHatch {
}
let vertices: Vec<HatchVertex> = mesh_points
.into_iter()
.map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 })
.map(|[x, y]| HatchVertex {
pos: [x, y, 0.0],
_pad: 0.0,
})
.collect();
let vertex_buffer = crate::scene::pipeline::gpu_upload::upload_buffer(
device,
@@ -362,8 +387,16 @@ impl TextureHatch {
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
// → round(perp/0)=NaN → an invisible hatch.
scale: model.scale.max(1e-6),
grad_cos: if base_mode == 3 { radial_center[0] } else { grad_cos },
grad_sin: if base_mode == 3 { radial_center[1] } else { grad_sin },
grad_cos: if base_mode == 3 {
radial_center[0]
} else {
grad_cos
},
grad_sin: if base_mode == 3 {
radial_center[1]
} else {
grad_sin
},
grad_min,
grad_range,
origin: [origin[0] as f32, origin[1] as f32],
+38 -10
View File
@@ -58,9 +58,21 @@ pub struct ImageInstance {
impl ImageInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation_low) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, draw_depth) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(ImageInstance, translation) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(ImageInstance, translation_low) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(ImageInstance, draw_depth) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -138,7 +150,11 @@ impl ImageGpu {
groups
.into_iter()
.flat_map(|group| {
let sampler = if group.first().is_some_and(|m| m.pixelated) { &pixelated } else { &smooth };
let sampler = if group.first().is_some_and(|m| m.pixelated) {
&pixelated
} else {
&smooth
};
Self::new(device, queue, &group, bgl1, sampler)
})
.collect()
@@ -223,7 +239,14 @@ impl ImageGpu {
"image.texture:{}:{}:{}",
model.file_path, x.content_start, y.content_start
);
let levels = mip_chain(&model.pixels, model.width, x.data_start, y.data_start, width, height);
let levels = mip_chain(
&model.pixels,
model.width,
x.data_start,
y.data_start,
width,
height,
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&tex_label),
size: wgpu::Extent3d {
@@ -235,8 +258,7 @@ impl ImageGpu {
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
for (level, (w, h, data)) in levels.iter().enumerate() {
@@ -260,8 +282,7 @@ impl ImageGpu {
},
);
}
let tex_view =
texture.create_view(&wgpu::TextureViewDescriptor::default());
let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("image.bind_group1"),
layout: bgl1,
@@ -317,7 +338,14 @@ struct TileRange {
/// lines on a transparent page (PDF, DWF, DGN underlays) stay visible when
/// zoomed out instead of falling between samples, and opaque images are
/// averaged as usual.
fn mip_chain(pixels: &[u8], stride: u32, x0: u32, y0: u32, w: u32, h: u32) -> Vec<(u32, u32, Vec<u8>)> {
fn mip_chain(
pixels: &[u8],
stride: u32,
x0: u32,
y0: u32,
w: u32,
h: u32,
) -> Vec<(u32, u32, Vec<u8>)> {
let (stride, x0, y0) = (stride as usize, x0 as usize, y0 as usize);
let mut base = Vec::with_capacity(w as usize * h as usize * 4);
for row in 0..h as usize {
+139 -100
View File
@@ -234,9 +234,7 @@ impl MeshInstanceGpu {
translation[2] as f32,
];
let linear = glam::DMat3::from_cols_array(&[
m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2],
m[0][0], m[1][0], m[2][0], m[0][1], m[1][1], m[2][1], m[0][2], m[1][2], m[2][2],
]);
let normal = if linear.determinant().abs() > 1e-18 {
linear.inverse().transpose()
@@ -483,10 +481,8 @@ fn make_chunk(
};
let mut wire_vertices = Vec::with_capacity(wire_indices.len());
for line in wire_indices.chunks_exact(2) {
let (Some(start), Some(end)) = (
verts.get(line[0] as usize),
verts.get(line[1] as usize),
) else {
let (Some(start), Some(end)) = (verts.get(line[0] as usize), verts.get(line[1] as usize))
else {
continue;
};
for vertex in [start, end] {
@@ -569,11 +565,20 @@ struct MeshSurfaceParams {
}
fn valid_rgba_size(width: u32, height: u32, pixels: &[u8]) -> bool {
width > 0 && height > 0
&& (width as usize).checked_mul(height as usize).and_then(|size| size.checked_mul(4)) == Some(pixels.len())
width > 0
&& height > 0
&& (width as usize)
.checked_mul(height as usize)
.and_then(|size| size.checked_mul(4))
== Some(pixels.len())
}
fn downscale_rgba_to_limit(width: u32, height: u32, rgba: &[u8], limit: u32) -> Option<(u32, u32, Vec<u8>)> {
fn downscale_rgba_to_limit(
width: u32,
height: u32,
rgba: &[u8],
limit: u32,
) -> Option<(u32, u32, Vec<u8>)> {
if limit == 0 || !valid_rgba_size(width, height, rgba) {
return None;
}
@@ -581,7 +586,12 @@ fn downscale_rgba_to_limit(width: u32, height: u32, rgba: &[u8], limit: u32) ->
let scale = (limit as f64 / width.max(height) as f64).min(1.0);
let new_width = ((width as f64 * scale).round() as u32).clamp(1, limit);
let new_height = ((height as f64 * scale).round() as u32).clamp(1, limit);
let resized = image::imageops::resize(&buffer, new_width, new_height, image::imageops::FilterType::Triangle);
let resized = image::imageops::resize(
&buffer,
new_width,
new_height,
image::imageops::FilterType::Triangle,
);
Some((new_width, new_height, resized.into_raw()))
}
@@ -674,18 +684,54 @@ pub fn create_material_resources(
let bump = material.and_then(|material| material.bump_map.image.as_deref());
let refraction = material.and_then(|material| material.refraction_map.image.as_deref());
let normal = material.and_then(|material| material.normal_map.image.as_deref());
let diffuse_view =
upload_rgba_texture(device, queue, "mesh.material.diffuse", diffuse, [255; 4], true);
let specular_view =
upload_rgba_texture(device, queue, "mesh.material.specular", specular, [255; 4], true);
let reflection_view =
upload_rgba_texture(device, queue, "mesh.material.reflection", reflection, [0, 0, 0, 255], true);
let opacity_view =
upload_rgba_texture(device, queue, "mesh.material.opacity", opacity, [255; 4], false);
let bump_view =
upload_rgba_texture(device, queue, "mesh.material.bump", bump, [128, 128, 128, 255], false);
let refraction_view =
upload_rgba_texture(device, queue, "mesh.material.refraction", refraction, [255; 4], true);
let diffuse_view = upload_rgba_texture(
device,
queue,
"mesh.material.diffuse",
diffuse,
[255; 4],
true,
);
let specular_view = upload_rgba_texture(
device,
queue,
"mesh.material.specular",
specular,
[255; 4],
true,
);
let reflection_view = upload_rgba_texture(
device,
queue,
"mesh.material.reflection",
reflection,
[0, 0, 0, 255],
true,
);
let opacity_view = upload_rgba_texture(
device,
queue,
"mesh.material.opacity",
opacity,
[255; 4],
false,
);
let bump_view = upload_rgba_texture(
device,
queue,
"mesh.material.bump",
bump,
[128, 128, 128, 255],
false,
);
let refraction_view = upload_rgba_texture(
device,
queue,
"mesh.material.refraction",
refraction,
[255; 4],
true,
);
let normal_view = upload_rgba_texture(
device,
queue,
@@ -850,8 +896,7 @@ pub fn create_material_bind_group_from_resources(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
face_color: [f32; 4],
) -> wgpu::BindGroup {
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let (material_params, specular, ambient, advanced, flags) = material_vertex_params(material);
let surface = MeshSurfaceParams {
face_color,
material: material_params,
@@ -947,14 +992,7 @@ pub fn create_material_bind_group(
) -> wgpu::BindGroup {
let resources = create_material_resources(device, queue, material);
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,
color,
)
create_material_bind_group_from_resources(device, queue, layout, &resources, material, color)
}
pub fn upload_chunk_material_bind_groups(
@@ -967,8 +1005,7 @@ pub fn upload_chunk_material_bind_groups(
let mut materials = rustc_hash::FxHashMap::default();
for chunk in chunks {
let resources = match chunk.material.as_ref() {
None => fallback
.get_or_insert_with(|| create_material_resources(device, queue, None)),
None => fallback.get_or_insert_with(|| create_material_resources(device, queue, None)),
Some(material) => {
let Some(handle) = material.handle else {
let resources = create_material_resources(device, queue, Some(material));
@@ -982,9 +1019,9 @@ pub fn upload_chunk_material_bind_groups(
));
continue;
};
materials.entry(handle.value()).or_insert_with(|| {
create_material_resources(device, queue, Some(material))
})
materials
.entry(handle.value())
.or_insert_with(|| create_material_resources(device, queue, Some(material)))
}
};
chunk.material_bind_group = Some(create_material_bind_group_from_resources(
@@ -1044,9 +1081,7 @@ fn material_key(
MaterialBatchKey(key)
}
fn material_mapper_key(
material: &crate::scene::model::material_model::MeshMaterial,
) -> u64 {
fn material_mapper_key(material: &crate::scene::model::material_model::MeshMaterial) -> u64 {
use std::hash::{Hash, Hasher};
let Some(mapper) = material.mapper else {
return 0;
@@ -1260,8 +1295,7 @@ fn triangle_mapping_normal(mesh: &MeshModel, triangle: &[u32]) -> [f32; 3] {
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2])
.sqrt();
let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
if length > f32::EPSILON {
[normal[0] / length, normal[1] / length, normal[2] / length]
} else {
@@ -1292,7 +1326,9 @@ fn build_instanced_chunks(
material.map_or(0, |material| {
let handle = material
.handle
.map_or(material as *const _ as usize as u64, |handle| handle.value());
.map_or(material as *const _ as usize as u64, |handle| {
handle.value()
});
handle ^ material_mapper_key(material).rotate_left(17)
})
} else {
@@ -1307,10 +1343,10 @@ fn build_instanced_chunks(
let needs_wire_vertices = first.include_edges && source.edge_verts.is_empty();
let split_geometry = (first.include_faces && material_has_box_projection(material))
|| mesh
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
> budget
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
> budget
|| first
.indices
.len()
@@ -1369,7 +1405,9 @@ fn build_instanced_chunks(
};
let verts: Vec<_> =
if !split_geometry && (shared_vertex_buffer.is_none() || needs_wire_vertices) {
(0..mesh.verts.len()).map(|index| vertex(index, None)).collect()
(0..mesh.verts.len())
.map(|index| vertex(index, None))
.collect()
} else {
Vec::new()
};
@@ -1744,10 +1782,7 @@ fn material_map_uv(
position[0] * m[8] + position[1] * m[9] + position[2] * m[10] + m[11],
];
match map.projection {
3 => [
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
p[2],
],
3 => [p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5, p[2]],
4 => {
let radius = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
if radius <= f32::EPSILON {
@@ -1892,8 +1927,7 @@ pub fn build_mesh_batch_filtered(
let max_verts = super::gpu_budget::max_elements::<MeshVertex>(device).max(3);
// A triangle contributes three line segments (six standalone edge
// vertices). Bound chunks by the largest buffer produced for wire meshes.
let max_tris =
(budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
let max_tris = (budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
let stubs = MeshBatchStubs::new(device, queue);
let mut chunks = Vec::new();
@@ -1903,14 +1937,11 @@ pub fn build_mesh_batch_filtered(
let mut wire_indices: Vec<u32> = Vec::new();
let mut edge_verts: Vec<MeshEdgeVertex> = Vec::new();
let mut highlight_ranges: Vec<MeshBatchRange> = Vec::new();
let mut chunk_handles: rustc_hash::FxHashSet<codec::Handle> =
rustc_hash::FxHashSet::default();
let mut chunk_handles: rustc_hash::FxHashSet<codec::Handle> = rustc_hash::FxHashSet::default();
let mut total_tris = 0u64;
let mut ordered: Vec<MeshBatchPart<'_>> = Vec::new();
let mut source_indices: rustc_hash::FxHashMap<
(bool, u64),
(std::sync::Arc<[u32]>, u64),
> = rustc_hash::FxHashMap::default();
let mut source_indices: rustc_hash::FxHashMap<(bool, u64), (std::sync::Arc<[u32]>, u64)> =
rustc_hash::FxHashMap::default();
let mut face_partitions: rustc_hash::FxHashMap<FacePartitionKey, Vec<CachedFacePart<'_>>> =
rustc_hash::FxHashMap::default();
for set in sets {
@@ -1922,9 +1953,9 @@ pub fn build_mesh_batch_filtered(
continue;
};
let display_handle = set.entity_handle();
if handles.is_some_and(|wanted| {
display_handle.is_none_or(|handle| !wanted.contains(&handle))
}) {
if handles
.is_some_and(|wanted| display_handle.is_none_or(|handle| !wanted.contains(&handle)))
{
continue;
}
let display_color = set.display_color().unwrap_or(mesh.color);
@@ -1933,8 +1964,7 @@ pub fn build_mesh_batch_filtered(
|source| (true, source.handle.value()),
);
let triangle_count = mesh.indices.len() / 3;
let has_face_materials =
mesh.triangle_material_handles.len() == triangle_count
let has_face_materials = mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty();
let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& mesh.triangle_colors.iter().any(Option::is_some);
@@ -1947,8 +1977,10 @@ pub fn build_mesh_batch_filtered(
.as_ref()
.map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors {
let base_color =
set.material.as_ref().map_or(display_color, |material| material.diffuse);
let base_color = set
.material
.as_ref()
.map_or(display_color, |material| material.diffuse);
let color = set
.visual_style
.as_ref()
@@ -2017,8 +2049,7 @@ pub fn build_mesh_batch_filtered(
} else {
set.material.as_ref()
};
let base_color =
material.map_or(display_color, |material| material.diffuse);
let base_color = material.map_or(display_color, |material| material.diffuse);
let base_color = if has_face_colors {
mesh.triangle_colors[triangle].unwrap_or(base_color)
} else {
@@ -2093,10 +2124,8 @@ pub fn build_mesh_batch_filtered(
mesh_spatial_key(part.set, spatial_bounds),
)
});
let mut instance_groups: std::collections::BTreeMap<
InstanceGroupKey,
Vec<MeshBatchPart<'_>>,
> = std::collections::BTreeMap::new();
let mut instance_groups: std::collections::BTreeMap<InstanceGroupKey, Vec<MeshBatchPart<'_>>> =
std::collections::BTreeMap::new();
let mut direct_parts = Vec::with_capacity(ordered.len());
for part in ordered {
let eligible = part.set.instance_transform.is_some()
@@ -2189,7 +2218,9 @@ pub fn build_mesh_batch_filtered(
let edge_color = set
.visual_style
.as_ref()
.map_or(part.display_color, |style| style.edge_color(part.display_color));
.map_or(part.display_color, |style| {
style.edge_color(part.display_color)
});
let vtx = |vi: usize,
local_mapping_normal: Option<[f32; 3]>,
model_mapping_normal: Option<[f32; 3]>| {
@@ -2198,21 +2229,13 @@ pub fn build_mesh_batch_filtered(
} else {
[0.0, 1.0, 0.0]
};
let local_position = uv_mesh
.verts
.get(vi)
.copied()
.unwrap_or(mesh.verts[vi]);
let local_position = uv_mesh.verts.get(vi).copied().unwrap_or(mesh.verts[vi]);
let local_position_low = uv_mesh
.verts_low
.get(vi)
.copied()
.unwrap_or_else(|| mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]));
let local_normal = uv_mesh
.normals
.get(vi)
.copied()
.unwrap_or(normal);
let local_normal = uv_mesh.normals.get(vi).copied().unwrap_or(normal);
let uv = material_uvs(
material,
local_position,
@@ -2260,9 +2283,7 @@ pub fn build_mesh_batch_filtered(
let available = max_verts.saturating_sub(edge_verts.len());
// LineList consumes pairs. Never split a segment between
// chunks even when the vertex budget is odd.
let take = available
.min(edge_end - edge_start)
& !1usize;
let take = available.min(edge_end - edge_start) & !1usize;
if take == 0 {
chunks.push(make_chunk(
device,
@@ -2422,8 +2443,7 @@ pub fn build_mesh_batch_filtered(
Vec::new()
};
if is_transp {
let sub_handles: rustc_hash::FxHashSet<_> =
entity_handle.into_iter().collect();
let sub_handles: rustc_hash::FxHashSet<_> = entity_handle.into_iter().collect();
chunks.push(make_chunk(
device,
queue,
@@ -2443,8 +2463,7 @@ pub fn build_mesh_batch_filtered(
None,
));
} else {
let sub_handles: rustc_hash::FxHashSet<_> =
entity_handle.into_iter().collect();
let sub_handles: rustc_hash::FxHashSet<_> = entity_handle.into_iter().collect();
chunks.push(make_chunk(
device,
queue,
@@ -2510,7 +2529,11 @@ pub fn build_mesh_batch_filtered(
verts.push(vtx(i, None, None));
}
if part.include_faces {
let fill = if is_transp { &mut transp_indices } else { &mut indices };
let fill = if is_transp {
&mut transp_indices
} else {
&mut indices
};
let index_start = fill.len() as u32;
for &idx in part.indices.iter() {
fill.push(base + idx);
@@ -2677,19 +2700,31 @@ mod texture_limit_tests {
// both dimensions must come out <= limit and aspect ratio preserved.
let (w, h) = (500u32, 10u32);
let rgba = vec![0u8; (w * h * 4) as usize];
let (new_w, new_h, buf) = downscale_rgba_to_limit(w, h, &rgba, 200).expect("well-formed input must resize");
assert!(new_w <= 200 && new_h <= 200, "both dimensions must respect the limit, got {new_w}x{new_h}");
assert_eq!(buf.len(), (new_w * new_h * 4) as usize, "the returned buffer must match its reported dimensions");
let (new_w, new_h, buf) =
downscale_rgba_to_limit(w, h, &rgba, 200).expect("well-formed input must resize");
assert!(
new_w <= 200 && new_h <= 200,
"both dimensions must respect the limit, got {new_w}x{new_h}"
);
assert_eq!(
buf.len(),
(new_w * new_h * 4) as usize,
"the returned buffer must match its reported dimensions"
);
// Aspect ratio 50:1 should be preserved within rounding.
let ratio = new_w as f64 / new_h as f64;
assert!((ratio - 50.0).abs() < 1.0, "expected aspect ratio near 50:1, got {ratio}");
assert!(
(ratio - 50.0).abs() < 1.0,
"expected aspect ratio near 50:1, got {ratio}"
);
}
#[test]
fn downscale_handles_a_square_image_over_the_limit() {
let (w, h) = (300u32, 300u32);
let rgba = vec![255u8; (w * h * 4) as usize];
let (new_w, new_h, buf) = downscale_rgba_to_limit(w, h, &rgba, 128).expect("well-formed input must resize");
let (new_w, new_h, buf) =
downscale_rgba_to_limit(w, h, &rgba, 128).expect("well-formed input must resize");
assert_eq!(new_w, 128);
assert_eq!(new_h, 128);
assert_eq!(buf.len(), (128 * 128 * 4) as usize);
@@ -2704,11 +2739,15 @@ mod texture_limit_tests {
}
#[test]
fn malformed_material_images_are_rejected_even_below_the_texture_limit() {
for (width, height, pixels) in [(0, 1, vec![]), (1, 0, vec![]), (1, 1, vec![0; 3]), (1, 1, vec![0; 5])] {
for (width, height, pixels) in [
(0, 1, vec![]),
(1, 0, vec![]),
(1, 1, vec![0; 3]),
(1, 1, vec![0; 5]),
] {
assert!(!super::valid_rgba_size(width, height, &pixels));
assert!(downscale_rgba_to_limit(width, height, &pixels, 16).is_none());
}
assert!(downscale_rgba_to_limit(1, 1, &[0; 4], 0).is_none());
}
}
+1506 -1451
View File
File diff suppressed because it is too large. Load diff
+9 -2
View File
@@ -84,7 +84,10 @@ impl PointCloudGpu {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("point_cloud.bind_group1"),
layout,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: params.as_entire_binding() }],
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: params.as_entire_binding(),
}],
});
Self {
clouds: Vec::new(),
@@ -103,7 +106,11 @@ impl PointCloudGpu {
/// Make `set` resident, keeping the buffers of clouds it already had.
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, set: &PointCloudSet) {
let mut old = std::mem::take(&mut self.clouds);
for cloud in set.clouds.iter().filter(|cloud| !cloud.instances.is_empty()) {
for cloud in set
.clouds
.iter()
.filter(|cloud| !cloud.instances.is_empty())
{
if let Some(at) = old.iter().position(|resident| resident.key == cloud.key) {
self.clouds.push(old.swap_remove(at));
continue;
+24 -6
View File
@@ -66,9 +66,21 @@ pub struct BlockTextInstance {
impl BlockTextInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation_low) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, draw_depth) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockTextInstance, translation) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockTextInstance, translation_low) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockTextInstance, draw_depth) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -537,8 +549,7 @@ pub fn upload_block_vertex_refs(
})
})
.collect();
let max_instances =
super::gpu_budget::max_elements::<BlockTextInstance>(device);
let max_instances = super::gpu_budget::max_elements::<BlockTextInstance>(device);
for chunk in instances.chunks(max_instances) {
let instance_buffer = super::gpu_upload::upload_buffer(
device,
@@ -606,7 +617,14 @@ mod tests {
};
let mut out = Vec::new();
// Large origin exercises the double-single precision path.
push_glyph_vertices(&mut out, &[q], [1_000_000.0, 2_000_000.0, 0.0], 1.0, [1.0; 4], 0.0);
push_glyph_vertices(
&mut out,
&[q],
[1_000_000.0, 2_000_000.0, 0.0],
1.0,
[1.0; 4],
0.0,
);
assert_eq!(out.len(), 6, "two triangles per glyph");
// Vertex 0 = BL corner (0,0) -> world origin, uv = (u_min, v_max).
+38 -34
View File
@@ -9,12 +9,12 @@
// This eliminates gimbal lock at top/bottom views and keeps the cube in sync
// with arcball orbit at all angles.
use crate::t;
use bytemuck::{Pod, Zeroable};
use glam::camera::rh::proj::directx::orthographic;
use glam::{Mat4, Vec3, Vec4};
use iced::wgpu;
use iced::{Rectangle, Size};
use crate::t;
#[path = "viewcube_text_atlas.rs"]
mod viewcube_text_atlas;
@@ -1133,14 +1133,13 @@ impl ViewCubePipeline {
&ring_idxs,
wgpu::BufferUsages::INDEX,
);
let ring_line_vertex_buffer =
super::gpu_upload::upload_buffer(
device,
queue,
"vc.ring_line_vb",
&ring_line_verts,
wgpu::BufferUsages::VERTEX,
);
let ring_line_vertex_buffer = 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,
@@ -1316,14 +1315,13 @@ impl ViewCubePipeline {
},
],
});
let composite_uniform_buffer =
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_uniform_buffer = 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,
@@ -1460,16 +1458,15 @@ impl ViewCubePipeline {
0.0,
]),
);
self.text
.update(
queue,
cam_rotation,
compass_rotation,
render_size,
render_size,
self.cube_px,
text_color,
);
self.text.update(
queue,
cam_rotation,
compass_rotation,
render_size,
render_size,
self.cube_px,
text_color,
);
}
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
@@ -1522,7 +1519,11 @@ impl ViewCubePipeline {
surface_clip: Rectangle<u32>,
clip: &Rectangle<u32>,
) -> bool {
let min_size = self.depth_texture_size.width.min(self.depth_texture_size.height).max(1);
let min_size = self
.depth_texture_size
.width
.min(self.depth_texture_size.height)
.max(1);
viewcube_should_render(surface_dest, surface_clip, clip, min_size)
}
}
@@ -1542,7 +1543,6 @@ pub fn viewcube_should_render(
}
impl ViewCubePipeline {
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
@@ -1574,7 +1574,14 @@ impl ViewCubePipeline {
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, render_width as f32, render_height as f32, 0.0, 1.0);
pass.set_viewport(
0.0,
0.0,
render_width as f32,
render_height as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
@@ -1582,10 +1589,7 @@ impl ViewCubePipeline {
pass.draw_indexed(0..self.index_count, 0, 0..1);
pass.set_bind_group(0, &self.ring_uniform_bind_group, &[]);
pass.set_vertex_buffer(0, self.ring_vertex_buffer.slice(..));
pass.set_index_buffer(
self.ring_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.set_index_buffer(self.ring_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..self.ring_index_count, 0, 0..1);
pass.set_pipeline(&self.line_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
+5 -13
View File
@@ -1,4 +1,6 @@
use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, Wrap};
use cosmic_text::{
Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, Wrap,
};
pub(super) const ATLAS_WIDTH: u32 = 512;
pub(super) const ATLAS_HEIGHT: u32 = 256;
@@ -37,24 +39,14 @@ pub(super) fn empty_label_atlas() -> LabelAtlas {
}
}
pub(super) fn build_label_atlas(
labels: &[String; FACE_TILE_COUNT],
) -> Option<LabelAtlas> {
pub(super) fn build_label_atlas(labels: &[String; FACE_TILE_COUNT]) -> Option<LabelAtlas> {
let (mut font_system, family, weight) = font_system(labels)?;
let mut swash_cache = SwashCache::new();
let mut texts: Vec<&str> = labels.iter().map(String::as_str).collect();
texts.extend(["N", "E", "S", "W"]);
let bitmaps: Vec<LabelBitmap> = texts
.into_iter()
.map(|text| {
rasterize_label(
&mut font_system,
&mut swash_cache,
text,
family,
weight,
)
})
.map(|text| rasterize_label(&mut font_system, &mut swash_cache, text, family, weight))
.collect();
Some(pack_bitmaps(&bitmaps))
+39 -23
View File
@@ -61,9 +61,21 @@ impl WipeoutPlacement {
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 },
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,
},
],
}
}
@@ -80,16 +92,16 @@ impl WipeoutPlacement {
#[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 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
@@ -191,7 +203,9 @@ impl WipeoutGpu {
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, .. } => {
HatchPattern::Gradient {
angle_deg, color2, ..
} => {
let r = angle_deg.to_radians();
(2u32, *color2, r.cos(), r.sin())
}
@@ -236,12 +250,7 @@ impl WipeoutGpu {
} 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 (x0, x1, y0, y1) = (min_x - pad, max_x + pad, min_y - pad, max_y + pad);
let quad = [
HatchVertex {
@@ -279,7 +288,8 @@ impl WipeoutGpu {
// ── Gradient: projection range (in snapped-local space) ──────────
let (grad_min, grad_range) = if mode == 2 {
let projs: Vec<f32> = gpu_boundary.iter()
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();
@@ -345,7 +355,8 @@ impl WipeoutGpu {
let mut boundary_data = BoundaryData {
verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS],
};
for (i, &[x, y]) in gpu_boundary.iter()
for (i, &[x, y]) in gpu_boundary
.iter()
.take(MAX_HATCH_BOUNDARY_VERTS)
.enumerate()
{
@@ -461,7 +472,12 @@ impl WipeoutGpu {
} 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];
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]);
+55 -102
View File
@@ -227,13 +227,18 @@ pub fn partition_wires<'a>(
&& wire.text_verts.is_empty()
{
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
if super::circle_gpu::extract_circle_instances_into(wire, depth, &mut circle_instances) {
if super::circle_gpu::extract_circle_instances_into(wire, depth, &mut circle_instances)
{
if let Some(handle) = handle_of(wire) {
contributors.insert(handle);
}
continue;
}
if super::ellipse_gpu::extract_ellipse_instances_into(wire, depth, &mut ellipse_instances) {
if super::ellipse_gpu::extract_ellipse_instances_into(
wire,
depth,
&mut ellipse_instances,
) {
if let Some(handle) = handle_of(wire) {
contributors.insert(handle);
}
@@ -284,8 +289,7 @@ pub fn split_wires(wires: &[WireModel]) -> (Vec<&WireModel>, Vec<&WireModel>) {
/// resolve by submission order, which a tail relocation would flip.
fn order_sensitive(wires: &[&WireModel], depth_map: &FxHashMap<u64, [f32; 2]>) -> bool {
wires.iter().any(|w| {
w.color[3] < 0.999
|| handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value()))
w.color[3] < 0.999 || handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value()))
})
}
@@ -494,8 +498,7 @@ impl WireArena {
const_bgl: &wgpu::BindGroupLayout,
mesh_edge: bool,
) -> Option<Self> {
let instance_limit =
super::gpu_budget::max_arena_elements::<WireInstance>(device) as u64;
let instance_limit = super::gpu_budget::max_arena_elements::<WireInstance>(device) as u64;
let constant_limit =
super::gpu_budget::max_arena_storage_elements::<WireConst>(device) as u64;
let ranges = handle_ranges(wires)?;
@@ -557,10 +560,7 @@ impl WireArena {
.par_iter()
.map(|plan| {
let run = &wires[plan.start..plan.end];
let capacity: usize = run
.iter()
.map(|w| w.points.len().saturating_sub(1))
.sum();
let capacity: usize = run.iter().map(|w| w.points.len().saturating_sub(1)).sum();
let mut instances: Vec<WireInstance> = Vec::with_capacity(capacity);
let mut consts: Vec<WireConst> = Vec::with_capacity(run.len());
for (local, &w) in run.iter().enumerate() {
@@ -568,7 +568,11 @@ impl WireArena {
// 3D mesh outline edges are occluded by true depth and must NOT
// take the draw-order z-bias (or hidden back edges peek through
// the shaded fill) — matching WireGpu::from_run.
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
let dd = if mesh_edge {
0.0
} else {
wire_draw_depth(w, depth_map)
};
let (mut emitted, cst) = emit_wire_native(w, wire_id, w.color, dd);
instances.append(&mut emitted);
consts.push(cst);
@@ -1040,10 +1044,7 @@ impl WireArena {
}
if perf {
let submitted: u64 = ranges
.iter()
.map(|(start, end)| (end - start) as u64)
.sum();
let submitted: u64 = ranges.iter().map(|(start, end)| (end - start) as u64).sum();
let elapsed_ms = perf_started
.map(|started| started.elapsed().as_secs_f64() * 1000.0)
.unwrap_or_default();
@@ -1139,8 +1140,7 @@ fn visible_ranges(
let mut ranges: Vec<(u32, u32)> = slabs
.values()
.filter(|slab| {
slab.inst_len > 0
&& !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h)
slab.inst_len > 0 && !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h)
})
.map(|slab| (slab.inst_off, slab.inst_off + slab.inst_len))
.collect();
@@ -1232,8 +1232,7 @@ impl PackedWireArena {
return None;
}
let mut instances = Vec::with_capacity(inst_count);
let mut slabs =
FxHashMap::with_capacity_and_hasher(packed.len(), Default::default());
let mut slabs = FxHashMap::with_capacity_and_hasher(packed.len(), Default::default());
for mut packed_slab in packed {
let inst_off = instances.len() as u32;
let inst_len = packed_slab.instances.len() as u32;
@@ -1290,8 +1289,7 @@ impl PackedWireArena {
new_handles_are_suffix: bool,
depth_map: &FxHashMap<u64, [f32; 2]>,
) -> bool {
let mut prepared: FxHashMap<Handle, PreparedPackedPatchRun> =
FxHashMap::default();
let mut prepared: FxHashMap<Handle, PreparedPackedPatchRun> = FxHashMap::default();
for &(handle, kind) in changes {
let run = runs.get(&handle).map(Vec::as_slice).unwrap_or(&[]);
if matches!(kind, ChangeKind::Removed) || run.is_empty() {
@@ -1308,12 +1306,8 @@ impl PackedWireArena {
}
let inst_len = insts.len() as u32;
if matches!(kind, ChangeKind::Modified) {
let known = self
.slabs
.get(&handle)
.or_else(|| self.vacant.get(&handle));
let shape_changed =
known.is_some_and(|slab| slab.inst_len != inst_len);
let known = self.slabs.get(&handle).or_else(|| self.vacant.get(&handle));
let shape_changed = known.is_some_and(|slab| slab.inst_len != inst_len);
let can_resize_tail = self.slabs.get(&handle).is_some_and(|slab| {
can_resize_packed_terminal_slab(
slab,
@@ -1334,9 +1328,7 @@ impl PackedWireArena {
base_depth: if self.mesh_edge {
0.0
} else {
depth_map
.get(&handle.value())
.map_or(0.0, |depth| depth[0])
depth_map.get(&handle.value()).map_or(0.0, |depth| depth[0])
},
aabb: run_aabb(run),
order_sensitive: order_sensitive(run, depth_map),
@@ -1348,8 +1340,7 @@ impl PackedWireArena {
let run = runs.get(&handle).map(Vec::as_slice).unwrap_or(&[]);
if matches!(kind, ChangeKind::Removed) || run.is_empty() {
if let Some(slab) = self.slabs.remove(&handle) {
let blanks =
vec![blank_packed_instance(); slab.inst_len as usize];
let blanks = vec![blank_packed_instance(); slab.inst_len as usize];
self.write_insts(queue, slab.inst_off, &blanks);
self.tombstoned += slab.inst_len;
if matches!(kind, ChangeKind::Modified) {
@@ -1379,8 +1370,7 @@ impl PackedWireArena {
.is_some_and(|slab| slab.inst_len == inst_len)
{
let slab = self.vacant.remove(&handle).unwrap();
self.tombstoned =
self.tombstoned.saturating_sub(slab.inst_len);
self.tombstoned = self.tombstoned.saturating_sub(slab.inst_len);
self.slabs.insert(handle, slab);
}
@@ -1430,8 +1420,7 @@ impl PackedWireArena {
}
self.vacant.remove(&handle);
if let Some(slab) = self.slabs.remove(&handle) {
let blanks =
vec![blank_packed_instance(); slab.inst_len as usize];
let blanks = vec![blank_packed_instance(); slab.inst_len as usize];
self.write_insts(queue, slab.inst_off, &blanks);
self.tombstoned += slab.inst_len;
}
@@ -1545,20 +1534,12 @@ impl PersistentWireArena {
depth_map: &FxHashMap<u64, [f32; 2]>,
) -> bool {
match &mut self.inner {
PersistentWireArenaKind::Indexed(arena) => arena.patch(
queue,
changes,
runs,
new_handles_are_suffix,
depth_map,
),
PersistentWireArenaKind::Packed(arena) => arena.patch(
queue,
changes,
runs,
new_handles_are_suffix,
depth_map,
),
PersistentWireArenaKind::Indexed(arena) => {
arena.patch(queue, changes, runs, new_handles_are_suffix, depth_map)
}
PersistentWireArenaKind::Packed(arena) => {
arena.patch(queue, changes, runs, new_handles_are_suffix, depth_map)
}
}
}
@@ -1694,34 +1675,10 @@ mod tests {
#[test]
fn packed_terminal_resize_obeys_order_and_capacity() {
assert!(can_resize_packed_terminal_slab(
&slab(),
110,
1000,
18,
1,
));
assert!(!can_resize_packed_terminal_slab(
&slab(),
111,
1000,
18,
1,
));
assert!(!can_resize_packed_terminal_slab(
&slab(),
110,
117,
18,
1,
));
assert!(!can_resize_packed_terminal_slab(
&slab(),
110,
1000,
18,
2,
));
assert!(can_resize_packed_terminal_slab(&slab(), 110, 1000, 18, 1,));
assert!(!can_resize_packed_terminal_slab(&slab(), 111, 1000, 18, 1,));
assert!(!can_resize_packed_terminal_slab(&slab(), 110, 117, 18, 1,));
assert!(!can_resize_packed_terminal_slab(&slab(), 110, 1000, 18, 2,));
}
#[test]
@@ -1734,12 +1691,14 @@ mod tests {
fn partition_wires_partitions_preview_analytical_curves() {
use crate::scene::model::wire_model::TangentGeom;
let mut circle_preview = WireModel::default();
circle_preview.tangent_geoms.push(TangentGeom::PlanarCircle {
center: [1.0, 2.0, 3.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
circle_preview
.tangent_geoms
.push(TangentGeom::PlanarCircle {
center: [1.0, 2.0, 3.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
let mut arc_preview = WireModel::default();
arc_preview.tangent_geoms.push(TangentGeom::Arc {
@@ -1752,14 +1711,16 @@ mod tests {
});
let mut ellipse_preview = WireModel::default();
ellipse_preview.tangent_geoms.push(TangentGeom::PlanarEllipse {
center: [7.0, 8.0, 9.0],
major_axis: [5.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.6,
start_param: 0.0,
end_param: std::f64::consts::TAU,
});
ellipse_preview
.tangent_geoms
.push(TangentGeom::PlanarEllipse {
center: [7.0, 8.0, 9.0],
major_axis: [5.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.6,
start_param: 0.0,
end_param: std::f64::consts::TAU,
});
let mut regular_line = WireModel::default();
regular_line.points.push([0.0, 0.0, 0.0]);
@@ -1779,11 +1740,7 @@ mod tests {
use crate::scene::model::wire_model::TangentGeom;
let mut circle = WireModel::default();
circle.points = vec![
[10.0, 0.0, 0.0],
[0.0, 10.0, 0.0],
[-10.0, 0.0, 0.0],
];
circle.points = vec![[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [-10.0, 0.0, 0.0]];
circle.tangent_geoms.push(TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
@@ -1794,11 +1751,7 @@ mod tests {
circle.pattern = [0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let mut arc = WireModel::default();
arc.points = vec![
[10.0, 0.0, 0.0],
[7.0, 7.0, 0.0],
[0.0, 10.0, 0.0],
];
arc.points = vec![[10.0, 0.0, 0.0], [7.0, 7.0, 0.0], [0.0, 10.0, 0.0]];
arc.tangent_geoms.push(TangentGeom::Arc {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
+233 -66
View File
@@ -80,14 +80,46 @@ impl WireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
// Must match `InstanceIn` in wire_indexed.wgsl.
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Snorm16x2 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, pos_a) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, pos_b) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, pos_b_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, distance_a) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, distance_b) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, wire_id) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Snorm16x2,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WireInstance>() as u64,
@@ -197,22 +229,74 @@ impl PackedWireInstance {
// the web viewport drew no lines at all (#414). The struct fields are
// laid out so each packed group is contiguous.
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, color) as u64, shader_location: 2, format: wgpu::VertexFormat::Unorm8x4 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pos_a) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pos_b) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, color) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Unorm8x4,
},
// dists = (distance_a, distance_b, half_width, pattern_length)
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, distance_a) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pat0) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pat1) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, distance_a) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pat0) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pat1) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
// misc = (draw_depth, align_end, align_total, world_half_width)
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_a_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b_low) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, draw_depth) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pos_a_low) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, pos_b_low) as u64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x3,
},
// taper = (world_hw_a, world_hw_b)
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, world_hw_a) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_high) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_normal_scale) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, world_hw_a) as u64,
shader_location: 9,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, marker_origin_high) as u64,
shader_location: 10,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, marker_origin_low) as u64,
shader_location: 11,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PackedWireInstance, marker_normal_scale) as u64,
shader_location: 12,
format: wgpu::VertexFormat::Float32x4,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<PackedWireInstance>() as u64,
@@ -297,12 +381,36 @@ pub struct BlockWireVertex {
impl BlockWireVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, distances) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64, shader_location: 5, format: wgpu::VertexFormat::Snorm16x2 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, pos_a) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, pos_b) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, pos_a_low) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, pos_b_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, distances) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Snorm16x2,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -323,9 +431,21 @@ pub struct BlockWireInstance {
impl BlockWireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, depth) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireInstance, translation) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireInstance, translation_low) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(BlockWireInstance, depth) as u64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x2,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -541,15 +661,27 @@ fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) {
for tg in &wire.tangent_geoms {
match tg {
crate::scene::model::wire_model::TangentGeom::Arc { axis_x, axis_y, .. }
| crate::scene::model::wire_model::TangentGeom::PlanarCircle { axis_x, axis_y, .. } => {
| crate::scene::model::wire_model::TangentGeom::PlanarCircle {
axis_x,
axis_y,
..
} => {
let ax = glam::DVec3::from_array(*axis_x);
let ay = glam::DVec3::from_array(*axis_y);
let n = ax.cross(ay).normalize_or(glam::DVec3::Z);
return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]);
return (
[0.0; 4],
[0.0; 4],
[n.x as f32, n.y as f32, n.z as f32, 0.0],
);
}
crate::scene::model::wire_model::TangentGeom::PlanarEllipse { normal, .. } => {
let n = glam::DVec3::from_array(*normal).normalize_or(glam::DVec3::Z);
return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]);
return (
[0.0; 4],
[0.0; 4],
[n.x as f32, n.y as f32, n.z as f32, 0.0],
);
}
_ => {}
}
@@ -625,7 +757,11 @@ fn band_miters(wire: &WireModel, normal: [f32; 4]) -> Vec<[f32; 2]> {
return None;
}
wrapped = true;
i = if step > 0 { first?.0 as isize } else { last?.0 as isize };
i = if step > 0 {
first?.0 as isize
} else {
last?.0 as isize
};
continue;
}
let Some(q) = at(i as usize) else {
@@ -644,7 +780,11 @@ fn band_miters(wire: &WireModel, normal: [f32; 4]) -> Vec<[f32; 2]> {
}
};
let axis = glam::DVec3::new(normal[0] as f64, normal[1] as f64, normal[2] as f64);
let axis = if axis.length() > 1e-4 { axis.normalize() } else { glam::DVec3::Z };
let axis = if axis.length() > 1e-4 {
axis.normalize()
} else {
glam::DVec3::Z
};
// tan of half the signed turn from `d1` to `d2` about the band normal.
let half_turn = |d1: glam::DVec3, d2: glam::DVec3| -> f32 {
let cos = d1.dot(d2);
@@ -652,7 +792,11 @@ fn band_miters(wire: &WireModel, normal: [f32; 4]) -> Vec<[f32; 2]> {
return 0.0;
}
let t = axis.dot(d1.cross(d2)) / (1.0 + cos);
if t.abs() > MITER_LIMIT { 0.0 } else { (t / MITER_LIMIT) as f32 }
if t.abs() > MITER_LIMIT {
0.0
} else {
(t / MITER_LIMIT) as f32
}
};
(0..n - 1)
.map(|i| {
@@ -677,8 +821,18 @@ pub(crate) fn emit_wire_packed(
draw_depth: f32,
) -> Vec<PackedWireInstance> {
let color_u8 = pack_color(color);
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
let pat0 = [
wire.pattern[0],
wire.pattern[1],
wire.pattern[2],
wire.pattern[3],
];
let pat1 = [
wire.pattern[4],
wire.pattern[5],
wire.pattern[6],
wire.pattern[7],
];
let half_width = wire.line_weight_px * 0.5;
let n = wire.points.len();
let seg_count = n.saturating_sub(1);
@@ -769,8 +923,18 @@ pub(crate) fn emit_wire_native(
let is_tapered = !wire.taper_widths.is_empty();
let cst = WireConst {
color,
pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]],
pat1: [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]],
pat0: [
wire.pattern[0],
wire.pattern[1],
wire.pattern[2],
wire.pattern[3],
],
pat1: [
wire.pattern[4],
wire.pattern[5],
wire.pattern[6],
wire.pattern[7],
],
half_width: wire.line_weight_px * 0.5,
pattern_length: wire.pattern_length,
draw_depth,
@@ -853,8 +1017,7 @@ pub(crate) fn wire_draw_depth(
wire: &WireModel,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> f32 {
let base = fast_parse_u64(&wire.name)
.and_then(|h| depth_map.get(&h).copied());
let base = fast_parse_u64(&wire.name).and_then(|h| depth_map.get(&h).copied());
match (base, wire.depth_override) {
(Some([d, half]), Some(local)) => d + local * half,
(Some([d, _]), None) => d,
@@ -865,10 +1028,8 @@ pub(crate) fn wire_draw_depth(
/// Cached block geometry keyed by source, edge mode, colour and base translation.
/// Vertices use the first instance's world coordinates, so changing that base
/// must invalidate the geometry as well as rebuild the relative placements.
pub type BlockGeometryCache = rustc_hash::FxHashMap<
BlockGeometryKey,
std::sync::Arc<Vec<BlockGeometryChunk>>,
>;
pub type BlockGeometryCache =
rustc_hash::FxHashMap<BlockGeometryKey, std::sync::Arc<Vec<BlockGeometryChunk>>>;
/// How this device draws block wires. The layout and the mode are decided
/// together when the pipeline is built and are never chosen independently, so
@@ -936,8 +1097,7 @@ impl BlockWireGpu {
mut cache: Option<&mut BlockGeometryCache>,
) -> Vec<Self> {
let BlockWireTarget { const_bgl, mode } = target;
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> =
rustc_hash::FxHashMap::default();
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for &wire in wires {
if color_override.is_none() && !wire.display_visible {
@@ -1073,16 +1233,15 @@ impl BlockWireGpu {
}
let max_verts =
super::gpu_budget::max_elements_grouped::<BlockWireVertex>(device, 6);
let shared = std::sync::Arc::new(device.create_bind_group(
&wgpu::BindGroupDescriptor {
let shared =
std::sync::Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("block_wire.const.bg"),
layout: const_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: const_buffer.as_entire_binding(),
}],
},
));
}));
vertices
.chunks(max_verts)
.map(|chunk| BlockGeometryChunk {
@@ -1127,7 +1286,11 @@ impl BlockWireGpu {
crate::perf_record!(
"[perf] block-wire-geometry mode={} definitions={} segments={} bytes={} \
bytes_if_packed={} poisoned={}",
if mode.uses_storage() { "storage" } else { "packed" },
if mode.uses_storage() {
"storage"
} else {
"packed"
},
live_keys.len(),
uploaded_segments,
uploaded_bytes,
@@ -1301,8 +1464,7 @@ impl WireGpu {
emit_wire_native(wire, idx as u32, color, wire_draw_depth(wire, depth_map))
})
.collect();
let mut instances =
Vec::with_capacity(per.iter().map(|(items, _)| items.len()).sum());
let mut instances = Vec::with_capacity(per.iter().map(|(items, _)| items.len()).sum());
let mut consts = Vec::with_capacity(per.len());
for (mut items, constant) in per {
instances.append(&mut items);
@@ -1327,9 +1489,7 @@ impl WireGpu {
use crate::par::prelude::*;
let per: Vec<Vec<PackedWireInstance>> = wires
.par_iter()
.map(|wire| {
emit_wire_packed(wire, color, wire_draw_depth(wire, depth_map))
})
.map(|wire| emit_wire_packed(wire, color, wire_draw_depth(wire, depth_map)))
.collect();
let mut instances = Vec::with_capacity(per.iter().map(Vec::len).sum());
for mut items in per {
@@ -1375,8 +1535,7 @@ impl WireGpu {
emit_wire_packed(wire, wire.color, depth)
})
.collect();
let mut instances =
Vec::with_capacity(per.iter().map(Vec::len).sum());
let mut instances = Vec::with_capacity(per.iter().map(Vec::len).sum());
for mut items in per {
instances.append(&mut items);
}
@@ -1456,7 +1615,11 @@ impl WireGpu {
// depth — they must NOT take the draw-order z-bias (which
// pulls 2D wires toward the camera), or the hidden edges of a
// small / distant mesh peek through its own shaded fill.
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
let dd = if mesh_edge {
0.0
} else {
wire_draw_depth(w, depth_map)
};
emit_wire_native(w, idx as u32, w.color, dd)
})
.collect();
@@ -1486,7 +1649,11 @@ impl WireGpu {
let per: Vec<Vec<PackedWireInstance>> = wires
.iter()
.map(|w| {
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
let dd = if mesh_edge {
0.0
} else {
wire_draw_depth(w, depth_map)
};
emit_wire_packed(w, w.color, dd)
})
.collect();
@@ -1648,9 +1815,8 @@ mod block_wire_storage_tests {
draw_order_shader!("block_wire_storage.wgsl"),
] {
assert!(source.contains("if world_hw < 0.0"));
assert!(source.contains(
"resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width)"
));
assert!(source
.contains("resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width)"));
}
}
@@ -1670,7 +1836,8 @@ mod block_wire_storage_tests {
#[test]
fn wire_indexed_shader_validates_with_naga() {
let source = draw_order_shader!("wire_indexed.wgsl");
let module = naga::front::wgsl::parse_str(source).expect("wire_indexed.wgsl parses cleanly");
let module =
naga::front::wgsl::parse_str(source).expect("wire_indexed.wgsl parses cleanly");
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),