diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 246e3b6ac..1cbfdc9e0 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2091,6 +2091,7 @@ impl OpenCADStudio { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 2dea32cbd..998b8327c 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1872,6 +1872,7 @@ fn tessellate_dimension_inner( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1902,6 +1903,7 @@ fn tessellate_dimension_inner( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1932,6 +1934,7 @@ fn tessellate_dimension_inner( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1963,6 +1966,7 @@ fn tessellate_dimension_inner( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -2010,6 +2014,7 @@ fn tessellate_dimension_inner( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 4fb4409b2..7d989e53c 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -625,6 +625,7 @@ impl LeaderTess for Leader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -800,6 +801,7 @@ impl LeaderTess for Leader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 0632394c3..25565c554 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1508,6 +1508,7 @@ impl MultiLeaderTess for MultiLeader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1844,6 +1845,7 @@ impl MultiLeaderTess for MultiLeader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1913,6 +1915,7 @@ impl MultiLeaderTess for MultiLeader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1997,6 +2000,7 @@ impl MultiLeaderTess for MultiLeader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -2036,6 +2040,7 @@ impl MultiLeaderTess for MultiLeader { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/table.rs b/src/entities/table.rs index 5535a264b..de3df94a5 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -1672,6 +1672,7 @@ pub fn tessellate_table( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 7fba969e2..4481bba96 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -225,6 +225,7 @@ impl CadCommand for AlignedDimensionCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -280,6 +281,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 913dd501b..7d30edd61 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -166,6 +166,7 @@ fn preview_wire(points: Vec) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 43c29c20e..ec331ea3d 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -197,6 +197,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 03310422f..6918dda4e 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -185,6 +185,7 @@ impl CadCommand for DimBaselineCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 20b1f024c..2bf1d97ba 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -165,6 +165,7 @@ impl CadCommand for DimContinueCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 1ca1306b5..6000b212a 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -91,6 +91,7 @@ impl CadCommand for DimJogLineCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index 13f166468..59c04b1a7 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -108,6 +108,7 @@ impl CadCommand for DimTeditCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 189c511df..1f67116f2 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -261,6 +261,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index a69c0b9cb..ca106e1bd 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -242,6 +242,7 @@ fn preview_wire(points: Vec) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index d8f331169..eb1963f5d 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -232,6 +232,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index 610de3aee..b24f436b0 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -467,6 +467,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 72b8ccec9..9827d4905 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -142,6 +142,7 @@ fn preview_wire(points: Vec) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index e4b376071..486caf2d9 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -98,6 +98,7 @@ impl CadCommand for QdimCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index f890a6a3f..3c8ff8e4f 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -202,6 +202,7 @@ fn preview_wire(points: Vec) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 7d66e16de..e5d590508 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -204,6 +204,7 @@ impl CadCommand for TableCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 1d05fb941..d78deb170 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -108,6 +108,7 @@ impl CadCommand for ToleranceCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index b7dee4772..d1990d1e7 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -206,6 +206,7 @@ impl CadCommand for AttdefCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index da673f3fc..265829137 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -235,6 +235,7 @@ impl HatchCommand { }] }); HatchModel { + render_instance: None, boundary: std::sync::Arc::new(rel), pattern: HatchPattern::Pattern(families), name: pat_name.into(), @@ -399,6 +400,7 @@ impl GradientCommand { fn make_hatch(&self, rings: Vec>) -> HatchModel { let (rel, origin, wcs) = pack_rings(&rings); HatchModel { + render_instance: None, boundary: std::sync::Arc::new(rel), pattern: HatchPattern::Gradient { angle_deg: 0.0, @@ -614,6 +616,7 @@ impl CadCommand for BoundaryCommand { // Store as a Hatch entity (solid fill) so it is selectable. let (rel, origin, wcs) = pack_rings(&rings); let model = HatchModel { + render_instance: None, boundary: std::sync::Arc::new(rel), pattern: HatchPattern::Solid, name: "SOLID".into(), diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 77e54073f..4bef42242 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -186,6 +186,7 @@ impl CadCommand for MlineCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index aeeac0073..5b3373942 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -128,6 +128,7 @@ impl CadCommand for ImageCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 2753b3a82..a3e9c132a 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -90,6 +90,7 @@ impl CadCommand for RayCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -189,6 +190,7 @@ impl CadCommand for XLineCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 2e6dfc5a9..da872b8af 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -108,6 +108,7 @@ impl CadCommand for RevCloudCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 1301139c9..97d5f32eb 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -367,6 +367,7 @@ impl CadCommand for AreaCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 17c2c21f7..d493ec6a1 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -90,6 +90,7 @@ impl CadCommand for DistCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 6e20ee273..1c96c33c6 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -75,6 +75,7 @@ impl MeasureGeomCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 7df1f70bd..bb41968a4 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3713,6 +3713,7 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 87f1c88e4..a8c420b71 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -516,6 +516,7 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index c610e6778..3aeac951a 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -59,6 +59,7 @@ impl CadCommand for PlotWindowCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index f6d311bb7..fe24a7c16 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -78,6 +78,7 @@ impl CadCommand for ZoomWindowCommand { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 534793440..c860e938a 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -146,6 +146,7 @@ pub struct BlockCache { expansion_prototypes: std::sync::Mutex< HashMap>>>>, >, + clip_prototypes: std::sync::Mutex), u64>>, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -164,6 +165,24 @@ struct CachedExpansion { } impl BlockCache { + pub fn clip_source_id( + &self, + source_id: u64, + polygon: &[[f64; 2]], + translation: [f64; 3], + ) -> u64 { + let mut shape = Vec::with_capacity(polygon.len() * 2); + for point in polygon { + shape.push((point[0] - translation[0]).to_bits()); + shape.push((point[1] - translation[1]).to_bits()); + } + *self + .clip_prototypes + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .entry((source_id, shape)) + .or_insert_with(crate::scene::model::instance_model::next_source_id) + } pub fn new() -> Self { Self::default() } @@ -911,9 +930,7 @@ pub fn expand_insert( xform = xform.then(&scale_about_p); } let name = ins_handle.value().to_string(); - let prototype_key = if view_aabb.is_none() - && world_per_pixel.is_none() - && !ins.is_array() + let prototype_key = if !ins.is_array() && cache.prototype_blocks.contains(&ins.block_name) { Some(expansion_prototype_key( @@ -994,7 +1011,71 @@ pub fn expand_insert( } } - for offset in &crate::scene::render_graph::array_offsets(ins) { + let ctx = ExpandCtx { + cache, + ins_color: ins_resolved_color, + ins_aci, + ins_pat_len, + ins_pat, + ins_lw_px, + l0: ins_layer, + l0_aci: ins_layer_aci, + selected, + pslt_factor, + view_aabb: None, + world_per_pixel: None, + is_xref, + bg_color, + }; + let offsets = crate::scene::render_graph::array_offsets(ins); + if offsets.len() > 1 { + let cell_xform = |offset: &[f64; 3]| { + if offset == &[0.0; 3] { + xform.clone() + } else { + Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2])) + .then(&xform) + } + }; + let first_xform = cell_xform(&offsets[0]); + let first_translation = transform_translation(&first_xform); + let mut first_batches = Batches::default(); + expand_defn( + defn, + &first_xform, + &ctx, + &mut first_batches, + &mut visited, + 0, + (0.0, 1.0), + ); + let mut first = first_batches.finalize(&name, selected, bg_color); + for wire in &mut first { + if wire.render_instance.is_none() { + wire.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id: crate::scene::model::instance_model::next_source_id(), + translation: first_translation, + }, + ); + } + } + let mut result = first.clone(); + for offset in offsets.iter().skip(1) { + let translation = transform_translation(&cell_xform(offset)); + let delta = [ + translation[0] - first_translation[0], + translation[1] - first_translation[1], + translation[2] - first_translation[2], + ]; + result.extend(first.iter().map(|wire| { + translated_prototype_wire(wire, &name, delta) + })); + } + return Some(result); + } + + for offset in &offsets { let base_xform = if offset == &[0.0; 3] { xform.clone() } else { @@ -1003,28 +1084,21 @@ pub fn expand_insert( )); translation.then(&xform) }; - let ctx = ExpandCtx { - cache, - ins_color: ins_resolved_color, - ins_aci, - ins_pat_len, - ins_pat, - ins_lw_px, - l0: ins_layer, - l0_aci: ins_layer_aci, - selected, - pslt_factor, - view_aabb, - world_per_pixel, - is_xref, - bg_color, - }; expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0)); } - let wires = batches.finalize(&name, selected, bg_color); + let mut wires = batches.finalize(&name, selected, bg_color); if let Some(guard) = prototype_guard.as_mut() { + let translation = transform_translation(&xform); + for wire in &mut wires { + if wire.render_instance.is_none() { + wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance { + source_id: crate::scene::model::instance_model::next_source_id(), + translation, + }); + } + } let cached = Arc::new(CachedExpansion { - translation: transform_translation(&xform), + translation, wires: Arc::new(wires.clone()), }); **guard = Some(cached); @@ -1088,9 +1162,18 @@ fn expansion_prototype_key( } } -fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel { +fn translated_prototype_wire( + source: &WireModel, + name: &str, + delta: [f64; 3], +) -> WireModel { let mut wire = source.clone(); wire.name = name.to_string(); + if let Some(instance) = wire.render_instance.as_mut() { + for axis in 0..3 { + instance.translation[axis] += delta[axis]; + } + } translate_double_single(&mut wire.points, &mut wire.points_low, delta); translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta); translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta); @@ -1182,6 +1265,39 @@ struct ExpandCtx<'a> { bg_color: [f32; 4], } +fn nested_prototype_key( + block_name: &str, + transform: &Transform, + ctx: &ExpandCtx<'_>, + depth_scale: f32, +) -> NestedPrototypeKey { + let matrix = &transform.matrix.m; + let linear = [ + matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(), + matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(), + matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(), + ]; + let mut style = Vec::with_capacity(32); + style.extend(ctx.ins_color.map(f32::to_bits)); + style.push(ctx.ins_pat_len.to_bits()); + style.extend(ctx.ins_pat.map(f32::to_bits)); + style.push(ctx.ins_lw_px.to_bits()); + style.extend(ctx.l0.color.map(f32::to_bits)); + style.push(ctx.l0.pat_len.to_bits()); + style.extend(ctx.l0.pat.map(f32::to_bits)); + style.push(ctx.l0.lw_px.to_bits()); + style.push(ctx.pslt_factor.to_bits()); + style.extend(ctx.bg_color.map(f32::to_bits)); + NestedPrototypeKey { + block_name: block_name.to_string(), + linear, + style, + selected: ctx.selected, + is_xref: ctx.is_xref, + depth_scale: depth_scale.to_bits(), + } +} + /// Fade `color` toward `bg` by 50%, preserving alpha. Used to mark xref /// geometry — the hue stays recognizable but the contrast against the /// background drops, reading as "washed out". @@ -1259,6 +1375,23 @@ struct BatchEntry { max_y: f32, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct NestedPrototypeKey { + block_name: String, + linear: [u64; 9], + style: Vec, + selected: bool, + is_xref: bool, + depth_scale: u32, +} + +#[derive(Clone, Debug)] +struct NestedPrototype { + translation: [f64; 3], + depth_base: f32, + wires: Arc>, +} + /// Hard cap on point count for a single batched WireModel. Above this the /// current batch is finalized (pushed into `closed`) and a fresh one is /// started under the same style. Each WireModel point becomes ~6 GPU @@ -1276,6 +1409,7 @@ struct Batches { /// WireModels by `clip_wires`. Appended verbatim at `finalize` (only their /// name/selected flag are stamped to match the host insert). extra_wires: Vec, + nested_prototypes: HashMap, } impl BatchEntry { @@ -1338,6 +1472,7 @@ impl Batches { depth_override: b.local_depth, fill_is_3d: false, fill_is_2d_solid: b.fill_is_2d_solid, + render_instance: None, pick_tris: b.pick_tris, pick_tris_low: b.pick_tris_low, dash_from_start: false, @@ -1529,54 +1664,133 @@ fn expand_defn( d_range.0 + nref.local_rank * d_range.1, d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0), ); + let composed_for = |offset: &[f64; 3]| { + if offset == &[0.0; 3] { + nref.xform.then(accum_xform) + } else { + Transform::from_translation(Vector3::new( + offset[0], offset[1], offset[2], + )) + .then(&nref.xform) + .then(accum_xform) + } + }; if let Some(cp) = &nref.clip_poly { - // XCLIP'd nested insert: expand into an isolated batch set, - // finalize to whole wires, clip them to the boundary mapped - // into world by the accumulated transform, then carry the - // clipped wires out via `extra_wires`. Single instance only — - // an array of clipped nested inserts is vanishingly rare, so - // the array case is not handled (boundary placement per - // instance would differ). - let composed = nref.xform.then(accum_xform); - let mut sub = Batches::default(); - expand_defn( - nested_defn, - &composed, - &inner_ctx, - &mut sub, - visited, - depth + 1, - nested_range, - ); - let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); - let world_poly: Vec<[f64; 2]> = cp + let base_composed = nref.xform.then(accum_xform); + let base_translation = transform_translation(&base_composed); + let base_poly: Vec<[f64; 2]> = cp .iter() .map(|&[x, y]| { let w = accum_xform.apply(Vector3::new(x, y, 0.0)); [w.x, w.y] }) .collect(); - crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly); - out.extra_wires.append(&mut wires); - } else { + let mut first: Option<(Vec, [f64; 3])> = None; for offset in &nref.instance_offsets { - let composed = if offset == &[0.0; 3] { - nref.xform.then(accum_xform) - } else { - let translation = Transform::from_translation(Vector3::new( - offset[0], offset[1], offset[2], - )); - translation.then(&nref.xform).then(accum_xform) - }; + let composed = composed_for(offset); + let translation = transform_translation(&composed); + if let Some((source, source_translation)) = &first { + let delta = [ + translation[0] - source_translation[0], + translation[1] - source_translation[1], + translation[2] - source_translation[2], + ]; + out.extra_wires.extend(source.iter().map(|wire| { + translated_prototype_wire(wire, "", delta) + })); + continue; + } + let mut sub = Batches::default(); expand_defn( nested_defn, &composed, &inner_ctx, - out, + &mut sub, visited, depth + 1, nested_range, ); + let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); + let poly_delta = [ + translation[0] - base_translation[0], + translation[1] - base_translation[1], + ]; + let world_poly: Vec<[f64; 2]> = base_poly + .iter() + .map(|point| [point[0] + poly_delta[0], point[1] + poly_delta[1]]) + .collect(); + crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly); + for wire in &mut wires { + wire.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id: crate::scene::model::instance_model::next_source_id(), + translation, + }, + ); + } + out.extra_wires.extend(wires.iter().cloned()); + first = Some((wires, translation)); + } + } else { + for offset in &nref.instance_offsets { + let composed = composed_for(offset); + let translation = transform_translation(&composed); + let key = nested_prototype_key( + &nref.block_name, + &composed, + &inner_ctx, + nested_range.1, + ); + if let Some(cached) = out.nested_prototypes.get(&key).cloned() { + let delta = [ + translation[0] - cached.translation[0], + translation[1] - cached.translation[1], + translation[2] - cached.translation[2], + ]; + let depth_delta = nested_range.0 - cached.depth_base; + out.extra_wires.extend(cached.wires.iter().map(|wire| { + let mut wire = translated_prototype_wire( + wire, + "", + delta, + ); + if let Some(depth) = wire.depth_override.as_mut() { + *depth += depth_delta; + } + wire + })); + } else { + let mut sub = Batches::default(); + expand_defn( + nested_defn, + &composed, + &inner_ctx, + &mut sub, + visited, + depth + 1, + nested_range, + ); + let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); + for wire in &mut wires { + if wire.render_instance.is_none() { + wire.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id: crate::scene::model::instance_model::next_source_id(), + translation, + }, + ); + } + } + out.extra_wires.extend(wires.iter().cloned()); + out.nested_prototypes.insert( + key, + NestedPrototype { + translation, + depth_base: nested_range.0, + wires: Arc::new(wires), + }, + ); + } } } visited.pop(); diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index c2a47bf4c..f57b025fa 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -713,6 +713,7 @@ pub(crate) fn tessellate_entity( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris, pick_tris_low, dash_from_start: false, @@ -1071,6 +1072,7 @@ pub(crate) fn tessellate_entity( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1143,6 +1145,16 @@ pub(crate) fn tessellate_entity( let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform); pick::xclip::clip_wires(&mut wires, &polygon); + for wire in &mut wires { + if let Some(mut instance) = wire.render_instance { + instance.source_id = cache.clip_source_id( + instance.source_id, + &polygon, + instance.translation, + ); + wire.render_instance = Some(instance); + } + } if document.header.xclip_frame != 0 && polygon.len() >= 3 { wires.push(pick::xclip::frame_wire( &polygon, @@ -1434,6 +1446,7 @@ fn lod_stub_wire( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1524,6 +1537,7 @@ fn lod_stub_wire_3d( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 3c3d3f576..fbfc47680 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -374,6 +374,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), // MLINE dashes: A-type aligned, but the end-dash length is @@ -673,6 +674,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -714,6 +716,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -762,6 +765,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -791,6 +795,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -850,6 +855,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -891,6 +897,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -929,6 +936,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -960,6 +968,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1025,6 +1034,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1144,6 +1154,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris, pick_tris_low, dash_from_start: false, @@ -1204,6 +1215,7 @@ pub fn tessellate( fill_tris_low, fill_is_3d, fill_is_2d_solid: matches!(entity, EntityType::Solid(_)), + render_instance: None, depth_override: None, }); } @@ -1215,6 +1227,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1259,6 +1272,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris, pick_tris_low, dash_from_start: false, @@ -1304,6 +1318,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris, pick_tris_low, dash_from_start: false, @@ -1422,6 +1437,7 @@ pub fn tessellate( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris, pick_tris_low, dash_from_start: false, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index e87c66a1e..b06c644bc 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1099,6 +1099,7 @@ impl Scene { .with_viewport(viewport); let mut hatch_block_memo = std::collections::HashMap::new(); let mut models = Vec::new(); + let mut hatch_sources = rustc_hash::FxHashMap::default(); graph.walk_root( self.render_scene_root(layout_block), |entity, context| { @@ -1185,6 +1186,35 @@ impl Scene { if tint_selected && self.selected.contains(&context.root_handle) { model.color = [0.15, 0.55, 1.00, model.color[3]]; } + let matrix = &context.transform.matrix.m; + let linear = [ + matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(), + matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(), + matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(), + ]; + let key = ( + source_hatch.common.handle.value(), + linear, + model + .boundary + .iter() + .flat_map(|point| point.map(f32::to_bits)) + .collect::>(), + model.color.map(f32::to_bits), + model.angle_offset.to_bits(), + model.scale.to_bits(), + model.line_weight_px.to_bits(), + model.aci, + ); + let source_id = *hatch_sources + .entry(key) + .or_insert_with(crate::scene::model::instance_model::next_source_id); + model.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id, + translation: [matrix[0][3], matrix[1][3], matrix[2][3]], + }, + ); models.push(model); }, ); @@ -1233,6 +1263,7 @@ impl Scene { depth_map.as_ref(), ); let mut models = Vec::new(); + let mut wipeout_sources = rustc_hash::FxHashMap::default(); graph.walk_root( self.render_scene_root(target_block), |entity, context| { @@ -1279,7 +1310,34 @@ impl Scene { } else { bg_color }; + let render_instance = if context.is_instanced() { + let matrix = &context.transform.matrix.m; + let linear = [ + matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(), + matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(), + matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(), + ]; + let key = ( + source.common.handle.value(), + linear, + boundary + .iter() + .flat_map(|point| point.map(f32::to_bits)) + .collect::>(), + color.map(f32::to_bits), + ); + let source_id = *wipeout_sources.entry(key).or_insert_with( + crate::scene::model::instance_model::next_source_id, + ); + Some(crate::scene::model::instance_model::RenderInstance { + source_id, + translation: [matrix[0][3], matrix[1][3], matrix[2][3]], + }) + } else { + None + }; models.push(HatchModel { + render_instance, boundary: Arc::new(boundary), boundary_wcs: None, pattern: model::hatch_model::HatchPattern::Solid, @@ -1622,6 +1680,7 @@ impl Scene { .collect(); Some(HatchModel { + render_instance: None, boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, pattern, @@ -1926,6 +1985,7 @@ impl Scene { }) .collect(); HatchModel { + render_instance: None, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, pattern: model::hatch_model::HatchPattern::Solid, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ad9c53115..b79cade3e 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1224,25 +1224,6 @@ fn transform_block_mesh_lod_set( use acadrust::types::Vector3; let mut out = set.clone(); out.instance_transform = Some(*xform); - let origin = xform.apply(Vector3::ZERO); - let vectors = [ - xform.apply_rotation(Vector3::UNIT_X), - xform.apply_rotation(Vector3::UNIT_Y), - xform.apply_rotation(Vector3::UNIT_Z), - ]; - out.curved_gens.retain_mut(|generator| { - let transformed = cadkernel::brep::mesh::transform_silhouette_affine( - &generator.source, - vectors.map(|vector| [vector.x, vector.y, vector.z]), - [origin.x, origin.y, origin.z], - ); - if let Some(source) = transformed { - generator.source = source; - true - } else { - false - } - }); let mut min_x = f32::INFINITY; let mut min_y = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; @@ -3214,6 +3195,7 @@ impl Scene { let ((x0, y0), (x1, y1)) = self.paper_limits()?; let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); Some(HatchModel { + render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), boundary_wcs: None, @@ -5841,6 +5823,7 @@ impl Scene { ) .with_viewport(viewport); let mut models = Vec::new(); + let mut image_sources = rustc_hash::FxHashMap::default(); let root = if self.block_edit_block.is_none() { viewport.map(|viewport| render_graph::SceneRoot::Viewport { paper_block: self.current_layout_block_handle(), @@ -5898,6 +5881,21 @@ impl Scene { (point.z - vertex.pos[2] as f64) as f32, ]; } + let matrix = &context.transform.matrix.m; + let linear = [ + matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(), + matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(), + matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(), + ]; + let source_id = *image_sources + .entry((handle.value(), linear)) + .or_insert_with(crate::scene::model::instance_model::next_source_id); + placed.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id, + translation: [matrix[0][3], matrix[1][3], matrix[2][3]], + }, + ); } placed.draw_depth = context.draw_depth(handle, depth_map.as_ref()); models.push(placed); diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 7f9a293de..641708f22 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -155,6 +155,7 @@ pub const GPU_BOUNDARY_SEP: f32 = 1.0e30; /// A hatched region defined by a closed polygon boundary. #[derive(Clone, Debug)] pub struct HatchModel { + pub render_instance: Option, /// World XY anchor (in the same offset-relative coordinate space as /// the rest of the scene — `world_offset` already subtracted, but /// kept at f64 precision). Boundary vertices are stored as f32 diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index e34ed181a..17c8c0f67 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -82,6 +82,7 @@ fn clip_triangles_px(img: &acadrust::entities::RasterImage) -> Vec<[f64; 2]> { #[derive(Clone, Debug)] pub struct ImageModel { + pub render_instance: Option, /// Original file path (used for reload / display in properties). pub file_path: String, /// RGBA8 pixel data in row-major order. Arc-wrapped so cloning ImageModel @@ -168,6 +169,7 @@ impl ImageModel { let decoded = resolve_image(&img.file_path)?; Some(Self { + render_instance: None, file_path: img.file_path.clone(), pixels: decoded.pixels, width: decoded.width, @@ -278,6 +280,7 @@ impl ImageModel { .collect(); Some(Self { + render_instance: None, file_path: def.file_path.clone(), pixels: raster.pixels.clone(), width: raster.width, @@ -334,6 +337,7 @@ impl ImageModel { let corners_low = [l0, l1, l2, l3]; let verts = quad_verts(&corners, &corners_low); Some(Self { + render_instance: None, file_path: "OLE2FRAME".to_string(), pixels: Arc::new(pixels), width, diff --git a/src/scene/model/instance_model.rs b/src/scene/model/instance_model.rs new file mode 100644 index 000000000..c4a19f41a --- /dev/null +++ b/src/scene/model/instance_model.rs @@ -0,0 +1,13 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_SOURCE_ID: AtomicU64 = AtomicU64::new(1); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RenderInstance { + pub source_id: u64, + pub translation: [f64; 3], +} + +pub fn next_source_id() -> u64 { + NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed) +} diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index 3f5b8cbf7..0120d272f 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -3,6 +3,7 @@ pub mod gdi; pub mod hatch_model; pub mod hatch_patterns; pub mod image_model; +pub mod instance_model; pub mod material_model; pub mod ole_pres; pub mod pdf_raster; diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 3ee671e94..cf8de97e9 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -156,6 +156,8 @@ pub struct WireModel { /// `true` only for a planar SOLID entity's interior. Wireframe 3D omits /// this fill while preserving its perimeter and every other 2-D overlay. pub fill_is_2d_solid: bool, + /// Shared block-render source and this placement's translation. + pub render_instance: Option, } impl WireModel { @@ -208,6 +210,7 @@ impl WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), text_verts: Vec::new(), @@ -467,6 +470,7 @@ impl Default for WireModel { depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), } diff --git a/src/scene/pick/xclip.rs b/src/scene/pick/xclip.rs index e49b999c3..7ff56d108 100644 --- a/src/scene/pick/xclip.rs +++ b/src/scene/pick/xclip.rs @@ -262,6 +262,7 @@ pub fn frame_wire( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index e33f37edd..068f7b52e 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -85,6 +85,37 @@ pub struct Face3DChunk { pub vertex_count: u32, } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Face3DInstance { + pub translation: [f32; 3], + pub translation_low: [f32; 3], + pub draw_depth: f32, + pub _pad: [f32; 3], +} + +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::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ATTRS, + } + } +} + +pub struct BlockFace3DChunk { + pub vertex_buffer: wgpu::Buffer, + pub instance_buffer: wgpu::Buffer, + pub vertex_count: u32, + pub instance_count: u32, +} + pub struct Face3DGpu { /// 3DFACE quads + PolyfaceMesh / PolygonMesh face triangles. /// HiddenLine routes this through the depth-only pipeline so the @@ -95,6 +126,8 @@ pub struct Face3DGpu { /// source wire has an empty `points` list. Always rendered with the /// normal face3d pipeline (visible in every mode). pub chunks_2d: Vec, + pub block_chunks_3d: Vec, + pub block_chunks_2d: Vec, } impl Face3DGpu { @@ -179,6 +212,9 @@ impl Face3DGpu { if wire.fill_tris.is_empty() { continue; } + if wire.render_instance.is_some() { + continue; + } if !show_2d_solid_fills && wire.fill_is_2d_solid { continue; } @@ -236,13 +272,153 @@ impl Face3DGpu { } } + let (block_chunks_3d, block_chunks_2d) = upload_block_chunks( + device, + all_wires, + keep_3d_mesh_fills, + show_2d_solid_fills, + depth_map, + ); Self { chunks_3d: upload_chunks(device, &verts_3d, "face3d.vbuf.3d"), chunks_2d: upload_chunks(device, &verts_2d, "face3d.vbuf.2d"), + block_chunks_3d, + block_chunks_2d, } } } +fn upload_block_chunks( + device: &wgpu::Device, + wires: &[WireModel], + keep_3d_mesh_fills: bool, + show_2d_solid_fills: bool, + depth_map: &rustc_hash::FxHashMap, +) -> (Vec, Vec) { + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); + for wire in wires { + let Some(instance) = wire.render_instance else { + continue; + }; + if wire.fill_tris.is_empty() + || (!show_2d_solid_fills && wire.fill_is_2d_solid) + || (wire.fill_is_3d && !keep_3d_mesh_fills) + { + continue; + } + let key = (instance.source_id, wire.fill_is_3d); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push((wire.fill_is_3d, Vec::new())); + slot + }); + groups[slot].1.push(wire); + } + + let mut chunks_3d = Vec::new(); + let mut chunks_2d = Vec::new(); + for (is_3d, group) in groups { + let Some(&source) = group.first() else { + continue; + }; + let Some(base) = source.render_instance else { + 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 vertices: Vec = source + .fill_tris + .iter() + .enumerate() + .map(|(index, &position)| Face3DVertex { + position, + color, + draw_depth: 0.0, + position_low: source + .fill_tris_low + .get(index) + .copied() + .unwrap_or([0.0; 3]), + }) + .collect(); + if vertices.is_empty() { + continue; + } + let budget = (device.limits().max_buffer_size as usize / 10) * 9; + let max_vertices = ((budget / std::mem::size_of::()).max(3) / 3) * 3; + let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9 + / std::mem::size_of::()) + .max(1); + let instances: Vec = group + .iter() + .filter_map(|wire| { + let instance = wire.render_instance?; + let delta = [ + instance.translation[0] - base.translation[0], + instance.translation[1] - base.translation[1], + instance.translation[2] - base.translation[2], + ]; + let high = delta.map(|value| value as f32); + let (mut zmin, mut zmax) = (f32::INFINITY, f32::NEG_INFINITY); + for point in &wire.fill_tris { + zmin = zmin.min(point[2]); + zmax = zmax.max(point[2]); + } + let draw_depth = if is_3d || zmax - zmin > 1e-4 { + 0.0 + } else { + super::wire_gpu::wire_draw_depth(wire, depth_map) + }; + Some(Face3DInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + draw_depth, + _pad: [0.0; 3], + }) + }) + .collect(); + let instance_buffers: Vec<(wgpu::Buffer, u32)> = instances + .chunks(max_instances) + .map(|chunk| { + ( + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("face3d.block.instances"), + contents: bytemuck::cast_slice(chunk), + usage: wgpu::BufferUsages::VERTEX, + }), + chunk.len() as u32, + ) + }) + .collect(); + for vertex_chunk in vertices.chunks(max_vertices) { + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("face3d.block.vertices"), + contents: bytemuck::cast_slice(vertex_chunk), + usage: wgpu::BufferUsages::VERTEX, + }); + for (instance_buffer, instance_count) in &instance_buffers { + let chunk = BlockFace3DChunk { + vertex_buffer: vertex_buffer.clone(), + instance_buffer: instance_buffer.clone(), + vertex_count: vertex_chunk.len() as u32, + instance_count: *instance_count, + }; + if is_3d { + chunks_3d.push(chunk); + } else { + chunks_2d.push(chunk); + } + } + } + } + (chunks_3d, chunks_2d) +} + /// Upload `verts` as one or more VERTEX buffers, each under 90% of the /// device's `max_buffer_size`, split on whole-triangle boundaries. Also /// keeps every chunk's vertex count well below `u32::MAX` so the draw diff --git a/src/scene/pipeline/hatch_gpu/mod.rs b/src/scene/pipeline/hatch_gpu/mod.rs index 822026bc3..86503453d 100644 --- a/src/scene/pipeline/hatch_gpu/mod.rs +++ b/src/scene/pipeline/hatch_gpu/mod.rs @@ -83,10 +83,10 @@ impl HatchGpu { include_str!("../../../shaders/hatch_texture.wgsl") })), }); - let vertex_layout = if uses_storage { - storage::HatchVertex::layout() + let vertex_layouts = if uses_storage { + vec![storage::HatchVertex::layout(), storage::HatchPlacement::layout()] } else { - texture::vertex_layout() + vec![texture::vertex_layout(), texture::TextureHatchPlacement::layout()] }; let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(if uses_storage { @@ -98,7 +98,7 @@ impl HatchGpu { vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), - buffers: &[vertex_layout], + buffers: &vertex_layouts, compilation_options: wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState { @@ -170,11 +170,17 @@ impl HatchGpu { *resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); } HatchBackend::Texture { resident, .. } => { - *resident = hatches + let renderable: Vec = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) - .map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout)) + .cloned() .collect(); + *resident = TextureHatch::from_models( + device, + queue, + &renderable, + &self.bind_group_layout, + ); } } } @@ -195,11 +201,17 @@ impl HatchGpu { *preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); } HatchBackend::Texture { preview, .. } => { - *preview = hatches + let renderable: Vec = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) - .map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout)) + .cloned() .collect(); + *preview = TextureHatch::from_models( + device, + queue, + &renderable, + &self.bind_group_layout, + ); } } } @@ -218,11 +230,21 @@ impl HatchGpu { else { return 0; }; + for index in 0..batch.unique_source_count { + batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index])); + } + for index in batch.unique_source_count..batch.source_visibility.len() { + batch.source_visibility[index] = 1; + } for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() { - batch.visibility[index] = u32::from(is_visible(aabb)); + batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 { + 1 + } else { + u32::from(is_visible(aabb)) + }; } batch.upload_visibility(queue); - batch.instance_aabbs.len() + batch.instance_aabbs.len() + batch.unique_source_count } pub fn draw<'pass>( @@ -239,14 +261,18 @@ impl HatchGpu { for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() { pass.set_bind_group(1, &batch.bind_group, &[]); pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..)); - pass.draw(0..batch.vertex_count, 0..1); + pass.set_vertex_buffer(1, batch.placement_buffer.slice(..)); + for (vertices, instances) in &batch.draws { + pass.draw(vertices.clone(), instances.clone()); + } } } HatchBackend::Texture { resident, preview } => { for hatch in resident.iter().chain(preview) { pass.set_bind_group(1, &hatch.bind_group, &[]); pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..)); - pass.draw(0..6, 0..1); + pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..)); + pass.draw(0..6, 0..hatch.instance_count); } } } diff --git a/src/scene/pipeline/hatch_gpu/storage.rs b/src/scene/pipeline/hatch_gpu/storage.rs index f6683fa47..01e9ede65 100644 --- a/src/scene/pipeline/hatch_gpu/storage.rs +++ b/src/scene/pipeline/hatch_gpu/storage.rs @@ -152,6 +152,30 @@ impl HatchVertex { } } +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct HatchPlacement { + pub translation: [f32; 2], + pub translation_low: [f32; 2], + pub draw_depth: f32, + pub visible: u32, +} + +impl HatchPlacement { + pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() 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 }, + ], + } + } +} + // ── Batch builder ────────────────────────────────────────────────────────── /// Pack a list of `HatchModel`s into the four concatenated storage @@ -160,7 +184,8 @@ impl HatchVertex { /// hatch render pass entirely). pub(super) struct StorageHatchBatch { pub vertex_buffer: wgpu::Buffer, - pub vertex_count: u32, + pub placement_buffer: wgpu::Buffer, + pub draws: Vec<(std::ops::Range, std::ops::Range)>, // The four storage buffers below are referenced via `bind_group` — // dropping them would invalidate it, but the bind group is the // only direct consumer. Keep them as fields to keep ownership in @@ -175,14 +200,17 @@ pub(super) struct StorageHatchBatch { /// `visibility[instance_index]` — when 0 it emits an out-of-NDC /// clip position so the GPU clips the primitive before the /// fragment stage runs. - 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, /// 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 128 B-per-instance /// data. - pub visibility: Vec, + pub placements: Vec, + pub source_visibility: Vec, + pub source_aabbs: Vec<[f32; 4]>, + pub unique_source_count: usize, /// CPU-side mirror of each instance's local-space AABB (world- /// offset-subtracted, world_origin already added back). Used by /// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull @@ -208,8 +236,25 @@ impl StorageHatchBatch { let mut boundary: Vec<[f32; 4]> = Vec::new(); let mut families: Vec = Vec::new(); let mut dashes: Vec = Vec::new(); + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for (index, hatch) in hatches.iter().enumerate() { + let key = hatch + .render_instance + .map(|instance| (true, instance.source_id)) + .unwrap_or((false, index as u64)); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(hatch); + } + groups.sort_by_key(|group| std::cmp::Reverse(group.len() == 1)); + let unique_source_count = groups.iter().take_while(|group| group.len() == 1).count(); - for h in hatches { + for group in &groups { + let h = group[0]; let boundary_offset = boundary.len() as u32; for &[x, y] in h.boundary.iter() { // NaN sub-loop separators become the finite sentinel on the @@ -325,7 +370,7 @@ impl StorageHatchBatch { boundary_count, family_offset, family_count, - draw_depth: h.draw_depth, + draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 }, poly_test: 1, grad_kind, _pad2: 0, @@ -368,7 +413,7 @@ impl StorageHatchBatch { boundary_count, family_offset, family_count, - draw_depth: h.draw_depth, + draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 }, // Always resolve inside/outside per fragment on the GPU (the // boundary is uploaded below). Dropping CPU pre-triangulation // keeps the vertex buffer at 6 verts/hatch regardless of @@ -406,12 +451,71 @@ impl StorageHatchBatch { verts.push(HatchVertex { local_xy: c, instance_index: i as u32 }); } } + let mut placements = Vec::new(); + let mut draws = Vec::with_capacity(groups.len()); + let mut instance_aabbs = Vec::new(); + if unique_source_count > 0 { + placements.push(HatchPlacement { + translation: [0.0; 2], + translation_low: [0.0; 2], + draw_depth: 0.0, + visible: 1, + }); + draws.push((0..unique_source_count as u32 * 6, 0..1)); + 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]); + union[2] = union[2].max(inst.aabb[2] + inst.world_origin[0]); + union[3] = union[3].max(inst.aabb[3] + inst.world_origin[1]); + } + instance_aabbs.push(union); + } + for (source_index, group) in groups.iter().enumerate().skip(unique_source_count) { + let source = group[0]; + let base = source + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let placement_start = placements.len() as u32; + for hatch in group { + let translation = hatch + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let delta = [translation[0] - base[0], translation[1] - base[1]]; + let high = [delta[0] as f32, delta[1] as f32]; + placements.push(HatchPlacement { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + ], + draw_depth: hatch.draw_depth, + visible: 1, + }); + let inst = &instances[source_index]; + instance_aabbs.push([ + inst.aabb[0] + inst.world_origin[0] + high[0], + inst.aabb[1] + inst.world_origin[1] + high[1], + inst.aabb[2] + inst.world_origin[0] + high[0], + inst.aabb[3] + inst.world_origin[1] + high[1], + ]); + } + draws.push(( + source_index as u32 * 6..source_index as u32 * 6 + 6, + placement_start..placements.len() as u32, + )); + } let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.vertex"), contents: bytemuck::cast_slice(&verts), usage: wgpu::BufferUsages::VERTEX, }); + let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("hatch.placements"), + contents: bytemuck::cast_slice(&placements), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.instances"), contents: bytemuck::cast_slice(&instances), @@ -466,27 +570,20 @@ impl StorageHatchBatch { }, ], }); - - // CPU AABB mirror — local-space rect with world_origin added - // back, ready for `aabb_offscreen` / `aabb_below_pixel` to - // compare against the camera view_proj. - let instance_aabbs: Vec<[f32; 4]> = instances + let source_aabbs = instances .iter() - .map(|i| { - let ox = i.world_origin[0]; - let oy = i.world_origin[1]; - [ - i.aabb[0] + ox, - i.aabb[1] + oy, - i.aabb[2] + ox, - i.aabb[3] + oy, - ] - }) + .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 { vertex_buffer, - vertex_count: verts.len() as u32, + placement_buffer, + draws, instance_buffer, boundary_buffer, family_buffer, @@ -494,7 +591,10 @@ impl StorageHatchBatch { visibility_buffer, bind_group, instance_count: instances.len() as u32, - visibility, + placements, + source_visibility: visibility, + source_aabbs, + unique_source_count, instance_aabbs, }) } @@ -502,10 +602,15 @@ impl StorageHatchBatch { /// Push the CPU `visibility` slice to GPU. Call when any /// element changes (typically per-frame from compute_hatch_lod). pub(super) fn upload_visibility(&self, queue: &wgpu::Queue) { + queue.write_buffer( + &self.placement_buffer, + 0, + bytemuck::cast_slice(&self.placements), + ); queue.write_buffer( &self.visibility_buffer, 0, - bytemuck::cast_slice(&self.visibility), + bytemuck::cast_slice(&self.source_visibility), ); } diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index fc3376fb0..d2037a922 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -39,6 +39,29 @@ pub(super) fn vertex_layout<'a>() -> wgpu::VertexBufferLayout<'a> { } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct TextureHatchPlacement { + translation: [f32; 2], + translation_low: [f32; 2], + draw_depth: f32, + _pad: [f32; 3], +} + +impl TextureHatchPlacement { + pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() 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 }, + ], + } + } +} + // ── Per-hatch uniform (binding 0) — 96 bytes, matches HatchUniforms in // hatch_texture.wgsl. ────────────────────────────────────────────────────── @@ -67,6 +90,8 @@ struct TextureHatchUniform { pub(super) struct TextureHatch { pub(super) vertex_buffer: wgpu::Buffer, + pub(super) placement_buffer: wgpu::Buffer, + pub(super) instance_count: u32, pub(super) bind_group: wgpu::BindGroup, /// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired /// into the web hatch draw loop — the native batched path doesn't do @@ -78,6 +103,32 @@ pub(super) struct TextureHatch { } impl TextureHatch { + pub(super) fn from_models( + device: &wgpu::Device, + queue: &wgpu::Queue, + models: &[HatchModel], + bgl1: &wgpu::BindGroupLayout, + ) -> Vec { + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for (index, model) in models.iter().enumerate() { + let key = model + .render_instance + .map(|instance| (true, instance.source_id)) + .unwrap_or((false, index as u64)); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(model); + } + groups + .into_iter() + .map(|group| Self::new(device, queue, &group, bgl1)) + .collect() + } + /// Group-1 layout: uniform header (binding 0) + non-filterable float data /// texture (binding 1). pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { @@ -111,9 +162,10 @@ impl TextureHatch { pub(super) fn new( device: &wgpu::Device, queue: &wgpu::Queue, - model: &HatchModel, + models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout, ) -> Self { + let model = models[0]; // ── Decode pattern mode (mirrors WipeoutGpu::new) ───────────────── // The gradient shape (kind + invert bit) rides in the mode's high // bits — the 96-byte uniform has no spare slot. @@ -180,6 +232,33 @@ impl TextureHatch { contents: bytemuck::cast_slice(&quad), usage: wgpu::BufferUsages::VERTEX, }); + let base = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let placements: Vec = models + .iter() + .map(|model| { + let translation = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let delta = [translation[0] - base[0], translation[1] - base[1]]; + let high = [delta[0] as f32, delta[1] as f32]; + TextureHatchPlacement { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + ], + draw_depth: model.draw_depth, + _pad: [0.0; 3], + } + }) + .collect(); + let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("hatch.texture.placements"), + contents: bytemuck::cast_slice(&placements), + usage: wgpu::BufferUsages::VERTEX, + }); // ── Gradient projection range (snapped-local space) ─────────────── let (grad_min, grad_range) = if mode == 2 { @@ -356,6 +435,8 @@ impl TextureHatch { Self { vertex_buffer, + placement_buffer, + instance_count: placements.len() as u32, bind_group, world_aabb, _uniform_buf, diff --git a/src/scene/pipeline/image_gpu.rs b/src/scene/pipeline/image_gpu.rs index 5232b400a..24528f839 100644 --- a/src/scene/pipeline/image_gpu.rs +++ b/src/scene/pipeline/image_gpu.rs @@ -46,6 +46,30 @@ impl ImageVertex { } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct ImageInstance { + pub translation: [f32; 3], + pub translation_low: [f32; 3], + pub draw_depth: f32, + pub _pad: [f32; 3], +} + +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::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ATTRS, + } + } +} + // ── Uniform ─────────────────────────────────────────────────────────────── #[repr(C)] @@ -62,9 +86,11 @@ struct ImageParams { pub struct ImageGpu { pub vertex_buffer: wgpu::Buffer, + pub instance_buffer: wgpu::Buffer, /// Number of triangle vertices in `vertex_buffer` — 6 for a plain quad, or /// more when the raster is clipped to a triangulated polygon. pub vertex_count: u32, + pub instance_count: u32, pub bind_group: wgpu::BindGroup, _texture: wgpu::Texture, _sampler: wgpu::Sampler, @@ -72,12 +98,39 @@ pub struct ImageGpu { } impl ImageGpu { - pub fn new( + pub fn from_models( device: &wgpu::Device, queue: &wgpu::Queue, - model: &ImageModel, + models: &[ImageModel], + bgl1: &wgpu::BindGroupLayout, + ) -> Vec { + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for (index, model) in models.iter().enumerate() { + let key = model + .render_instance + .map(|instance| (true, instance.source_id)) + .unwrap_or((false, index as u64)); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(model); + } + groups + .into_iter() + .filter_map(|group| Self::new(device, queue, &group, bgl1)) + .collect() + } + + fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + models: &[&ImageModel], bgl1: &wgpu::BindGroupLayout, ) -> Option { + let &model = models.first()?; if model.pixels.is_empty() || model.width == 0 || model.height == 0 { return None; } @@ -129,7 +182,7 @@ impl ImageGpu { // ── Opacity uniform ─────────────────────────────────────────────── let params = ImageParams { opacity: model.opacity.clamp(0.0, 1.0), - draw_depth: model.draw_depth, + draw_depth: 0.0, _pad: [0.0; 2], }; let _params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -179,10 +232,44 @@ impl ImageGpu { contents: bytemuck::cast_slice(&verts), usage: wgpu::BufferUsages::VERTEX, }); + let base = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let instances: Vec = models + .iter() + .map(|model| { + let translation = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let delta = [ + translation[0] - base[0], + translation[1] - base[1], + translation[2] - base[2], + ]; + let high = delta.map(|value| value as f32); + ImageInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + draw_depth: model.draw_depth, + _pad: [0.0; 3], + } + }) + .collect(); + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("image.instances"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }); Some(Self { vertex_buffer, + instance_buffer, vertex_count, + instance_count: instances.len() as u32, bind_group, _texture: texture, _sampler, diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index ccf739461..2c3f2e0e5 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -11,19 +11,8 @@ use iced::wgpu::util::DeviceExt; pub struct MeshVertex { pub position: [f32; 3], pub normal: [f32; 3], - pub color: [f32; 4], pub position_low: [f32; 3], - /// gloss, reflectivity, self-illumination, luminance - pub material: [f32; 4], - /// specular RGB and refraction index - pub specular: [f32; 4], pub uv_diffuse: [f32; 2], - /// ambient RGB and translucence - pub ambient: [f32; 4], - /// normal strength, bump scale, reflectance scale, transmittance scale - pub advanced: [f32; 4], - /// illumination model, channel flags, material mode, luminance mode - pub flags: [u32; 4], pub uv_specular: [f32; 2], pub uv_reflection: [f32; 2], pub uv_opacity: [f32; 2], @@ -45,46 +34,16 @@ impl MeshVertex { shader_location: 1, format: wgpu::VertexFormat::Float32x3, }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, color) as u64, - shader_location: 2, - format: wgpu::VertexFormat::Float32x4, - }, wgpu::VertexAttribute { offset: std::mem::offset_of!(MeshVertex, position_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3, }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, material) as u64, - shader_location: 4, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, specular) as u64, - shader_location: 5, - format: wgpu::VertexFormat::Float32x4, - }, wgpu::VertexAttribute { offset: std::mem::offset_of!(MeshVertex, uv_diffuse) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x2, }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, ambient) as u64, - shader_location: 7, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, advanced) as u64, - shader_location: 8, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, flags) as u64, - shader_location: 9, - format: wgpu::VertexFormat::Uint32x4, - }, wgpu::VertexAttribute { offset: std::mem::offset_of!(MeshVertex, uv_specular) as u64, shader_location: 10, @@ -112,29 +71,101 @@ impl MeshVertex { pub fn edge_layout<'a>() -> wgpu::VertexBufferLayout<'a> { const ATTRS: &[wgpu::VertexAttribute] = &[ wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, position) as u64, + offset: std::mem::offset_of!(MeshEdgeVertex, position) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3, }, wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, color) as u64, + offset: std::mem::offset_of!(MeshEdgeVertex, color) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x4, }, wgpu::VertexAttribute { - offset: std::mem::offset_of!(MeshVertex, position_low) as u64, + offset: std::mem::offset_of!(MeshEdgeVertex, position_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3, }, ]; wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as u64, + array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: ATTRS, } } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct MeshEdgeVertex { + pub position: [f32; 3], + pub color: [f32; 4], + pub position_low: [f32; 3], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct SilhouetteVertex { + pub position: [f32; 3], + pub color: [f32; 4], + pub position_low: [f32; 3], +} + +impl SilhouetteVertex { + pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + const ATTRS: &[wgpu::VertexAttribute] = &[ + wgpu::VertexAttribute { + offset: std::mem::offset_of!(SilhouetteVertex, position) as u64, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(SilhouetteVertex, color) as u64, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(SilhouetteVertex, position_low) as u64, + shader_location: 3, + format: wgpu::VertexFormat::Float32x3, + }, + ]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: ATTRS, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct SilhouetteInstance { + pub translation: [f32; 3], + pub translation_low: [f32; 3], +} + +impl SilhouetteInstance { + pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + const ATTRS: &[wgpu::VertexAttribute] = &[ + wgpu::VertexAttribute { + offset: std::mem::offset_of!(SilhouetteInstance, translation) as u64, + shader_location: 4, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(SilhouetteInstance, translation_low) as u64, + shader_location: 5, + format: wgpu::VertexFormat::Float32x3, + }, + ]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ATTRS, + } + } +} + #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct MeshInstanceGpu { @@ -246,6 +277,7 @@ pub struct MeshBatchChunk { pub world_aabb: [f32; 6], pub visible: bool, pub material: Option, + pub face_color: [f32; 4], pub material_bind_group: Option, } @@ -259,57 +291,85 @@ pub struct MeshBatchRange { pub instance_count: u32, } +struct MeshBatchStubs { + index: wgpu::Buffer, + vertex: wgpu::Buffer, +} + +impl MeshBatchStubs { + fn new(device: &wgpu::Device) -> Self { + Self { + index: device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("mesh.batch.empty_ibuf"), + contents: bytemuck::cast_slice(&[0u32]), + usage: wgpu::BufferUsages::INDEX, + }), + vertex: device.create_buffer(&wgpu::BufferDescriptor { + label: Some("mesh.batch.empty_vbuf"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::VERTEX, + mapped_at_creation: false, + }), + } + } +} + fn make_chunk( device: &wgpu::Device, + queue: &wgpu::Queue, + stubs: &MeshBatchStubs, verts: &[MeshVertex], indices: &[u32], transp_indices: &[u32], wire_indices: &[u32], - edge_verts: &[MeshVertex], + edge_verts: &[MeshEdgeVertex], highlight_ranges: &[MeshBatchRange], instances: &[MeshInstanceGpu], handles: &rustc_hash::FxHashSet, bounds_override: Option<[f32; 6]>, material: Option<&crate::scene::model::material_model::MeshMaterial>, + face_color: [f32; 4], + vertex_buffer_override: Option, + edge_buffer_override: Option, ) -> MeshBatchChunk { - // `create_buffer_init` with an empty slice yields a zero-sized buffer that - // some backends reject for INDEX usage; a chunk can legitimately hold only - // opaque or only transparent tris, so fall back to a 1-index stub (count - // stays 0, so the draw loop skips it). let mk_index = |data: &[u32], label: &'static str| { - let stub = [0u32]; - let src = if data.is_empty() { &stub[..] } else { data }; - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + if data.is_empty() { + return stubs.index.clone(); + } + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), - contents: bytemuck::cast_slice(src), - usage: wgpu::BufferUsages::INDEX, - }) + size: std::mem::size_of_val(data) as u64, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data)); + buffer }; let mk_vertex = |data: &[MeshVertex], label: &'static str| { - let stub = [MeshVertex { - position: [0.0; 3], - normal: [0.0, 1.0, 0.0], - color: [0.0; 4], - position_low: [0.0; 3], - material: [0.0; 4], - specular: [1.0, 1.0, 1.0, 1.0], - uv_diffuse: [0.0; 2], - ambient: [0.3, 0.3, 0.3, 0.0], - advanced: [1.0; 4], - flags: [0, 127, 0, 0], - uv_specular: [0.0; 2], - uv_reflection: [0.0; 2], - uv_opacity: [0.0; 2], - uv_bump: [0.0; 2], - uv_refraction: [0.0; 2], - uv_normal: [0.0; 2], - }]; - let src = if data.is_empty() { &stub[..] } else { data }; - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + if data.is_empty() { + return stubs.vertex.clone(); + } + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), - contents: bytemuck::cast_slice(src), - usage: wgpu::BufferUsages::VERTEX, - }) + size: std::mem::size_of_val(data) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data)); + buffer + }; + let mk_edge_vertex = |data: &[MeshEdgeVertex], label: &'static str| { + if data.is_empty() { + return stubs.vertex.clone(); + } + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: std::mem::size_of_val(data) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data)); + buffer }; let identity = [MeshInstanceGpu::identity()]; let instance_data = if instances.is_empty() { @@ -331,7 +391,18 @@ fn make_chunk( f32::NEG_INFINITY, f32::NEG_INFINITY, ]; - for vertex in verts.iter().chain(edge_verts) { + for vertex in verts { + let point = [ + vertex.position[0] + vertex.position_low[0], + vertex.position[1] + vertex.position_low[1], + vertex.position[2] + vertex.position_low[2], + ]; + for axis in 0..3 { + computed_aabb[axis] = computed_aabb[axis].min(point[axis]); + computed_aabb[axis + 3] = computed_aabb[axis + 3].max(point[axis]); + } + } + for vertex in edge_verts { let point = [ vertex.position[0] + vertex.position_low[0], vertex.position[1] + vertex.position_low[1], @@ -344,26 +415,34 @@ fn make_chunk( } let world_aabb = bounds_override.unwrap_or(computed_aabb); MeshBatchChunk { - vertex_buffer: mk_vertex(verts, "mesh.batch.vbuf"), + vertex_buffer: vertex_buffer_override + .unwrap_or_else(|| mk_vertex(verts, "mesh.batch.vbuf")), index_buffer: mk_index(indices, "mesh.batch.ibuf"), index_count: indices.len() as u32, transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"), transp_index_count: transp_indices.len() as u32, wire_index_buffer: mk_index(wire_indices, "mesh.batch.wire_ibuf"), wire_index_count: wire_indices.len() as u32, - edge_vertex_buffer: mk_vertex(edge_verts, "mesh.batch.edge_vbuf"), + edge_vertex_buffer: edge_buffer_override + .unwrap_or_else(|| mk_edge_vertex(edge_verts, "mesh.batch.edge_vbuf")), edge_vertex_count: edge_verts.len() as u32, - instance_buffer: device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + instance_buffer: { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("mesh.batch.instances"), - contents: bytemuck::cast_slice(instance_data), - usage: instance_usage, - }), + size: std::mem::size_of_val(instance_data) as u64, + usage: instance_usage | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&buffer, 0, bytemuck::cast_slice(instance_data)); + buffer + }, instance_count: instance_data.len() as u32, highlight_ranges: highlight_ranges.to_vec(), handles: handles.clone(), world_aabb, visible: true, material: material.cloned(), + face_color, material_bind_group: None, } } @@ -391,6 +470,17 @@ struct MaterialMapParams { indirect: [f32; 4], } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct MeshSurfaceParams { + face_color: [f32; 4], + material: [f32; 4], + specular: [f32; 4], + ambient: [f32; 4], + advanced: [f32; 4], + flags: [u32; 4], +} + fn upload_rgba_texture( device: &wgpu::Device, queue: &wgpu::Queue, @@ -437,13 +527,29 @@ fn upload_rgba_texture( texture.create_view(&wgpu::TextureViewDescriptor::default()) } -pub fn create_material_bind_group( +pub struct MeshMaterialResources { + diffuse_view: wgpu::TextureView, + specular_view: wgpu::TextureView, + reflection_view: wgpu::TextureView, + opacity_view: wgpu::TextureView, + bump_view: wgpu::TextureView, + refraction_view: wgpu::TextureView, + normal_view: wgpu::TextureView, + diffuse_sampler: wgpu::Sampler, + specular_sampler: wgpu::Sampler, + reflection_sampler: wgpu::Sampler, + opacity_sampler: wgpu::Sampler, + bump_sampler: wgpu::Sampler, + refraction_sampler: wgpu::Sampler, + normal_sampler: wgpu::Sampler, + params_buffer: wgpu::Buffer, +} + +pub fn create_material_resources( device: &wgpu::Device, queue: &wgpu::Queue, - layout: &wgpu::BindGroupLayout, material: Option<&crate::scene::model::material_model::MeshMaterial>, - instance_buffer: Option<&wgpu::Buffer>, -) -> wgpu::BindGroup { +) -> MeshMaterialResources { let diffuse = material.and_then(|material| material.diffuse_map.image.as_deref()); let specular = material.and_then(|material| material.specular_map.image.as_deref()); let reflection = material.and_then(|material| material.reflection_map.image.as_deref()); @@ -598,6 +704,33 @@ pub fn create_material_bind_group( contents: bytemuck::bytes_of(¶ms), usage: wgpu::BufferUsages::UNIFORM, }); + MeshMaterialResources { + diffuse_view, + specular_view, + reflection_view, + opacity_view, + bump_view, + refraction_view, + normal_view, + diffuse_sampler, + specular_sampler, + reflection_sampler, + opacity_sampler, + bump_sampler, + refraction_sampler, + normal_sampler, + params_buffer, + } +} + +pub fn create_material_bind_group_from_resources( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + resources: &MeshMaterialResources, + instance_buffer: Option<&wgpu::Buffer>, + material: Option<&crate::scene::model::material_model::MeshMaterial>, + face_color: [f32; 4], +) -> wgpu::BindGroup { let fallback_instances; let instance_buffer = match instance_buffer { Some(buffer) => buffer, @@ -617,79 +750,158 @@ pub fn create_material_bind_group( &fallback_instances } }; + let (material_params, specular, ambient, advanced, flags) = + material_vertex_params(material); + let surface = MeshSurfaceParams { + face_color, + material: material_params, + specular, + ambient, + advanced, + flags, + }; + let surface_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("mesh.material.surface"), + contents: bytemuck::bytes_of(&surface), + usage: wgpu::BufferUsages::UNIFORM, + }); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("mesh.material.bind_group"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_view), + resource: wgpu::BindingResource::TextureView(&resources.diffuse_view), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::TextureView(&specular_view), + resource: wgpu::BindingResource::TextureView(&resources.specular_view), }, wgpu::BindGroupEntry { binding: 2, - resource: wgpu::BindingResource::TextureView(&reflection_view), + resource: wgpu::BindingResource::TextureView(&resources.reflection_view), }, wgpu::BindGroupEntry { binding: 3, - resource: wgpu::BindingResource::TextureView(&opacity_view), + resource: wgpu::BindingResource::TextureView(&resources.opacity_view), }, wgpu::BindGroupEntry { binding: 4, - resource: wgpu::BindingResource::TextureView(&bump_view), + resource: wgpu::BindingResource::TextureView(&resources.bump_view), }, wgpu::BindGroupEntry { binding: 5, - resource: wgpu::BindingResource::TextureView(&refraction_view), + resource: wgpu::BindingResource::TextureView(&resources.refraction_view), }, wgpu::BindGroupEntry { binding: 6, - resource: wgpu::BindingResource::TextureView(&normal_view), + resource: wgpu::BindingResource::TextureView(&resources.normal_view), }, wgpu::BindGroupEntry { binding: 7, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + resource: wgpu::BindingResource::Sampler(&resources.diffuse_sampler), }, wgpu::BindGroupEntry { binding: 8, - resource: wgpu::BindingResource::Sampler(&specular_sampler), + resource: wgpu::BindingResource::Sampler(&resources.specular_sampler), }, wgpu::BindGroupEntry { binding: 9, - resource: wgpu::BindingResource::Sampler(&reflection_sampler), + resource: wgpu::BindingResource::Sampler(&resources.reflection_sampler), }, wgpu::BindGroupEntry { binding: 10, - resource: wgpu::BindingResource::Sampler(&opacity_sampler), + resource: wgpu::BindingResource::Sampler(&resources.opacity_sampler), }, wgpu::BindGroupEntry { binding: 11, - resource: wgpu::BindingResource::Sampler(&bump_sampler), + resource: wgpu::BindingResource::Sampler(&resources.bump_sampler), }, wgpu::BindGroupEntry { binding: 12, - resource: wgpu::BindingResource::Sampler(&refraction_sampler), + resource: wgpu::BindingResource::Sampler(&resources.refraction_sampler), }, wgpu::BindGroupEntry { binding: 13, - resource: wgpu::BindingResource::Sampler(&normal_sampler), + resource: wgpu::BindingResource::Sampler(&resources.normal_sampler), }, wgpu::BindGroupEntry { binding: 14, - resource: params_buffer.as_entire_binding(), + resource: resources.params_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 15, resource: instance_buffer.as_entire_binding(), }, + wgpu::BindGroupEntry { + binding: 16, + resource: surface_buffer.as_entire_binding(), + }, ], }) } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub fn create_material_bind_group( + device: &wgpu::Device, + queue: &wgpu::Queue, + layout: &wgpu::BindGroupLayout, + material: Option<&crate::scene::model::material_model::MeshMaterial>, + instance_buffer: Option<&wgpu::Buffer>, +) -> 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, + layout, + &resources, + instance_buffer, + material, + color, + ) +} + +pub fn upload_chunk_material_bind_groups( + device: &wgpu::Device, + queue: &wgpu::Queue, + layout: &wgpu::BindGroupLayout, + chunks: &mut [MeshBatchChunk], +) { + let mut fallback = None; + 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)), + Some(material) => { + let Some(handle) = material.handle else { + let resources = create_material_resources(device, queue, Some(material)); + chunk.material_bind_group = Some(create_material_bind_group_from_resources( + device, + layout, + &resources, + Some(&chunk.instance_buffer), + chunk.material.as_ref(), + chunk.face_color, + )); + continue; + }; + 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( + device, + layout, + resources, + Some(&chunk.instance_buffer), + chunk.material.as_ref(), + chunk.face_color, + )); + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct MaterialBatchKey([u32; 40]); fn material_key( @@ -767,13 +979,34 @@ struct MeshBatchPart<'a> { uv_mesh: &'a MeshModel, material: Option<&'a crate::scene::model::material_model::MeshMaterial>, color: [f32; 4], - indices: Vec, + indices: std::sync::Arc<[u32]>, + index_hash: u64, include_faces: bool, include_edges: bool, part_slot: u32, } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, PartialEq, Eq, Hash)] +struct FacePartitionKey { + instanced: bool, + source: u64, + base_material: MaterialBatchKey, + face_materials: Vec<(u64, MaterialBatchKey)>, + visual_style: [u64; 12], + display_color: [u32; 4], + include_faces: bool, + include_edges: bool, +} + +#[derive(Clone)] +struct CachedFacePart<'a> { + material: Option<&'a crate::scene::model::material_model::MeshMaterial>, + color: [f32; 4], + indices: std::sync::Arc<[u32]>, + index_hash: u64, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct InstanceGroupKey { material: MaterialBatchKey, source: u64, @@ -791,65 +1024,121 @@ fn index_hash(indices: &[u32]) -> u64 { hasher.finish() } +fn visual_style_partition_key( + style: Option<&crate::scene::model::visual_style_model::MeshVisualStyle>, +) -> [u64; 12] { + let mut key = [0; 12]; + let Some(style) = style else { + return key; + }; + key[0] = style.face_lighting_model as u64; + key[1] = style.face_color_mode as u64; + key[2] = style.face_modifier as u64; + key[3] = style.face_opacity.to_bits() as u64; + if let Some(color) = style.mono_color { + key[4] = 1; + for (slot, value) in key[5..9].iter_mut().zip(color) { + *slot = value.to_bits() as u64; + } + } + key +} + +#[derive(Default)] +struct InstancedBuildProfile { + vertices: std::time::Duration, + edges: std::time::Duration, + instances: std::time::Duration, + upload: std::time::Duration, + vertex_count: usize, + edge_count: usize, + index_count: usize, + chunk_count: usize, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct InstancedVertexKey { + source: u64, + material: u64, +} + +#[derive(Default)] +struct InstancedBufferCache { + vertices: rustc_hash::FxHashMap, + edges: rustc_hash::FxHashMap<(u64, [u32; 4]), wgpu::Buffer>, +} + +fn material_has_textures( + material: Option<&crate::scene::model::material_model::MeshMaterial>, +) -> bool { + material.is_some_and(|material| { + [ + &material.diffuse_map, + &material.specular_map, + &material.reflection_map, + &material.opacity_map, + &material.bump_map, + &material.refraction_map, + &material.normal_map, + ] + .into_iter() + .any(|map| map.image.is_some()) + }) +} + fn build_instanced_chunk( device: &wgpu::Device, + queue: &wgpu::Queue, + stubs: &MeshBatchStubs, + buffers: &mut InstancedBufferCache, parts: &[MeshBatchPart<'_>], -) -> Option<(MeshBatchChunk, u64)> { + profile_enabled: bool, +) -> Option<(MeshBatchChunk, u64, InstancedBuildProfile)> { + let started = profile_enabled.then(std::time::Instant::now); let first = parts.first()?; let source = first.set.instance_source.as_ref()?; let mesh = first.mesh; let material = first.material; let color = first.color; + let source_handle = source.handle.value(); + let material_identity = if material_has_textures(material) { + material.map_or(0, |material| { + material + .handle + .map_or(material as *const _ as usize as u64, |handle| handle.value()) + }) + } else { + 0 + }; + let vertex_key = InstancedVertexKey { + source: source_handle, + material: material_identity, + }; + let shared_vertex_buffer = buffers.vertices.get(&vertex_key).cloned(); let has_normals = mesh.normals.len() == mesh.verts.len(); let bounds = mesh_bounds(mesh); - let (material_params, specular, ambient, advanced, flags) = - material_vertex_params(material); - let vertex = |index: usize, edge: bool| { - let normal = if edge { - [0.0, 1.0, 0.0] - } else if has_normals { + let vertex = |index: usize| { + let normal = if has_normals { mesh.normals[index] } else { [0.0, 1.0, 0.0] }; - let position = if edge { - source.edge_verts[index] - } else { - mesh.verts[index] - }; - let position_low = if edge { - source - .edge_verts_low - .get(index) - .copied() - .unwrap_or([0.0; 3]) - } else { - mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]) - }; - let uvs = if edge { - [[0.0; 2]; 7] - } else { - material_uvs( - material, - position, - normal, - bounds, - position, - normal, - bounds, - ) - }; + let position = mesh.verts[index]; + let position_low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]); + let uvs = material_uvs( + material, + position, + normal, + bounds, + position, + normal, + bounds, + ); MeshVertex { position, normal, - color, position_low, - material: material_params, - specular, uv_diffuse: uvs[0], - ambient, - advanced, - flags, uv_specular: uvs[1], uv_reflection: uvs[2], uv_opacity: uvs[3], @@ -858,9 +1147,12 @@ fn build_instanced_chunk( uv_normal: uvs[6], } }; - let verts: Vec<_> = (0..mesh.verts.len()) - .map(|index| vertex(index, false)) - .collect(); + let verts: Vec<_> = if shared_vertex_buffer.is_none() { + (0..mesh.verts.len()).map(vertex).collect() + } else { + Vec::new() + }; + let vertices_at = profile_enabled.then(std::time::Instant::now); let edge_color = first .set .visual_style @@ -868,18 +1160,27 @@ fn build_instanced_chunk( .map_or(first.display_mesh.color, |style| { style.edge_color(first.display_mesh.color) }); - let edge_verts: Vec<_> = if first.include_edges { + let edge_key = (source_handle, edge_color.map(f32::to_bits)); + let shared_edge_buffer = first + .include_edges + .then(|| buffers.edges.get(&edge_key).cloned()) + .flatten(); + let edge_verts: Vec<_> = if first.include_edges && shared_edge_buffer.is_none() { (0..source.edge_verts.len()) - .map(|index| { - let mut out = vertex(index, true); - out.color = edge_color; - out + .map(|index| MeshEdgeVertex { + position: source.edge_verts[index], + color: edge_color, + position_low: source + .edge_verts_low + .get(index) + .copied() + .unwrap_or([0.0; 3]), }) .collect() } else { Vec::new() }; - let has_feature_edges = !edge_verts.is_empty(); + let has_feature_edges = first.include_edges && !source.edge_verts.is_empty(); let mut wire_indices = Vec::new(); if first.include_edges && !has_feature_edges { wire_indices.reserve(first.indices.len() * 2); @@ -894,8 +1195,9 @@ fn build_instanced_chunk( ]); } } + let edges_at = profile_enabled.then(std::time::Instant::now); let transparent = material_is_transparent(material, color); - let face_indices = first.include_faces.then_some(first.indices.as_slice()); + let face_indices = first.include_faces.then_some(first.indices.as_ref()); let opaque_indices = if transparent { &[][..] } else { @@ -947,14 +1249,16 @@ fn build_instanced_chunk( } } } + let instances_at = profile_enabled.then(std::time::Instant::now); let triangles = if first.include_faces { (first.indices.len() / 3) as u64 * instances.len() as u64 } else { 0 }; - Some(( - make_chunk( + let chunk = make_chunk( device, + queue, + stubs, &verts, opaque_indices, transparent_indices, @@ -965,8 +1269,46 @@ fn build_instanced_chunk( &handles, Some(bounds), material, - ), + color, + shared_vertex_buffer, + shared_edge_buffer, + ); + buffers + .vertices + .entry(vertex_key) + .or_insert_with(|| chunk.vertex_buffer.clone()); + if first.include_edges { + buffers + .edges + .entry(edge_key) + .or_insert_with(|| chunk.edge_vertex_buffer.clone()); + } + let uploaded_at = profile_enabled.then(std::time::Instant::now); + Some(( + chunk, triangles, + InstancedBuildProfile { + vertices: started + .zip(vertices_at) + .map(|(start, end)| end.duration_since(start)) + .unwrap_or_default(), + edges: vertices_at + .zip(edges_at) + .map(|(start, end)| end.duration_since(start)) + .unwrap_or_default(), + instances: edges_at + .zip(instances_at) + .map(|(start, end)| end.duration_since(start)) + .unwrap_or_default(), + upload: instances_at + .zip(uploaded_at) + .map(|(start, end)| end.duration_since(start)) + .unwrap_or_default(), + vertex_count: verts.len(), + edge_count: edge_verts.len(), + index_count: first.indices.len(), + chunk_count: 1, + }, )) } @@ -1092,14 +1434,29 @@ fn material_uvs( let Some(material) = material else { return [[0.0; 2]; 7]; }; + let uv = |map: &crate::scene::model::material_model::MeshTextureMap| { + if map.image.is_none() { + [0.0; 2] + } else { + material_map_uv( + map, + local_position, + local_normal, + local_bounds, + model_position, + model_normal, + model_bounds, + ) + } + }; [ - material_map_uv(&material.diffuse_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.specular_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.reflection_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.opacity_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.bump_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.refraction_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), - material_map_uv(&material.normal_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds), + uv(&material.diffuse_map), + uv(&material.specular_map), + uv(&material.reflection_map), + uv(&material.opacity_map), + uv(&material.bump_map), + uv(&material.refraction_map), + uv(&material.normal_map), ] } @@ -1157,15 +1514,21 @@ fn material_vertex_params( /// wire-index buffer (6 u32 = 24 B/triangle — the fattest index buffer) are /// bounded; a single mesh too large for one chunk is split into triangle-soup /// sub-chunks so an XREF-heavy model can never overflow a single buffer (#203). -pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec, u64) { - build_mesh_batch_filtered(device, sets, None) +pub fn build_mesh_batch( + device: &wgpu::Device, + queue: &wgpu::Queue, + sets: &[MeshLodSet], +) -> (Vec, u64) { + build_mesh_batch_filtered(device, queue, sets, None) } pub fn build_mesh_batch_filtered( device: &wgpu::Device, + queue: &wgpu::Queue, sets: &[MeshLodSet], handles: Option<&rustc_hash::FxHashSet>, ) -> (Vec, u64) { + let perf_started = crate::perf::enabled().then(std::time::Instant::now); // Derive the caps from the real device limit and vertex size. The previous // fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew // MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap. @@ -1174,18 +1537,25 @@ pub fn build_mesh_batch_filtered( let vsize = std::mem::size_of::(); let max_verts = (budget / vsize).max(3); let max_tris = (budget / (6 * 4)).max(1); // wire-index buffer: 6 u32 per tri + let stubs = MeshBatchStubs::new(device); let mut chunks = Vec::new(); let mut verts: Vec = Vec::new(); let mut indices: Vec = Vec::new(); let mut transp_indices: Vec = Vec::new(); let mut wire_indices: Vec = Vec::new(); - let mut edge_verts: Vec = Vec::new(); + let mut edge_verts: Vec = Vec::new(); let mut highlight_ranges: Vec = Vec::new(); let mut chunk_handles: rustc_hash::FxHashSet = rustc_hash::FxHashSet::default(); let mut total_tris = 0u64; let mut ordered: Vec> = Vec::new(); + 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>> = + rustc_hash::FxHashMap::default(); for set in sets { let Some(display_mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else { continue; @@ -1205,6 +1575,10 @@ pub fn build_mesh_batch_filtered( .as_ref() .and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty())); let mesh = source_mesh.unwrap_or(display_mesh); + let source_identity = set.instance_source.as_ref().map_or_else( + || (false, display_mesh as *const MeshModel as usize as u64), + |source| (true, source.handle.value()), + ); let triangle_count = display_mesh.indices.len() / 3; let has_face_materials = display_mesh.triangle_material_handles.len() == triangle_count @@ -1226,6 +1600,14 @@ pub fn build_mesh_batch_filtered( .visual_style .as_ref() .map_or(base_color, |style| style.face_color(base_color)); + let (shared_indices, shared_hash) = source_indices + .entry(source_identity) + .or_insert_with(|| { + let indices = std::sync::Arc::<[u32]>::from(display_mesh.indices.as_slice()); + let hash = index_hash(indices.as_ref()); + (indices, hash) + }) + .clone(); ordered.push(MeshBatchPart { set, mesh, @@ -1233,61 +1615,104 @@ pub fn build_mesh_batch_filtered( uv_mesh: mesh, material: set.material.as_ref(), color, - indices: display_mesh.indices.clone(), + indices: shared_indices, + index_hash: shared_hash, include_faces, include_edges, part_slot: 0, }); continue; } - let mut groups: std::collections::BTreeMap< - MaterialBatchKey, - ( - Option<&crate::scene::model::material_model::MeshMaterial>, - [f32; 4], - Vec, - ), - > = std::collections::BTreeMap::new(); - for (triangle, indices) in display_mesh.indices.chunks_exact(3).enumerate() { - let material = if has_face_materials { - display_mesh.triangle_material_handles[triangle] - .and_then(|handle| set.face_materials.get(&handle)) - .or(set.material.as_ref()) - } else { - set.material.as_ref() - }; - let base_color = - material.map_or(display_mesh.color, |material| material.diffuse); - let base_color = if has_face_colors { - display_mesh.triangle_colors[triangle].unwrap_or(base_color) - } else { - base_color - }; - let color = set - .visual_style - .as_ref() - .map_or(base_color, |style| style.face_color(base_color)); - groups - .entry(material_key(material, color)) - .or_insert_with(|| (material, color, Vec::new())) - .2 - .extend_from_slice(indices); - } - for (part_index, (_, (material, color, indices))) in groups.into_iter().enumerate() { + let mut face_materials: Vec<_> = set + .face_materials + .iter() + .map(|(handle, material)| { + ( + handle.value(), + material_key(Some(material), material.diffuse), + ) + }) + .collect(); + face_materials.sort_by_key(|(handle, _)| *handle); + let partition_key = FacePartitionKey { + instanced: source_identity.0, + source: source_identity.1, + base_material: material_key(set.material.as_ref(), display_mesh.color), + face_materials, + visual_style: visual_style_partition_key(set.visual_style.as_ref()), + display_color: display_mesh.color.map(f32::to_bits), + include_faces, + include_edges, + }; + let cached_parts = if let Some(parts) = face_partitions.get(&partition_key) { + parts.clone() + } else { + let mut groups: std::collections::BTreeMap< + MaterialBatchKey, + ( + Option<&crate::scene::model::material_model::MeshMaterial>, + [f32; 4], + Vec, + ), + > = std::collections::BTreeMap::new(); + for (triangle, indices) in display_mesh.indices.chunks_exact(3).enumerate() { + let material = if has_face_materials { + display_mesh.triangle_material_handles[triangle] + .and_then(|handle| set.face_materials.get(&handle)) + .or(set.material.as_ref()) + } else { + set.material.as_ref() + }; + let base_color = + material.map_or(display_mesh.color, |material| material.diffuse); + let base_color = if has_face_colors { + display_mesh.triangle_colors[triangle].unwrap_or(base_color) + } else { + base_color + }; + let color = set + .visual_style + .as_ref() + .map_or(base_color, |style| style.face_color(base_color)); + groups + .entry(material_key(material, color)) + .or_insert_with(|| (material, color, Vec::new())) + .2 + .extend_from_slice(indices); + } + let parts: Vec<_> = groups + .into_values() + .map(|(material, color, indices)| { + let indices = std::sync::Arc::<[u32]>::from(indices); + let index_hash = index_hash(indices.as_ref()); + CachedFacePart { + material, + color, + indices, + index_hash, + } + }) + .collect(); + face_partitions.insert(partition_key, parts.clone()); + parts + }; + for (part_index, part) in cached_parts.into_iter().enumerate() { ordered.push(MeshBatchPart { set, mesh, display_mesh, uv_mesh: mesh, - material, - color, - indices, + material: part.material, + color: part.color, + indices: part.indices, + index_hash: part.index_hash, include_faces, include_edges: include_edges && part_index == 0, part_slot: part_index as u32, }); } } + let prepared_at = std::time::Instant::now(); let spatial_bounds = sets.iter().fold( [ f32::INFINITY, @@ -1350,7 +1775,7 @@ pub fn build_mesh_batch_filtered( source, part_slot: part.part_slot, index_count: part.indices.len(), - index_hash: index_hash(&part.indices), + index_hash: part.index_hash, include_faces: part.include_faces, include_edges: part.include_edges, }) @@ -1382,7 +1807,7 @@ pub fn build_mesh_batch_filtered( source .edge_verts .len() - .saturating_mul(std::mem::size_of::()) + .saturating_mul(std::mem::size_of::()) }), ); let saved_bytes = geometry_bytes.saturating_mul(parts.len().saturating_sub(1)); @@ -1403,9 +1828,11 @@ pub fn build_mesh_batch_filtered( mesh_spatial_key(part.set, spatial_bounds), ) }); + let grouped_at = std::time::Instant::now(); let ordered = direct_parts; let mut active_key: Option = None; let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None; + let mut active_color = [1.0; 4]; for part in ordered { let set = part.set; let mesh = part.mesh; @@ -1423,6 +1850,8 @@ pub fn build_mesh_batch_filtered( { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1433,6 +1862,9 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); verts.clear(); indices.clear(); @@ -1444,12 +1876,11 @@ pub fn build_mesh_batch_filtered( } active_key = Some(key); active_material = material; + active_color = part_color; if let Some(handle) = entity_handle { chunk_handles.insert(handle); } let has_normals = mesh.normals.len() == mesh.verts.len(); - let (material_params, specular, ambient, advanced, flags) = - material_vertex_params(material); let uv_mesh = part.uv_mesh; let local_bounds = mesh_bounds(uv_mesh); let model_bounds = mesh_bounds(mesh); @@ -1485,14 +1916,8 @@ pub fn build_mesh_batch_filtered( MeshVertex { position: mesh.verts[vi], normal, - color: part_color, position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]), - material: material_params, - specular, uv_diffuse: uv[0], - ambient, - advanced, - flags, uv_specular: uv[1], uv_reflection: uv[2], uv_opacity: uv[3], @@ -1530,6 +1955,8 @@ pub fn build_mesh_batch_filtered( if take == 0 { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1540,6 +1967,9 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); verts.clear(); indices.clear(); @@ -1554,29 +1984,18 @@ pub fn build_mesh_batch_filtered( continue; } for i in edge_start..edge_start + take { - edge_verts.push(MeshVertex { + edge_verts.push(MeshEdgeVertex { position: set.edge_verts[i], - normal: [0.0, 1.0, 0.0], color: edge_color, position_low: set.edge_verts_low.get(i).copied().unwrap_or([0.0; 3]), - material: material_params, - specular, - uv_diffuse: [0.0; 2], - ambient, - advanced, - flags, - uv_specular: [0.0; 2], - uv_reflection: [0.0; 2], - uv_opacity: [0.0; 2], - uv_bump: [0.0; 2], - uv_refraction: [0.0; 2], - uv_normal: [0.0; 2], }); } edge_start += take; if edge_start < edge_end { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1587,6 +2006,9 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); verts.clear(); indices.clear(); @@ -1608,6 +2030,8 @@ pub fn build_mesh_batch_filtered( if !verts.is_empty() || !edge_verts.is_empty() { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1618,6 +2042,9 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); verts.clear(); indices.clear(); @@ -1671,6 +2098,8 @@ pub fn build_mesh_batch_filtered( entity_handle.into_iter().collect(); chunks.push(make_chunk( device, + queue, + &stubs, &sv, &[], &si, @@ -1681,12 +2110,17 @@ pub fn build_mesh_batch_filtered( &sub_handles, None, active_material, + active_color, + None, + None, )); } else { let sub_handles: rustc_hash::FxHashSet<_> = entity_handle.into_iter().collect(); chunks.push(make_chunk( device, + queue, + &stubs, &sv, &si, &[], @@ -1697,6 +2131,9 @@ pub fn build_mesh_batch_filtered( &sub_handles, None, active_material, + active_color, + None, + None, )); } t = end; @@ -1713,6 +2150,8 @@ pub fn build_mesh_batch_filtered( { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1723,6 +2162,9 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); verts.clear(); indices.clear(); @@ -1742,7 +2184,7 @@ pub fn build_mesh_batch_filtered( if part.include_faces { let fill = if is_transp { &mut transp_indices } else { &mut indices }; let index_start = fill.len() as u32; - for &idx in &part.indices { + for &idx in part.indices.iter() { fill.push(base + idx); } if let Some(handle) = entity_handle { @@ -1770,6 +2212,8 @@ pub fn build_mesh_batch_filtered( { chunks.push(make_chunk( device, + queue, + &stubs, &verts, &indices, &transp_indices, @@ -1780,8 +2224,14 @@ pub fn build_mesh_batch_filtered( &chunk_handles, None, active_material, + active_color, + None, + None, )); } + let direct_at = std::time::Instant::now(); + let mut instanced_profile = InstancedBuildProfile::default(); + let mut instanced_buffers = InstancedBufferCache::default(); for parts in instance_groups.values() { let Some(first) = parts.first() else { continue; @@ -1801,7 +2251,7 @@ pub fn build_mesh_batch_filtered( source .edge_verts .len() - .saturating_mul(std::mem::size_of::()) + .saturating_mul(std::mem::size_of::()) }), ) .max(1); @@ -1814,11 +2264,49 @@ pub fn build_mesh_batch_filtered( .div_ceil(max_clusters) .max(64); for cluster in parts.chunks(cluster_len) { - if let Some((chunk, triangles)) = build_instanced_chunk(device, cluster) { + if let Some((chunk, triangles, profile)) = + build_instanced_chunk( + device, + queue, + &stubs, + &mut instanced_buffers, + cluster, + perf_started.is_some(), + ) + { total_tris += triangles; chunks.push(chunk); + instanced_profile.vertices += profile.vertices; + instanced_profile.edges += profile.edges; + instanced_profile.instances += profile.instances; + instanced_profile.upload += profile.upload; + instanced_profile.vertex_count += profile.vertex_count; + instanced_profile.edge_count += profile.edge_count; + instanced_profile.index_count += profile.index_count; + instanced_profile.chunk_count += profile.chunk_count; } } } + if let Some(started) = perf_started { + let done = std::time::Instant::now(); + crate::perf_record!( + "[perf] mesh-build-detail prepare={:.1} group={:.1} direct={:.1} instanced={:.1}", + prepared_at.duration_since(started).as_secs_f64() * 1000.0, + grouped_at.duration_since(prepared_at).as_secs_f64() * 1000.0, + direct_at.duration_since(grouped_at).as_secs_f64() * 1000.0, + done.duration_since(direct_at).as_secs_f64() * 1000.0, + ); + crate::perf_record!( + "[perf] mesh-instanced vertices={:.1} edges={:.1} instances={:.1} upload={:.1} chunks={} verts={} edge_verts={} indices={}", + instanced_profile.vertices.as_secs_f64() * 1000.0, + instanced_profile.edges.as_secs_f64() * 1000.0, + instanced_profile.instances.as_secs_f64() * 1000.0, + instanced_profile.upload.as_secs_f64() * 1000.0, + instanced_profile.chunk_count, + instanced_profile.vertex_count, + instanced_profile.edge_count, + instanced_profile.index_count, + ); + } (chunks, total_tris) } diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 0a07af591..8a3ce25a5 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -22,12 +22,19 @@ pub use wipeout_gpu::WipeoutGpu; pub use image_gpu::ImageGpu; pub use uniforms::Uniforms; pub use viewcube::ViewCubePipeline; -pub use wire_gpu::WireGpu; +pub use wire_gpu::{BlockWireGpu, WireGpu}; use crate::scene::model::hatch_model::HatchModel; use crate::scene::model::image_model::ImageModel; use crate::scene::model::mesh_model::MeshLodSet; use crate::scene::model::wire_model::WireModel; + +struct SilhouetteChunk { + vertex_buffer: wgpu::Buffer, + instance_buffer: wgpu::Buffer, + vertex_count: u32, + instance_count: u32, +} use device_capabilities::DeviceCapabilities; /// MSAA sample count for the main drawing pipelines. @@ -78,18 +85,22 @@ pub struct Pipeline { background_pipeline: wgpu::RenderPipeline, shadow_pipeline: wgpu::RenderPipeline, wire_pipeline: wgpu::RenderPipeline, + block_wire_pipeline: wgpu::RenderPipeline, /// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP). clip_mask_pipeline: wgpu::RenderPipeline, /// Black-fragment variant of `wire_pipeline` for 3D mesh outline edges in /// filled render modes. wire_black_pipeline: wgpu::RenderPipeline, + block_wire_black_pipeline: wgpu::RenderPipeline, /// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false. /// Used to draw ghost copies of selected wires through occluding geometry. wire_xray_pipeline: wgpu::RenderPipeline, + block_wire_xray_pipeline: wgpu::RenderPipeline, /// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire / /// xray pipelines). `Some` on any storage-capable device; `None` in packed /// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`. pub(crate) wire_const_bgl: Option, + block_wire_const_bgl: wgpu::BindGroupLayout, wipeout_pipeline: wgpu::RenderPipeline, /// Capability-selected hatch renderer. Storage and texture transports are /// private backends behind one upload/LOD/draw lifecycle. @@ -98,8 +109,10 @@ pub struct Pipeline { /// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the /// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts). text_pipeline: wgpu::RenderPipeline, + block_text_pipeline: wgpu::RenderPipeline, /// Depth-independent variant used by selection / rollover highlighting. text_highlight_pipeline: wgpu::RenderPipeline, + block_text_highlight_pipeline: wgpu::RenderPipeline, mesh_pipeline: wgpu::RenderPipeline, /// Depth-write-disabled variant of `mesh_pipeline` for non-opaque solids. mesh_transparent_pipeline: wgpu::RenderPipeline, @@ -112,6 +125,8 @@ pub struct Pipeline { mesh_wireframe_pipeline: wgpu::RenderPipeline, /// Edge pipeline that forces black, for the edge overlay in filled modes. mesh_edge_black_pipeline: wgpu::RenderPipeline, + silhouette_pipeline: wgpu::RenderPipeline, + silhouette_black_pipeline: wgpu::RenderPipeline, /// Depth-only variant of the mesh pipeline (TriangleList, no color /// writes, writes depth). Used in HiddenLine mode so 3D solids /// occlude wires behind them without painting visible pixels. @@ -129,9 +144,11 @@ pub struct Pipeline { mesh_edge_indirect: Option, mesh_cull_count: u32, face3d_pipeline: wgpu::RenderPipeline, + block_face3d_pipeline: wgpu::RenderPipeline, /// Depth-only variant of the face3d pipeline (no color writes, /// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine. face3d_depth_pipeline: wgpu::RenderPipeline, + block_face3d_depth_pipeline: wgpu::RenderPipeline, uniform_buffer: wgpu::Buffer, frame_bgl: wgpu::BindGroupLayout, uniform_bind_group: wgpu::BindGroup, @@ -155,6 +172,8 @@ pub struct Pipeline { /// All glyph-quad vertices for the frame, one buffer, `None` when empty. text_vbuf: Option, text_vcount: u32, + block_text_gpu: Vec, + block_text_highlight_gpu: Vec, /// Tinted glyph quads of just the selected / hovered text, drawn over the /// base text pass so a selection / rollover recolours the glyphs (the text /// analogue of the selected-wire xray overlay). Rebuilt on selection change. @@ -168,9 +187,8 @@ pub struct Pipeline { /// it back into the base set (issue #316). text_preview_vbuf: Option, text_preview_vcount: u32, - /// Per-frame silhouette line list from the kernel mesh and current view. - silhouette_vbuf: Option, - silhouette_vcount: u32, + /// Per-frame silhouette line lists from the kernel mesh and current view. + silhouette_chunks: Vec, /// Last requested render size (the full viewport rect, in pixels). The /// geometry passes render at this size; the blit UV is scaled by /// `depth_texture_size / alloc_size` so it samples only the filled region. @@ -207,6 +225,7 @@ pub struct Pipeline { /// replace this thin draw-range list on camera changes without touching the /// shared resident buffer. pub(crate) gpu_wires: std::sync::Arc>, + pub(crate) gpu_block_wires: std::sync::Arc>, /// Persistent per-entity wire instance arena (capability-selected format). /// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an /// edit patches one entity's slab in place instead of rebuilding every wire. @@ -232,6 +251,7 @@ pub struct Pipeline { pub(crate) hatch_lod_key: (usize, u64, u32, u32, bool), pub(crate) wipeout_lod_key: (usize, u64, u32, u32, bool), pub(crate) mesh_lod_key: (usize, u64, u32, u32), + pub(crate) silhouette_key: (usize, u64, u64, bool), /// This content viewport's non-rectangular clip boundary as a triangle-fan /// vertex buffer in the render target's normalized device coords (`None` = /// rectangular / unclipped, where the viewport's own render rectangle does @@ -240,6 +260,7 @@ pub struct Pipeline { clip_boundary: Option<(wgpu::Buffer, u32)>, /// Ghost copies (25% alpha) of selected wires for the X-ray depth pass. gpu_selected_wires: Vec, + gpu_selected_block_wires: Vec, /// Command-preview / interim / grip-drag overlay wires. Re-uploaded every /// frame they are present (small), drawn on top of the base wire pass — so /// a live drag never re-uploads the resident base buffer. @@ -578,6 +599,12 @@ impl Pipeline { bind_group_layouts: &wire_bgls, immediate_size: 0, }); + let block_wire_const_bgl = wire_gpu::block_const_bind_group_layout(device); + let block_wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("block_wire.pipeline_layout"), + bind_group_layouts: &[Some(&frame_bgl), Some(&block_wire_const_bgl)], + immediate_size: 0, + }); let depth_tex = create_depth_texture(device, Size::new(1, 1)); let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default()); @@ -591,6 +618,12 @@ impl Pipeline { wire_gpu::WirePipelineMode::Packed => include_str!("../../shaders/wire.wgsl"), })), }); + let block_wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("block_wire.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "../../shaders/block_wire.wgsl" + ))), + }); // Stencil test shared by every paper content pipeline: draw only where // the stencil equals the bound reference. Non-clipped content binds @@ -865,6 +898,73 @@ impl Pipeline { multiview_mask: None, cache: None, }); + let make_block_wire_pipeline = | + label: &'static str, + fragment: &'static str, + depth_write_enabled: bool, + depth_compare: wgpu::CompareFunction, + | { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&block_wire_layout), + vertex: wgpu::VertexState { + module: &block_wire_shader, + entry_point: Some("vs_main"), + buffers: &[ + wire_gpu::BlockWireVertex::layout(), + wire_gpu::BlockWireInstance::layout(), + ], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: Some(depth_write_enabled), + depth_compare: Some(depth_compare), + stencil: content_stencil.clone(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: MSAA_SAMPLES, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &block_wire_shader, + entry_point: Some(fragment), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview_mask: None, + cache: None, + }) + }; + let block_wire_pipeline = make_block_wire_pipeline( + "block_wire.pipeline", + "fs_main", + true, + wgpu::CompareFunction::LessEqual, + ); + let block_wire_black_pipeline = make_block_wire_pipeline( + "block_wire.black.pipeline", + "fs_black", + true, + wgpu::CompareFunction::LessEqual, + ); + let block_wire_xray_pipeline = make_block_wire_pipeline( + "block_wire.xray.pipeline", + "fs_main", + false, + wgpu::CompareFunction::Always, + ); // ── Hatch pipeline ───────────────────────────────────────────────── // binding 0 (HatchUniforms) is read by the vertex shader too — it @@ -911,7 +1011,10 @@ impl Pipeline { vertex: wgpu::VertexState { module: &wipeout_shader, entry_point: Some("vs_main"), - buffers: &[wipeout_gpu::HatchVertex::layout()], + buffers: &[ + wipeout_gpu::HatchVertex::layout(), + wipeout_gpu::WipeoutPlacement::layout(), + ], compilation_options: wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState { @@ -1255,6 +1358,16 @@ impl Pipeline { }, count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 16, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); let mesh_default_material_bind_group = mesh_gpu::create_material_bind_group( @@ -1471,14 +1584,18 @@ impl Pipeline { // Edge/wireframe pipeline (LineList). `fs_edge` outputs the flat entity // colour — no lighting — for the lines-only modes. A `fs_edge_black` // twin (below) forces black for the edge overlay in filled modes. - let make_edge_pipeline = |label: &'static str, fs: &'static str| { + let make_edge_pipeline = | + label: &'static str, + fs: &'static str, + vertex_layout: wgpu::VertexBufferLayout<'static>, + | { device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label), layout: Some(&mesh_layout), vertex: wgpu::VertexState { module: &mesh_shader, entry_point: Some("vs_edge"), - buffers: &[mesh_gpu::MeshVertex::edge_layout()], + buffers: &[vertex_layout], compilation_options: wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState { @@ -1512,8 +1629,76 @@ impl Pipeline { cache: None, }) }; - let mesh_wireframe_pipeline = make_edge_pipeline("mesh.wireframe.pipeline", "fs_edge"); - let mesh_edge_black_pipeline = make_edge_pipeline("mesh.edge_black.pipeline", "fs_edge_black"); + let mesh_wireframe_pipeline = make_edge_pipeline( + "mesh.wireframe.pipeline", + "fs_edge", + mesh_gpu::MeshVertex::edge_layout(), + ); + let mesh_edge_black_pipeline = make_edge_pipeline( + "mesh.edge_black.pipeline", + "fs_edge_black", + mesh_gpu::MeshVertex::edge_layout(), + ); + let silhouette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("mesh.silhouette.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "../../shaders/silhouette.wgsl" + ))), + }); + let silhouette_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("mesh.silhouette.layout"), + bind_group_layouts: &[Some(&frame_bgl)], + immediate_size: 0, + }); + let make_silhouette_pipeline = |label: &'static str, fragment: &'static str| { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&silhouette_layout), + vertex: wgpu::VertexState { + module: &silhouette_shader, + entry_point: Some("vs_main"), + buffers: &[ + mesh_gpu::SilhouetteVertex::layout(), + mesh_gpu::SilhouetteInstance::layout(), + ], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::LineList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: Some(true), + depth_compare: Some(wgpu::CompareFunction::LessEqual), + stencil: content_stencil.clone(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: MSAA_SAMPLES, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &silhouette_shader, + entry_point: Some(fragment), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview_mask: None, + cache: None, + }) + }; + let silhouette_pipeline = + make_silhouette_pipeline("mesh.silhouette.pipeline", "fs_main"); + let silhouette_black_pipeline = + make_silhouette_pipeline("mesh.silhouette_black.pipeline", "fs_black"); // Depth-only variant — TriangleList, back-face culling stays on // (we only want front-facing fragments to write depth so wires @@ -1570,6 +1755,12 @@ impl Pipeline { "../../shaders/face3d.wgsl" ))), }); + let block_face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("block_face3d.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "../../shaders/block_face3d.wgsl" + ))), + }); let face3d_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("face3d.pipeline_layout"), @@ -1667,6 +1858,65 @@ impl Pipeline { multiview_mask: None, cache: None, }); + let make_block_face3d_pipeline = | + label: &'static str, + depth_only: bool, + | { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&face3d_layout), + vertex: wgpu::VertexState { + module: &block_face3d_shader, + entry_point: Some("vs_main"), + buffers: &[ + face3d_gpu::Face3DVertex::layout(), + face3d_gpu::Face3DInstance::layout(), + ], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: Some(true), + depth_compare: Some(wgpu::CompareFunction::LessEqual), + stencil: content_stencil.clone(), + bias: wgpu::DepthBiasState { + constant: 1, + slope_scale: 1.0, + clamp: 0.0, + }, + }), + multisample: wgpu::MultisampleState { + count: MSAA_SAMPLES, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &block_face3d_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: (!depth_only).then_some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: if depth_only { + wgpu::ColorWrites::empty() + } else { + wgpu::ColorWrites::ALL + }, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview_mask: None, + cache: None, + }) + }; + let block_face3d_pipeline = + make_block_face3d_pipeline("block_face3d.pipeline", false); + let block_face3d_depth_pipeline = + make_block_face3d_pipeline("block_face3d.depth.pipeline", true); // ── Image pipeline ───────────────────────────────────────────────── let image_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -1724,7 +1974,10 @@ impl Pipeline { vertex: wgpu::VertexState { module: &image_shader, entry_point: Some("vs_main"), - buffers: &[image_gpu::ImageVertex::layout()], + buffers: &[ + image_gpu::ImageVertex::layout(), + image_gpu::ImageInstance::layout(), + ], compilation_options: wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState { @@ -1760,7 +2013,12 @@ impl Pipeline { // ── Text (SDF glyph quads) ───────────────────────────────────────── let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device); - let (text_pipeline, text_highlight_pipeline) = + let ( + text_pipeline, + text_highlight_pipeline, + block_text_pipeline, + block_text_highlight_pipeline, + ) = text_gpu::create_pipelines( device, &frame_bgl, @@ -1902,31 +2160,40 @@ impl Pipeline { background_pipeline, shadow_pipeline, wire_pipeline, + block_wire_pipeline, clip_mask_pipeline, wire_black_pipeline, + block_wire_black_pipeline, wire_xray_pipeline, + block_wire_xray_pipeline, wire_const_bgl, + block_wire_const_bgl, wipeout_pipeline, hatch_gpu, image_pipeline, text_pipeline, + block_text_pipeline, text_highlight_pipeline, + block_text_highlight_pipeline, text_atlas_bgl, text_atlas_gpu: None, text_vbuf: None, text_vcount: 0, + block_text_gpu: Vec::new(), + block_text_highlight_gpu: Vec::new(), text_highlight_vbuf: None, text_highlight_vcount: 0, text_preview_vbuf: None, text_preview_vcount: 0, - silhouette_vbuf: None, - silhouette_vcount: 0, + silhouette_chunks: Vec::new(), mesh_pipeline, mesh_transparent_pipeline, mesh_selected_pipeline, mesh_hover_pipeline, mesh_wireframe_pipeline, mesh_edge_black_pipeline, + silhouette_pipeline, + silhouette_black_pipeline, mesh_depth_pipeline, mesh_material_bgl, mesh_default_material_bind_group, @@ -1941,7 +2208,9 @@ impl Pipeline { mesh_edge_indirect: None, mesh_cull_count: 0, face3d_pipeline, + block_face3d_pipeline, face3d_depth_pipeline, + block_face3d_depth_pipeline, uniform_buffer, frame_bgl, uniform_bind_group, @@ -1971,6 +2240,7 @@ impl Pipeline { blit_uniform_buffer, surface_format: format, gpu_wires: std::sync::Arc::new(vec![]), + gpu_block_wires: std::sync::Arc::new(vec![]), wire_arena: None, wire_arena_mesh: None, wire_arena_fallback: std::sync::Arc::new(Vec::new()), @@ -1981,8 +2251,10 @@ impl Pipeline { hatch_lod_key: (usize::MAX, u64::MAX, 0, 0, false), wipeout_lod_key: (usize::MAX, u64::MAX, 0, 0, false), mesh_lod_key: (usize::MAX, u64::MAX, 0, 0), + silhouette_key: (usize::MAX, u64::MAX, u64::MAX, false), clip_boundary: None, gpu_selected_wires: vec![], + gpu_selected_block_wires: vec![], gpu_preview_wires: vec![], gpu_wipeouts: vec![], wipeout_skip_flags: vec![], @@ -2033,6 +2305,7 @@ impl Pipeline { depth_map: &rustc_hash::FxHashMap, ) -> ( std::sync::Arc>, + std::sync::Arc>, std::sync::Arc>>, ) { // Batch the wire pass: instead of one GPU buffer + one draw call per @@ -2058,16 +2331,40 @@ impl Pipeline { let is_mesh_edge = |w: &WireModel| !w.points.is_empty() && mesh_names.contains(w.name.as_str()); let mut batches: Vec = Vec::new(); + let mut block_wires: Vec<&WireModel> = Vec::new(); let mut i = 0; while i < wires.len() { + if wires[i].render_instance.is_some() { + block_wires.push(&wires[i]); + i += 1; + continue; + } let mesh_edge = is_mesh_edge(&wires[i]); let mut j = i + 1; - while j < wires.len() && is_mesh_edge(&wires[j]) == mesh_edge { + while j < wires.len() + && wires[j].render_instance.is_none() + && is_mesh_edge(&wires[j]) == mesh_edge + { j += 1; } - batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, mesh_edge, self.wire_const_bgl.as_ref())); + let refs: Vec<&WireModel> = wires[i..j].iter().collect(); + batches.extend(WireGpu::from_run_refs( + device, + &refs, + depth_map, + mesh_edge, + self.wire_const_bgl.as_ref(), + )); i = j; } + let block_batches = BlockWireGpu::from_wires( + device, + &block_wires, + depth_map, + &mesh_names, + None, + &self.block_wire_const_bgl, + ); // Index handle → wire slots once, here, so the per-hover selection // overlay can gather just the highlighted wires instead of scanning + @@ -2079,7 +2376,11 @@ impl Pipeline { index.entry(h).or_default().push(idx as u32); } } - (std::sync::Arc::new(batches), std::sync::Arc::new(index)) + ( + std::sync::Arc::new(batches), + std::sync::Arc::new(block_batches), + std::sync::Arc::new(index), + ) } /// Build the selection xray overlay: full-brightness copies of the wires @@ -2102,6 +2403,7 @@ impl Pipeline { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() { self.gpu_selected_wires = vec![]; + self.gpu_selected_block_wires = vec![]; return; } // Gather borrowed highlighted wires via the prebuilt index — @@ -2137,21 +2439,59 @@ impl Pipeline { hover_wires.push(wire); } } + let selected_regular: Vec<&WireModel> = selected_wires + .iter() + .copied() + .filter(|wire| wire.render_instance.is_none()) + .collect(); + let hover_regular: Vec<&WireModel> = hover_wires + .iter() + .copied() + .filter(|wire| wire.render_instance.is_none()) + .collect(); + let selected_blocks: Vec<&WireModel> = selected_wires + .iter() + .copied() + .filter(|wire| wire.render_instance.is_some()) + .collect(); + let hover_blocks: Vec<&WireModel> = hover_wires + .iter() + .copied() + .filter(|wire| wire.render_instance.is_some()) + .collect(); let mut gpu = WireGpu::from_highlight_refs( device, - &selected_wires, + &selected_regular, WireModel::SELECTED, depth_map, self.wire_const_bgl.as_ref(), ); gpu.extend(WireGpu::from_highlight_refs( device, - &hover_wires, + &hover_regular, WireModel::HOVER, depth_map, self.wire_const_bgl.as_ref(), )); self.gpu_selected_wires = gpu; + let mesh_names = rustc_hash::FxHashSet::default(); + let mut block_gpu = BlockWireGpu::from_wires( + device, + &selected_blocks, + depth_map, + &mesh_names, + Some(WireModel::SELECTED), + &self.block_wire_const_bgl, + ); + block_gpu.extend(BlockWireGpu::from_wires( + device, + &hover_blocks, + depth_map, + &mesh_names, + Some(WireModel::HOVER), + &self.block_wire_const_bgl, + )); + self.gpu_selected_block_wires = block_gpu; if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 1.0 { @@ -2180,34 +2520,62 @@ impl Pipeline { selected: &rustc_hash::FxHashSet, hovered: &rustc_hash::FxHashSet, annotation_context_wires: &[WireModel], + depth_map: &rustc_hash::FxHashMap, ) { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() { self.text_highlight_vbuf = None; self.text_highlight_vcount = 0; + self.block_text_highlight_gpu.clear(); return; } let mut out: Vec = Vec::new(); - let push = - |handle_val: u64, tint: [f32; 4], wires: &[WireModel], out: &mut Vec| { - if let Some(idxs) = self.wire_handle_index.get(&handle_val) { - for &i in idxs { - if let Some(w) = wires.get(i as usize) { - for v in &w.text_verts { - out.push(text_gpu::TextVertex { - color: [tint[0], tint[1], tint[2], v.color[3]], - ..*v - }); - } + let mut selected_blocks: Vec<&WireModel> = Vec::new(); + let mut hover_blocks: Vec<&WireModel> = Vec::new(); + fn push<'a>( + index: &rustc_hash::FxHashMap>, + handle_val: u64, + tint: [f32; 4], + wires: &'a [WireModel], + out: &mut Vec, + blocks: &mut Vec<&'a WireModel>, + ) { + if let Some(idxs) = index.get(&handle_val) { + for &i in idxs { + if let Some(w) = wires.get(i as usize) { + if w.render_instance.is_some() { + blocks.push(w); + continue; + } + for v in &w.text_verts { + out.push(text_gpu::TextVertex { + color: [tint[0], tint[1], tint[2], v.color[3]], + ..*v + }); } } } - }; + } + } for h in selected { - push(h.value(), WireModel::SELECTED, wires, &mut out); + push( + &self.wire_handle_index, + h.value(), + WireModel::SELECTED, + wires, + &mut out, + &mut selected_blocks, + ); } for h in hovered.iter().filter(|handle| !selected.contains(handle)) { - push(h.value(), WireModel::HOVER, wires, &mut out); + push( + &self.wire_handle_index, + h.value(), + WireModel::HOVER, + wires, + &mut out, + &mut hover_blocks, + ); } for wire in annotation_context_wires { let tint = if wire.selected { @@ -2224,6 +2592,19 @@ impl Pipeline { } self.text_highlight_vcount = out.len() as u32; self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out); + let mut block_gpu = text_gpu::upload_block_vertex_refs( + device, + &selected_blocks, + depth_map, + Some(WireModel::SELECTED), + ); + block_gpu.extend(text_gpu::upload_block_vertex_refs( + device, + &hover_blocks, + depth_map, + Some(WireModel::HOVER), + )); + self.block_text_highlight_gpu = block_gpu; if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 1.0 { @@ -2295,58 +2676,190 @@ impl Pipeline { sets: &[crate::scene::model::mesh_model::MeshLodSet], view_dir: glam::Vec3, ) { + let perf_started = crate::perf::enabled().then(iced::time::Instant::now); let view = glam::DVec3::new(view_dir.x as f64, view_dir.y as f64, view_dir.z as f64) .normalize_or(glam::DVec3::NEG_Z); - use crate::scene::pipeline::mesh_gpu::MeshVertex; - let mut verts: Vec = Vec::new(); - for set in sets { - let color = set.lods.first().map(|m| m.color).unwrap_or([0.0, 0.0, 0.0, 1.0]); - let mk = |w: glam::DVec3| -> MeshVertex { + use crate::scene::pipeline::mesh_gpu::{SilhouetteInstance, SilhouetteVertex}; + use wgpu::util::DeviceExt; + const SILHOUETTE_CHUNK_BYTES: u64 = 64 * 1024 * 1024; + let chunk_bytes = device + .limits() + .max_buffer_size + .min(SILHOUETTE_CHUNK_BYTES) as usize; + let max_vertices = chunk_bytes / std::mem::size_of::() / 2 * 2; + if max_vertices < 2 { + self.silhouette_chunks.clear(); + return; + } + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for (index, set) in sets.iter().enumerate() { + let color = set + .lods + .first() + .map(|mesh| mesh.color) + .unwrap_or([0.0, 0.0, 0.0, 1.0]); + let key = match (&set.instance_source, set.instance_transform) { + (Some(source), Some(transform)) => { + let matrix = &transform.matrix.m; + ( + true, + source.handle.value(), + [ + matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(), + matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(), + matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(), + ], + color.map(f32::to_bits), + ) + } + _ => (false, index as u64, [0; 9], color.map(f32::to_bits)), + }; + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(set); + } + + let source_count = groups.len(); + let mut source_vertex_count = 0usize; + let mut chunks = Vec::new(); + for group in groups { + let Some(&source) = group.first() else { + continue; + }; + let translation = |set: &crate::scene::model::mesh_model::MeshLodSet| { + set.instance_transform.map_or([0.0; 3], |transform| { + let matrix = &transform.matrix.m; + [matrix[0][3], matrix[1][3], matrix[2][3]] + }) + }; + let base = translation(source); + let instances: Vec = group + .iter() + .map(|set| { + let placement = translation(set); + let delta = [ + placement[0] - base[0], + placement[1] - base[1], + placement[2] - base[2], + ]; + let high = delta.map(|value| value as f32); + SilhouetteInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + } + }) + .collect(); + let max_instances = chunk_bytes / std::mem::size_of::(); + let instance_buffers: Vec<(wgpu::Buffer, u32)> = instances + .chunks(max_instances.max(1)) + .map(|instances| { + ( + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("mesh.silhouette.instances"), + contents: bytemuck::cast_slice(instances), + usage: wgpu::BufferUsages::VERTEX, + }), + instances.len() as u32, + ) + }) + .collect(); + let color = source + .lods + .first() + .map(|mesh| mesh.color) + .unwrap_or([0.0, 0.0, 0.0, 1.0]); + let mk = |w: glam::DVec3| -> SilhouetteVertex { let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32); - MeshVertex { + SilhouetteVertex { position: [hx, hy, hz], - normal: [0.0, 1.0, 0.0], color, position_low: [ (w.x - hx as f64) as f32, (w.y - hy as f64) as f32, (w.z - hz as f64) as f32, ], - material: [0.5, 0.0, 0.0, 0.0], - specular: [1.0, 1.0, 1.0, 1.0], - uv_diffuse: [0.0; 2], - ambient: [0.3, 0.3, 0.3, 0.0], - advanced: [1.0; 4], - flags: [0, 127, 0, 0], - uv_specular: [0.0; 2], - uv_reflection: [0.0; 2], - uv_opacity: [0.0; 2], - uv_bump: [0.0; 2], - uv_refraction: [0.0; 2], - uv_normal: [0.0; 2], } }; - for generator in &set.curved_gens { - for point in cadkernel::brep::mesh::silhouette( - &generator.source, + let mut verts: Vec = Vec::with_capacity(max_vertices); + let push_chunk = | + verts: &[SilhouetteVertex], + chunks: &mut Vec, + | { + let vertex_count = verts.len() / 2 * 2; + if vertex_count == 0 { + return; + } + let vertex_buffer = device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("mesh.silhouette.vbuf"), + contents: bytemuck::cast_slice(&verts[..vertex_count]), + usage: wgpu::BufferUsages::VERTEX, + }, + ); + for (instance_buffer, instance_count) in &instance_buffers { + chunks.push(SilhouetteChunk { + vertex_buffer: vertex_buffer.clone(), + instance_buffer: instance_buffer.clone(), + vertex_count: vertex_count as u32, + instance_count: *instance_count, + }); + } + }; + for generator in &source.curved_gens { + let transformed; + let silhouette_source = if let Some(transform) = source.instance_transform { + let origin = transform.apply(acadrust::types::Vector3::ZERO); + let vectors = [ + transform.apply_rotation(acadrust::types::Vector3::UNIT_X), + transform.apply_rotation(acadrust::types::Vector3::UNIT_Y), + transform.apply_rotation(acadrust::types::Vector3::UNIT_Z), + ]; + transformed = cadkernel::brep::mesh::transform_silhouette_affine( + &generator.source, + vectors.map(|vector| [vector.x, vector.y, vector.z]), + [origin.x, origin.y, origin.z], + ); + let Some(transformed) = transformed.as_ref() else { + continue; + }; + transformed + } else { + &generator.source + }; + let points = cadkernel::brep::mesh::silhouette( + silhouette_source, [view.x, view.y, view.z], - ) { + ); + source_vertex_count += points.len(); + for point in points { verts.push(mk(glam::DVec3::from_array(point))); + if verts.len() == max_vertices { + push_chunk(&verts, &mut chunks); + verts.clear(); + } } } + push_chunk(&verts, &mut chunks); } - if verts.is_empty() { - self.silhouette_vbuf = None; - self.silhouette_vcount = 0; - return; + self.silhouette_chunks = chunks; + if let Some(started) = perf_started { + crate::perf_record!( + "[perf] silhouettes {:>7.1}ms sets={} sources={} source_vertices={} chunks={}", + started.elapsed().as_secs_f64() * 1000.0, + sets.len(), + source_count, + source_vertex_count, + self.silhouette_chunks.len(), + ); } - use wgpu::util::DeviceExt; - self.silhouette_vbuf = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("mesh.silhouette.vbuf"), - contents: bytemuck::cast_slice(&verts), - usage: wgpu::BufferUsages::VERTEX, - })); - self.silhouette_vcount = verts.len() as u32; } /// Upload this content viewport's non-rectangular clip boundary as a @@ -2496,16 +3009,15 @@ impl Pipeline { meshes: &[MeshLodSet], ) { let started = iced::time::Instant::now(); - let (mut chunks, triangles) = mesh_gpu::build_mesh_batch(device, meshes); - for chunk in &mut chunks { - chunk.material_bind_group = Some(mesh_gpu::create_material_bind_group( - device, - queue, - &self.mesh_material_bgl, - chunk.material.as_ref(), - Some(&chunk.instance_buffer), - )); - } + let (mut chunks, triangles) = mesh_gpu::build_mesh_batch(device, queue, meshes); + let built_at = iced::time::Instant::now(); + mesh_gpu::upload_chunk_material_bind_groups( + device, + queue, + &self.mesh_material_bgl, + &mut chunks, + ); + let materials_at = iced::time::Instant::now(); self.mesh_ranges_by_handle.clear(); for (chunk_index, chunk) in chunks.iter().enumerate() { for range in &chunk.highlight_ranges { @@ -2529,7 +3041,9 @@ impl Pipeline { self.mesh_disabled_chunks.clear(); self.mesh_dynamic_handles.clear(); self.gpu_mesh_batch = chunks; + let indexed_at = iced::time::Instant::now(); self.rebuild_mesh_cull_resources(device); + let culled_at = iced::time::Instant::now(); if crate::perf::enabled() { let instances: u64 = self .gpu_mesh_batch @@ -2537,8 +3051,12 @@ impl Pipeline { .map(|chunk| chunk.instance_count as u64) .sum(); crate::perf_record!( - "[perf] mesh-batch {:>7.1}ms sets={} chunks={} instances={} triangles={}", + "[perf] mesh-batch {:>7.1}ms build={:.1} material={:.1} index={:.1} cull={:.1} sets={} chunks={} instances={} triangles={}", started.elapsed().as_secs_f64() * 1000.0, + built_at.duration_since(started).as_secs_f64() * 1000.0, + materials_at.duration_since(built_at).as_secs_f64() * 1000.0, + indexed_at.duration_since(materials_at).as_secs_f64() * 1000.0, + culled_at.duration_since(indexed_at).as_secs_f64() * 1000.0, meshes.len(), self.gpu_mesh_batch.len(), instances, @@ -2779,18 +3297,16 @@ impl Pipeline { } let (mut chunks, _) = mesh_gpu::build_mesh_batch_filtered( device, + queue, meshes, Some(&self.mesh_dynamic_handles), ); - for chunk in &mut chunks { - chunk.material_bind_group = Some(mesh_gpu::create_material_bind_group( - device, - queue, - &self.mesh_material_bgl, - chunk.material.as_ref(), - Some(&chunk.instance_buffer), - )); - } + mesh_gpu::upload_chunk_material_bind_groups( + device, + queue, + &self.mesh_material_bgl, + &mut chunks, + ); self.gpu_mesh_dynamic = chunks; self.rebuild_mesh_cull_resources(device); self.rebuild_mesh_range_map(); @@ -2925,11 +3441,12 @@ impl Pipeline { } pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) { - self.gpu_wipeouts = wipeouts + let renderable: Vec = wipeouts .iter() .filter(|h| h.boundary.len() >= 3) - .map(|h| WipeoutGpu::new(device, h, &self.wipeout_bgl1)) + .cloned() .collect(); + self.gpu_wipeouts = WipeoutGpu::from_models(device, &renderable, &self.wipeout_bgl1); } pub fn upload_images( @@ -2938,10 +3455,7 @@ impl Pipeline { queue: &wgpu::Queue, images: &[ImageModel], ) { - self.gpu_images = images - .iter() - .filter_map(|m| ImageGpu::new(device, queue, m, &self.image_bgl1)) - .collect(); + self.gpu_images = ImageGpu::from_models(device, queue, images, &self.image_bgl1); } /// Upload the frame's SDF text-quad vertices, and (re)build the GPU glyph @@ -2952,6 +3466,8 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, verts: &[text_gpu::TextVertex], + wires: &[WireModel], + depth_map: &rustc_hash::FxHashMap, ) { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() { @@ -2967,6 +3483,7 @@ impl Pipeline { } self.text_vbuf = text_gpu::upload_vertices(device, verts); self.text_vcount = verts.len() as u32; + self.block_text_gpu = text_gpu::upload_block_vertices(device, wires, depth_map); if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 1.0 { @@ -3299,7 +3816,8 @@ impl Pipeline { for img in self.gpu_images.iter() { pass.set_bind_group(1, &img.bind_group, &[]); pass.set_vertex_buffer(0, img.vertex_buffer.slice(..)); - pass.draw(0..img.vertex_count, 0..1); + pass.set_vertex_buffer(1, img.instance_buffer.slice(..)); + pass.draw(0..img.vertex_count, 0..img.instance_count); } } @@ -3468,10 +3986,11 @@ impl Pipeline { } } } - // DISPSILH silhouettes (whole batch, one buffer). - if let Some(ref vb) = self.silhouette_vbuf { - pass.set_vertex_buffer(0, vb.slice(..)); - pass.draw(0..self.silhouette_vcount, 0..1); + pass.set_pipeline(&self.silhouette_black_pipeline); + for chunk in &self.silhouette_chunks { + pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..)); + pass.draw(0..chunk.vertex_count, 0..chunk.instance_count); } } else { if mesh_wireframe { @@ -3525,10 +4044,11 @@ impl Pipeline { } } } - // DISPSILH silhouettes. - if let Some(ref vb) = self.silhouette_vbuf { - pass.set_vertex_buffer(0, vb.slice(..)); - pass.draw(0..self.silhouette_vcount, 0..1); + pass.set_pipeline(&self.silhouette_pipeline); + for chunk in &self.silhouette_chunks { + pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..)); + pass.draw(0..chunk.vertex_count, 0..chunk.instance_count); } } else { // Opaque fills first (they write depth). @@ -3713,10 +4233,11 @@ impl Pipeline { } } } - // DISPSILH silhouettes over the shaded fill. - if let Some(ref vb) = self.silhouette_vbuf { - pass.set_vertex_buffer(0, vb.slice(..)); - pass.draw(0..self.silhouette_vcount, 0..1); + pass.set_pipeline(&self.silhouette_black_pipeline); + for chunk in &self.silhouette_chunks { + pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..)); + pass.draw(0..chunk.vertex_count, 0..chunk.instance_count); } } } @@ -3765,6 +4286,18 @@ impl Pipeline { pass.draw(0..c.vertex_count, 0..1); } } + if !fill.block_chunks_3d.is_empty() { + if hidden_line { + pass.set_pipeline(&self.block_face3d_depth_pipeline); + } else { + pass.set_pipeline(&self.block_face3d_pipeline); + } + for c in &fill.block_chunks_3d { + pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, c.instance_buffer.slice(..)); + pass.draw(0..c.vertex_count, 0..c.instance_count); + } + } if !fill.chunks_2d.is_empty() { pass.set_pipeline(&self.face3d_pipeline); for c in &fill.chunks_2d { @@ -3772,6 +4305,14 @@ impl Pipeline { pass.draw(0..c.vertex_count, 0..1); } } + if !fill.block_chunks_2d.is_empty() { + pass.set_pipeline(&self.block_face3d_pipeline); + for c in &fill.block_chunks_2d { + pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, c.instance_buffer.slice(..)); + pass.draw(0..c.vertex_count, 0..c.instance_count); + } + } } } @@ -3887,6 +4428,26 @@ impl Pipeline { wire.first_instance..wire.first_instance + wire.instance_count, ); } + let mut block_black_active = false; + pass.set_pipeline(&self.block_wire_pipeline); + for wire in self.gpu_block_wires.iter() { + if wire.instance_count == 0 || (!show_3d_edges && wire.is_3d_mesh_edge) { + continue; + } + let use_black = want_solid_with_edges && wire.is_3d_mesh_edge; + if use_black != block_black_active { + pass.set_pipeline(if use_black { + &self.block_wire_black_pipeline + } else { + &self.block_wire_pipeline + }); + block_black_active = use_black; + } + pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]); + pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, wire.instance_buffer.slice(..)); + pass.draw(0..wire.vertex_count, 0..wire.instance_count); + } // Live overlay wires (command preview / interim / grip drag) always // on top: the xray pipeline (depth_compare=Always, no depth write) // keeps them visible through any occluding geometry — a 3D solid, or @@ -3914,9 +4475,10 @@ impl Pipeline { // overlay, after wipeouts, so normal text cannot hide its own tint. if let Some(atlas) = &self.text_atlas_gpu { let have_base = self.text_vbuf.is_some() && self.text_vcount > 0; + let have_blocks = !self.block_text_gpu.is_empty(); let have_preview = self.text_preview_vbuf.is_some() && self.text_preview_vcount > 0; - if have_base || have_preview { + if have_base || have_blocks || have_preview { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("text.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -3951,9 +4513,18 @@ impl Pipeline { pass.draw(0..self.text_vcount, 0..1); } } + if have_blocks { + pass.set_pipeline(&self.block_text_pipeline); + for text in &self.block_text_gpu { + pass.set_vertex_buffer(0, text.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, text.instance_buffer.slice(..)); + pass.draw(0..text.vertex_count, 0..text.instance_count); + } + } // Grip-drag / command-preview glyphs, drawn over the base text. if let Some(pbuf) = &self.text_preview_vbuf { if self.text_preview_vcount > 0 { + pass.set_pipeline(&self.text_pipeline); pass.set_vertex_buffer(0, pbuf.slice(..)); pass.draw(0..self.text_preview_vcount, 0..1); } @@ -3996,7 +4567,8 @@ impl Pipeline { } pass.set_bind_group(1, &wipeout.bind_group, &[]); pass.set_vertex_buffer(0, wipeout.vertex_buffer.slice(..)); - pass.draw(0..6, 0..1); + pass.set_vertex_buffer(1, wipeout.instance_buffer.slice(..)); + pass.draw(0..6, 0..wipeout.instance_count); } } @@ -4004,8 +4576,12 @@ impl Pipeline { // Redraws selected wires and text with depth_compare=Always so both // appear on top of all other geometry at full brightness. let have_text_highlight = - self.text_highlight_vbuf.is_some() && self.text_highlight_vcount > 0; - if !self.gpu_selected_wires.is_empty() || have_text_highlight { + (self.text_highlight_vbuf.is_some() && self.text_highlight_vcount > 0) + || !self.block_text_highlight_gpu.is_empty(); + if !self.gpu_selected_wires.is_empty() + || !self.gpu_selected_block_wires.is_empty() + || have_text_highlight + { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("selection_xray.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -4047,6 +4623,18 @@ impl Pipeline { } } } + if !self.gpu_selected_block_wires.is_empty() { + pass.set_pipeline(&self.block_wire_xray_pipeline); + for wire in &self.gpu_selected_block_wires { + if wire.instance_count == 0 { + continue; + } + pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]); + pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, wire.instance_buffer.slice(..)); + pass.draw(0..wire.vertex_count, 0..wire.instance_count); + } + } if let (Some(atlas), Some(hlbuf)) = (&self.text_atlas_gpu, &self.text_highlight_vbuf) { @@ -4057,6 +4645,17 @@ impl Pipeline { pass.draw(0..self.text_highlight_vcount, 0..1); } } + if let Some(atlas) = &self.text_atlas_gpu { + if !self.block_text_highlight_gpu.is_empty() { + pass.set_pipeline(&self.block_text_highlight_pipeline); + pass.set_bind_group(1, &atlas.bind_group, &[]); + for text in &self.block_text_highlight_gpu { + pass.set_vertex_buffer(0, text.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, text.instance_buffer.slice(..)); + pass.draw(0..text.vertex_count, 0..text.instance_count); + } + } + } } // ── Resolve MSAA → resolve texture ──────────────────────────────── @@ -4383,6 +4982,7 @@ pub struct MultiPipeline { u64, ( std::sync::Arc>, + std::sync::Arc>, std::sync::Arc>>, ), >, diff --git a/src/scene/pipeline/text_gpu.rs b/src/scene/pipeline/text_gpu.rs index 77776e980..0eb080735 100644 --- a/src/scene/pipeline/text_gpu.rs +++ b/src/scene/pipeline/text_gpu.rs @@ -68,6 +68,37 @@ impl TextVertex { } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct BlockTextInstance { + pub translation: [f32; 3], + pub translation_low: [f32; 3], + pub draw_depth: f32, + pub _pad: [f32; 3], +} + +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::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ATTRS, + } + } +} + +pub struct BlockTextGpu { + pub vertex_buffer: wgpu::Buffer, + pub instance_buffer: wgpu::Buffer, + pub vertex_count: u32, + pub instance_count: u32, +} + /// Split an f64 into the double-single (high f32, low residual f32) pair the /// shaders reconstruct relative-to-eye. pub(crate) fn split_ds(v: f64) -> (f32, f32) { @@ -261,7 +292,12 @@ pub fn create_pipelines( color_format: wgpu::TextureFormat, sample_count: u32, content_stencil: &wgpu::StencilState, -) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) { +) -> ( + wgpu::RenderPipeline, + wgpu::RenderPipeline, + wgpu::RenderPipeline, + wgpu::RenderPipeline, +) { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("text.wgsl"), source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()), @@ -311,6 +347,50 @@ pub fn create_pipelines( cache: None, }) }; + let block_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("block_text.wgsl"), + source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/block_text.wgsl").into()), + }); + let create_block = |label, depth_write_enabled, depth_compare| { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &block_shader, + entry_point: Some("vs_main"), + buffers: &[TextVertex::layout(), BlockTextInstance::layout()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &block_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: color_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled, + depth_compare, + stencil: content_stencil.clone(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) + }; ( create( "text.pipeline", @@ -322,9 +402,117 @@ pub fn create_pipelines( Some(false), Some(wgpu::CompareFunction::Always), ), + create_block( + "block_text.pipeline", + Some(true), + Some(wgpu::CompareFunction::LessEqual), + ), + create_block( + "block_text.highlight.pipeline", + Some(false), + Some(wgpu::CompareFunction::Always), + ), ) } +pub fn upload_block_vertices( + device: &wgpu::Device, + wires: &[crate::scene::model::wire_model::WireModel], + depth_map: &rustc_hash::FxHashMap, +) -> Vec { + let refs: Vec<&crate::scene::model::wire_model::WireModel> = wires.iter().collect(); + upload_block_vertex_refs(device, &refs, depth_map, None) +} + +pub fn upload_block_vertex_refs( + device: &wgpu::Device, + wires: &[&crate::scene::model::wire_model::WireModel], + depth_map: &rustc_hash::FxHashMap, + tint: Option<[f32; 4]>, +) -> Vec { + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for &wire in wires { + let Some(instance) = wire.render_instance else { + continue; + }; + if wire.text_verts.is_empty() { + continue; + } + let slot = *slots.entry(instance.source_id).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(wire); + } + let mut out = Vec::new(); + for group in groups { + let Some(&source) = group.first() else { + continue; + }; + let Some(base) = source.render_instance else { + continue; + }; + let tinted; + let vertices = if let Some(tint) = tint { + tinted = source + .text_verts + .iter() + .map(|vertex| TextVertex { + color: [tint[0], tint[1], tint[2], vertex.color[3]], + ..*vertex + }) + .collect::>(); + tinted.as_slice() + } else { + source.text_verts.as_slice() + }; + let Some(vertex_buffer) = upload_vertices(device, vertices) else { + continue; + }; + let instances: Vec = group + .iter() + .filter_map(|wire| { + let instance = wire.render_instance?; + let delta = [ + instance.translation[0] - base.translation[0], + instance.translation[1] - base.translation[1], + instance.translation[2] - base.translation[2], + ]; + let high = delta.map(|value| value as f32); + Some(BlockTextInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + draw_depth: super::wire_gpu::wire_draw_depth(wire, depth_map), + _pad: [0.0; 3], + }) + }) + .collect(); + let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9 + / std::mem::size_of::()) + .max(1); + for chunk in instances.chunks(max_instances) { + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("block_text.instances"), + contents: bytemuck::cast_slice(chunk), + usage: wgpu::BufferUsages::VERTEX, + }); + out.push(BlockTextGpu { + vertex_buffer: vertex_buffer.clone(), + instance_buffer, + vertex_count: vertices.len() as u32, + instance_count: chunk.len() as u32, + }); + } + } + out +} + /// Upload a finished vertex list; `None` if empty. pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option { if verts.is_empty() { diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 4e068c0ec..720d7e621 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -45,6 +45,29 @@ impl HatchVertex { } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct WipeoutPlacement { + pub translation: [f32; 2], + pub translation_low: [f32; 2], + pub draw_depth: f32, + pub _pad: [f32; 3], +} + +impl WipeoutPlacement { + pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() 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 }, + ], + } + } +} + // ── Uniform structs ─────────────────────────────────────────────────────── /// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned). @@ -120,6 +143,8 @@ pub struct FamilyBatchData { pub struct WipeoutGpu { pub vertex_buffer: wgpu::Buffer, + pub instance_buffer: wgpu::Buffer, + pub instance_count: u32, pub bind_group: wgpu::BindGroup, /// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the /// boundary polygon. Used by the per-frame LOD pass to skip hatches @@ -131,7 +156,33 @@ pub struct WipeoutGpu { } impl WipeoutGpu { - pub fn new(device: &wgpu::Device, model: &HatchModel, bgl1: &wgpu::BindGroupLayout) -> Self { + pub fn from_models( + device: &wgpu::Device, + models: &[HatchModel], + bgl1: &wgpu::BindGroupLayout, + ) -> Vec { + let mut slots = rustc_hash::FxHashMap::default(); + let mut groups: Vec> = Vec::new(); + for (index, model) in models.iter().enumerate() { + let key = model + .render_instance + .map(|instance| (true, instance.source_id)) + .unwrap_or((false, index as u64)); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push(Vec::new()); + slot + }); + groups[slot].push(model); + } + groups + .into_iter() + .map(|group| Self::new(device, &group, bgl1)) + .collect() + } + + fn new(device: &wgpu::Device, models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout) -> Self { + let model = models[0]; // ── Decode pattern mode ────────────────────────────────────────── let (mode, color2, grad_cos, grad_sin) = match &model.pattern { HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32), @@ -300,7 +351,7 @@ impl WipeoutGpu { (origin[0] - origin[0] as f32 as f64) as f32, (origin[1] - origin[1] as f32 as f64) as f32, ], - draw_depth: model.draw_depth, + draw_depth: 0.0, _pad: [0.0; 3], }; @@ -373,16 +424,52 @@ impl WipeoutGpu { // is in that absolute local space rather than relative to the // anchor. (Sub-pixel LOD via `aabb_below_pixel` is unaffected: // it measures a projected diagonal, invariant under translation.) + let base = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let placements: Vec = models + .iter() + .map(|model| { + let translation = model + .render_instance + .map_or([0.0; 3], |instance| instance.translation); + let delta = [translation[0] - base[0], translation[1] - base[1]]; + let high = [delta[0] as f32, delta[1] as f32]; + WipeoutPlacement { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + ], + draw_depth: model.draw_depth, + _pad: [0.0; 3], + } + }) + .collect(); + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("wipeout.instances"), + contents: bytemuck::cast_slice(&placements), + usage: wgpu::BufferUsages::VERTEX, + }); let ox = model.world_origin[0] as f32; let oy = model.world_origin[1] as f32; let world_aabb = if min_x.is_finite() && min_y.is_finite() { - [min_x + ox, min_y + oy, max_x + ox, max_y + oy] + let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY]; + for placement in &placements { + aabb[0] = aabb[0].min(min_x + ox + placement.translation[0]); + aabb[1] = aabb[1].min(min_y + oy + placement.translation[1]); + aabb[2] = aabb[2].max(max_x + ox + placement.translation[0]); + aabb[3] = aabb[3].max(max_y + oy + placement.translation[1]); + } + aabb } else { [min_x, min_y, max_x, max_y] }; Self { vertex_buffer, + instance_buffer, + instance_count: placements.len() as u32, bind_group, world_aabb, _uniform_buf, diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 01b79a616..49212ca48 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -285,6 +285,84 @@ pub struct WireGpu { pub const_bind_group: Option>, } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct BlockWireVertex { + pub pos_a: [f32; 3], + pub pos_a_low: [f32; 3], + pub pos_b: [f32; 3], + pub pos_b_low: [f32; 3], + pub distances: [f32; 2], + pub taper_ratio: [u16; 2], +} + +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::Unorm16x2 }, + ]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: ATTRS, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct BlockWireInstance { + pub translation: [f32; 3], + pub translation_low: [f32; 3], + pub depth: [f32; 2], +} + +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::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ATTRS, + } + } +} + +#[derive(Clone)] +pub struct BlockWireGpu { + pub vertex_buffer: wgpu::Buffer, + pub instance_buffer: wgpu::Buffer, + pub vertex_count: u32, + pub instance_count: u32, + pub is_3d_mesh_edge: bool, + pub const_bind_group: std::sync::Arc, +} + +pub fn block_const_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("block_wire_const.bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }) +} + /// Expand one `WireModel` into its per-segment instance stream (1 instance per /// finite segment). Pulled out so both the single-wire and batched paths share /// the same emission logic, and so the batched path can `par_iter` across @@ -563,6 +641,133 @@ pub(crate) fn wire_draw_depth( } } +impl BlockWireGpu { + pub fn from_wires( + device: &wgpu::Device, + wires: &[&WireModel], + depth_map: &rustc_hash::FxHashMap, + mesh_names: &rustc_hash::FxHashSet<&str>, + color_override: Option<[f32; 4]>, + const_bgl: &wgpu::BindGroupLayout, + ) -> Vec { + use wgpu::util::DeviceExt; + + let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = + rustc_hash::FxHashMap::default(); + let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); + for &wire in wires { + let Some(instance) = wire.render_instance else { + continue; + }; + if wire.points.len() < 2 { + continue; + } + let mesh_edge = mesh_names.contains(wire.name.as_str()); + let key = (instance.source_id, mesh_edge); + let slot = *slots.entry(key).or_insert_with(|| { + let slot = groups.len(); + groups.push((mesh_edge, Vec::new())); + slot + }); + groups[slot].1.push(wire); + } + + let mut out = Vec::new(); + for (mesh_edge, group) in groups { + let Some(&source) = group.first() else { + continue; + }; + let Some(base) = source.render_instance else { + continue; + }; + let color = color_override.unwrap_or(source.color); + let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0); + if segments.is_empty() { + continue; + } + constant.draw_depth = 0.0; + let mut vertices = Vec::with_capacity(segments.len() * 6); + for segment in segments { + let vertex = BlockWireVertex { + pos_a: segment.pos_a, + pos_a_low: segment.pos_a_low, + pos_b: segment.pos_b, + pos_b_low: segment.pos_b_low, + distances: [segment.distance_a, segment.distance_b], + taper_ratio: segment.taper_ratio, + }; + vertices.extend_from_slice(&[vertex; 6]); + } + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("block_wire.vertices"), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let const_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("block_wire.const"), + contents: bytemuck::bytes_of(&constant), + usage: wgpu::BufferUsages::UNIFORM, + }); + let const_bind_group = 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(), + }], + }, + )); + let instances: Vec = group + .iter() + .filter_map(|wire| { + let instance = wire.render_instance?; + let delta = [ + instance.translation[0] - base.translation[0], + instance.translation[1] - base.translation[1], + instance.translation[2] - base.translation[2], + ]; + let high = delta.map(|value| value as f32); + Some(BlockWireInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + depth: [ + if mesh_edge { + 0.0 + } else { + wire_draw_depth(wire, depth_map) + }, + 0.0, + ], + }) + }) + .collect(); + let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9 + / std::mem::size_of::()) + .max(1); + for chunk in instances.chunks(max_instances) { + out.push(Self { + vertex_buffer: vertex_buffer.clone(), + instance_buffer: instance_buffer_mapped( + device, + "block_wire.instances", + chunk, + ), + vertex_count: vertices.len() as u32, + instance_count: chunk.len() as u32, + is_3d_mesh_edge: mesh_edge, + const_bind_group: const_bind_group.clone(), + }); + } + } + out + } +} + /// Build the shared per-wire `WireConst` storage buffer and its bind group /// (storage path). All chunks from one build reference the same /// buffer via their global `wire_id`, so a single bind group is cloned into diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 3da1e735e..a6b42d9ea 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -150,6 +150,7 @@ impl Scene { return None; } let mut model = HatchModel { + render_instance: None, world_origin: origin, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 06b67ed0f..1c4812e62 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -314,6 +314,7 @@ pub fn apply_along( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -357,6 +358,7 @@ pub fn apply_along( depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, + render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index ce875da18..d92c79b85 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -389,6 +389,7 @@ impl shader::Primitive for Primitive { inner.hatch_lod_key = (usize::MAX, u64::MAX, 0, 0, false); inner.wipeout_lod_key = (usize::MAX, u64::MAX, 0, 0, false); inner.mesh_lod_key = (usize::MAX, u64::MAX, 0, 0); + inner.silhouette_key = (usize::MAX, u64::MAX, u64::MAX, false); inner.render_sig = u64::MAX; } // The MSAA / depth / resolve textures are always sized to the @@ -463,9 +464,13 @@ impl shader::Primitive for Primitive { continue; } let Some(vp_wires) = vp.wires.upgrade() else { + inner.render_sig = u64::MAX; + inner.skip_geometry = true; continue; }; let Some(draw_depths) = vp.draw_depths.upgrade() else { + inner.render_sig = u64::MAX; + inner.skip_geometry = true; continue; }; // Third component is the *selected-set* signature (not @@ -531,8 +536,15 @@ impl shader::Primitive for Primitive { .cached_text_source .as_ref() .map_or(true, |source| !Arc::ptr_eq(source, &vp.text_verts)) + || vp.wire_content_id != inner.cached_wire_id { - inner.upload_text(device, queue, &vp.text_verts[..]); + inner.upload_text( + device, + queue, + &vp.text_verts[..], + &vp_wires[..], + &draw_depths, + ); inner.cached_text_source = Some(Arc::clone(&vp.text_verts)); } inner.cached_fill_mode = fill_mode; @@ -607,7 +619,8 @@ impl shader::Primitive for Primitive { && (inner.wire_const_bgl.is_some() || ((vp.wire_patch.is_some() || inner.wire_arena_id != u64::MAX) - && packed_arena_owner)); + && packed_arena_owner)) + && !vp_wires.iter().any(|wire| wire.render_instance.is_some()); if use_wire_arena { use crate::scene::pipeline::wire_arena::{ self, PersistentWireArena as WireArena, @@ -824,6 +837,7 @@ impl shader::Primitive for Primitive { gpus.extend(arena.wire_gpus()); } inner.gpu_wires = std::sync::Arc::new(gpus); + inner.gpu_block_wires = std::sync::Arc::new(Vec::new()); if _patched { wire_arena::patch_handle_index( &mut inner.wire_handle_index, @@ -881,17 +895,26 @@ impl shader::Primitive for Primitive { if pipeline.wire_buffer_cache.len() > 16 { pipeline .wire_buffer_cache - .retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1); + .retain(|_, (w, b, _)| { + std::sync::Arc::strong_count(w) > 1 + || std::sync::Arc::strong_count(b) > 1 + }); } entry } }; inner.gpu_wires = built.0; - inner.wire_handle_index = built.1; + inner.gpu_block_wires = built.1; + inner.wire_handle_index = built.2; } // end !arena_served inner.cached_wire_id = vp.wire_content_id; if _perf { let gi: u32 = inner.gpu_wires.iter().map(|w| w.instance_count).sum(); + let bi: u32 = inner + .gpu_block_wires + .iter() + .map(|w| w.instance_count) + .sum(); let outcome = if !arena_served { "shared-fullupload" } else if _patched { @@ -902,11 +925,12 @@ impl shader::Primitive for Primitive { "arena-build" }; crate::perf_record!( - "[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}", + "[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={} block_instances={}", _t0.elapsed().as_secs_f64() * 1000.0, outcome, vp_wires.len(), gi, + bi, ); } } @@ -953,6 +977,7 @@ impl shader::Primitive for Primitive { &vp.selected_handles, &vp.hover_handles, &vp.annotation_context_wires, + &draw_depths, ); inner.cached_annotation_highlight_source = Some(Arc::clone(&vp.annotation_context_wires)); @@ -1007,13 +1032,21 @@ impl shader::Primitive for Primitive { vp.uniforms.eye_high[1] as f64 + vp.uniforms.eye_low[1] as f64, vp.uniforms.eye_high[2] as f64 + vp.uniforms.eye_low[2] as f64, ); - // Rebuild view-dependent silhouettes each frame when requested by - // DISPSILH or by a visual style such as HiddenLine. Only modes that - // draw edges consume them — pure shaded hides them. - if vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges) { - inner.upload_silhouettes(device, &vp.meshes[..], vp.view_dir); - } else { - inner.upload_silhouettes(device, &[], vp.view_dir); + let silhouette_enabled = + vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges); + let silhouette_key = ( + Arc::as_ptr(&vp.meshes) as usize, + vp.wire_content_id, + vp.camera_generation, + silhouette_enabled, + ); + if inner.silhouette_key != silhouette_key { + inner.upload_silhouettes( + device, + if silhouette_enabled { &vp.meshes[..] } else { &[] }, + vp.view_dir, + ); + inner.silhouette_key = silhouette_key; } inner.upload_clip_boundary(device, &vp.clip_boundary_ndc); let hatch_lod_key = ( @@ -2832,6 +2865,9 @@ impl Scene { } let mut out: Vec = Vec::new(); for w in wires { + if w.render_instance.is_some() { + continue; + } if !w.text_verts.is_empty() { // Bake the host wire's draw-order depth into its glyphs so // text layers like the rest of the entity: its own background diff --git a/src/shaders/block_face3d.wgsl b/src/shaders/block_face3d.wgsl new file mode 100644 index 000000000..e3520039d --- /dev/null +++ b/src/shaders/block_face3d.wgsl @@ -0,0 +1,48 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + _pad: vec2, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, +} +@group(0) @binding(0) var u: Uniforms; + +struct VertexIn { + @location(0) position: vec3, + @location(1) color: vec4, + @location(2) source_depth: f32, + @location(3) position_low: vec3, + @location(4) translation: vec3, + @location(5) translation_low: vec3, + @location(6) instance_depth: f32, +} + +struct VertexOut { + @builtin(position) clip_pos: vec4, + @location(0) color: vec4, +} + +const DRAW_ORDER_BIAS: f32 = 0.001; + +@vertex +fn vs_main(v: VertexIn) -> VertexOut { + var out: VertexOut; + let rel = (v.position + v.translation - u.eye_high) + + (v.position_low + v.translation_low - u.eye_low); + out.clip_pos = u.view_rot * vec4(rel, 1.0); + out.clip_pos.z = out.clip_pos.z + - (v.source_depth + v.instance_depth) * DRAW_ORDER_BIAS * out.clip_pos.w; + out.color = v.color; + return out; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 { + return in.color; +} diff --git a/src/shaders/block_text.wgsl b/src/shaders/block_text.wgsl new file mode 100644 index 000000000..f994d1115 --- /dev/null +++ b/src/shaders/block_text.wgsl @@ -0,0 +1,59 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + _pad: vec2, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, +} +@group(0) @binding(0) var u: Uniforms; +@group(1) @binding(0) var atlas_tex: texture_2d; +@group(1) @binding(1) var atlas_samp: sampler; + +struct VertIn { + @location(0) pos: vec3, + @location(1) pos_low: vec3, + @location(2) uv: vec2, + @location(3) color: vec4, + @location(4) source_depth: f32, + @location(5) translation: vec3, + @location(6) translation_low: vec3, + @location(7) instance_depth: f32, +} + +struct VertOut { + @builtin(position) clip_pos: vec4, + @location(0) uv: vec2, + @location(1) color: vec4, +} + +const DRAW_ORDER_BIAS: f32 = 0.001; + +@vertex +fn vs_main(v: VertIn) -> VertOut { + var out: VertOut; + let rel = (v.pos + v.translation - u.eye_high) + + (v.pos_low + v.translation_low - u.eye_low); + out.clip_pos = u.view_rot * vec4(rel, 1.0); + out.clip_pos.z = out.clip_pos.z + - (v.source_depth + v.instance_depth) * DRAW_ORDER_BIAS * out.clip_pos.w; + out.uv = v.uv; + out.color = v.color; + return out; +} + +@fragment +fn fs_main(in: VertOut) -> @location(0) vec4 { + let sd = textureSample(atlas_tex, atlas_samp, in.uv).r; + let aa = max(fwidth(sd), 1e-4); + let alpha = smoothstep(0.5 - aa, 0.5 + aa, sd); + if alpha <= 0.0 { + discard; + } + return vec4(in.color.rgb, in.color.a * alpha); +} diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl new file mode 100644 index 000000000..c33c9fa13 --- /dev/null +++ b/src/shaders/block_wire.wgsl @@ -0,0 +1,250 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + linetype_scale: f32, + _pad: f32, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, +} +@group(0) @binding(0) var u: Uniforms; + +struct WireConst { + color: vec4, + pat0: vec4, + pat1: vec4, + half_width: f32, + pattern_length: f32, + draw_depth: f32, + align_end: f32, + align_total: f32, + world_half_width: f32, + _pad1: f32, + _pad2: f32, +} +@group(1) @binding(0) var wire_const: WireConst; + +struct VertexIn { + @location(0) pos_a: vec3, + @location(1) pos_b: vec3, + @location(2) pos_a_low: vec3, + @location(3) pos_b_low: vec3, + @location(4) distances: vec2, + @location(5) taper_ratio: vec2, + @location(6) translation: vec3, + @location(7) translation_low: vec3, + @location(8) depth: vec2, +} + +const DRAW_ORDER_BIAS: f32 = 0.001; + +struct VertexOut { + @builtin(position) clip_pos: vec4, + @location(0) color: vec4, + @location(1) distance: f32, + @location(2) pattern_length: f32, + @location(3) pat0: vec4, + @location(4) pat1: vec4, + @location(5) @interpolate(flat) min_elem: f32, + @location(6) @interpolate(flat) align_end: f32, + @location(7) @interpolate(flat) align_total: f32, + @location(8) cap: vec2, + @location(9) @interpolate(flat) cap_ends: vec3, +} + +fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { + if taper_ratio > 0.0 { + return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); + } + if world_hw > 0.0 { + return max(world_hw / u.world_per_pixel, 0.5); + } + return select(0.5, px_hw, u.lwdisplay_enable > 0.5); +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut { + let corner = vertex_index % 6u; + let which_end_arr = array(0.0, 1.0, 1.0, 0.0, 1.0, 0.0); + let side_arr = array(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0); + let which_end = which_end_arr[corner]; + let side = side_arr[corner]; + + let rel_a = (in.pos_a + in.translation - u.eye_high) + + (in.pos_a_low + in.translation_low - u.eye_low); + let rel_b = (in.pos_b + in.translation - u.eye_high) + + (in.pos_b_low + in.translation_low - u.eye_low); + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; + let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; + let segment = screen_b - screen_a; + let segment_length = length(segment); + var direction = vec2(1.0, 0.0); + if segment_length > 1e-4 { + direction = segment / segment_length; + } + let perpendicular = vec2(-direction.y, direction.x); + let clip_position = mix(clip_a, clip_b, which_end); + let half_width_a = resolve_hw( + in.taper_ratio.x, + wire_const.world_half_width, + wire_const.half_width, + ); + let half_width_b = resolve_hw( + in.taper_ratio.y, + wire_const.world_half_width, + wire_const.half_width, + ); + let half_width = mix(half_width_a, half_width_b, which_end); + let extension = which_end * 2.0 - 1.0; + let offset_px = perpendicular * half_width * side + + direction * half_width * extension; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + let final_clip = clip_position + + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + + let scale = u.linetype_scale; + var min_element = wire_const.pattern_length * scale; + let elements = array( + wire_const.pat0.x * scale, + wire_const.pat0.y * scale, + wire_const.pat0.z * scale, + wire_const.pat0.w * scale, + wire_const.pat1.x * scale, + wire_const.pat1.y * scale, + wire_const.pat1.z * scale, + wire_const.pat1.w * scale, + ); + for (var i = 0u; i < 8u; i++) { + let value = abs(elements[i]); + if value > 0.0 && value < min_element { + min_element = value; + } + } + + var out: VertexOut; + out.clip_pos = final_clip; + out.clip_pos.z -= in.depth.x * DRAW_ORDER_BIAS * out.clip_pos.w; + out.color = wire_const.color; + out.distance = mix(in.distances.x, in.distances.y, which_end) + + extension * half_width * u.world_per_pixel; + out.pattern_length = wire_const.pattern_length * scale; + out.pat0 = wire_const.pat0 * scale; + out.pat1 = wire_const.pat1 * scale; + out.min_elem = min_element; + out.align_end = wire_const.align_end * scale; + out.align_total = wire_const.align_total; + out.cap = vec2( + which_end * segment_length + extension * half_width, + half_width * side, + ); + out.cap_ends = vec3(segment_length, half_width_a, half_width_b); + return out; +} + +fn in_dash( + distance: f32, + pattern_length: f32, + pat0: vec4, + pat1: vec4, + align_end: f32, + align_total: f32, +) -> bool { + let elements = array( + pat0.x, pat0.y, pat0.z, pat0.w, + pat1.x, pat1.y, pat1.z, pat1.w, + ); + var count = 0u; + for (var i = 0u; i < 8u; i++) { + if elements[i] != 0.0 { + count = i + 1u; + } + } + var value: f32; + if align_total > 0.0 { + if distance <= align_end || distance >= align_total - align_end { + return true; + } + var first_dash = 0.0; + for (var i = 0u; i < count; i++) { + if elements[i] > 0.0 { + first_dash = elements[i]; + break; + } + } + value = ((distance - align_end + first_dash) % pattern_length + + pattern_length) % pattern_length; + } else { + value = ((distance % pattern_length) + pattern_length) % pattern_length; + } + var position = 0.0; + let dot_half = u.world_per_pixel * 0.75; + for (var i = 0u; i < count; i++) { + let element = elements[i]; + if element == 0.0 { + let delta = abs(value - position); + if min(delta, pattern_length - delta) <= dot_half { + return true; + } + } else if element > 0.0 { + if value >= position && value < position + element { + return true; + } + position += element; + } else { + position -= element; + } + } + return false; +} + +fn clipped_cap(cap: vec2, ends: vec3) -> bool { + if cap.x < 0.0 { + return length(cap) > ends.y; + } + if cap.x > ends.x { + return length(vec2(cap.x - ends.x, cap.y)) > ends.z; + } + return false; +} + +fn visible_fragment(in: VertexOut) -> bool { + if clipped_cap(in.cap, in.cap_ends) { + return false; + } + if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel { + return in_dash( + in.distance, + in.pattern_length, + in.pat0, + in.pat1, + in.align_end, + in.align_total, + ); + } + return true; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 { + if !visible_fragment(in) { + discard; + } + let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5); + return vec4(in.color.rgb, alpha); +} + +@fragment +fn fs_black(in: VertexOut) -> @location(0) vec4 { + if !visible_fragment(in) { + discard; + } + let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5); + return vec4(0.0, 0.0, 0.0, alpha); +} diff --git a/src/shaders/hatch.wgsl b/src/shaders/hatch.wgsl index 15c24b3cc..cef1f51dc 100644 --- a/src/shaders/hatch.wgsl +++ b/src/shaders/hatch.wgsl @@ -97,6 +97,10 @@ struct LineFamily { struct VIn { @location(0) local_xy: vec2, @location(1) instance_index: u32, + @location(2) translation: vec2, + @location(3) translation_low: vec2, + @location(4) draw_depth: f32, + @location(5) visible: u32, } struct VOut { @@ -114,7 +118,7 @@ struct VOut { // frustum-culls the primitive and no fragment runs. (WGSL // forbids literal NaN so this out-of-NDC trick replaces the // usual NaN-degenerate-triangle.) - if visibility[v.instance_index] == 0u { + if visibility[v.instance_index] == 0u || v.visible == 0u { o.clip = vec4(2.0, 2.0, 2.0, 1.0); o.xz = vec2(0.0, 0.0); o.instance_index = v.instance_index; @@ -126,14 +130,15 @@ struct VOut { // against eye_high (Sterbenz); local + anchor low + (−eye_low) carry the // residual. `local` is small (boundary-relative), so adding it in the low // term keeps full precision at UTM-scale anchors. - let hi = vec3(inst.world_origin.x - u.eye_high.x, - inst.world_origin.y - u.eye_high.y, + let hi = vec3(inst.world_origin.x + v.translation.x - u.eye_high.x, + inst.world_origin.y + v.translation.y - u.eye_high.y, -u.eye_high.z); - let lo = vec3(local.x + inst.world_origin_low.x - u.eye_low.x, - local.y + inst.world_origin_low.y - u.eye_low.y, + let lo = vec3(local.x + inst.world_origin_low.x + v.translation_low.x - u.eye_low.x, + local.y + inst.world_origin_low.y + v.translation_low.y - u.eye_low.y, -u.eye_low.z); o.clip = u.view_rot * vec4(hi + lo, 1.0); - o.clip.z = o.clip.z - inst.draw_depth * DRAW_ORDER_BIAS * o.clip.w; + o.clip.z = o.clip.z + - (inst.draw_depth + v.draw_depth) * DRAW_ORDER_BIAS * o.clip.w; o.xz = local; o.instance_index = v.instance_index; return o; diff --git a/src/shaders/hatch_texture.wgsl b/src/shaders/hatch_texture.wgsl index ac13f2d2a..3a7775382 100644 --- a/src/shaders/hatch_texture.wgsl +++ b/src/shaders/hatch_texture.wgsl @@ -92,7 +92,12 @@ fn load_family(fi: u32) -> LineFamily { // ── Vertex shader (identical to wipeout.wgsl) ────────────────────────────── -struct VIn { @location(0) pos: vec3 } +struct VIn { + @location(0) pos: vec3, + @location(1) translation: vec2, + @location(2) translation_low: vec2, + @location(3) draw_depth: f32, +} struct VOut { @builtin(position) clip: vec4, @location(0) xz: vec2, @@ -102,13 +107,14 @@ struct VOut { var o: VOut; // Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz); // boundary-local v.pos + anchor low + (−eye_low) carry the residual. - let hi = vec3(h.origin.x - u.eye_high.x, - h.origin.y - u.eye_high.y, + let hi = vec3(h.origin.x + v.translation.x - u.eye_high.x, + h.origin.y + v.translation.y - u.eye_high.y, -u.eye_high.z); - let lo = vec3(v.pos.x + h.origin_low.x - u.eye_low.x, - v.pos.y + h.origin_low.y - u.eye_low.y, + let lo = vec3(v.pos.x + h.origin_low.x + v.translation_low.x - u.eye_low.x, + v.pos.y + h.origin_low.y + v.translation_low.y - u.eye_low.y, v.pos.z - u.eye_low.z); o.clip = u.view_rot * vec4(hi + lo, 1.0); + o.clip.z = o.clip.z - v.draw_depth * 0.001 * o.clip.w; o.xz = vec2(v.pos.x, v.pos.y); return o; } diff --git a/src/shaders/image.wgsl b/src/shaders/image.wgsl index 76f4dbd07..9e795a5a5 100644 --- a/src/shaders/image.wgsl +++ b/src/shaders/image.wgsl @@ -41,6 +41,9 @@ struct VertIn { @location(0) pos: vec3, @location(1) uv: vec2, @location(2) pos_low: vec3, + @location(3) translation: vec3, + @location(4) translation_low: vec3, + @location(5) draw_depth: f32, }; struct VertOut { @@ -51,9 +54,11 @@ struct VertOut { @vertex fn vs_main(in: VertIn) -> VertOut { var out: VertOut; - let rel = (in.pos - u.eye_high) + (in.pos_low - u.eye_low); + let rel = (in.pos + in.translation - u.eye_high) + + (in.pos_low + in.translation_low - u.eye_low); out.clip_pos = u.view_rot * vec4(rel, 1.0); - out.clip_pos.z = out.clip_pos.z - img_params.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w; + out.clip_pos.z = out.clip_pos.z + - (img_params.draw_depth + in.draw_depth) * DRAW_ORDER_BIAS * out.clip_pos.w; out.uv = in.uv; return out; } diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index 7cc3fe517..849b7d22f 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -77,6 +77,17 @@ struct MaterialMaps { @group(1) @binding(13) var normal_sampler: sampler; @group(1) @binding(14) var maps: MaterialMaps; +struct MeshSurface { + face_color: vec4, + material: vec4, + specular: vec4, + ambient: vec4, + advanced: vec4, + flags: vec4, +}; + +@group(1) @binding(16) var surface: MeshSurface; + struct MeshInstance { model_row_0: vec4, model_row_1: vec4, @@ -93,19 +104,8 @@ var mesh_instances: array; struct VertexIn { @location(0) position: vec3, @location(1) normal: vec3, - @location(2) color: vec4, @location(3) position_low: vec3, - // gloss, reflectivity, self illumination, luminance - @location(4) material: vec4, - // specular RGB, refraction index - @location(5) specular: vec4, @location(6) uv_diffuse: vec2, - // ambient RGB, translucence - @location(7) ambient: vec4, - // normal strength, bump scale, reflectance scale, transmittance scale - @location(8) advanced: vec4, - // illumination model, channel flags, material mode, luminance mode - @location(9) flags: vec4, @location(10) uv_specular_reflection: vec4, @location(11) uv_opacity_bump: vec4, @location(12) uv_refraction_normal: vec4, @@ -113,21 +113,15 @@ struct VertexIn { struct VertexOut { @builtin(position) clip_pos: vec4, - @location(0) color: vec4, - @location(1) normal: vec3, - @location(2) world_pos: vec3, - @location(3) material: vec4, - @location(4) specular: vec4, - @location(5) uv_diffuse: vec2, - @location(6) ambient: vec4, - @location(7) advanced: vec4, - @location(8) @interpolate(flat) flags: vec4, - @location(9) uv_specular: vec2, - @location(10) uv_reflection: vec2, - @location(11) uv_opacity: vec2, - @location(12) uv_bump: vec2, - @location(13) uv_refraction: vec2, - @location(14) uv_normal: vec2, + @location(0) normal: vec3, + @location(1) world_pos: vec3, + @location(2) uv_diffuse: vec2, + @location(3) uv_specular: vec2, + @location(4) uv_reflection: vec2, + @location(5) uv_opacity: vec2, + @location(6) uv_bump: vec2, + @location(7) uv_refraction: vec2, + @location(8) uv_normal: vec2, }; struct EdgeVertexIn { @@ -168,19 +162,13 @@ fn vs_main( let instance = mesh_instances[instance_index]; let rel = relative_position(v.position, v.position_low, instance); out.clip_pos = u.view_rot * vec4(rel, 1.0); - out.color = v.color; out.normal = normalize(vec3( dot(instance.normal_row_0.xyz, v.normal), dot(instance.normal_row_1.xyz, v.normal), dot(instance.normal_row_2.xyz, v.normal), )); out.world_pos = rel; - out.material = v.material; - out.specular = v.specular; out.uv_diffuse = v.uv_diffuse; - out.ambient = v.ambient; - out.advanced = v.advanced; - out.flags = v.flags; out.uv_specular = v.uv_specular_reflection.xy; out.uv_reflection = v.uv_specular_reflection.zw; out.uv_opacity = v.uv_opacity_bump.xy; @@ -305,7 +293,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); let strength = maps.blends1.z * map_weight(in.uv_normal, maps.tiling1.z) - * max(in.advanced.x, 0.0); + * max(surface.advanced.x, 0.0); n = normalize(mix(n, mapped, clamp(strength, 0.0, 1.0))); } else if (maps.present1.x != 0u) { let height = dot( @@ -321,7 +309,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); } - var albedo = in.color.rgb; + var albedo = surface.face_color.rgb; if (maps.present0.x != 0u) { let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb; let blend = clamp( @@ -348,7 +336,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { } } - var specular_color = select(in.specular.rgb, albedo, in.flags.x == 1u); + var specular_color = select(surface.specular.rgb, albedo, surface.flags.x == 1u); if (maps.present0.y != 0u) { let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb; let blend = clamp( @@ -359,14 +347,14 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { specular_color = mix(specular_color, texel, blend); } let view = normalize(-in.world_pos); - let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0)); + let gloss_exp = mix(2.0, 128.0, clamp(surface.material.x, 0.0, 1.0)); let fresnel0 = pow( - (max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0), + (max(surface.specular.w, 1.0) - 1.0) / (max(surface.specular.w, 1.0) + 1.0), 2.0, ); let fresnel = fresnel0 + (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0); - let specular_strength = clamp(1.0 + in.material.y + fresnel, 0.0, 1.5); + let specular_strength = clamp(1.0 + surface.material.y + fresnel, 0.0, 1.5); let viewport_highlight = select( 1.0, max(u.visual_style.z, 0.0), @@ -441,10 +429,10 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { * pow(half_response, gloss_exp) * specular_strength * viewport_highlight - * max(in.advanced.z, 0.0) + * max(surface.advanced.z, 0.0) * strength; } - let ambient_light = clamp(in.ambient.rgb, vec3(0.0), vec3(1.0)) + let ambient_light = clamp(surface.ambient.rgb, vec3(0.0), vec3(1.0)) * clamp(u.lighting.yzw, vec3(0.0), vec3(1.0)); var lit = ambient_light + albedo * clamp(direct_light, vec3(0.0), vec3(2.0)) @@ -473,7 +461,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { * specular_color * specular_strength * viewport_highlight - * max(in.advanced.z, 0.0) + * max(surface.advanced.z, 0.0) * max(u.environment_params.w, 0.0); } if (maps.present0.z != 0u) { @@ -488,8 +476,8 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { clamp( maps.blends0.z * map_weight(in.uv_reflection, maps.tiling0.z) - * in.material.y - * max(in.advanced.z, 0.0), + * surface.material.y + * max(surface.advanced.z, 0.0), 0.0, 1.0, ), @@ -507,16 +495,16 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { clamp( maps.blends1.y * map_weight(in.uv_refraction, maps.tiling1.y) - * in.ambient.a - * max(in.advanced.w, 0.0), + * surface.ambient.a + * max(surface.advanced.w, 0.0), 0.0, 1.0, ), ); } - var rgb = mix(lit, albedo, clamp(in.material.z, 0.0, 1.0)); - if (in.flags.w == 1u) { - rgb = lit + albedo * max(in.material.w, 0.0); + var rgb = mix(lit, albedo, clamp(surface.material.z, 0.0, 1.0)); + if (surface.flags.w == 1u) { + rgb = lit + albedo * max(surface.material.w, 0.0); } let contrast = 1.0 + clamp(u.view_tone.y, -1.0, 1.0); rgb = (rgb - vec3(0.5)) * contrast + vec3(0.5); @@ -533,7 +521,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { let fog_density = mix(u.fog_params.z, u.fog_params.w, fog_position); rgb = mix(rgb, u.fog_color.rgb, clamp(fog_density, 0.0, 1.0)); } - var material_alpha = in.color.a; + var material_alpha = surface.face_color.a; if (maps.present0.w != 0u) { let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r; let blend = clamp( @@ -543,7 +531,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); material_alpha *= mix(1.0, opacity, blend); } - material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95); + material_alpha *= 1.0 - clamp(surface.ambient.a * max(surface.advanced.w, 0.0), 0.0, 0.95); material_alpha *= clamp(u.visual_style.y, 0.0, 1.0); let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5); return vec4(rgb, alpha); @@ -553,7 +541,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { // wireframe and hidden-line edge passes so lines read at their true colour. @fragment fn fs_edge(in: EdgeVertexOut) -> @location(0) vec4 { - return vec4(in.color.rgb, 1.0); + return vec4(surface.face_color.rgb, 1.0); } // Edge fragment for filled render modes: force black so edges frame the shaded @@ -566,7 +554,7 @@ fn fs_edge_black(_in: EdgeVertexOut) -> @location(0) vec4 { @fragment fn fs_highlight_selected(in: VertexOut) -> @location(0) vec4 { return vec4( - mix(in.color.rgb, vec3(0.15, 0.55, 1.0), 0.60), + mix(surface.face_color.rgb, vec3(0.15, 0.55, 1.0), 0.60), 0.90, ); } @@ -574,7 +562,7 @@ fn fs_highlight_selected(in: VertexOut) -> @location(0) vec4 { @fragment fn fs_highlight_hover(in: VertexOut) -> @location(0) vec4 { return vec4( - mix(in.color.rgb, vec3(0.95, 0.55, 0.10), 0.35), + mix(surface.face_color.rgb, vec3(0.95, 0.55, 0.10), 0.35), 0.82, ); } diff --git a/src/shaders/silhouette.wgsl b/src/shaders/silhouette.wgsl new file mode 100644 index 000000000..3453ef75e --- /dev/null +++ b/src/shaders/silhouette.wgsl @@ -0,0 +1,48 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + _pad: vec2, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, +} + +@group(0) @binding(0) var u: Uniforms; + +struct VertexIn { + @location(0) position: vec3, + @location(2) color: vec4, + @location(3) position_low: vec3, + @location(4) translation: vec3, + @location(5) translation_low: vec3, +} + +struct VertexOut { + @builtin(position) clip_pos: vec4, + @location(0) color: vec4, +} + +@vertex +fn vs_main(v: VertexIn) -> VertexOut { + var out: VertexOut; + let rel = (v.position + v.translation - u.eye_high) + + (v.position_low + v.translation_low - u.eye_low); + out.clip_pos = u.view_rot * vec4(rel, 1.0); + out.color = v.color; + return out; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 { + return in.color; +} + +@fragment +fn fs_black(in: VertexOut) -> @location(0) vec4 { + return vec4(0.0, 0.0, 0.0, in.color.a); +} diff --git a/src/shaders/wipeout.wgsl b/src/shaders/wipeout.wgsl index 78c9da889..3b7d97ab3 100644 --- a/src/shaders/wipeout.wgsl +++ b/src/shaders/wipeout.wgsl @@ -89,7 +89,12 @@ struct FamilyBatch { // ── Vertex shader ────────────────────────────────────────────────────────── -struct VIn { @location(0) pos: vec3 } +struct VIn { + @location(0) pos: vec3, + @location(1) translation: vec2, + @location(2) translation_low: vec2, + @location(3) draw_depth: f32, +} struct VOut { @builtin(position) clip: vec4, @location(0) xz: vec2, @@ -107,17 +112,17 @@ struct VOut { // pattern math doesn't catastrophically cancel. // Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz); // boundary-local v.pos + anchor low + (−eye_low) carry the residual. - let hi = vec3(h.origin.x - u.eye_high.x, - h.origin.y - u.eye_high.y, + let hi = vec3(h.origin.x + v.translation.x - u.eye_high.x, + h.origin.y + v.translation.y - u.eye_high.y, -u.eye_high.z); - let lo = vec3(v.pos.x + h.origin_low.x - u.eye_low.x, - v.pos.y + h.origin_low.y - u.eye_low.y, + let lo = vec3(v.pos.x + h.origin_low.x + v.translation_low.x - u.eye_low.x, + v.pos.y + h.origin_low.y + v.translation_low.y - u.eye_low.y, v.pos.z - u.eye_low.z); o.clip = u.view_rot * vec4(hi + lo, 1.0); // Draw-order bias (see wire.wgsl): the mask erases what draws BELOW its // entity and loses to what draws above — including its own frame wire, // which rides a +half-rank override. - o.clip.z = o.clip.z - h.draw_depth * 0.001 * o.clip.w; + o.clip.z = o.clip.z - (h.draw_depth + v.draw_depth) * 0.001 * o.clip.w; o.xz = vec2(v.pos.x, v.pos.y); return o; } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 631676fee..fda11b9cc 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -121,6 +121,7 @@ impl canvas::Program for HatchPatternPreview { } HatchPattern::Pattern(_) => { let model = HatchModel { + render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![ [pad, pad],