From 4fd9b98da4eeada8def3b0b409e274e2783a4d6a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 12 Aug 2026 02:33:04 +0300 Subject: [PATCH] perf(render): instance block geometry Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive. --- src/app/update/viewport.rs | 1 + src/entities/dimension.rs | 5 + src/entities/leader.rs | 2 + src/entities/multileader.rs | 5 + src/entities/table.rs | 1 + src/modules/annotate/aligned_dim.rs | 2 + src/modules/annotate/angular_dim.rs | 1 + src/modules/annotate/diameter_dim.rs | 1 + src/modules/annotate/dim_baseline.rs | 1 + src/modules/annotate/dim_continue.rs | 1 + src/modules/annotate/dimjogline.rs | 1 + src/modules/annotate/dimtedit.rs | 1 + src/modules/annotate/leader_cmd.rs | 1 + src/modules/annotate/linear_dim.rs | 1 + src/modules/annotate/mleader_cmd.rs | 1 + src/modules/annotate/mleader_edit.rs | 1 + src/modules/annotate/ordinate_dim.rs | 1 + src/modules/annotate/qdim.rs | 1 + src/modules/annotate/radius_dim.rs | 1 + src/modules/annotate/table_cmd.rs | 1 + src/modules/annotate/tolerance_cmd.rs | 1 + src/modules/draw/draw/attdef.rs | 1 + src/modules/draw/draw/hatch.rs | 3 + src/modules/draw/draw/mline.rs | 1 + src/modules/draw/draw/raster_image.rs | 1 + src/modules/draw/draw/ray.rs | 2 + src/modules/draw/draw/revcloud.rs | 1 + src/modules/draw/inquiry/area.rs | 1 + src/modules/draw/inquiry/dist.rs | 1 + src/modules/draw/inquiry/measuregeom.rs | 1 + src/modules/draw/modify/trim.rs | 1 + src/modules/model/primitive_cmd.rs | 1 + src/modules/view/plot_window.rs | 1 + src/modules/view/zoom_window.rs | 1 + src/scene/cache/block_cache.rs | 324 ++++++-- src/scene/convert/tess.rs | 14 + src/scene/convert/tessellate.rs | 16 + src/scene/entity.rs | 60 ++ src/scene/mod.rs | 36 +- src/scene/model/hatch_model.rs | 1 + src/scene/model/image_model.rs | 4 + src/scene/model/instance_model.rs | 13 + src/scene/model/mod.rs | 1 + src/scene/model/wire_model.rs | 4 + src/scene/pick/xclip.rs | 1 + src/scene/pipeline/face3d_gpu.rs | 176 +++++ src/scene/pipeline/hatch_gpu/mod.rs | 50 +- src/scene/pipeline/hatch_gpu/storage.rs | 153 +++- src/scene/pipeline/hatch_gpu/texture.rs | 83 +- src/scene/pipeline/image_gpu.rs | 93 ++- src/scene/pipeline/mesh_gpu.rs | 960 ++++++++++++++++++------ src/scene/pipeline/mod.rs | 818 +++++++++++++++++--- src/scene/pipeline/text_gpu.rs | 190 ++++- src/scene/pipeline/wipeout_gpu.rs | 93 ++- src/scene/pipeline/wire_gpu.rs | 205 +++++ src/scene/preview.rs | 1 + src/scene/text/complex_lt.rs | 2 + src/scene/view/render.rs | 60 +- src/shaders/block_face3d.wgsl | 48 ++ src/shaders/block_text.wgsl | 59 ++ src/shaders/block_wire.wgsl | 250 ++++++ src/shaders/hatch.wgsl | 17 +- src/shaders/hatch_texture.wgsl | 16 +- src/shaders/image.wgsl | 9 +- src/shaders/mesh.wgsl | 94 +-- src/shaders/silhouette.wgsl | 48 ++ src/shaders/wipeout.wgsl | 17 +- src/ui/properties.rs | 1 + 68 files changed, 3417 insertions(+), 547 deletions(-) create mode 100644 src/scene/model/instance_model.rs create mode 100644 src/shaders/block_face3d.wgsl create mode 100644 src/shaders/block_text.wgsl create mode 100644 src/shaders/block_wire.wgsl create mode 100644 src/shaders/silhouette.wgsl 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],