1#![allow(clippy::many_single_char_names)]
21use crate::gpu::batch::{
67 build_circle_batch, build_fill_batch, build_fill_extrusion_batch,
68 build_fill_pattern_batch, build_heatmap_batch, build_hillshade_batches,
69 build_line_batch, build_line_pattern_batch, build_placeholder_batches,
70 build_symbol_batch, build_terrain_batches, build_tile_batches,
71 build_vector_batch, find_terrain_texture_actual, CircleBatchEntry,
72 FillBatchEntry, FillExtrusionBatchEntry, FillPatternBatchEntry,
73 HeatmapBatchEntry, HillshadeBatch, LineBatchEntry,
74 LinePatternBatchEntry, SymbolBatchEntry, TerrainBatch, TilePageBatches,
75 VectorBatchEntry,
76};
77use crate::gpu::depth::create_depth_texture;
78use crate::gpu::column_vertex::{ColumnInstanceData, ColumnVertex};
79use crate::gpu::grid_extrusion_vertex::GridExtrusionVertex;
80use crate::gpu::grid_scalar_vertex::GridScalarVertex;
81use crate::gpu::image_overlay_vertex::ImageOverlayVertex;
82use crate::gpu::model_vertex::ModelVertex;
83use crate::gpu::terrain_buffers::TerrainInteractionBuffers;
84use crate::gpu::terrain_grid_vertex::TerrainGridVertex;
85use crate::gpu::tile_atlas::TileAtlas;
86use crate::painter::{PainterPass, PainterPlan};
87use crate::pipeline::circle_pipeline::CirclePipeline;
88use crate::pipeline::column_pipeline::ColumnPipeline;
89use crate::pipeline::fill_extrusion_pipeline::FillExtrusionPipeline;
90use crate::pipeline::fill_pattern_pipeline::FillPatternPipeline;
91use crate::pipeline::fill_pipeline::FillPipeline;
92use crate::pipeline::grid_scalar_pipeline::GridScalarPipeline;
93use crate::pipeline::grid_extrusion_pipeline::GridExtrusionPipeline;
94use crate::pipeline::heatmap_colormap_pipeline::HeatmapColormapPipeline;
95use crate::pipeline::heatmap_pipeline::HeatmapPipeline;
96use crate::pipeline::hillshade_pipeline::HillshadePipeline;
97use crate::pipeline::image_overlay_pipeline::ImageOverlayPipeline;
98use crate::pipeline::line_pipeline::LinePipeline;
99use crate::pipeline::line_pattern_pipeline::LinePatternPipeline;
100use crate::pipeline::model_pipeline::ModelPipeline;
101use crate::pipeline::symbol_pipeline::SymbolPipeline;
102use crate::pipeline::terrain_data_pipeline::TerrainDataPipeline;
103use crate::pipeline::terrain_pipeline::TerrainPipeline;
104use crate::pipeline::tile_pipeline::TilePipeline;
105use crate::pipeline::uniforms::ViewProjUniform;
106use crate::pipeline::vector_pipeline::VectorPipeline;
107use glam::{DVec3, Mat4};
108use rustial_engine::{
109 materialize_terrain_mesh, DecodedImage, LayerId, MapState, ModelInstance, TerrainMeshData,
110 TileData, VectorMeshData, VectorRenderMode, VisibleTile, VisualizationOverlay,
111};
112use rustial_engine as rustial_math;
113use rustial_engine::TileId;
114use std::sync::Arc;
115use wgpu::util::DeviceExt;
116
117#[repr(C)]
118#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
119struct TerrainTileUniform {
120 geo_bounds: [f32; 4],
121 scene_origin: [f32; 4],
122 elev_params: [f32; 4],
123 elev_region: [f32; 4],
124}
125
126struct SharedTerrainGridMesh {
127 vertex_buffer: wgpu::Buffer,
128 index_buffer: wgpu::Buffer,
129 index_count: u32,
130}
131
132struct CachedHeightTexture {
133 generation: u64,
134 view: wgpu::TextureView,
135}
136
137#[repr(C)]
138#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
139struct GridScalarUniform {
140 origin_counts: [f32; 4],
141 grid_params: [f32; 4],
142 scene_origin: [f32; 4],
143 value_params: [f32; 4],
144 base_altitude: [f32; 4],
145}
146
147struct SharedColumnMesh {
148 vertex_buffer: wgpu::Buffer,
149 index_buffer: wgpu::Buffer,
150 index_count: u32,
151}
152
153struct CachedGridScalarOverlay {
154 vertex_buffer: wgpu::Buffer,
155 index_buffer: wgpu::Buffer,
156 index_count: u32,
157 vertex_count: usize,
158 #[allow(dead_code)]
159 uniform_buffer: wgpu::Buffer,
160 bind_group: wgpu::BindGroup,
161 #[allow(dead_code)]
162 scalar_texture: wgpu::Texture,
163 #[allow(dead_code)]
164 ramp_texture: wgpu::Texture,
165 generation: u64,
166 value_generation: u64,
167 ramp_fingerprint: u64,
168 grid_fingerprint: u64,
169 terrain_fingerprint: u64,
170 projection: rustial_engine::CameraProjection,
171 origin_key: [i64; 3],
172}
173
174struct CachedGridExtrusionOverlay {
175 vertex_buffer: wgpu::Buffer,
176 index_buffer: wgpu::Buffer,
177 index_count: u32,
178 vertex_count: usize,
179 generation: u64,
180 value_generation: u64,
181 origin_key: [i64; 3],
182 grid_fingerprint: u64,
183 params_fingerprint: u64,
184 ramp_fingerprint: u64,
185 terrain_fingerprint: u64,
186}
187
188struct CachedColumnOverlay {
189 instance_buffer: wgpu::Buffer,
190 instance_count: u32,
191 generation: u64,
192 origin_key: [i64; 3],
193 columns_fingerprint: u64,
194 ramp_fingerprint: u64,
195 instance_data: Vec<ColumnInstanceData>,
196}
197
198struct CachedPointCloudOverlay {
199 instance_buffer: wgpu::Buffer,
200 instance_count: u32,
201 generation: u64,
202 origin_key: [i64; 3],
203 points_fingerprint: u64,
204 ramp_fingerprint: u64,
205 instance_data: Vec<ColumnInstanceData>,
206}
207
208#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
210pub struct VisualizationPerfStats {
211 pub grid_scalar_rebuilds: u32,
213 pub grid_scalar_value_updates: u32,
215 pub grid_extrusion_rebuilds: u32,
217 pub grid_extrusion_value_updates: u32,
219 pub column_rebuilds: u32,
221 pub column_partial_writes: u32,
223 pub column_partial_write_ranges: u32,
225 pub point_cloud_rebuilds: u32,
227 pub point_cloud_partial_writes: u32,
229 pub point_cloud_partial_write_ranges: u32,
231}
232
233struct CachedTerrainTileBind {
240 #[allow(dead_code)]
241 uniform_buffer: wgpu::Buffer,
242 bind_group: wgpu::BindGroup,
243 origin_key: [i64; 3],
245 generation: u64,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251struct TerrainTileBindKey {
252 tile: TileId,
253 pipeline: TerrainPipelineKind,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
259enum TerrainPipelineKind {
260 Terrain,
261 TerrainData,
262 Hillshade,
263}
264
265struct TerrainDataDirtyState {
271 dirty: bool,
273 last_vp: [f32; 16],
276 last_terrain_fingerprint: u64,
278}
279
280impl Default for TerrainDataDirtyState {
281 fn default() -> Self {
282 Self {
283 dirty: true,
284 last_vp: [0.0; 16],
285 last_terrain_fingerprint: 0,
286 }
287 }
288}
289
290impl TerrainDataDirtyState {
291 fn needs_update(
293 &self,
294 vp: &glam::DMat4,
295 terrain_meshes: &[TerrainMeshData],
296 ) -> bool {
297 if self.dirty {
298 return true;
299 }
300 let vp_f32 = vp.to_cols_array().map(|v| v as f32);
301 if vp_f32 != self.last_vp {
302 return true;
303 }
304 let fp = Self::terrain_fingerprint(terrain_meshes);
305 fp != self.last_terrain_fingerprint
306 }
307
308 fn mark_clean(
310 &mut self,
311 vp: &glam::DMat4,
312 terrain_meshes: &[TerrainMeshData],
313 ) {
314 self.dirty = false;
315 self.last_vp = vp.to_cols_array().map(|v| v as f32);
316 self.last_terrain_fingerprint = Self::terrain_fingerprint(terrain_meshes);
317 }
318
319 fn terrain_fingerprint(terrain_meshes: &[TerrainMeshData]) -> u64 {
320 let mut h: u64 = terrain_meshes.len() as u64;
321 for mesh in terrain_meshes {
322 h = h
323 .wrapping_mul(31)
324 .wrapping_add(mesh.tile.zoom as u64)
325 .wrapping_mul(31)
326 .wrapping_add(mesh.tile.x as u64)
327 .wrapping_mul(31)
328 .wrapping_add(mesh.tile.y as u64)
329 .wrapping_mul(31)
330 .wrapping_add(mesh.generation);
331 }
332 h
333 }
334}
335
336fn diff_column_instance_ranges(
337 old: &[ColumnInstanceData],
338 new: &[ColumnInstanceData],
339) -> Vec<std::ops::Range<usize>> {
340 if old.len() != new.len() {
341 return if new.is_empty() { Vec::new() } else { vec![0..new.len()] };
342 }
343
344 let mut ranges = Vec::new();
345 let mut current_start: Option<usize> = None;
346
347 for (index, (old_item, new_item)) in old.iter().zip(new.iter()).enumerate() {
348 if old_item != new_item {
349 if current_start.is_none() {
350 current_start = Some(index);
351 }
352 } else if let Some(start) = current_start.take() {
353 ranges.push(start..index);
354 }
355 }
356
357 if let Some(start) = current_start {
358 ranges.push(start..new.len());
359 }
360
361 ranges
362}
363
364#[derive(Debug, Clone, PartialEq)]
368struct TileBatchCacheKey {
369 tiles: Vec<(TileId, TileId, u32)>,
376 origin: [i64; 3],
378 projection: rustial_engine::CameraProjection,
380}
381
382impl TileBatchCacheKey {
383 fn new(
384 visible_tiles: &[VisibleTile],
385 camera_origin: DVec3,
386 projection: rustial_engine::CameraProjection,
387 ) -> Self {
388 let tiles: Vec<(TileId, TileId, u32)> = visible_tiles
389 .iter()
390 .map(|vt| (vt.target, vt.actual, vt.fade_opacity.to_bits()))
391 .collect();
392 let origin = [
393 (camera_origin.x * 100.0) as i64,
394 (camera_origin.y * 100.0) as i64,
395 (camera_origin.z * 100.0) as i64,
396 ];
397 Self { tiles, origin, projection }
398 }
399}
400
401#[derive(Debug, Clone, PartialEq)]
404struct VectorBatchCacheKey {
405 layers: Vec<(usize, usize)>,
407 origin: [i64; 3],
409}
410
411impl VectorBatchCacheKey {
412 fn new(vector_meshes: &[VectorMeshData], camera_origin: DVec3) -> Self {
413 let layers: Vec<(usize, usize)> = vector_meshes
414 .iter()
415 .map(|m| (m.positions.len(), m.indices.len()))
416 .collect();
417 let origin = [
418 (camera_origin.x * 100.0) as i64,
419 (camera_origin.y * 100.0) as i64,
420 (camera_origin.z * 100.0) as i64,
421 ];
422 Self { layers, origin }
423 }
424}
425
426pub struct RenderParams<'a> {
435 pub state: &'a MapState,
437 pub device: &'a wgpu::Device,
439 pub queue: &'a wgpu::Queue,
441 pub color_view: &'a wgpu::TextureView,
443 pub visible_tiles: &'a [VisibleTile],
445 pub vector_meshes: &'a [VectorMeshData],
447 pub model_instances: &'a [ModelInstance],
449 pub clear_color: [f32; 4],
453}
454
455pub struct WgpuMapRenderer {
489 tile_pipeline: TilePipeline,
491 terrain_pipeline: TerrainPipeline,
492 terrain_data_pipeline: TerrainDataPipeline,
493 hillshade_pipeline: HillshadePipeline,
494 grid_scalar_pipeline: GridScalarPipeline,
495 grid_extrusion_pipeline: GridExtrusionPipeline,
496 column_pipeline: ColumnPipeline,
497 vector_pipeline: VectorPipeline,
498 fill_pipeline: FillPipeline,
499 fill_pattern_pipeline: FillPatternPipeline,
500 fill_extrusion_pipeline: FillExtrusionPipeline,
501 line_pipeline: LinePipeline,
502 line_pattern_pipeline: LinePatternPipeline,
503 circle_pipeline: CirclePipeline,
504 heatmap_pipeline: HeatmapPipeline,
505 heatmap_colormap_pipeline: HeatmapColormapPipeline,
507 symbol_pipeline: SymbolPipeline,
508 model_pipeline: ModelPipeline,
509 image_overlay_pipeline: ImageOverlayPipeline,
510
511 uniform_buffer: wgpu::Buffer,
516 uniform_bind_group: wgpu::BindGroup,
518 terrain_uniform_bind_group: wgpu::BindGroup,
520 terrain_data_uniform_bind_group: wgpu::BindGroup,
522 hillshade_uniform_bind_group: wgpu::BindGroup,
524 grid_scalar_uniform_bind_group: wgpu::BindGroup,
526 grid_extrusion_uniform_bind_group: wgpu::BindGroup,
528 column_uniform_bind_group: wgpu::BindGroup,
530 vector_uniform_bind_group: wgpu::BindGroup,
532 fill_extrusion_uniform_bind_group: wgpu::BindGroup,
534 model_uniform_bind_group: wgpu::BindGroup,
536 line_uniform_bind_group: wgpu::BindGroup,
538 circle_uniform_bind_group: wgpu::BindGroup,
540 heatmap_uniform_bind_group: wgpu::BindGroup,
542 heatmap_colormap_uniform_bind_group: wgpu::BindGroup,
544 heatmap_accum_texture: wgpu::Texture,
546 heatmap_accum_view: wgpu::TextureView,
548 _heatmap_ramp_texture: wgpu::Texture,
550 heatmap_ramp_view: wgpu::TextureView,
552 heatmap_colormap_textures_bind_group: wgpu::BindGroup,
554 symbol_uniform_bind_group: wgpu::BindGroup,
556 image_overlay_uniform_bind_group: wgpu::BindGroup,
558
559 sampler: wgpu::Sampler,
563 grid_scalar_ramp_sampler: wgpu::Sampler,
565 fill_pattern_sampler: wgpu::Sampler,
567
568 depth_view: wgpu::TextureView,
571 width: u32,
573 height: u32,
575 terrain_interaction_buffers: TerrainInteractionBuffers,
577
578 tile_atlas: TileAtlas,
581 hillshade_atlas: TileAtlas,
583 page_bind_groups: Vec<wgpu::BindGroup>,
587 page_terrain_bind_groups: Vec<wgpu::BindGroup>,
589 page_hillshade_bind_groups: Vec<wgpu::BindGroup>,
591
592 model_mesh_cache: std::collections::HashMap<ModelMeshKey, CachedModelMesh>,
596 shared_terrain_grids: std::collections::HashMap<u16, SharedTerrainGridMesh>,
598 height_texture_cache: std::collections::HashMap<TileId, CachedHeightTexture>,
600 shared_column_mesh: Option<SharedColumnMesh>,
602 grid_scalar_overlay_cache: std::collections::HashMap<LayerId, CachedGridScalarOverlay>,
604 grid_extrusion_overlay_cache: std::collections::HashMap<LayerId, CachedGridExtrusionOverlay>,
606 column_overlay_cache: std::collections::HashMap<LayerId, CachedColumnOverlay>,
608 point_cloud_overlay_cache: std::collections::HashMap<LayerId, CachedPointCloudOverlay>,
610
611 cached_tile_batches: Vec<TilePageBatches>,
614 tile_batch_cache_key: Option<TileBatchCacheKey>,
616 cached_vector_batches: Vec<Option<VectorBatchEntry>>,
618 vector_batch_cache_key: Option<VectorBatchCacheKey>,
620 cached_fill_extrusion_batches: Vec<Option<FillExtrusionBatchEntry>>,
622 cached_fill_batches: Vec<Option<FillBatchEntry>>,
624 cached_fill_pattern_batches: Vec<Option<FillPatternBatchEntry>>,
626 cached_line_batches: Vec<Option<LineBatchEntry>>,
628 cached_line_pattern_batches: Vec<Option<LinePatternBatchEntry>>,
630 cached_circle_batches: Vec<Option<CircleBatchEntry>>,
632 cached_heatmap_batches: Vec<Option<HeatmapBatchEntry>>,
634 cached_symbol_batch: Option<SymbolBatchEntry>,
636 symbol_atlas_texture: Option<(wgpu::Texture, wgpu::TextureView)>,
638 symbol_atlas_bind_group: Option<wgpu::BindGroup>,
640 symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas,
642 symbol_glyph_provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
644
645 terrain_tile_bind_cache: std::collections::HashMap<TerrainTileBindKey, CachedTerrainTileBind>,
648
649 terrain_data_dirty: TerrainDataDirtyState,
653
654 cached_model_transforms: Option<CachedModelTransforms>,
657 cached_placeholder_batch: Option<VectorBatchEntry>,
659 cached_image_overlay_batches: Vec<CachedImageOverlayBatch>,
661 visualization_perf_stats: VisualizationPerfStats,
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
681struct ModelMeshKey {
682 pos_len: usize,
683 idx_len: usize,
684 fingerprint: u64,
685}
686
687impl ModelMeshKey {
688 fn from_mesh(mesh: &rustial_engine::ModelMesh) -> Self {
689 let mut fingerprint: u64 = mesh.positions.len() as u64;
690 if let Some(first) = mesh.positions.first() {
691 fingerprint = fingerprint
692 .wrapping_mul(31)
693 .wrapping_add(first[0].to_bits() as u64)
694 .wrapping_mul(31)
695 .wrapping_add(first[1].to_bits() as u64)
696 .wrapping_mul(31)
697 .wrapping_add(first[2].to_bits() as u64);
698 }
699 if let Some(&first_idx) = mesh.indices.first() {
700 fingerprint = fingerprint.wrapping_mul(31).wrapping_add(first_idx as u64);
701 }
702 Self {
703 pos_len: mesh.positions.len(),
704 idx_len: mesh.indices.len(),
705 fingerprint,
706 }
707 }
708}
709
710struct CachedModelMesh {
712 vertex_buffer: wgpu::Buffer,
713 index_buffer: wgpu::Buffer,
714 index_count: u32,
715}
716
717struct CachedModelTransforms {
722 #[allow(dead_code)]
723 buffer: wgpu::Buffer,
724 bind_group: wgpu::BindGroup,
725 stride: usize,
727 instance_count: usize,
729 fingerprint: u64,
732}
733
734struct CachedImageOverlayBatch {
736 vertex_buffer: wgpu::Buffer,
737 index_buffer: wgpu::Buffer,
738 texture: wgpu::Texture,
739 #[allow(dead_code)]
740 texture_view: wgpu::TextureView,
741 texture_bind_group: wgpu::BindGroup,
742 layer_id: rustial_engine::LayerId,
744 tex_dimensions: (u32, u32),
746 data_arc_ptr: usize,
748}
749
750impl WgpuMapRenderer {
755 pub fn new(
765 device: &wgpu::Device,
766 _queue: &wgpu::Queue,
767 format: wgpu::TextureFormat,
768 width: u32,
769 height: u32,
770 ) -> Self {
771 let tile_pipeline = TilePipeline::new(device, format);
772 let terrain_pipeline =
773 TerrainPipeline::new(device, format, &tile_pipeline.uniform_bind_group_layout);
774 let terrain_data_pipeline = TerrainDataPipeline::new(device);
775 let hillshade_pipeline = HillshadePipeline::new(device, format);
776 let grid_scalar_pipeline = GridScalarPipeline::new(device, format);
777 let grid_extrusion_pipeline = GridExtrusionPipeline::new(device, format);
778 let column_pipeline = ColumnPipeline::new(device, format);
779 let vector_pipeline = VectorPipeline::new(device, format);
780 let fill_pipeline = FillPipeline::new(device, format);
781 let fill_pattern_pipeline = FillPatternPipeline::new(device, format);
782 let fill_extrusion_pipeline = FillExtrusionPipeline::new(device, format);
783 let line_pipeline = LinePipeline::new(device, format);
784 let line_pattern_pipeline = LinePatternPipeline::new(device, format);
785 let circle_pipeline = CirclePipeline::new(device, format);
786 let heatmap_pipeline = HeatmapPipeline::new(device);
787 let heatmap_colormap_pipeline = HeatmapColormapPipeline::new(device, format);
788 let symbol_pipeline = SymbolPipeline::new(device, format);
789 let model_pipeline = ModelPipeline::new(device, format);
790 let image_overlay_pipeline = ImageOverlayPipeline::new(device, format);
791
792 let uniform_data = ViewProjUniform::from_dmat4(&glam::DMat4::IDENTITY);
794 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
795 label: Some("rustial_uniform_buf"),
796 contents: bytemuck::bytes_of(&uniform_data),
797 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
798 });
799
800 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
805 label: Some("rustial_uniform_bg"),
806 layout: &tile_pipeline.uniform_bind_group_layout,
807 entries: &[wgpu::BindGroupEntry {
808 binding: 0,
809 resource: uniform_buffer.as_entire_binding(),
810 }],
811 });
812
813 let column_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
814 label: Some("rustial_column_uniform_bg"),
815 layout: &column_pipeline.uniform_bind_group_layout,
816 entries: &[wgpu::BindGroupEntry {
817 binding: 0,
818 resource: uniform_buffer.as_entire_binding(),
819 }],
820 });
821
822 let grid_extrusion_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
823 label: Some("rustial_grid_extrusion_uniform_bg"),
824 layout: &grid_extrusion_pipeline.uniform_bind_group_layout,
825 entries: &[wgpu::BindGroupEntry {
826 binding: 0,
827 resource: uniform_buffer.as_entire_binding(),
828 }],
829 });
830
831 let terrain_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
832 label: Some("rustial_terrain_uniform_bg"),
833 layout: &terrain_pipeline.uniform_bind_group_layout,
834 entries: &[wgpu::BindGroupEntry {
835 binding: 0,
836 resource: uniform_buffer.as_entire_binding(),
837 }],
838 });
839
840 let hillshade_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
841 label: Some("rustial_hillshade_uniform_bg"),
842 layout: &hillshade_pipeline.uniform_bind_group_layout,
843 entries: &[wgpu::BindGroupEntry {
844 binding: 0,
845 resource: uniform_buffer.as_entire_binding(),
846 }],
847 });
848
849 let grid_scalar_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
850 label: Some("rustial_grid_scalar_uniform_bg"),
851 layout: &grid_scalar_pipeline.uniform_bind_group_layout,
852 entries: &[wgpu::BindGroupEntry {
853 binding: 0,
854 resource: uniform_buffer.as_entire_binding(),
855 }],
856 });
857
858 let vector_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
859 label: Some("rustial_vector_uniform_bg"),
860 layout: &vector_pipeline.uniform_bind_group_layout,
861 entries: &[wgpu::BindGroupEntry {
862 binding: 0,
863 resource: uniform_buffer.as_entire_binding(),
864 }],
865 });
866
867 let fill_extrusion_uniform_bind_group =
868 device.create_bind_group(&wgpu::BindGroupDescriptor {
869 label: Some("rustial_fill_extrusion_uniform_bg"),
870 layout: &fill_extrusion_pipeline.uniform_bind_group_layout,
871 entries: &[wgpu::BindGroupEntry {
872 binding: 0,
873 resource: uniform_buffer.as_entire_binding(),
874 }],
875 });
876
877 let model_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
878 label: Some("rustial_model_uniform_bg"),
879 layout: &model_pipeline.uniform_bind_group_layout,
880 entries: &[wgpu::BindGroupEntry {
881 binding: 0,
882 resource: uniform_buffer.as_entire_binding(),
883 }],
884 });
885
886 let line_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
887 label: Some("rustial_line_uniform_bg"),
888 layout: &line_pipeline.uniform_bind_group_layout,
889 entries: &[wgpu::BindGroupEntry {
890 binding: 0,
891 resource: uniform_buffer.as_entire_binding(),
892 }],
893 });
894
895 let circle_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
896 label: Some("rustial_circle_uniform_bg"),
897 layout: &circle_pipeline.uniform_bind_group_layout,
898 entries: &[wgpu::BindGroupEntry {
899 binding: 0,
900 resource: uniform_buffer.as_entire_binding(),
901 }],
902 });
903
904 let heatmap_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
905 label: Some("rustial_heatmap_uniform_bg"),
906 layout: &heatmap_pipeline.uniform_bind_group_layout,
907 entries: &[wgpu::BindGroupEntry {
908 binding: 0,
909 resource: uniform_buffer.as_entire_binding(),
910 }],
911 });
912
913 let heatmap_colormap_uniform_bind_group =
914 device.create_bind_group(&wgpu::BindGroupDescriptor {
915 label: Some("rustial_heatmap_colormap_uniform_bg"),
916 layout: &heatmap_colormap_pipeline.uniform_bind_group_layout,
917 entries: &[wgpu::BindGroupEntry {
918 binding: 0,
919 resource: uniform_buffer.as_entire_binding(),
920 }],
921 });
922
923 let symbol_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
924 label: Some("rustial_symbol_uniform_bg"),
925 layout: &symbol_pipeline.uniform_bind_group_layout,
926 entries: &[wgpu::BindGroupEntry {
927 binding: 0,
928 resource: uniform_buffer.as_entire_binding(),
929 }],
930 });
931
932 let image_overlay_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
933 label: Some("rustial_image_overlay_uniform_bg"),
934 layout: &image_overlay_pipeline.uniform_bind_group_layout,
935 entries: &[wgpu::BindGroupEntry {
936 binding: 0,
937 resource: uniform_buffer.as_entire_binding(),
938 }],
939 });
940
941 let terrain_data_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
942 label: Some("rustial_terrain_data_uniform_bg"),
943 layout: &terrain_data_pipeline.uniform_bind_group_layout,
944 entries: &[wgpu::BindGroupEntry {
945 binding: 0,
946 resource: uniform_buffer.as_entire_binding(),
947 }],
948 });
949
950 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
951 label: Some("rustial_sampler"),
952 address_mode_u: wgpu::AddressMode::ClampToEdge,
953 address_mode_v: wgpu::AddressMode::ClampToEdge,
954 mag_filter: wgpu::FilterMode::Linear,
955 min_filter: wgpu::FilterMode::Linear,
956 mipmap_filter: wgpu::FilterMode::Linear,
957 anisotropy_clamp: 16,
958 ..Default::default()
959 });
960
961 let grid_scalar_ramp_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
962 label: Some("rustial_grid_scalar_ramp_sampler"),
963 address_mode_u: wgpu::AddressMode::ClampToEdge,
964 address_mode_v: wgpu::AddressMode::ClampToEdge,
965 mag_filter: wgpu::FilterMode::Linear,
966 min_filter: wgpu::FilterMode::Linear,
967 mipmap_filter: wgpu::FilterMode::Nearest,
968 ..Default::default()
969 });
970
971 let fill_pattern_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
972 label: Some("rustial_fill_pattern_sampler"),
973 address_mode_u: wgpu::AddressMode::Repeat,
974 address_mode_v: wgpu::AddressMode::Repeat,
975 mag_filter: wgpu::FilterMode::Linear,
976 min_filter: wgpu::FilterMode::Linear,
977 mipmap_filter: wgpu::FilterMode::Linear,
978 ..Default::default()
979 });
980
981 let w = width.max(1);
983 let h = height.max(1);
984 let depth_view = create_depth_texture(device, w, h);
985 let terrain_interaction_buffers = TerrainInteractionBuffers::new(device, w, h);
986
987 let (heatmap_accum_texture, heatmap_accum_view) =
989 create_heatmap_accum_texture(device, w, h);
990 let heatmap_ramp_texture = create_default_heatmap_ramp_texture(device, _queue);
991 let heatmap_ramp_view =
992 heatmap_ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
993 let heatmap_colormap_textures_bind_group =
994 create_heatmap_colormap_bind_group(
995 device,
996 &heatmap_colormap_pipeline.textures_bind_group_layout,
997 &heatmap_accum_view,
998 &heatmap_ramp_view,
999 &sampler,
1000 );
1001
1002 Self {
1003 tile_pipeline,
1004 terrain_pipeline,
1005 terrain_data_pipeline,
1006 hillshade_pipeline,
1007 grid_scalar_pipeline,
1008 grid_extrusion_pipeline,
1009 column_pipeline,
1010 vector_pipeline,
1011 fill_pipeline,
1012 fill_pattern_pipeline,
1013 fill_extrusion_pipeline,
1014 line_pipeline,
1015 line_pattern_pipeline,
1016 circle_pipeline,
1017 heatmap_pipeline,
1018 heatmap_colormap_pipeline,
1019 symbol_pipeline,
1020 model_pipeline,
1021 image_overlay_pipeline,
1022 uniform_buffer,
1023 uniform_bind_group,
1024 terrain_uniform_bind_group,
1025 terrain_data_uniform_bind_group,
1026 hillshade_uniform_bind_group,
1027 grid_scalar_uniform_bind_group,
1028 grid_extrusion_uniform_bind_group,
1029 column_uniform_bind_group,
1030 vector_uniform_bind_group,
1031 fill_extrusion_uniform_bind_group,
1032 model_uniform_bind_group,
1033 line_uniform_bind_group,
1034 circle_uniform_bind_group,
1035 heatmap_uniform_bind_group,
1036 heatmap_colormap_uniform_bind_group,
1037 heatmap_accum_texture,
1038 heatmap_accum_view,
1039 _heatmap_ramp_texture: heatmap_ramp_texture,
1040 heatmap_ramp_view,
1041 heatmap_colormap_textures_bind_group,
1042 symbol_uniform_bind_group,
1043 image_overlay_uniform_bind_group,
1044 sampler,
1045 grid_scalar_ramp_sampler,
1046 fill_pattern_sampler,
1047 depth_view,
1048 width: w,
1049 height: h,
1050 terrain_interaction_buffers,
1051 tile_atlas: TileAtlas::new(),
1052 hillshade_atlas: TileAtlas::new(),
1053 page_bind_groups: Vec::new(),
1054 page_terrain_bind_groups: Vec::new(),
1055 page_hillshade_bind_groups: Vec::new(),
1056 model_mesh_cache: std::collections::HashMap::new(),
1057 shared_terrain_grids: std::collections::HashMap::new(),
1058 height_texture_cache: std::collections::HashMap::new(),
1059 shared_column_mesh: None,
1060 grid_scalar_overlay_cache: std::collections::HashMap::new(),
1061 grid_extrusion_overlay_cache: std::collections::HashMap::new(),
1062 column_overlay_cache: std::collections::HashMap::new(),
1063 point_cloud_overlay_cache: std::collections::HashMap::new(),
1064 cached_tile_batches: Vec::new(),
1065 tile_batch_cache_key: None,
1066 cached_vector_batches: Vec::new(),
1067 vector_batch_cache_key: None,
1068 cached_fill_extrusion_batches: Vec::new(),
1069 cached_fill_batches: Vec::new(),
1070 cached_fill_pattern_batches: Vec::new(),
1071 cached_line_batches: Vec::new(),
1072 cached_line_pattern_batches: Vec::new(),
1073 cached_circle_batches: Vec::new(),
1074 cached_heatmap_batches: Vec::new(),
1075 cached_symbol_batch: None,
1076 symbol_atlas_texture: None,
1077 symbol_atlas_bind_group: None,
1078 symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas::new(),
1079 symbol_glyph_provider: Box::new(rustial_engine::symbols::ProceduralGlyphProvider::new()),
1080 terrain_tile_bind_cache: std::collections::HashMap::new(),
1081 terrain_data_dirty: TerrainDataDirtyState::default(),
1082 cached_model_transforms: None,
1083 cached_placeholder_batch: None,
1084 cached_image_overlay_batches: Vec::new(),
1085 visualization_perf_stats: VisualizationPerfStats::default(),
1086 }
1087 }
1088
1089 pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
1096 self.width = width.max(1);
1097 self.height = height.max(1);
1098 self.depth_view = create_depth_texture(device, self.width, self.height);
1099 self.terrain_interaction_buffers.resize(device, self.width, self.height);
1100 self.terrain_data_dirty.dirty = true;
1101
1102 let (tex, view) = create_heatmap_accum_texture(device, self.width, self.height);
1105 self.heatmap_accum_texture = tex;
1106 self.heatmap_accum_view = view;
1107 self.heatmap_colormap_textures_bind_group = create_heatmap_colormap_bind_group(
1108 device,
1109 &self.heatmap_colormap_pipeline.textures_bind_group_layout,
1110 &self.heatmap_accum_view,
1111 &self.heatmap_ramp_view,
1112 &self.sampler,
1113 );
1114 }
1115
1116 pub fn set_glyph_provider(
1124 &mut self,
1125 provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
1126 ) {
1127 self.symbol_glyph_provider = provider;
1128 }
1129
1130 pub fn upload_tile(
1140 &mut self,
1141 device: &wgpu::Device,
1142 tile_id: TileId,
1143 image: &DecodedImage,
1144 ) {
1145 if self.tile_atlas.contains(&tile_id) {
1146 return;
1147 }
1148
1149 if let Err(err) = image.validate_rgba8() {
1150 log::warn!("wgpu upload_tile: skipping invalid tile {:?}: {}", tile_id, err);
1151 return;
1152 }
1153
1154 self.tile_atlas.insert(device, tile_id, image);
1155 self.tile_batch_cache_key = None;
1156
1157 self.rebuild_page_bind_groups(device);
1159 }
1160
1161 pub fn upload_hillshade(
1163 &mut self,
1164 device: &wgpu::Device,
1165 tile_id: TileId,
1166 image: &DecodedImage,
1167 ) {
1168 if self.hillshade_atlas.contains(&tile_id) {
1169 return;
1170 }
1171 if let Err(err) = image.validate_rgba8() {
1172 log::warn!("wgpu upload_hillshade: skipping invalid tile {:?}: {}", tile_id, err);
1173 return;
1174 }
1175 self.hillshade_atlas.insert(device, tile_id, image);
1176 self.rebuild_page_bind_groups(device);
1177 }
1178
1179 pub fn flush_atlas_uploads(&mut self, queue: &wgpu::Queue) {
1187 self.tile_atlas.flush_uploads(queue);
1188 self.hillshade_atlas.flush_uploads(queue);
1189 }
1190
1191 fn get_or_create_shared_column_mesh(&mut self, device: &wgpu::Device) -> &SharedColumnMesh {
1192 if self.shared_column_mesh.is_none() {
1193 let (vertices, indices) = build_unit_column_mesh();
1194 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1195 label: Some("column_unit_box_vb"),
1196 contents: bytemuck::cast_slice(&vertices),
1197 usage: wgpu::BufferUsages::VERTEX,
1198 });
1199 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1200 label: Some("column_unit_box_ib"),
1201 contents: bytemuck::cast_slice(&indices),
1202 usage: wgpu::BufferUsages::INDEX,
1203 });
1204 self.shared_column_mesh = Some(SharedColumnMesh {
1205 vertex_buffer,
1206 index_buffer,
1207 index_count: indices.len() as u32,
1208 });
1209 }
1210 self.shared_column_mesh.as_ref().expect("column mesh")
1211 }
1212
1213 fn get_or_create_grid_scalar_overlay(
1214 &mut self,
1215 device: &wgpu::Device,
1216 queue: &wgpu::Queue,
1217 overlay: &VisualizationOverlay,
1218 state: &MapState,
1219 scene_origin: DVec3,
1220 terrain_fingerprint: u64,
1221 ) -> Option<()> {
1222 let VisualizationOverlay::GridScalar { layer_id, grid, field, ramp } = overlay else {
1223 return None;
1224 };
1225
1226 let origin_key = [
1227 (scene_origin.x * 100.0) as i64,
1228 (scene_origin.y * 100.0) as i64,
1229 (scene_origin.z * 100.0) as i64,
1230 ];
1231 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1232 let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
1233 let projection = state.camera().projection();
1234 let (vertices, indices) = build_grid_scalar_geometry(grid, state, scene_origin);
1235
1236 let recreate = if let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) {
1237 cached.generation != field.generation
1238 || cached.ramp_fingerprint != ramp_fingerprint
1239 || cached.grid_fingerprint != grid_fingerprint
1240 || cached.projection != projection
1241 || cached.index_count as usize != indices.len()
1242 || cached.vertex_count != vertices.len()
1243 } else {
1244 true
1245 };
1246
1247 if recreate {
1248 self.visualization_perf_stats.grid_scalar_rebuilds += 1;
1249 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1250 label: Some(&format!("grid_scalar_vb_{layer_id}")),
1251 contents: bytemuck::cast_slice(&vertices),
1252 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1253 });
1254 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1255 label: Some(&format!("grid_scalar_ib_{layer_id}")),
1256 contents: bytemuck::cast_slice(&indices),
1257 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1258 });
1259 let scalar_texture = create_grid_scalar_texture(device, queue, field);
1260 let scalar_view = scalar_texture.create_view(&wgpu::TextureViewDescriptor::default());
1261 let ramp_texture = create_grid_scalar_ramp_texture(device, queue, ramp);
1262 let ramp_view = ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
1263 let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
1264 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1265 label: Some(&format!("grid_scalar_uniform_{layer_id}")),
1266 contents: bytemuck::bytes_of(&uniform),
1267 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1268 });
1269 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1270 label: Some(&format!("grid_scalar_bg_{layer_id}")),
1271 layout: &self.grid_scalar_pipeline.overlay_bind_group_layout,
1272 entries: &[
1273 wgpu::BindGroupEntry {
1274 binding: 0,
1275 resource: uniform_buffer.as_entire_binding(),
1276 },
1277 wgpu::BindGroupEntry {
1278 binding: 1,
1279 resource: wgpu::BindingResource::TextureView(&scalar_view),
1280 },
1281 wgpu::BindGroupEntry {
1282 binding: 2,
1283 resource: wgpu::BindingResource::TextureView(&ramp_view),
1284 },
1285 wgpu::BindGroupEntry {
1286 binding: 3,
1287 resource: wgpu::BindingResource::Sampler(&self.grid_scalar_ramp_sampler),
1288 },
1289 ],
1290 });
1291 self.grid_scalar_overlay_cache.insert(
1292 *layer_id,
1293 CachedGridScalarOverlay {
1294 vertex_buffer,
1295 index_buffer,
1296 index_count: indices.len() as u32,
1297 vertex_count: vertices.len(),
1298 uniform_buffer,
1299 bind_group,
1300 scalar_texture,
1301 ramp_texture,
1302 generation: field.generation,
1303 value_generation: field.value_generation,
1304 ramp_fingerprint,
1305 grid_fingerprint,
1306 terrain_fingerprint,
1307 projection,
1308 origin_key,
1309 },
1310 );
1311 return Some(());
1312 }
1313
1314 if let Some(cached) = self.grid_scalar_overlay_cache.get_mut(layer_id) {
1315 let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
1316 if cached.value_generation != field.value_generation {
1317 self.visualization_perf_stats.grid_scalar_value_updates += 1;
1318 write_grid_scalar_texture(queue, &cached.scalar_texture, field);
1319 cached.value_generation = field.value_generation;
1320 queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1321 }
1322 if cached.origin_key != origin_key || cached.terrain_fingerprint != terrain_fingerprint {
1323 queue.write_buffer(
1324 &cached.vertex_buffer,
1325 0,
1326 bytemuck::cast_slice::<GridScalarVertex, u8>(&vertices),
1327 );
1328 queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1329 cached.origin_key = origin_key;
1330 cached.terrain_fingerprint = terrain_fingerprint;
1331 }
1332 }
1333
1334 Some(())
1335 }
1336
1337 fn get_or_create_point_cloud_overlay(
1338 &mut self,
1339 device: &wgpu::Device,
1340 queue: &wgpu::Queue,
1341 overlay: &VisualizationOverlay,
1342 state: &MapState,
1343 scene_origin: DVec3,
1344 ) -> Option<()> {
1345 let VisualizationOverlay::Points { layer_id, points, ramp } = overlay else {
1346 return None;
1347 };
1348
1349 let origin_key = [
1350 (scene_origin.x * 100.0) as i64,
1351 (scene_origin.y * 100.0) as i64,
1352 (scene_origin.z * 100.0) as i64,
1353 ];
1354 let points_fingerprint = point_set_fingerprint(points);
1355 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1356 let instances = build_point_instances(points, ramp, state, scene_origin);
1357
1358 let needs_rebuild = if let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) {
1359 cached.generation != points.generation
1360 || cached.ramp_fingerprint != ramp_fingerprint
1361 || cached.instance_count as usize != instances.len()
1362 } else {
1363 true
1364 };
1365
1366 if needs_rebuild {
1367 self.visualization_perf_stats.point_cloud_rebuilds += 1;
1368 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1369 label: Some(&format!("point_cloud_instances_{layer_id}")),
1370 contents: bytemuck::cast_slice(&instances),
1371 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1372 });
1373 self.point_cloud_overlay_cache.insert(
1374 *layer_id,
1375 CachedPointCloudOverlay {
1376 instance_buffer,
1377 instance_count: instances.len() as u32,
1378 generation: points.generation,
1379 origin_key,
1380 points_fingerprint,
1381 ramp_fingerprint,
1382 instance_data: instances,
1383 },
1384 );
1385 return Some(());
1386 }
1387
1388 if let Some(cached) = self.point_cloud_overlay_cache.get_mut(layer_id) {
1389 let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
1390 if !ranges.is_empty() {
1391 self.visualization_perf_stats.point_cloud_partial_writes += 1;
1392 self.visualization_perf_stats.point_cloud_partial_write_ranges += ranges.len() as u32;
1393 }
1394 for range in ranges {
1395 let start = range.start;
1396 let end = range.end;
1397 let byte_offset =
1398 (start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
1399 queue.write_buffer(
1400 &cached.instance_buffer,
1401 byte_offset,
1402 bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
1403 );
1404 }
1405 cached.instance_data = instances;
1406 cached.origin_key = origin_key;
1407 cached.points_fingerprint = points_fingerprint;
1408 }
1409
1410 Some(())
1411 }
1412
1413 fn get_or_create_grid_extrusion_overlay(
1414 &mut self,
1415 device: &wgpu::Device,
1416 queue: &wgpu::Queue,
1417 overlay: &VisualizationOverlay,
1418 state: &MapState,
1419 scene_origin: DVec3,
1420 terrain_fingerprint: u64,
1421 ) -> Option<()> {
1422 let VisualizationOverlay::GridExtrusion {
1423 layer_id,
1424 grid,
1425 field,
1426 ramp,
1427 params,
1428 } = overlay else {
1429 return None;
1430 };
1431
1432 let origin_key = [
1433 (scene_origin.x * 100.0) as i64,
1434 (scene_origin.y * 100.0) as i64,
1435 (scene_origin.z * 100.0) as i64,
1436 ];
1437 let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
1438 let params_fingerprint = grid_extrusion_params_fingerprint(params);
1439 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1440
1441 let (vertices, indices) = build_grid_extrusion_geometry(grid, field, ramp, params, state, scene_origin);
1442
1443 let needs_rebuild = if let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) {
1444 cached.generation != field.generation
1445 || cached.grid_fingerprint != grid_fingerprint
1446 || cached.params_fingerprint != params_fingerprint
1447 || cached.ramp_fingerprint != ramp_fingerprint
1448 || cached.terrain_fingerprint != terrain_fingerprint
1449 || cached.index_count as usize != indices.len()
1450 || cached.vertex_count != vertices.len()
1451 } else {
1452 true
1453 };
1454
1455 if needs_rebuild {
1456 self.visualization_perf_stats.grid_extrusion_rebuilds += 1;
1457 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1458 label: Some(&format!("grid_extrusion_vb_{layer_id}")),
1459 contents: bytemuck::cast_slice(&vertices),
1460 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1461 });
1462 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1463 label: Some(&format!("grid_extrusion_ib_{layer_id}")),
1464 contents: bytemuck::cast_slice(&indices),
1465 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1466 });
1467
1468 self.grid_extrusion_overlay_cache.insert(
1469 *layer_id,
1470 CachedGridExtrusionOverlay {
1471 vertex_buffer,
1472 index_buffer,
1473 index_count: indices.len() as u32,
1474 vertex_count: vertices.len(),
1475 generation: field.generation,
1476 value_generation: field.value_generation,
1477 origin_key,
1478 grid_fingerprint,
1479 params_fingerprint,
1480 ramp_fingerprint,
1481 terrain_fingerprint,
1482 },
1483 );
1484 return Some(());
1485 }
1486
1487 if let Some(cached) = self.grid_extrusion_overlay_cache.get_mut(layer_id) {
1488 if cached.value_generation != field.value_generation || cached.origin_key != origin_key {
1489 self.visualization_perf_stats.grid_extrusion_value_updates += 1;
1490 let vertex_bytes = bytemuck::cast_slice::<GridExtrusionVertex, u8>(&vertices);
1491 queue.write_buffer(&cached.vertex_buffer, 0, vertex_bytes);
1492 }
1493 cached.value_generation = field.value_generation;
1494 cached.origin_key = origin_key;
1495 cached.terrain_fingerprint = terrain_fingerprint;
1496 }
1497 Some(())
1498 }
1499
1500 fn get_or_create_column_overlay(
1501 &mut self,
1502 device: &wgpu::Device,
1503 queue: &wgpu::Queue,
1504 overlay: &VisualizationOverlay,
1505 state: &MapState,
1506 scene_origin: DVec3,
1507 ) -> Option<()> {
1508 let VisualizationOverlay::Columns { layer_id, columns, ramp } = overlay else {
1509 return None;
1510 };
1511
1512 let origin_key = [
1513 (scene_origin.x * 100.0) as i64,
1514 (scene_origin.y * 100.0) as i64,
1515 (scene_origin.z * 100.0) as i64,
1516 ];
1517 let columns_fingerprint = column_set_fingerprint(columns);
1518 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1519 let instances = build_column_instances(columns, ramp, state, scene_origin);
1520
1521 let needs_rebuild = if let Some(cached) = self.column_overlay_cache.get(layer_id) {
1522 cached.generation != columns.generation
1523 || cached.ramp_fingerprint != ramp_fingerprint
1524 || cached.instance_count as usize != instances.len()
1525 } else {
1526 true
1527 };
1528
1529 if needs_rebuild {
1530 self.visualization_perf_stats.column_rebuilds += 1;
1531 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1532 label: Some(&format!("column_instances_{layer_id}")),
1533 contents: bytemuck::cast_slice(&instances),
1534 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1535 });
1536 self.column_overlay_cache.insert(
1537 *layer_id,
1538 CachedColumnOverlay {
1539 instance_buffer,
1540 instance_count: instances.len() as u32,
1541 generation: columns.generation,
1542 origin_key,
1543 columns_fingerprint,
1544 ramp_fingerprint,
1545 instance_data: instances,
1546 },
1547 );
1548 return Some(());
1549 }
1550
1551 if let Some(cached) = self.column_overlay_cache.get_mut(layer_id) {
1552 let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
1553 if !ranges.is_empty() {
1554 self.visualization_perf_stats.column_partial_writes += 1;
1555 self.visualization_perf_stats.column_partial_write_ranges += ranges.len() as u32;
1556 }
1557 for range in ranges {
1558 let start = range.start;
1559 let end = range.end;
1560 let byte_offset =
1561 (start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
1562 queue.write_buffer(
1563 &cached.instance_buffer,
1564 byte_offset,
1565 bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
1566 );
1567 }
1568 cached.instance_data = instances;
1569 cached.origin_key = origin_key;
1570 cached.columns_fingerprint = columns_fingerprint;
1571 }
1572
1573 Some(())
1574 }
1575
1576 pub fn render(
1583 &mut self,
1584 state: &MapState,
1585 device: &wgpu::Device,
1586 queue: &wgpu::Queue,
1587 color_view: &wgpu::TextureView,
1588 visible_tiles: &[VisibleTile],
1589 ) {
1590 let clear_color = state.computed_fog().clear_color;
1591 self.render_full(&RenderParams {
1592 state,
1593 device,
1594 queue,
1595 color_view,
1596 visible_tiles,
1597 vector_meshes: &[],
1598 model_instances: &[],
1599 clear_color,
1600 });
1601 }
1602
1603 pub fn render_full(&mut self, params: &RenderParams<'_>) {
1619 self.visualization_perf_stats = VisualizationPerfStats::default();
1620 let scene_camera_origin = params.state.scene_world_origin();
1622 let frame = params.state.frame_output();
1623 let visualization = &frame.visualization;
1624 let view = params.state.camera().view_matrix(DVec3::ZERO);
1625 let proj = params.state.camera().projection_matrix();
1626 let vp = proj * view;
1627
1628 let cam = params.state.camera();
1631 let eye = cam.eye_offset();
1632 let fog = params.state.computed_fog();
1633 let clear_color = fog.clear_color;
1634
1635 let mut uniform = ViewProjUniform::from_dmat4(&vp);
1636 uniform.fog_color = fog.fog_color;
1637 uniform.eye_pos = [eye.x as f32, eye.y as f32, eye.z as f32, 0.0];
1638 uniform.fog_params = [fog.fog_start, fog.fog_end, fog.fog_density, 0.0];
1639 if let Some(hillshade) = params.state.hillshade() {
1640 uniform.hillshade_highlight = hillshade.highlight_color;
1641 uniform.hillshade_shadow = hillshade.shadow_color;
1642 uniform.hillshade_accent = hillshade.accent_color;
1643 uniform.hillshade_light = [
1644 hillshade.illumination_direction,
1645 hillshade.illumination_altitude,
1646 hillshade.exaggeration,
1647 hillshade.opacity,
1648 ];
1649 }
1650
1651 params
1652 .queue
1653 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1654
1655 for vt in params.visible_tiles {
1657 if let Some(TileData::Raster(ref img)) = vt.data {
1658 self.upload_tile(params.device, vt.actual, img);
1659 }
1660 }
1661 for raster in params.state.hillshade_rasters() {
1662 self.upload_hillshade(params.device, raster.tile, &raster.image);
1663 }
1664
1665 self.flush_atlas_uploads(params.queue);
1667
1668 for vt in params.visible_tiles {
1670 self.tile_atlas.mark_used(&vt.actual);
1671 }
1672 let terrain_meshes = params.state.terrain_meshes();
1673 for mesh in terrain_meshes {
1674 if let Some(actual_tile) = find_terrain_texture_actual(mesh.tile, params.visible_tiles) {
1675 self.tile_atlas.mark_used(&actual_tile);
1676 }
1677 }
1678 for raster in params.state.hillshade_rasters() {
1679 self.hillshade_atlas.mark_used(&raster.tile);
1680 }
1681
1682 let use_shared_terrain = !terrain_meshes.is_empty()
1683 && matches!(
1684 params.state.camera().projection(),
1685 rustial_engine::CameraProjection::WebMercator
1686 | rustial_engine::CameraProjection::Equirectangular
1687 )
1688 && terrain_meshes.iter().all(|mesh| mesh.elevation_texture.is_some());
1689
1690 let materialized_terrain_meshes: Vec<TerrainMeshData> = if use_shared_terrain {
1691 Vec::new()
1692 } else {
1693 terrain_meshes
1694 .iter()
1695 .map(|mesh| {
1696 materialize_terrain_mesh(
1697 mesh,
1698 params.state.camera().projection(),
1699 rustial_engine::skirt_height(
1700 mesh.tile.zoom,
1701 mesh.vertical_exaggeration as f64,
1702 ),
1703 )
1704 })
1705 .collect()
1706 };
1707
1708 if !params.model_instances.is_empty() {
1710 self.cache_model_meshes(params.device, params.model_instances);
1711 self.cache_model_transforms(
1712 params.device,
1713 params.model_instances,
1714 scene_camera_origin,
1715 params.state,
1716 );
1717 } else {
1718 self.cached_model_transforms = None;
1719 }
1720
1721 let tile_batch_key = TileBatchCacheKey::new(
1723 params.visible_tiles,
1724 scene_camera_origin,
1725 params.state.camera().projection(),
1726 );
1727 if self.tile_batch_cache_key.as_ref() != Some(&tile_batch_key) {
1728 self.cached_tile_batches = build_tile_batches(
1729 params.device,
1730 params.visible_tiles,
1731 &self.tile_atlas,
1732 scene_camera_origin,
1733 params.state.camera().projection(),
1734 );
1735 self.tile_batch_cache_key = Some(tile_batch_key);
1736 }
1737
1738 let terrain_batches = if !use_shared_terrain && !materialized_terrain_meshes.is_empty() {
1739 build_terrain_batches(
1740 params.device,
1741 &materialized_terrain_meshes,
1742 &self.tile_atlas,
1743 scene_camera_origin,
1744 params.visible_tiles,
1745 )
1746 } else {
1747 Vec::new()
1748 };
1749
1750 let hillshade_batches = if !materialized_terrain_meshes.is_empty() && !params.state.hillshade_rasters().is_empty() {
1751 build_hillshade_batches(
1752 params.device,
1753 &materialized_terrain_meshes,
1754 params.state.hillshade_rasters(),
1755 &self.hillshade_atlas,
1756 scene_camera_origin,
1757 )
1758 } else {
1759 Vec::new()
1760 };
1761
1762 let vector_batch_key = VectorBatchCacheKey::new(params.vector_meshes, scene_camera_origin);
1763 if self.vector_batch_cache_key.as_ref() != Some(&vector_batch_key) {
1764 self.cached_vector_batches = params
1765 .vector_meshes
1766 .iter()
1767 .filter(|mesh| mesh.render_mode == VectorRenderMode::Generic)
1768 .map(|mesh| build_vector_batch(params.device, mesh, scene_camera_origin))
1769 .collect();
1770 self.cached_fill_batches = params
1771 .vector_meshes
1772 .iter()
1773 .filter(|mesh| mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_none())
1774 .map(|mesh| build_fill_batch(
1775 params.device,
1776 mesh,
1777 scene_camera_origin,
1778 &self.uniform_buffer,
1779 &self.fill_pipeline.uniform_bind_group_layout,
1780 ))
1781 .collect();
1782 self.cached_fill_pattern_batches = params
1783 .vector_meshes
1784 .iter()
1785 .filter(|mesh| mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_some())
1786 .map(|mesh| build_fill_pattern_batch(
1787 params.device,
1788 params.queue,
1789 mesh,
1790 scene_camera_origin,
1791 &self.uniform_buffer,
1792 &self.fill_pattern_pipeline.uniform_bind_group_layout,
1793 &self.fill_pattern_pipeline.texture_bind_group_layout,
1794 &self.fill_pattern_sampler,
1795 ))
1796 .collect();
1797 self.cached_fill_extrusion_batches = params
1798 .vector_meshes
1799 .iter()
1800 .filter(|mesh| mesh.render_mode == VectorRenderMode::FillExtrusion)
1801 .map(|mesh| build_fill_extrusion_batch(params.device, mesh, scene_camera_origin))
1802 .collect();
1803 self.cached_line_batches = params
1804 .vector_meshes
1805 .iter()
1806 .filter(|mesh| mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_none())
1807 .map(|mesh| build_line_batch(params.device, mesh, scene_camera_origin))
1808 .collect();
1809 self.cached_line_pattern_batches = params
1810 .vector_meshes
1811 .iter()
1812 .filter(|mesh| mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_some())
1813 .map(|mesh| build_line_pattern_batch(
1814 params.device,
1815 params.queue,
1816 mesh,
1817 scene_camera_origin,
1818 &self.uniform_buffer,
1819 &self.line_pattern_pipeline.uniform_bind_group_layout,
1820 &self.line_pattern_pipeline.texture_bind_group_layout,
1821 &self.fill_pattern_sampler,
1822 ))
1823 .collect();
1824 self.cached_circle_batches = params
1825 .vector_meshes
1826 .iter()
1827 .filter(|mesh| mesh.render_mode == VectorRenderMode::Circle)
1828 .map(|mesh| build_circle_batch(params.device, mesh, scene_camera_origin))
1829 .collect();
1830 self.cached_heatmap_batches = params
1831 .vector_meshes
1832 .iter()
1833 .filter(|mesh| mesh.render_mode == VectorRenderMode::Heatmap)
1834 .map(|mesh| build_heatmap_batch(params.device, mesh, scene_camera_origin))
1835 .collect();
1836 self.vector_batch_cache_key = Some(vector_batch_key);
1837 }
1838
1839 {
1841 let symbols = &frame.symbols;
1842 if !symbols.is_empty() {
1843 self.symbol_glyph_atlas = rustial_engine::symbols::GlyphAtlas::new();
1845 for symbol in symbols.iter() {
1846 if symbol.visible && symbol.opacity > 0.0 {
1847 if let Some(text) = &symbol.text {
1848 self.symbol_glyph_atlas.request_text(&symbol.font_stack, text);
1849 }
1850 }
1851 }
1852 self.symbol_glyph_atlas.load_requested(&*self.symbol_glyph_provider);
1854
1855 let dims = self.symbol_glyph_atlas.dimensions();
1856 if dims[0] > 0 && dims[1] > 0 {
1857 let tex = params.device.create_texture(&wgpu::TextureDescriptor {
1859 label: Some("symbol_atlas_tex"),
1860 size: wgpu::Extent3d {
1861 width: dims[0] as u32,
1862 height: dims[1] as u32,
1863 depth_or_array_layers: 1,
1864 },
1865 mip_level_count: 1,
1866 sample_count: 1,
1867 dimension: wgpu::TextureDimension::D2,
1868 format: wgpu::TextureFormat::R8Unorm,
1869 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1870 view_formats: &[],
1871 });
1872 params.queue.write_texture(
1873 wgpu::TexelCopyTextureInfo {
1874 texture: &tex,
1875 mip_level: 0,
1876 origin: wgpu::Origin3d::ZERO,
1877 aspect: wgpu::TextureAspect::All,
1878 },
1879 self.symbol_glyph_atlas.alpha(),
1880 wgpu::TexelCopyBufferLayout {
1881 offset: 0,
1882 bytes_per_row: Some(dims[0] as u32),
1883 rows_per_image: Some(dims[1] as u32),
1884 },
1885 wgpu::Extent3d {
1886 width: dims[0] as u32,
1887 height: dims[1] as u32,
1888 depth_or_array_layers: 1,
1889 },
1890 );
1891 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1892 let atlas_bg = params.device.create_bind_group(&wgpu::BindGroupDescriptor {
1893 label: Some("symbol_atlas_bg"),
1894 layout: &self.symbol_pipeline.atlas_bind_group_layout,
1895 entries: &[
1896 wgpu::BindGroupEntry {
1897 binding: 0,
1898 resource: wgpu::BindingResource::TextureView(&view),
1899 },
1900 wgpu::BindGroupEntry {
1901 binding: 1,
1902 resource: wgpu::BindingResource::Sampler(&self.sampler),
1903 },
1904 ],
1905 });
1906 self.symbol_atlas_texture = Some((tex, view));
1907 self.symbol_atlas_bind_group = Some(atlas_bg);
1908 }
1909
1910 let mut laid_out_symbols: Vec<rustial_engine::symbols::PlacedSymbol> = symbols.to_vec();
1912 rustial_engine::symbols::layout_symbol_glyphs(
1913 &mut laid_out_symbols,
1914 &self.symbol_glyph_atlas,
1915 );
1916
1917 self.cached_symbol_batch = build_symbol_batch(
1919 params.device,
1920 &laid_out_symbols,
1921 &self.symbol_glyph_atlas,
1922 scene_camera_origin,
1923 self.symbol_glyph_atlas.render_em_px(),
1924 );
1925 } else {
1926 self.cached_symbol_batch = None;
1927 self.symbol_atlas_bind_group = None;
1928 self.symbol_atlas_texture = None;
1929 }
1930 }
1931
1932 self.cached_placeholder_batch = build_placeholder_batches(
1934 params.device,
1935 &frame.placeholders,
1936 params.state.placeholder_style(),
1937 scene_camera_origin,
1938 );
1939
1940 self.build_image_overlay_batches(
1942 params.device,
1943 params.queue,
1944 &frame.image_overlays,
1945 scene_camera_origin,
1946 );
1947
1948 if use_shared_terrain {
1950 for mesh in terrain_meshes {
1951 self.get_or_create_shared_grid(params.device, mesh.grid_resolution);
1952 let scene_origin = scene_camera_origin;
1953 self.get_or_create_terrain_tile_bind(
1955 params.device, params.queue, mesh, params.state, scene_origin,
1956 TerrainPipelineKind::Terrain,
1957 );
1958 self.get_or_create_terrain_tile_bind(
1959 params.device, params.queue, mesh, params.state, scene_origin,
1960 TerrainPipelineKind::TerrainData,
1961 );
1962 self.get_or_create_terrain_tile_bind(
1963 params.device, params.queue, mesh, params.state, scene_origin,
1964 TerrainPipelineKind::Hillshade,
1965 );
1966 }
1967 }
1968
1969 let grid_scalar_overlays: Vec<_> = visualization
1970 .iter()
1971 .filter_map(|overlay| match overlay {
1972 VisualizationOverlay::GridScalar { .. } => Some(overlay),
1973 _ => None,
1974 })
1975 .collect();
1976 let visible_grid_scalar_overlays: Vec<_> = grid_scalar_overlays
1977 .iter()
1978 .copied()
1979 .filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
1980 .collect();
1981 let grid_extrusion_overlays: Vec<_> = visualization
1982 .iter()
1983 .filter_map(|overlay| match overlay {
1984 VisualizationOverlay::GridExtrusion { .. } => Some(overlay),
1985 _ => None,
1986 })
1987 .collect();
1988 let visible_grid_extrusion_overlays: Vec<_> = grid_extrusion_overlays
1989 .iter()
1990 .copied()
1991 .filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
1992 .collect();
1993 let column_overlays: Vec<_> = visualization
1994 .iter()
1995 .filter_map(|overlay| match overlay {
1996 VisualizationOverlay::Columns { .. } => Some(overlay),
1997 _ => None,
1998 })
1999 .collect();
2000 let visible_column_overlays: Vec<_> = column_overlays
2001 .iter()
2002 .copied()
2003 .filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
2004 .collect();
2005 let point_cloud_overlays: Vec<_> = visualization
2006 .iter()
2007 .filter_map(|overlay| match overlay {
2008 VisualizationOverlay::Points { .. } => Some(overlay),
2009 _ => None,
2010 })
2011 .collect();
2012 let visible_point_cloud_overlays: Vec<_> = point_cloud_overlays
2013 .iter()
2014 .copied()
2015 .filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
2016 .collect();
2017 let terrain_fingerprint = TerrainDataDirtyState::terrain_fingerprint(terrain_meshes);
2018 if !visible_grid_scalar_overlays.is_empty() {
2019 for overlay in &visible_grid_scalar_overlays {
2020 self.get_or_create_grid_scalar_overlay(
2021 params.device,
2022 params.queue,
2023 overlay,
2024 params.state,
2025 scene_camera_origin,
2026 terrain_fingerprint,
2027 );
2028 }
2029 }
2030 if !visible_column_overlays.is_empty() {
2031 self.get_or_create_shared_column_mesh(params.device);
2032 for overlay in &visible_column_overlays {
2033 self.get_or_create_column_overlay(
2034 params.device,
2035 params.queue,
2036 overlay,
2037 params.state,
2038 scene_camera_origin,
2039 );
2040 }
2041 }
2042 if !visible_point_cloud_overlays.is_empty() {
2043 self.get_or_create_shared_column_mesh(params.device);
2044 for overlay in &visible_point_cloud_overlays {
2045 self.get_or_create_point_cloud_overlay(
2046 params.device,
2047 params.queue,
2048 overlay,
2049 params.state,
2050 scene_camera_origin,
2051 );
2052 }
2053 }
2054 if !visible_grid_extrusion_overlays.is_empty() {
2055 for overlay in &visible_grid_extrusion_overlays {
2056 self.get_or_create_grid_extrusion_overlay(
2057 params.device,
2058 params.queue,
2059 overlay,
2060 params.state,
2061 scene_camera_origin,
2062 terrain_fingerprint,
2063 );
2064 }
2065 }
2066
2067 let mut encoder = params
2069 .device
2070 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2071 label: Some("rustial_encoder"),
2072 });
2073
2074 let has_heatmap = !self.cached_heatmap_batches.is_empty()
2075 && self.cached_heatmap_batches.iter().any(|b| b.is_some());
2076
2077 let painter_plan = PainterPlan::new(
2078 !terrain_meshes.is_empty(),
2079 !params.state.hillshade_rasters().is_empty(),
2080 has_heatmap,
2081 );
2082
2083 for painter_pass in painter_plan.iter() {
2084 match painter_pass {
2085 PainterPass::SkyAtmosphere => {
2086 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2087 label: Some("rustial_pass_sky_atmosphere"),
2088 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2089 view: params.color_view,
2090 resolve_target: None,
2091 ops: wgpu::Operations {
2092 load: wgpu::LoadOp::Clear(wgpu::Color {
2093 r: clear_color[0] as f64,
2094 g: clear_color[1] as f64,
2095 b: clear_color[2] as f64,
2096 a: clear_color[3] as f64,
2097 }),
2098 store: wgpu::StoreOp::Store,
2099 },
2100 })],
2101 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2102 view: &self.depth_view,
2103 depth_ops: Some(wgpu::Operations {
2104 load: wgpu::LoadOp::Clear(1.0),
2105 store: wgpu::StoreOp::Store,
2106 }),
2107 stencil_ops: None,
2108 }),
2109 ..Default::default()
2110 });
2111 }
2112 PainterPass::TerrainData => {
2113 if !self.terrain_data_dirty.needs_update(&vp, terrain_meshes) {
2119 continue;
2120 }
2121 {
2122 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2123 label: Some("rustial_pass_terrain_data"),
2124 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2125 view: self.terrain_interaction_buffers.coord_view(),
2126 resolve_target: None,
2127 ops: wgpu::Operations {
2128 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2129 store: wgpu::StoreOp::Store,
2130 },
2131 })],
2132 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2133 view: self.terrain_interaction_buffers.depth_view(),
2134 depth_ops: Some(wgpu::Operations {
2135 load: wgpu::LoadOp::Clear(1.0),
2136 store: wgpu::StoreOp::Store,
2137 }),
2138 stencil_ops: None,
2139 }),
2140 ..Default::default()
2141 });
2142 if use_shared_terrain {
2143 self.render_shared_terrain_data_tiles(
2144 &mut pass,
2145 params.state,
2146 terrain_meshes,
2147 );
2148 } else {
2149 self.render_terrain_data_batches(&mut pass, &terrain_batches);
2150 }
2151 }
2152 self.terrain_data_dirty.mark_clean(&vp, terrain_meshes);
2153 }
2154 PainterPass::OpaqueScene => {
2155 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2156 label: Some("rustial_pass_opaque"),
2157 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2158 view: params.color_view,
2159 resolve_target: None,
2160 ops: wgpu::Operations {
2161 load: wgpu::LoadOp::Load,
2162 store: wgpu::StoreOp::Store,
2163 },
2164 })],
2165 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2166 view: &self.depth_view,
2167 depth_ops: Some(wgpu::Operations {
2168 load: wgpu::LoadOp::Load,
2169 store: wgpu::StoreOp::Store,
2170 }),
2171 stencil_ops: None,
2172 }),
2173 ..Default::default()
2174 });
2175
2176 if let Some(ref ph_batch) = self.cached_placeholder_batch {
2179 pass.set_pipeline(&self.vector_pipeline.pipeline);
2180 pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
2181 pass.set_vertex_buffer(0, ph_batch.vertex_buffer.slice(..));
2182 pass.set_index_buffer(
2183 ph_batch.index_buffer.slice(..),
2184 wgpu::IndexFormat::Uint32,
2185 );
2186 pass.draw_indexed(0..ph_batch.index_count, 0, 0..1);
2187 }
2188
2189 if use_shared_terrain {
2190 self.render_shared_terrain_tiles(
2191 &mut pass,
2192 params.state,
2193 terrain_meshes,
2194 params.visible_tiles,
2195 );
2196 } else if !terrain_batches.is_empty() {
2197 self.render_terrain_batches(&mut pass, &terrain_batches);
2198 } else {
2199 self.render_tile_batches(&mut pass, &self.cached_tile_batches);
2200 }
2201
2202 if !visible_grid_scalar_overlays.is_empty() {
2203 self.render_grid_scalar_overlays(&mut pass, &visible_grid_scalar_overlays);
2204 }
2205 if !visible_grid_extrusion_overlays.is_empty() {
2206 self.render_grid_extrusion_overlays(&mut pass, &visible_grid_extrusion_overlays);
2207 }
2208 if !visible_column_overlays.is_empty() {
2209 self.render_column_overlays(&mut pass, &visible_column_overlays);
2210 }
2211 if !visible_point_cloud_overlays.is_empty() {
2212 self.render_point_cloud_overlays(&mut pass, &visible_point_cloud_overlays);
2213 }
2214
2215 self.render_vector_batches(&mut pass, &self.cached_vector_batches);
2216 self.render_fill_batches(&mut pass, &self.cached_fill_batches);
2217 self.render_fill_pattern_batches(&mut pass, &self.cached_fill_pattern_batches);
2218 self.render_fill_extrusion_batches(&mut pass, &self.cached_fill_extrusion_batches);
2219 self.render_line_batches(&mut pass, &self.cached_line_batches);
2220 self.render_line_pattern_batches(&mut pass, &self.cached_line_pattern_batches);
2221 self.render_circle_batches(&mut pass, &self.cached_circle_batches);
2222 self.render_image_overlay_batches(&mut pass);
2226 self.render_symbol_batch(&mut pass);
2227
2228 if !params.model_instances.is_empty() {
2229 self.render_models(
2230 &mut pass,
2231 params.model_instances,
2232 params.device,
2233 );
2234 }
2235 }
2236 PainterPass::HeatmapAccumulation => {
2237 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2240 label: Some("rustial_pass_heatmap_accum"),
2241 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2242 view: &self.heatmap_accum_view,
2243 resolve_target: None,
2244 ops: wgpu::Operations {
2245 load: wgpu::LoadOp::Clear(wgpu::Color {
2246 r: 0.0,
2247 g: 0.0,
2248 b: 0.0,
2249 a: 0.0,
2250 }),
2251 store: wgpu::StoreOp::Store,
2252 },
2253 })],
2254 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2255 view: &self.depth_view,
2256 depth_ops: Some(wgpu::Operations {
2257 load: wgpu::LoadOp::Load,
2258 store: wgpu::StoreOp::Store,
2259 }),
2260 stencil_ops: None,
2261 }),
2262 ..Default::default()
2263 });
2264 self.render_heatmap_batches(&mut pass, &self.cached_heatmap_batches);
2265 }
2266 PainterPass::HeatmapColormap => {
2267 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2271 label: Some("rustial_pass_heatmap_colormap"),
2272 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2273 view: params.color_view,
2274 resolve_target: None,
2275 ops: wgpu::Operations {
2276 load: wgpu::LoadOp::Load,
2277 store: wgpu::StoreOp::Store,
2278 },
2279 })],
2280 depth_stencil_attachment: None,
2281 ..Default::default()
2282 });
2283 pass.set_pipeline(&self.heatmap_colormap_pipeline.pipeline);
2284 pass.set_bind_group(0, &self.heatmap_colormap_uniform_bind_group, &[]);
2285 pass.set_bind_group(1, &self.heatmap_colormap_textures_bind_group, &[]);
2286 pass.draw(0..3, 0..1); }
2288 PainterPass::HillshadeOverlay => {
2289 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2290 label: Some("rustial_pass_hillshade_overlay"),
2291 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2292 view: params.color_view,
2293 resolve_target: None,
2294 ops: wgpu::Operations {
2295 load: wgpu::LoadOp::Load,
2296 store: wgpu::StoreOp::Store,
2297 },
2298 })],
2299 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2300 view: &self.depth_view,
2301 depth_ops: Some(wgpu::Operations {
2302 load: wgpu::LoadOp::Load,
2303 store: wgpu::StoreOp::Store,
2304 }),
2305 stencil_ops: None,
2306 }),
2307 ..Default::default()
2308 });
2309
2310 if use_shared_terrain {
2311 self.render_shared_hillshade_tiles(
2312 &mut pass,
2313 params.state,
2314 terrain_meshes,
2315 );
2316 } else {
2317 self.render_hillshade_batches(&mut pass, &hillshade_batches);
2318 }
2319 }
2320 }
2321 }
2322
2323 params.queue.submit(std::iter::once(encoder.finish()));
2325
2326 self.prune_height_texture_cache(terrain_meshes);
2327 self.prune_terrain_tile_bind_cache(terrain_meshes);
2328 self.prune_grid_scalar_overlay_cache(&grid_scalar_overlays);
2329 self.prune_grid_extrusion_overlay_cache(&grid_extrusion_overlays);
2330 self.prune_column_overlay_cache(&column_overlays);
2331 self.prune_point_cloud_overlay_cache(&point_cloud_overlays);
2332
2333 let tile_count_before = self.tile_atlas.len();
2335 self.tile_atlas.end_frame();
2336 if self.tile_atlas.len() != tile_count_before {
2337 self.tile_batch_cache_key = None;
2338 }
2339 self.hillshade_atlas.end_frame();
2340 }
2341
2342 fn render_tile_batches<'a>(
2346 &'a self,
2347 pass: &mut wgpu::RenderPass<'a>,
2348 batches: &'a [TilePageBatches],
2349 ) {
2350 pass.set_bind_group(0, &self.uniform_bind_group, &[]);
2351
2352 for (page_idx, batch) in batches.iter().enumerate() {
2353 let bg = match self.page_bind_groups.get(page_idx) {
2354 Some(bg) => bg,
2355 None => continue,
2356 };
2357
2358 if let Some(batch) = batch.opaque.as_ref() {
2359 pass.set_pipeline(&self.tile_pipeline.pipeline);
2360 pass.set_bind_group(1, bg, &[]);
2361 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2362 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2363 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2364 }
2365
2366 if let Some(batch) = batch.translucent.as_ref() {
2367 pass.set_pipeline(&self.tile_pipeline.translucent_pipeline);
2368 pass.set_bind_group(1, bg, &[]);
2369 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2370 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2371 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2372 }
2373 }
2374 }
2375
2376 fn render_grid_scalar_overlays<'a>(
2377 &'a self,
2378 pass: &mut wgpu::RenderPass<'a>,
2379 overlays: &[&'a VisualizationOverlay],
2380 ) {
2381 pass.set_pipeline(&self.grid_scalar_pipeline.pipeline);
2382 pass.set_bind_group(0, &self.grid_scalar_uniform_bind_group, &[]);
2383
2384 for overlay in overlays {
2385 let VisualizationOverlay::GridScalar { layer_id, .. } = overlay else {
2386 continue;
2387 };
2388 let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) else {
2389 continue;
2390 };
2391 pass.set_bind_group(1, &cached.bind_group, &[]);
2392 pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
2393 pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2394 pass.draw_indexed(0..cached.index_count, 0, 0..1);
2395 }
2396 }
2397
2398 fn render_grid_extrusion_overlays<'a>(
2399 &'a self,
2400 pass: &mut wgpu::RenderPass<'a>,
2401 overlays: &[&'a VisualizationOverlay],
2402 ) {
2403 pass.set_pipeline(&self.grid_extrusion_pipeline.pipeline);
2404 pass.set_bind_group(0, &self.grid_extrusion_uniform_bind_group, &[]);
2405
2406 for overlay in overlays {
2407 let VisualizationOverlay::GridExtrusion { layer_id, .. } = overlay else {
2408 continue;
2409 };
2410 let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) else {
2411 continue;
2412 };
2413 pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
2414 pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2415 pass.draw_indexed(0..cached.index_count, 0, 0..1);
2416 }
2417 }
2418
2419 fn render_column_overlays<'a>(
2420 &'a self,
2421 pass: &mut wgpu::RenderPass<'a>,
2422 overlays: &[&'a VisualizationOverlay],
2423 ) {
2424 let Some(mesh) = self.shared_column_mesh.as_ref() else {
2425 return;
2426 };
2427
2428 pass.set_pipeline(&self.column_pipeline.pipeline);
2429 pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
2430
2431 for overlay in overlays {
2432 let VisualizationOverlay::Columns { layer_id, .. } = overlay else {
2433 continue;
2434 };
2435 let Some(cached) = self.column_overlay_cache.get(layer_id) else {
2436 continue;
2437 };
2438 if cached.instance_count == 0 {
2439 continue;
2440 }
2441 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2442 pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
2443 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2444 pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
2445 }
2446 }
2447
2448 fn render_point_cloud_overlays<'a>(
2449 &'a self,
2450 pass: &mut wgpu::RenderPass<'a>,
2451 overlays: &[&'a VisualizationOverlay],
2452 ) {
2453 let Some(mesh) = self.shared_column_mesh.as_ref() else {
2454 return;
2455 };
2456
2457 pass.set_pipeline(&self.column_pipeline.pipeline);
2458 pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
2459
2460 for overlay in overlays {
2461 let VisualizationOverlay::Points { layer_id, .. } = overlay else {
2462 continue;
2463 };
2464 let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) else {
2465 continue;
2466 };
2467 if cached.instance_count == 0 {
2468 continue;
2469 }
2470 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2471 pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
2472 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2473 pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
2474 }
2475 }
2476
2477 fn render_terrain_batches<'a>(
2481 &'a self,
2482 pass: &mut wgpu::RenderPass<'a>,
2483 batches: &'a [Option<TerrainBatch>],
2484 ) {
2485 pass.set_pipeline(&self.terrain_pipeline.pipeline);
2486 pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
2487
2488 for (page_idx, batch) in batches.iter().enumerate() {
2489 let batch = match batch {
2490 Some(b) => b,
2491 None => continue,
2492 };
2493 let bg = match self.page_terrain_bind_groups.get(page_idx) {
2494 Some(bg) => bg,
2495 None => continue,
2496 };
2497
2498 pass.set_bind_group(1, bg, &[]);
2499 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2500 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2501 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2502 }
2503 }
2504
2505 fn render_terrain_data_batches<'a>(
2508 &'a self,
2509 pass: &mut wgpu::RenderPass<'a>,
2510 batches: &'a [Option<TerrainBatch>],
2511 ) {
2512 pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
2513 pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
2514
2515 for batch in batches.iter().flatten() {
2516 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2517 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2518 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2519 }
2520 }
2521
2522 fn render_hillshade_batches<'a>(
2526 &'a self,
2527 pass: &mut wgpu::RenderPass<'a>,
2528 batches: &'a [Option<HillshadeBatch>],
2529 ) {
2530 pass.set_pipeline(&self.hillshade_pipeline.pipeline);
2531 pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
2532
2533 for (page_idx, batch) in batches.iter().enumerate() {
2534 let batch = match batch {
2535 Some(b) => b,
2536 None => continue,
2537 };
2538 let bg = match self.page_hillshade_bind_groups.get(page_idx) {
2539 Some(bg) => bg,
2540 None => continue,
2541 };
2542
2543 pass.set_bind_group(1, bg, &[]);
2544 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2545 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2546 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2547 }
2548 }
2549
2550 fn render_vector_batches<'a>(
2555 &'a self,
2556 pass: &mut wgpu::RenderPass<'a>,
2557 batches: &'a [Option<VectorBatchEntry>],
2558 ) {
2559 let mut pipeline_set = false;
2560
2561 for batch in batches.iter().flatten() {
2562 if !pipeline_set {
2563 pass.set_pipeline(&self.vector_pipeline.pipeline);
2564 pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
2565 pipeline_set = true;
2566 }
2567
2568 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2569 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2570 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2571 }
2572 }
2573
2574 fn render_fill_batches<'a>(
2578 &'a self,
2579 pass: &mut wgpu::RenderPass<'a>,
2580 batches: &'a [Option<FillBatchEntry>],
2581 ) {
2582 for batch in batches.iter().flatten() {
2583 pass.set_pipeline(&self.fill_pipeline.pipeline);
2584 pass.set_bind_group(0, &batch.bind_group, &[]);
2586 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2587 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2588 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2589 }
2590 }
2591
2592 fn render_fill_pattern_batches<'a>(
2596 &'a self,
2597 pass: &mut wgpu::RenderPass<'a>,
2598 batches: &'a [Option<FillPatternBatchEntry>],
2599 ) {
2600 for batch in batches.iter().flatten() {
2601 pass.set_pipeline(&self.fill_pattern_pipeline.pipeline);
2602 pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
2603 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
2604 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2605 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2606 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2607 }
2608 }
2609
2610 fn render_fill_extrusion_batches<'a>(
2614 &'a self,
2615 pass: &mut wgpu::RenderPass<'a>,
2616 batches: &'a [Option<FillExtrusionBatchEntry>],
2617 ) {
2618 let mut pipeline_set = false;
2619
2620 for batch in batches.iter().flatten() {
2621 if !pipeline_set {
2622 pass.set_pipeline(&self.fill_extrusion_pipeline.pipeline);
2623 pass.set_bind_group(0, &self.fill_extrusion_uniform_bind_group, &[]);
2624 pipeline_set = true;
2625 }
2626
2627 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2628 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2629 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2630 }
2631 }
2632
2633 fn render_line_batches<'a>(
2634 &'a self,
2635 pass: &mut wgpu::RenderPass<'a>,
2636 batches: &'a [Option<LineBatchEntry>],
2637 ) {
2638 let mut pipeline_set = false;
2639
2640 for batch in batches.iter().flatten() {
2641 if !pipeline_set {
2642 pass.set_pipeline(&self.line_pipeline.pipeline);
2643 pass.set_bind_group(0, &self.line_uniform_bind_group, &[]);
2644 pipeline_set = true;
2645 }
2646
2647 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2648 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2649 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2650 }
2651 }
2652
2653 fn render_line_pattern_batches<'a>(
2654 &'a self,
2655 pass: &mut wgpu::RenderPass<'a>,
2656 batches: &'a [Option<LinePatternBatchEntry>],
2657 ) {
2658 for batch in batches.iter().flatten() {
2659 pass.set_pipeline(&self.line_pattern_pipeline.pipeline);
2660 pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
2661 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
2662 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2663 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2664 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2665 }
2666 }
2667
2668 fn render_circle_batches<'a>(
2669 &'a self,
2670 pass: &mut wgpu::RenderPass<'a>,
2671 batches: &'a [Option<CircleBatchEntry>],
2672 ) {
2673 let mut pipeline_set = false;
2674
2675 for batch in batches.iter().flatten() {
2676 if !pipeline_set {
2677 pass.set_pipeline(&self.circle_pipeline.pipeline);
2678 pass.set_bind_group(0, &self.circle_uniform_bind_group, &[]);
2679 pipeline_set = true;
2680 }
2681
2682 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2683 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2684 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2685 }
2686 }
2687
2688 fn render_heatmap_batches<'a>(
2689 &'a self,
2690 pass: &mut wgpu::RenderPass<'a>,
2691 batches: &'a [Option<HeatmapBatchEntry>],
2692 ) {
2693 let mut pipeline_set = false;
2694
2695 for batch in batches.iter().flatten() {
2696 if !pipeline_set {
2697 pass.set_pipeline(&self.heatmap_pipeline.pipeline);
2698 pass.set_bind_group(0, &self.heatmap_uniform_bind_group, &[]);
2699 pipeline_set = true;
2700 }
2701
2702 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2703 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2704 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2705 }
2706 }
2707
2708 fn build_image_overlay_batches(
2715 &mut self,
2716 device: &wgpu::Device,
2717 queue: &wgpu::Queue,
2718 overlays: &[rustial_engine::layers::ImageOverlayData],
2719 camera_origin: glam::DVec3,
2720 ) {
2721 let mut old_cache: Vec<CachedImageOverlayBatch> =
2723 std::mem::take(&mut self.cached_image_overlay_batches);
2724
2725 for overlay in overlays {
2726 if overlay.width == 0 || overlay.height == 0 || overlay.data.is_empty() {
2727 continue;
2728 }
2729
2730 let data_arc_ptr = Arc::as_ptr(&overlay.data) as usize;
2731
2732 let cached_idx = old_cache
2734 .iter()
2735 .position(|c| c.layer_id == overlay.layer_id);
2736
2737 let uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
2739 let vertices: Vec<ImageOverlayVertex> = overlay
2740 .corners
2741 .iter()
2742 .zip(uvs.iter())
2743 .map(|(corner, uv)| {
2744 let rel = [
2745 (corner[0] - camera_origin.x) as f32,
2746 (corner[1] - camera_origin.y) as f32,
2747 (corner[2] - camera_origin.z) as f32,
2748 ];
2749 ImageOverlayVertex {
2750 position: rel,
2751 uv: *uv,
2752 opacity: overlay.opacity,
2753 }
2754 })
2755 .collect();
2756 let indices: Vec<u32> = vec![0, 1, 2, 0, 2, 3];
2757
2758 if let Some(idx) = cached_idx {
2759 let mut cached = old_cache.remove(idx);
2760
2761 cached.vertex_buffer =
2763 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2764 label: Some("image_overlay_vb"),
2765 contents: bytemuck::cast_slice(&vertices),
2766 usage: wgpu::BufferUsages::VERTEX,
2767 });
2768 cached.index_buffer =
2769 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2770 label: Some("image_overlay_ib"),
2771 contents: bytemuck::cast_slice(&indices),
2772 usage: wgpu::BufferUsages::INDEX,
2773 });
2774
2775 if cached.data_arc_ptr != data_arc_ptr {
2777 if cached.tex_dimensions == (overlay.width, overlay.height) {
2778 queue.write_texture(
2780 wgpu::TexelCopyTextureInfo {
2781 texture: &cached.texture,
2782 mip_level: 0,
2783 origin: wgpu::Origin3d::ZERO,
2784 aspect: wgpu::TextureAspect::All,
2785 },
2786 &overlay.data,
2787 wgpu::TexelCopyBufferLayout {
2788 offset: 0,
2789 bytes_per_row: Some(overlay.width * 4),
2790 rows_per_image: Some(overlay.height),
2791 },
2792 wgpu::Extent3d {
2793 width: overlay.width,
2794 height: overlay.height,
2795 depth_or_array_layers: 1,
2796 },
2797 );
2798 } else {
2799 let (texture, texture_view, texture_bind_group) =
2801 self.create_overlay_texture(device, queue, overlay);
2802 cached.texture = texture;
2803 cached.texture_view = texture_view;
2804 cached.texture_bind_group = texture_bind_group;
2805 cached.tex_dimensions = (overlay.width, overlay.height);
2806 }
2807 cached.data_arc_ptr = data_arc_ptr;
2808 }
2809
2810 self.cached_image_overlay_batches.push(cached);
2811 } else {
2812 let vertex_buffer =
2814 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2815 label: Some("image_overlay_vb"),
2816 contents: bytemuck::cast_slice(&vertices),
2817 usage: wgpu::BufferUsages::VERTEX,
2818 });
2819 let index_buffer =
2820 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2821 label: Some("image_overlay_ib"),
2822 contents: bytemuck::cast_slice(&indices),
2823 usage: wgpu::BufferUsages::INDEX,
2824 });
2825 let (texture, texture_view, texture_bind_group) =
2826 self.create_overlay_texture(device, queue, overlay);
2827
2828 self.cached_image_overlay_batches.push(CachedImageOverlayBatch {
2829 vertex_buffer,
2830 index_buffer,
2831 texture,
2832 texture_view,
2833 texture_bind_group,
2834 layer_id: overlay.layer_id,
2835 tex_dimensions: (overlay.width, overlay.height),
2836 data_arc_ptr,
2837 });
2838 }
2839 }
2840 }
2842
2843 fn create_overlay_texture(
2845 &self,
2846 device: &wgpu::Device,
2847 queue: &wgpu::Queue,
2848 overlay: &rustial_engine::layers::ImageOverlayData,
2849 ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) {
2850 let texture = device.create_texture(&wgpu::TextureDescriptor {
2851 label: Some("image_overlay_tex"),
2852 size: wgpu::Extent3d {
2853 width: overlay.width,
2854 height: overlay.height,
2855 depth_or_array_layers: 1,
2856 },
2857 mip_level_count: 1,
2858 sample_count: 1,
2859 dimension: wgpu::TextureDimension::D2,
2860 format: wgpu::TextureFormat::Rgba8UnormSrgb,
2861 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2862 view_formats: &[],
2863 });
2864 queue.write_texture(
2865 wgpu::TexelCopyTextureInfo {
2866 texture: &texture,
2867 mip_level: 0,
2868 origin: wgpu::Origin3d::ZERO,
2869 aspect: wgpu::TextureAspect::All,
2870 },
2871 &overlay.data,
2872 wgpu::TexelCopyBufferLayout {
2873 offset: 0,
2874 bytes_per_row: Some(overlay.width * 4),
2875 rows_per_image: Some(overlay.height),
2876 },
2877 wgpu::Extent3d {
2878 width: overlay.width,
2879 height: overlay.height,
2880 depth_or_array_layers: 1,
2881 },
2882 );
2883 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2884 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2885 label: Some("image_overlay_tex_bg"),
2886 layout: &self.image_overlay_pipeline.texture_bind_group_layout,
2887 entries: &[
2888 wgpu::BindGroupEntry {
2889 binding: 0,
2890 resource: wgpu::BindingResource::TextureView(&texture_view),
2891 },
2892 wgpu::BindGroupEntry {
2893 binding: 1,
2894 resource: wgpu::BindingResource::Sampler(&self.sampler),
2895 },
2896 ],
2897 });
2898 (texture, texture_view, texture_bind_group)
2899 }
2900
2901 fn render_image_overlay_batches<'a>(
2903 &'a self,
2904 pass: &mut wgpu::RenderPass<'a>,
2905 ) {
2906 if self.cached_image_overlay_batches.is_empty() {
2907 return;
2908 }
2909 pass.set_pipeline(&self.image_overlay_pipeline.pipeline);
2910 pass.set_bind_group(0, &self.image_overlay_uniform_bind_group, &[]);
2911 for batch in &self.cached_image_overlay_batches {
2912 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
2913 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2914 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2915 pass.draw_indexed(0..6, 0, 0..1);
2916 }
2917 }
2918
2919 fn render_symbol_batch<'a>(
2920 &'a self,
2921 pass: &mut wgpu::RenderPass<'a>,
2922 ) {
2923 let batch = match &self.cached_symbol_batch {
2924 Some(b) => b,
2925 None => return,
2926 };
2927 let atlas_bg = match &self.symbol_atlas_bind_group {
2928 Some(bg) => bg,
2929 None => return,
2930 };
2931
2932 pass.set_pipeline(&self.symbol_pipeline.pipeline);
2933 pass.set_bind_group(0, &self.symbol_uniform_bind_group, &[]);
2934 pass.set_bind_group(1, atlas_bg, &[]);
2935 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2936 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2937 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2938 }
2939
2940 fn render_models<'a>(
2941 &'a self,
2942 pass: &mut wgpu::RenderPass<'a>,
2943 model_instances: &[ModelInstance],
2944 device: &wgpu::Device,
2945 ) {
2946 if model_instances.is_empty() {
2947 return;
2948 }
2949
2950 let cached = match &self.cached_model_transforms {
2951 Some(c) if c.instance_count == model_instances.len() => c,
2952 _ => return,
2953 };
2954
2955 pass.set_pipeline(&self.model_pipeline.pipeline);
2956 pass.set_bind_group(0, &self.model_uniform_bind_group, &[]);
2957
2958 for (i, instance) in model_instances.iter().enumerate() {
2959 let dyn_offset = (i * cached.stride) as u32;
2960 let mesh_key = ModelMeshKey::from_mesh(&instance.mesh);
2961 let cached_mesh = self.model_mesh_cache.get(&mesh_key);
2962
2963 if let Some(cached_mesh) = cached_mesh {
2964 pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
2965 pass.set_vertex_buffer(0, cached_mesh.vertex_buffer.slice(..));
2966 pass.set_index_buffer(cached_mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2967 pass.draw_indexed(0..cached_mesh.index_count, 0, 0..1);
2968 } else {
2969 let vertices = build_model_vertices(&instance.mesh);
2970 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2971 label: Some("model_inline_vb"),
2972 contents: bytemuck::cast_slice(&vertices),
2973 usage: wgpu::BufferUsages::VERTEX,
2974 });
2975 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2976 label: Some("model_inline_ib"),
2977 contents: bytemuck::cast_slice(&instance.mesh.indices),
2978 usage: wgpu::BufferUsages::INDEX,
2979 });
2980 let index_count = instance.mesh.indices.len() as u32;
2981 pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
2982 pass.set_vertex_buffer(0, vb.slice(..));
2983 pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
2984 pass.draw_indexed(0..index_count, 0, 0..1);
2985 }
2986 }
2987 }
2988
2989 pub fn cache_model_meshes(
2995 &mut self,
2996 device: &wgpu::Device,
2997 model_instances: &[ModelInstance],
2998 ) {
2999 for instance in model_instances {
3000 let key = ModelMeshKey::from_mesh(&instance.mesh);
3001 if self.model_mesh_cache.contains_key(&key) {
3002 continue;
3003 }
3004
3005 let vertices = build_model_vertices(&instance.mesh);
3006 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3007 label: Some("cached_model_vb"),
3008 contents: bytemuck::cast_slice(&vertices),
3009 usage: wgpu::BufferUsages::VERTEX,
3010 });
3011 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3012 label: Some("cached_model_ib"),
3013 contents: bytemuck::cast_slice(&instance.mesh.indices),
3014 usage: wgpu::BufferUsages::INDEX,
3015 });
3016
3017 self.model_mesh_cache.insert(
3018 key,
3019 CachedModelMesh {
3020 vertex_buffer: vb,
3021 index_buffer: ib,
3022 index_count: instance.mesh.indices.len() as u32,
3023 },
3024 );
3025 }
3026 }
3027
3028 fn cache_model_transforms(
3037 &mut self,
3038 device: &wgpu::Device,
3039 model_instances: &[ModelInstance],
3040 camera_origin: DVec3,
3041 state: &MapState,
3042 ) {
3043 let min_align = device.limits().min_uniform_buffer_offset_alignment as usize;
3044 let stride = 64_usize.div_ceil(min_align) * min_align;
3045
3046 let mut fp: u64 = model_instances.len() as u64;
3048 let origin_key = [
3049 (camera_origin.x * 100.0) as i64,
3050 (camera_origin.y * 100.0) as i64,
3051 (camera_origin.z * 100.0) as i64,
3052 ];
3053 fp = fp
3054 .wrapping_mul(31)
3055 .wrapping_add(origin_key[0] as u64)
3056 .wrapping_mul(31)
3057 .wrapping_add(origin_key[1] as u64)
3058 .wrapping_mul(31)
3059 .wrapping_add(origin_key[2] as u64);
3060 for instance in model_instances {
3061 fp = fp
3062 .wrapping_mul(31)
3063 .wrapping_add(instance.position.lat.to_bits())
3064 .wrapping_mul(31)
3065 .wrapping_add(instance.position.lon.to_bits())
3066 .wrapping_mul(31)
3067 .wrapping_add(instance.scale.to_bits())
3068 .wrapping_mul(31)
3069 .wrapping_add(instance.heading.to_bits());
3070 }
3071
3072 if let Some(ref cached) = self.cached_model_transforms {
3073 if cached.fingerprint == fp && cached.instance_count == model_instances.len() {
3074 return;
3075 }
3076 }
3077
3078 let mut transform_bytes = vec![0u8; stride * model_instances.len()];
3079 for (i, instance) in model_instances.iter().enumerate() {
3080 let terrain_elev = state.elevation_at(&instance.position);
3081 let altitude = instance.resolve_altitude(terrain_elev);
3082
3083 let world_pos = state.camera().projection().project(&instance.position);
3084 let rel_x = (world_pos.position.x - camera_origin.x) as f32;
3085 let rel_y = (world_pos.position.y - camera_origin.y) as f32;
3086 let rel_z = (altitude - camera_origin.z) as f32;
3087
3088 let scale = instance.scale as f32;
3089 let heading = instance.heading as f32;
3090 let pitch = instance.pitch as f32;
3091 let roll = instance.roll as f32;
3092
3093 let rotation = glam::Quat::from_rotation_z(heading)
3094 * glam::Quat::from_rotation_x(pitch)
3095 * glam::Quat::from_rotation_y(roll);
3096 let transform = Mat4::from_translation(glam::Vec3::new(rel_x, rel_y, rel_z))
3097 * Mat4::from_quat(rotation)
3098 * Mat4::from_scale(glam::Vec3::splat(scale));
3099
3100 let mat = transform.to_cols_array_2d();
3101 let mat_bytes = bytemuck::cast_slice(&mat);
3102 let offset = i * stride;
3103 transform_bytes[offset..offset + 64].copy_from_slice(mat_bytes);
3104 }
3105
3106 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3107 label: Some("model_transforms_cached_buf"),
3108 contents: &transform_bytes,
3109 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3110 });
3111
3112 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3113 label: Some("model_transforms_cached_bg"),
3114 layout: &self.model_pipeline.model_bind_group_layout,
3115 entries: &[wgpu::BindGroupEntry {
3116 binding: 0,
3117 resource: buffer.as_entire_binding(),
3118 }],
3119 });
3120
3121 self.cached_model_transforms = Some(CachedModelTransforms {
3122 buffer,
3123 bind_group,
3124 stride,
3125 instance_count: model_instances.len(),
3126 fingerprint: fp,
3127 })
3128 }
3129
3130 fn rebuild_page_bind_groups(&mut self, device: &wgpu::Device) {
3134 let tile_pages = self.tile_atlas.page_count();
3135 while self.page_bind_groups.len() < tile_pages {
3136 let idx = self.page_bind_groups.len();
3137 let view = &self.tile_atlas.pages[idx].view;
3138 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3139 label: Some(&format!("rustial_tile_page_bg_{idx}")),
3140 layout: &self.tile_pipeline.texture_bind_group_layout,
3141 entries: &[
3142 wgpu::BindGroupEntry {
3143 binding: 0,
3144 resource: wgpu::BindingResource::TextureView(view),
3145 },
3146 wgpu::BindGroupEntry {
3147 binding: 1,
3148 resource: wgpu::BindingResource::Sampler(&self.sampler),
3149 },
3150 ],
3151 });
3152 self.page_bind_groups.push(bg);
3153
3154 let terrain_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3155 label: Some(&format!("rustial_terrain_page_bg_{idx}")),
3156 layout: &self.terrain_pipeline.texture_bind_group_layout,
3157 entries: &[
3158 wgpu::BindGroupEntry {
3159 binding: 0,
3160 resource: wgpu::BindingResource::TextureView(view),
3161 },
3162 wgpu::BindGroupEntry {
3163 binding: 1,
3164 resource: wgpu::BindingResource::Sampler(&self.sampler),
3165 },
3166 ],
3167 });
3168 self.page_terrain_bind_groups.push(terrain_bg);
3169 }
3170
3171 let hillshade_pages = self.hillshade_atlas.page_count();
3172 while self.page_hillshade_bind_groups.len() < hillshade_pages {
3173 let idx = self.page_hillshade_bind_groups.len();
3174 let view = &self.hillshade_atlas.pages[idx].view;
3175 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3176 label: Some(&format!("rustial_hillshade_page_bg_{idx}")),
3177 layout: &self.hillshade_pipeline.texture_bind_group_layout,
3178 entries: &[
3179 wgpu::BindGroupEntry {
3180 binding: 0,
3181 resource: wgpu::BindingResource::TextureView(view),
3182 },
3183 wgpu::BindGroupEntry {
3184 binding: 1,
3185 resource: wgpu::BindingResource::Sampler(&self.sampler),
3186 },
3187 ],
3188 });
3189 self.page_hillshade_bind_groups.push(bg);
3190 }
3191 }
3192
3193 fn get_or_create_shared_grid(
3196 &mut self,
3197 device: &wgpu::Device,
3198 resolution: u16,
3199 ) -> &SharedTerrainGridMesh {
3200 if !self.shared_terrain_grids.contains_key(&resolution) {
3201 let (vertices, indices) = build_shared_terrain_grid(resolution as usize);
3202 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3203 label: Some(&format!("terrain_grid_vb_{resolution}")),
3204 contents: bytemuck::cast_slice(&vertices),
3205 usage: wgpu::BufferUsages::VERTEX,
3206 });
3207 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3208 label: Some(&format!("terrain_grid_ib_{resolution}")),
3209 contents: bytemuck::cast_slice(&indices),
3210 usage: wgpu::BufferUsages::INDEX,
3211 });
3212 self.shared_terrain_grids.insert(
3213 resolution,
3214 SharedTerrainGridMesh {
3215 vertex_buffer: vb,
3216 index_buffer: ib,
3217 index_count: indices.len() as u32,
3218 },
3219 );
3220 }
3221 self.shared_terrain_grids.get(&resolution).unwrap()
3222 }
3223
3224 fn get_or_create_terrain_tile_bind(
3225 &mut self,
3226 device: &wgpu::Device,
3227 queue: &wgpu::Queue,
3228 mesh: &TerrainMeshData,
3229 state: &MapState,
3230 scene_origin: DVec3,
3231 pipeline_kind: TerrainPipelineKind,
3232 ) -> Option<()> {
3233 let elevation = mesh.elevation_texture.as_ref()?;
3234 let key = TerrainTileBindKey {
3235 tile: mesh.tile,
3236 pipeline: pipeline_kind,
3237 };
3238 let origin_key = [
3239 (scene_origin.x * 100.0) as i64,
3240 (scene_origin.y * 100.0) as i64,
3241 (scene_origin.z * 100.0) as i64,
3242 ];
3243
3244 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3246 if cached.origin_key == origin_key && cached.generation == mesh.generation {
3247 return Some(());
3248 }
3249 }
3250
3251 let tile = mesh.tile;
3253 let gen = mesh.generation;
3254 let needs_height = self
3255 .height_texture_cache
3256 .get(&tile)
3257 .map_or(true, |c| c.generation != gen);
3258 if needs_height {
3259 let size = wgpu::Extent3d {
3260 width: elevation.width.max(1),
3261 height: elevation.height.max(1),
3262 depth_or_array_layers: 1,
3263 };
3264 let texture = device.create_texture(&wgpu::TextureDescriptor {
3265 label: Some(&format!("height_tex_{:?}", tile)),
3266 size,
3267 mip_level_count: 1,
3268 sample_count: 1,
3269 dimension: wgpu::TextureDimension::D2,
3270 format: wgpu::TextureFormat::R32Float,
3271 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3272 view_formats: &[],
3273 });
3274 queue.write_texture(
3275 wgpu::TexelCopyTextureInfo {
3276 texture: &texture,
3277 mip_level: 0,
3278 origin: wgpu::Origin3d::ZERO,
3279 aspect: wgpu::TextureAspect::All,
3280 },
3281 bytemuck::cast_slice(&elevation.data),
3282 wgpu::TexelCopyBufferLayout {
3283 offset: 0,
3284 bytes_per_row: Some(elevation.width.max(1) * 4),
3285 rows_per_image: None,
3286 },
3287 size,
3288 );
3289 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3290 self.height_texture_cache
3291 .insert(tile, CachedHeightTexture { generation: gen, view });
3292 }
3293
3294 let tile_uniform = build_terrain_tile_uniform(mesh, elevation, state, scene_origin);
3296
3297 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3298 label: Some(&format!("terrain_tile_uniform_{:?}_{:?}", mesh.tile, pipeline_kind)),
3299 contents: bytemuck::bytes_of(&tile_uniform),
3300 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3301 });
3302
3303 let layout = match pipeline_kind {
3304 TerrainPipelineKind::Terrain => &self.terrain_pipeline.tile_bind_group_layout,
3305 TerrainPipelineKind::TerrainData => &self.terrain_data_pipeline.tile_bind_group_layout,
3306 TerrainPipelineKind::Hillshade => &self.hillshade_pipeline.tile_bind_group_layout,
3307 };
3308
3309 let height_view_ref = &self.height_texture_cache.get(&tile)?.view;
3311 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3312 label: Some(&format!("terrain_tile_bg_{:?}_{:?}", mesh.tile, pipeline_kind)),
3313 layout,
3314 entries: &[
3315 wgpu::BindGroupEntry {
3316 binding: 0,
3317 resource: uniform_buffer.as_entire_binding(),
3318 },
3319 wgpu::BindGroupEntry {
3320 binding: 1,
3321 resource: wgpu::BindingResource::TextureView(height_view_ref),
3322 },
3323 ],
3324 });
3325
3326 self.terrain_tile_bind_cache.insert(
3327 key,
3328 CachedTerrainTileBind {
3329 uniform_buffer,
3330 bind_group,
3331 origin_key,
3332 generation: mesh.generation,
3333 },
3334 );
3335 Some(())
3336 }
3337
3338 fn render_shared_terrain_tiles<'a>(
3339 &'a self,
3340 pass: &mut wgpu::RenderPass<'a>,
3341 _state: &MapState,
3342 terrain_meshes: &[TerrainMeshData],
3343 visible_tiles: &[VisibleTile],
3344 ) {
3345 pass.set_pipeline(&self.terrain_pipeline.pipeline);
3346 pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
3347
3348 for mesh in terrain_meshes {
3349 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3350 Some(g) => g,
3351 None => continue,
3352 };
3353
3354 if let Some(actual) = find_terrain_texture_actual(mesh.tile, visible_tiles) {
3355 if let Some(region) = self.tile_atlas.get(&actual) {
3356 if let Some(bg) = self.page_terrain_bind_groups.get(region.page) {
3357 pass.set_bind_group(1, bg, &[]);
3358 }
3359 }
3360 }
3361
3362 let key = TerrainTileBindKey {
3363 tile: mesh.tile,
3364 pipeline: TerrainPipelineKind::Terrain,
3365 };
3366 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3367 pass.set_bind_group(2, &cached.bind_group, &[]);
3368 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3369 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3370 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3371 }
3372 }
3373 }
3374
3375 fn render_shared_terrain_data_tiles<'a>(
3376 &'a self,
3377 pass: &mut wgpu::RenderPass<'a>,
3378 _state: &MapState,
3379 terrain_meshes: &[TerrainMeshData],
3380 ) {
3381 pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
3382 pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
3383
3384 for mesh in terrain_meshes {
3385 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3386 Some(g) => g,
3387 None => continue,
3388 };
3389
3390 let key = TerrainTileBindKey {
3391 tile: mesh.tile,
3392 pipeline: TerrainPipelineKind::TerrainData,
3393 };
3394 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3395 pass.set_bind_group(1, &cached.bind_group, &[]);
3396 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3397 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3398 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3399 }
3400 }
3401 }
3402
3403 fn render_shared_hillshade_tiles<'a>(
3404 &'a self,
3405 pass: &mut wgpu::RenderPass<'a>,
3406 _state: &MapState,
3407 terrain_meshes: &[TerrainMeshData],
3408 ) {
3409 pass.set_pipeline(&self.hillshade_pipeline.pipeline);
3410 pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
3411
3412 for mesh in terrain_meshes {
3413 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3414 Some(g) => g,
3415 None => continue,
3416 };
3417
3418 if let Some(region) = self.hillshade_atlas.get(&mesh.tile) {
3419 if let Some(bg) = self.page_hillshade_bind_groups.get(region.page) {
3420 pass.set_bind_group(1, bg, &[]);
3421 }
3422 }
3423
3424 let key = TerrainTileBindKey {
3425 tile: mesh.tile,
3426 pipeline: TerrainPipelineKind::Hillshade,
3427 };
3428 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3429 pass.set_bind_group(2, &cached.bind_group, &[]);
3430 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3431 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3432 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3433 }
3434 }
3435 }
3436
3437 fn prune_height_texture_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
3440 let live: std::collections::HashSet<TileId> =
3441 terrain_meshes.iter().map(|m| m.tile).collect();
3442 self.height_texture_cache.retain(|tile, _| live.contains(tile));
3443 }
3444
3445 fn prune_terrain_tile_bind_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
3446 let live: std::collections::HashSet<TileId> =
3447 terrain_meshes.iter().map(|m| m.tile).collect();
3448 self.terrain_tile_bind_cache
3449 .retain(|key, _| live.contains(&key.tile));
3450 }
3451
3452 fn prune_grid_scalar_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3453 let live: std::collections::HashSet<LayerId> = overlays
3454 .iter()
3455 .filter_map(|overlay| match overlay {
3456 VisualizationOverlay::GridScalar { layer_id, .. } => Some(*layer_id),
3457 _ => None,
3458 })
3459 .collect();
3460 self.grid_scalar_overlay_cache
3461 .retain(|layer_id, _| live.contains(layer_id));
3462 }
3463
3464 fn prune_grid_extrusion_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3465 let live: std::collections::HashSet<LayerId> = overlays
3466 .iter()
3467 .filter_map(|overlay| match overlay {
3468 VisualizationOverlay::GridExtrusion { layer_id, .. } => Some(*layer_id),
3469 _ => None,
3470 })
3471 .collect();
3472 self.grid_extrusion_overlay_cache
3473 .retain(|layer_id, _| live.contains(layer_id));
3474 }
3475
3476 fn prune_column_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3477 let live: std::collections::HashSet<LayerId> = overlays
3478 .iter()
3479 .filter_map(|overlay| match overlay {
3480 VisualizationOverlay::Columns { layer_id, .. } => Some(*layer_id),
3481 _ => None,
3482 })
3483 .collect();
3484 self.column_overlay_cache
3485 .retain(|layer_id, _| live.contains(layer_id));
3486 }
3487
3488 fn prune_point_cloud_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3489 let live: std::collections::HashSet<LayerId> = overlays
3490 .iter()
3491 .filter_map(|overlay| match overlay {
3492 VisualizationOverlay::Points { layer_id, .. } => Some(*layer_id),
3493 _ => None,
3494 })
3495 .collect();
3496 self.point_cloud_overlay_cache
3497 .retain(|layer_id, _| live.contains(layer_id));
3498 }
3499
3500 pub fn render_to_buffer(
3524 &mut self,
3525 state: &MapState,
3526 device: &wgpu::Device,
3527 queue: &wgpu::Queue,
3528 visible_tiles: &[VisibleTile],
3529 vector_meshes: &[VectorMeshData],
3530 model_instances: &[ModelInstance],
3531 ) -> Option<Vec<u8>> {
3532 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
3533
3534 let color_tex = device.create_texture(&wgpu::TextureDescriptor {
3535 label: Some("rustial_render_to_buffer_color"),
3536 size: wgpu::Extent3d {
3537 width: self.width,
3538 height: self.height,
3539 depth_or_array_layers: 1,
3540 },
3541 mip_level_count: 1,
3542 sample_count: 1,
3543 dimension: wgpu::TextureDimension::D2,
3544 format,
3545 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3546 view_formats: &[],
3547 });
3548 let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
3549
3550 let clear_color = state.computed_fog().clear_color;
3551
3552 self.render_full(&RenderParams {
3553 state,
3554 device,
3555 queue,
3556 color_view: &color_view,
3557 visible_tiles,
3558 vector_meshes,
3559 model_instances,
3560 clear_color,
3561 });
3562
3563 let bytes_per_row = self.width * 4;
3564 let buffer_size = (bytes_per_row * self.height) as wgpu::BufferAddress;
3565 let readback = device.create_buffer(&wgpu::BufferDescriptor {
3566 label: Some("rustial_render_to_buffer_readback"),
3567 size: buffer_size,
3568 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3569 mapped_at_creation: false,
3570 });
3571
3572 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3573 label: Some("rustial_render_to_buffer_encoder"),
3574 });
3575 encoder.copy_texture_to_buffer(
3576 wgpu::TexelCopyTextureInfo {
3577 texture: &color_tex,
3578 mip_level: 0,
3579 origin: wgpu::Origin3d::ZERO,
3580 aspect: wgpu::TextureAspect::All,
3581 },
3582 wgpu::TexelCopyBufferInfo {
3583 buffer: &readback,
3584 layout: wgpu::TexelCopyBufferLayout {
3585 offset: 0,
3586 bytes_per_row: Some(bytes_per_row),
3587 rows_per_image: Some(self.height),
3588 },
3589 },
3590 wgpu::Extent3d {
3591 width: self.width,
3592 height: self.height,
3593 depth_or_array_layers: 1,
3594 },
3595 );
3596 queue.submit(std::iter::once(encoder.finish()));
3597
3598 let slice = readback.slice(..);
3599 let (tx, rx) = std::sync::mpsc::channel();
3600 slice.map_async(wgpu::MapMode::Read, move |res| {
3601 let _ = tx.send(res);
3602 });
3603 let _ = device.poll(wgpu::PollType::wait());
3604 rx.recv().ok()?.ok()?;
3605
3606 let data = slice.get_mapped_range().to_vec();
3607 readback.unmap();
3608 Some(data)
3609 }
3610
3611 pub fn width(&self) -> u32 {
3613 self.width
3614 }
3615
3616 pub fn height(&self) -> u32 {
3618 self.height
3619 }
3620
3621 pub fn visualization_perf_stats(&self) -> VisualizationPerfStats {
3623 self.visualization_perf_stats
3624 }
3625
3626 pub fn tile_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
3632 self.tile_atlas.diagnostics()
3633 }
3634
3635 pub fn hillshade_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
3637 self.hillshade_atlas.diagnostics()
3638 }
3639}
3640
3641fn build_grid_scalar_geometry(
3646 grid: &rustial_engine::GeoGrid,
3647 state: &MapState,
3648 scene_origin: DVec3,
3649) -> (Vec<GridScalarVertex>, Vec<u32>) {
3650 let rows = grid.rows.max(1);
3651 let cols = grid.cols.max(1);
3652 let mut vertices = Vec::with_capacity((rows + 1) * (cols + 1));
3653 let mut indices = Vec::with_capacity(rows * cols * 6);
3654
3655 for row in 0..=rows {
3656 for col in 0..=cols {
3657 let u = col as f32 / cols as f32;
3658 let v = row as f32 / rows as f32;
3659 let coord = grid_corner_coord(grid, row, col, state);
3660 let projected = state.camera().projection().project(&coord);
3661 vertices.push(GridScalarVertex {
3662 position: [
3663 (projected.position.x - scene_origin.x) as f32,
3664 (projected.position.y - scene_origin.y) as f32,
3665 (projected.position.z - scene_origin.z + 0.05) as f32,
3666 ],
3667 uv: [u, v],
3668 });
3669 }
3670 }
3671
3672 for row in 0..rows {
3673 for col in 0..cols {
3674 let tl = (row * (cols + 1) + col) as u32;
3675 let tr = tl + 1;
3676 let bl = ((row + 1) * (cols + 1) + col) as u32;
3677 let br = bl + 1;
3678 indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
3679 }
3680 }
3681
3682 (vertices, indices)
3683}
3684
3685fn grid_corner_coord(
3686 grid: &rustial_engine::GeoGrid,
3687 row: usize,
3688 col: usize,
3689 state: &MapState,
3690) -> rustial_math::GeoCoord {
3691 let dx = col as f64 * grid.cell_width;
3692 let dy = row as f64 * grid.cell_height;
3693 let (sin_r, cos_r) = grid.rotation.sin_cos();
3694 let rx = dx * cos_r - dy * sin_r;
3695 let ry = dx * sin_r + dy * cos_r;
3696 let coord = offset_geo_coord(&grid.origin, rx, ry);
3697 let altitude = resolve_grid_surface_altitude(grid, &coord, state);
3698 rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude)
3699}
3700
3701fn create_grid_scalar_texture(
3702 device: &wgpu::Device,
3703 queue: &wgpu::Queue,
3704 field: &rustial_engine::ScalarField2D,
3705) -> wgpu::Texture {
3706 let size = wgpu::Extent3d {
3707 width: field.cols.max(1) as u32,
3708 height: field.rows.max(1) as u32,
3709 depth_or_array_layers: 1,
3710 };
3711 let texture = device.create_texture(&wgpu::TextureDescriptor {
3712 label: Some("grid_scalar_field_texture"),
3713 size,
3714 mip_level_count: 1,
3715 sample_count: 1,
3716 dimension: wgpu::TextureDimension::D2,
3717 format: wgpu::TextureFormat::R32Float,
3718 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3719 view_formats: &[],
3720 });
3721 write_grid_scalar_texture(queue, &texture, field);
3722 texture
3723}
3724
3725fn write_grid_scalar_texture(
3726 queue: &wgpu::Queue,
3727 texture: &wgpu::Texture,
3728 field: &rustial_engine::ScalarField2D,
3729) {
3730 let size = wgpu::Extent3d {
3731 width: field.cols.max(1) as u32,
3732 height: field.rows.max(1) as u32,
3733 depth_or_array_layers: 1,
3734 };
3735 queue.write_texture(
3736 wgpu::TexelCopyTextureInfo {
3737 texture,
3738 mip_level: 0,
3739 origin: wgpu::Origin3d::ZERO,
3740 aspect: wgpu::TextureAspect::All,
3741 },
3742 bytemuck::cast_slice(&field.data),
3743 wgpu::TexelCopyBufferLayout {
3744 offset: 0,
3745 bytes_per_row: Some(field.cols.max(1) as u32 * 4),
3746 rows_per_image: Some(field.rows.max(1) as u32),
3747 },
3748 size,
3749 );
3750}
3751
3752fn create_grid_scalar_ramp_texture(
3753 device: &wgpu::Device,
3754 queue: &wgpu::Queue,
3755 ramp: &rustial_engine::ColorRamp,
3756) -> wgpu::Texture {
3757 let width = 256u32;
3758 let data = ramp.as_texture_data(width);
3759 let size = wgpu::Extent3d {
3760 width,
3761 height: 1,
3762 depth_or_array_layers: 1,
3763 };
3764 let texture = device.create_texture(&wgpu::TextureDescriptor {
3765 label: Some("grid_scalar_ramp_texture"),
3766 size,
3767 mip_level_count: 1,
3768 sample_count: 1,
3769 dimension: wgpu::TextureDimension::D2,
3770 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3771 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3772 view_formats: &[],
3773 });
3774 queue.write_texture(
3775 wgpu::TexelCopyTextureInfo {
3776 texture: &texture,
3777 mip_level: 0,
3778 origin: wgpu::Origin3d::ZERO,
3779 aspect: wgpu::TextureAspect::All,
3780 },
3781 &data,
3782 wgpu::TexelCopyBufferLayout {
3783 offset: 0,
3784 bytes_per_row: Some(width * 4),
3785 rows_per_image: Some(1),
3786 },
3787 size,
3788 );
3789 texture
3790}
3791
3792fn create_heatmap_accum_texture(
3798 device: &wgpu::Device,
3799 width: u32,
3800 height: u32,
3801) -> (wgpu::Texture, wgpu::TextureView) {
3802 let texture = device.create_texture(&wgpu::TextureDescriptor {
3803 label: Some("heatmap_accum_texture"),
3804 size: wgpu::Extent3d {
3805 width,
3806 height,
3807 depth_or_array_layers: 1,
3808 },
3809 mip_level_count: 1,
3810 sample_count: 1,
3811 dimension: wgpu::TextureDimension::D2,
3812 format: wgpu::TextureFormat::R16Float,
3813 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3814 view_formats: &[],
3815 });
3816 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3817 (texture, view)
3818}
3819
3820fn create_default_heatmap_ramp_texture(
3825 device: &wgpu::Device,
3826 queue: &wgpu::Queue,
3827) -> wgpu::Texture {
3828 const WIDTH: u32 = 256;
3829 let stops: &[(f32, [u8; 4])] = &[
3830 (0.00, [0, 0, 0, 0]),
3831 (0.10, [65, 105, 225, 255]),
3832 (0.30, [0, 255, 255, 255]),
3833 (0.50, [0, 255, 0, 255]),
3834 (0.70, [255, 255, 0, 255]),
3835 (1.00, [255, 0, 0, 255]),
3836 ];
3837
3838 let mut data = vec![0u8; WIDTH as usize * 4];
3839 for i in 0..WIDTH as usize {
3840 let t = i as f32 / (WIDTH - 1) as f32;
3841 let mut lo = 0;
3843 for s in 1..stops.len() {
3844 if stops[s].0 >= t {
3845 lo = s - 1;
3846 break;
3847 }
3848 }
3849 let hi = (lo + 1).min(stops.len() - 1);
3850 let range = stops[hi].0 - stops[lo].0;
3851 let frac = if range > 0.0 {
3852 (t - stops[lo].0) / range
3853 } else {
3854 0.0
3855 };
3856 for c in 0..4 {
3857 let a = stops[lo].1[c] as f32;
3858 let b = stops[hi].1[c] as f32;
3859 data[i * 4 + c] = (a + (b - a) * frac).round() as u8;
3860 }
3861 }
3862
3863 let size = wgpu::Extent3d {
3864 width: WIDTH,
3865 height: 1,
3866 depth_or_array_layers: 1,
3867 };
3868 let texture = device.create_texture(&wgpu::TextureDescriptor {
3869 label: Some("heatmap_ramp_texture"),
3870 size,
3871 mip_level_count: 1,
3872 sample_count: 1,
3873 dimension: wgpu::TextureDimension::D2,
3874 format: wgpu::TextureFormat::Rgba8Unorm,
3875 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3876 view_formats: &[],
3877 });
3878 queue.write_texture(
3879 wgpu::TexelCopyTextureInfo {
3880 texture: &texture,
3881 mip_level: 0,
3882 origin: wgpu::Origin3d::ZERO,
3883 aspect: wgpu::TextureAspect::All,
3884 },
3885 &data,
3886 wgpu::TexelCopyBufferLayout {
3887 offset: 0,
3888 bytes_per_row: Some(WIDTH * 4),
3889 rows_per_image: Some(1),
3890 },
3891 size,
3892 );
3893 texture
3894}
3895
3896fn create_heatmap_colormap_bind_group(
3898 device: &wgpu::Device,
3899 layout: &wgpu::BindGroupLayout,
3900 accum_view: &wgpu::TextureView,
3901 ramp_view: &wgpu::TextureView,
3902 sampler: &wgpu::Sampler,
3903) -> wgpu::BindGroup {
3904 device.create_bind_group(&wgpu::BindGroupDescriptor {
3905 label: Some("heatmap_colormap_textures_bg"),
3906 layout,
3907 entries: &[
3908 wgpu::BindGroupEntry {
3909 binding: 0,
3910 resource: wgpu::BindingResource::TextureView(accum_view),
3911 },
3912 wgpu::BindGroupEntry {
3913 binding: 1,
3914 resource: wgpu::BindingResource::TextureView(ramp_view),
3915 },
3916 wgpu::BindGroupEntry {
3917 binding: 2,
3918 resource: wgpu::BindingResource::Sampler(sampler),
3919 },
3920 ],
3921 })
3922}
3923
3924fn build_grid_scalar_uniform(
3925 grid: &rustial_engine::GeoGrid,
3926 field: &rustial_engine::ScalarField2D,
3927 state: &MapState,
3928 scene_origin: DVec3,
3929 opacity: f32,
3930) -> GridScalarUniform {
3931 let projection_kind = match state.camera().projection() {
3932 rustial_engine::CameraProjection::WebMercator => 0.0,
3933 rustial_engine::CameraProjection::Equirectangular => 1.0,
3934 _ => 0.0,
3935 };
3936 let base_altitude = match grid.altitude_mode {
3937 rustial_engine::AltitudeMode::ClampToGround => 0.0,
3938 rustial_engine::AltitudeMode::RelativeToGround => grid.origin.alt as f32,
3939 rustial_engine::AltitudeMode::Absolute => grid.origin.alt as f32,
3940 };
3941 GridScalarUniform {
3942 origin_counts: [grid.origin.lat as f32, grid.origin.lon as f32, grid.rows as f32, grid.cols as f32],
3943 grid_params: [grid.cell_width as f32, grid.cell_height as f32, grid.rotation as f32, opacity],
3944 scene_origin: [scene_origin.x as f32, scene_origin.y as f32, scene_origin.z as f32, projection_kind],
3945 value_params: [
3946 field.min,
3947 field.max,
3948 field.nan_value.unwrap_or(0.0),
3949 if field.nan_value.is_some() { 1.0 } else { 0.0 },
3950 ],
3951 base_altitude: [base_altitude, 0.0, 0.0, 0.0],
3952 }
3953}
3954
3955fn grid_scalar_ramp_fingerprint(ramp: &rustial_engine::ColorRamp) -> u64 {
3956 let mut h = ramp.stops.len() as u64;
3957 for stop in &ramp.stops {
3958 h = h
3959 .wrapping_mul(31)
3960 .wrapping_add(stop.value.to_bits() as u64)
3961 .wrapping_mul(31)
3962 .wrapping_add(stop.color[0].to_bits() as u64)
3963 .wrapping_mul(31)
3964 .wrapping_add(stop.color[1].to_bits() as u64)
3965 .wrapping_mul(31)
3966 .wrapping_add(stop.color[2].to_bits() as u64)
3967 .wrapping_mul(31)
3968 .wrapping_add(stop.color[3].to_bits() as u64);
3969 }
3970 h
3971}
3972
3973fn grid_extrusion_params_fingerprint(params: &rustial_engine::ExtrusionParams) -> u64 {
3974 (params.height_scale.to_bits())
3975 .wrapping_mul(31)
3976 .wrapping_add(params.base_meters.to_bits())
3977}
3978
3979fn grid_extrusion_grid_fingerprint(grid: &rustial_engine::GeoGrid) -> u64 {
3980 let mut h = 17u64;
3981 h = h.wrapping_mul(31).wrapping_add(grid.origin.lat.to_bits());
3982 h = h.wrapping_mul(31).wrapping_add(grid.origin.lon.to_bits());
3983 h = h.wrapping_mul(31).wrapping_add(grid.origin.alt.to_bits());
3984 h = h.wrapping_mul(31).wrapping_add(grid.rows as u64);
3985 h = h.wrapping_mul(31).wrapping_add(grid.cols as u64);
3986 h = h.wrapping_mul(31).wrapping_add(grid.cell_width.to_bits());
3987 h = h.wrapping_mul(31).wrapping_add(grid.cell_height.to_bits());
3988 h = h.wrapping_mul(31).wrapping_add(grid.rotation.to_bits());
3989 h = h.wrapping_mul(31).wrapping_add(match grid.altitude_mode {
3990 rustial_engine::AltitudeMode::ClampToGround => 0,
3991 rustial_engine::AltitudeMode::RelativeToGround => 1,
3992 rustial_engine::AltitudeMode::Absolute => 2,
3993 });
3994 h
3995}
3996
3997fn build_grid_extrusion_geometry(
3998 grid: &rustial_engine::GeoGrid,
3999 field: &rustial_engine::ScalarField2D,
4000 ramp: &rustial_engine::ColorRamp,
4001 params: &rustial_engine::ExtrusionParams,
4002 state: &MapState,
4003 scene_origin: DVec3,
4004) -> (Vec<GridExtrusionVertex>, Vec<u32>) {
4005 let mut vertices = Vec::new();
4006 let mut indices = Vec::new();
4007
4008 for row in 0..grid.rows {
4009 for col in 0..grid.cols {
4010 let Some(value) = field.sample(row, col) else {
4011 continue;
4012 };
4013
4014 let t = field.normalized(row, col).unwrap_or(0.5);
4015 let color = ramp.evaluate(t);
4016 let corners = grid_cell_corners_world(grid, row, col, state, scene_origin, params);
4017 append_extruded_cell_geometry(
4018 &mut vertices,
4019 &mut indices,
4020 corners,
4021 (value as f32) * params.height_scale as f32,
4022 color,
4023 );
4024 }
4025 }
4026
4027 (vertices, indices)
4028}
4029
4030fn append_extruded_cell_geometry(
4031 vertices: &mut Vec<GridExtrusionVertex>,
4032 indices: &mut Vec<u32>,
4033 corners: [[f32; 3]; 4],
4034 extrusion_height: f32,
4035 color: [f32; 4],
4036) {
4037 let [nw, ne, sw, se] = corners;
4038 let top = [
4039 [nw[0], nw[1], nw[2] + extrusion_height],
4040 [ne[0], ne[1], ne[2] + extrusion_height],
4041 [sw[0], sw[1], sw[2] + extrusion_height],
4042 [se[0], se[1], se[2] + extrusion_height],
4043 ];
4044 let base = [
4045 nw,
4046 ne,
4047 sw,
4048 se,
4049 ];
4050
4051 append_quad(vertices, indices, top[0], top[1], top[2], top[3], [0.0, 0.0, 1.0], color);
4052 append_quad(vertices, indices, base[0], base[1], top[0], top[1], [0.0, -1.0, 0.0], color);
4053 append_quad(vertices, indices, top[2], top[3], base[2], base[3], [0.0, 1.0, 0.0], color);
4054 append_quad(vertices, indices, base[0], top[0], base[2], top[2], [-1.0, 0.0, 0.0], color);
4055 append_quad(vertices, indices, top[1], base[1], top[3], base[3], [1.0, 0.0, 0.0], color);
4056}
4057
4058fn append_quad(
4059 vertices: &mut Vec<GridExtrusionVertex>,
4060 indices: &mut Vec<u32>,
4061 a: [f32; 3],
4062 b: [f32; 3],
4063 c: [f32; 3],
4064 d: [f32; 3],
4065 normal: [f32; 3],
4066 color: [f32; 4],
4067) {
4068 let base_index = vertices.len() as u32;
4069 vertices.extend_from_slice(&[
4070 GridExtrusionVertex { position: a, normal, color },
4071 GridExtrusionVertex { position: b, normal, color },
4072 GridExtrusionVertex { position: c, normal, color },
4073 GridExtrusionVertex { position: d, normal, color },
4074 ]);
4075 indices.extend_from_slice(&[
4076 base_index,
4077 base_index + 2,
4078 base_index + 1,
4079 base_index + 1,
4080 base_index + 2,
4081 base_index + 3,
4082 ]);
4083}
4084
4085fn grid_cell_corners_world(
4086 grid: &rustial_engine::GeoGrid,
4087 row: usize,
4088 col: usize,
4089 state: &MapState,
4090 scene_origin: DVec3,
4091 params: &rustial_engine::ExtrusionParams,
4092) -> [[f32; 3]; 4] {
4093 let nw = project_grid_offset(
4094 grid,
4095 col as f64 * grid.cell_width,
4096 row as f64 * grid.cell_height,
4097 state,
4098 scene_origin,
4099 params,
4100 );
4101 let ne = project_grid_offset(
4102 grid,
4103 (col + 1) as f64 * grid.cell_width,
4104 row as f64 * grid.cell_height,
4105 state,
4106 scene_origin,
4107 params,
4108 );
4109 let sw = project_grid_offset(
4110 grid,
4111 col as f64 * grid.cell_width,
4112 (row + 1) as f64 * grid.cell_height,
4113 state,
4114 scene_origin,
4115 params,
4116 );
4117 let se = project_grid_offset(
4118 grid,
4119 (col + 1) as f64 * grid.cell_width,
4120 (row + 1) as f64 * grid.cell_height,
4121 state,
4122 scene_origin,
4123 params,
4124 );
4125 [nw, ne, sw, se]
4126}
4127
4128fn project_grid_offset(
4129 grid: &rustial_engine::GeoGrid,
4130 dx: f64,
4131 dy: f64,
4132 state: &MapState,
4133 scene_origin: DVec3,
4134 params: &rustial_engine::ExtrusionParams,
4135) -> [f32; 3] {
4136 let (sin_r, cos_r) = grid.rotation.sin_cos();
4137 let rx = dx * cos_r - dy * sin_r;
4138 let ry = dx * sin_r + dy * cos_r;
4139 let coord = offset_geo_coord(&grid.origin, rx, ry);
4140 let altitude = resolve_grid_base_altitude(grid, &coord, state, params) as f64;
4141 let elevated_coord = rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude);
4142 let projected = state.camera().projection().project(&elevated_coord);
4143 [
4144 (projected.position.x - scene_origin.x) as f32,
4145 (projected.position.y - scene_origin.y) as f32,
4146 (projected.position.z - scene_origin.z) as f32,
4147 ]
4148}
4149
4150fn offset_geo_coord(origin: &rustial_math::GeoCoord, dx_meters: f64, dy_meters: f64) -> rustial_math::GeoCoord {
4151 const METERS_PER_DEG_LAT: f64 = 111_320.0;
4152 let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
4153 let cos_lat = origin.lat.to_radians().cos().max(1e-10);
4154 let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
4155 rustial_math::GeoCoord::new(lat, lon, origin.alt)
4156}
4157
4158fn resolve_grid_base_altitude(
4159 grid: &rustial_engine::GeoGrid,
4160 coord: &rustial_math::GeoCoord,
4161 state: &MapState,
4162 params: &rustial_engine::ExtrusionParams,
4163) -> f32 {
4164 let terrain = state.elevation_at(coord).unwrap_or(0.0);
4165 match grid.altitude_mode {
4166 rustial_engine::AltitudeMode::ClampToGround => (terrain + params.base_meters) as f32,
4167 rustial_engine::AltitudeMode::RelativeToGround => {
4168 (terrain + grid.origin.alt + params.base_meters) as f32
4169 }
4170 rustial_engine::AltitudeMode::Absolute => (grid.origin.alt + params.base_meters) as f32,
4171 }
4172}
4173
4174fn resolve_grid_surface_altitude(
4175 grid: &rustial_engine::GeoGrid,
4176 coord: &rustial_math::GeoCoord,
4177 state: &MapState,
4178) -> f64 {
4179 let terrain = state.elevation_at(coord).unwrap_or(0.0);
4180 match grid.altitude_mode {
4181 rustial_engine::AltitudeMode::ClampToGround => terrain,
4182 rustial_engine::AltitudeMode::RelativeToGround => terrain + grid.origin.alt,
4183 rustial_engine::AltitudeMode::Absolute => grid.origin.alt,
4184 }
4185}
4186
4187fn build_unit_column_mesh() -> (Vec<ColumnVertex>, Vec<u32>) {
4188 let vertices = vec![
4189 ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [0.0, 0.0, 1.0] },
4191 ColumnVertex { position: [0.5, -0.5, 1.0], normal: [0.0, 0.0, 1.0] },
4192 ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [0.0, 0.0, 1.0] },
4193 ColumnVertex { position: [0.5, 0.5, 1.0], normal: [0.0, 0.0, 1.0] },
4194 ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [0.0, 0.0, -1.0] },
4196 ColumnVertex { position: [0.5, -0.5, 0.0], normal: [0.0, 0.0, -1.0] },
4197 ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 0.0, -1.0] },
4198 ColumnVertex { position: [0.5, 0.5, 0.0], normal: [0.0, 0.0, -1.0] },
4199 ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [0.0, -1.0, 0.0] },
4201 ColumnVertex { position: [0.5, -0.5, 0.0], normal: [0.0, -1.0, 0.0] },
4202 ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [0.0, -1.0, 0.0] },
4203 ColumnVertex { position: [0.5, -0.5, 1.0], normal: [0.0, -1.0, 0.0] },
4204 ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [0.0, 1.0, 0.0] },
4206 ColumnVertex { position: [0.5, 0.5, 1.0], normal: [0.0, 1.0, 0.0] },
4207 ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 1.0, 0.0] },
4208 ColumnVertex { position: [0.5, 0.5, 0.0], normal: [0.0, 1.0, 0.0] },
4209 ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [-1.0, 0.0, 0.0] },
4211 ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [-1.0, 0.0, 0.0] },
4212 ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [-1.0, 0.0, 0.0] },
4213 ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [-1.0, 0.0, 0.0] },
4214 ColumnVertex { position: [0.5, -0.5, 1.0], normal: [1.0, 0.0, 0.0] },
4216 ColumnVertex { position: [0.5, -0.5, 0.0], normal: [1.0, 0.0, 0.0] },
4217 ColumnVertex { position: [0.5, 0.5, 1.0], normal: [1.0, 0.0, 0.0] },
4218 ColumnVertex { position: [0.5, 0.5, 0.0], normal: [1.0, 0.0, 0.0] },
4219 ];
4220 let indices = vec![
4221 0, 2, 1, 1, 2, 3,
4222 4, 5, 6, 5, 7, 6,
4223 8, 10, 9, 9, 10, 11,
4224 12, 14, 13, 13, 14, 15,
4225 16, 18, 17, 17, 18, 19,
4226 20, 22, 21, 21, 22, 23,
4227 ];
4228 (vertices, indices)
4229}
4230
4231fn build_column_instances(
4232 columns: &rustial_engine::ColumnInstanceSet,
4233 ramp: &rustial_engine::ColorRamp,
4234 state: &MapState,
4235 scene_origin: DVec3,
4236) -> Vec<ColumnInstanceData> {
4237 let (min_height, max_height) = column_height_range(columns);
4238 columns
4239 .columns
4240 .iter()
4241 .map(|column| {
4242 let projected = state.camera().projection().project(&column.position);
4243 let base_z = resolve_column_base_altitude(column, state);
4244 let normalized = if (max_height - min_height).abs() < f64::EPSILON {
4245 0.5
4246 } else {
4247 ((column.height - min_height) / (max_height - min_height)).clamp(0.0, 1.0)
4248 } as f32;
4249 let color = column.color.unwrap_or_else(|| ramp.evaluate(normalized));
4250 ColumnInstanceData {
4251 base_position: [
4252 (projected.position.x - scene_origin.x) as f32,
4253 (projected.position.y - scene_origin.y) as f32,
4254 (base_z - scene_origin.z) as f32,
4255 ],
4256 dimensions: [column.width as f32, column.height as f32, 0.0, 0.0],
4257 color,
4258 }
4259 })
4260 .collect()
4261}
4262
4263fn build_point_instances(
4264 points: &rustial_engine::PointInstanceSet,
4265 ramp: &rustial_engine::ColorRamp,
4266 state: &MapState,
4267 scene_origin: DVec3,
4268) -> Vec<ColumnInstanceData> {
4269 points
4270 .points
4271 .iter()
4272 .map(|point| {
4273 let projected = state.camera().projection().project(&point.position);
4274 let center_z = resolve_point_altitude(point, state);
4275 let diameter = (point.radius * 2.0) as f32;
4276 let color = point.color.unwrap_or_else(|| ramp.evaluate(point.intensity.clamp(0.0, 1.0)));
4277 ColumnInstanceData {
4278 base_position: [
4279 (projected.position.x - scene_origin.x) as f32,
4280 (projected.position.y - scene_origin.y) as f32,
4281 (center_z - scene_origin.z - point.radius) as f32,
4282 ],
4283 dimensions: [diameter, diameter, 0.0, 0.0],
4284 color,
4285 }
4286 })
4287 .collect()
4288}
4289
4290fn column_height_range(columns: &rustial_engine::ColumnInstanceSet) -> (f64, f64) {
4291 let mut min_height = f64::INFINITY;
4292 let mut max_height = f64::NEG_INFINITY;
4293 for column in &columns.columns {
4294 min_height = min_height.min(column.height);
4295 max_height = max_height.max(column.height);
4296 }
4297 if min_height.is_infinite() || max_height.is_infinite() {
4298 (0.0, 0.0)
4299 } else {
4300 (min_height, max_height)
4301 }
4302}
4303
4304fn resolve_point_altitude(
4305 point: &rustial_engine::PointInstance,
4306 state: &MapState,
4307) -> f64 {
4308 let terrain = state.elevation_at(&point.position).unwrap_or(0.0);
4309 match point.altitude_mode {
4310 rustial_engine::AltitudeMode::ClampToGround => terrain,
4311 rustial_engine::AltitudeMode::RelativeToGround => terrain + point.position.alt,
4312 rustial_engine::AltitudeMode::Absolute => point.position.alt,
4313 }
4314}
4315
4316fn resolve_column_base_altitude(
4317 column: &rustial_engine::ColumnInstance,
4318 state: &MapState,
4319) -> f64 {
4320 let terrain = state.elevation_at(&column.position).unwrap_or(0.0);
4321 match column.altitude_mode {
4322 rustial_engine::AltitudeMode::ClampToGround => terrain + column.base,
4323 rustial_engine::AltitudeMode::RelativeToGround => terrain + column.position.alt + column.base,
4324 rustial_engine::AltitudeMode::Absolute => column.position.alt + column.base,
4325 }
4326}
4327
4328fn column_set_fingerprint(columns: &rustial_engine::ColumnInstanceSet) -> u64 {
4329 let mut h = columns.columns.len() as u64;
4330 for column in &columns.columns {
4331 h = h.wrapping_mul(31).wrapping_add(column.position.lat.to_bits());
4332 h = h.wrapping_mul(31).wrapping_add(column.position.lon.to_bits());
4333 h = h.wrapping_mul(31).wrapping_add(column.position.alt.to_bits());
4334 h = h.wrapping_mul(31).wrapping_add(column.height.to_bits());
4335 h = h.wrapping_mul(31).wrapping_add(column.base.to_bits());
4336 h = h.wrapping_mul(31).wrapping_add(column.width.to_bits());
4337 h = h.wrapping_mul(31).wrapping_add(column.pick_id);
4338 h = h.wrapping_mul(31).wrapping_add(match column.altitude_mode {
4339 rustial_engine::AltitudeMode::ClampToGround => 0,
4340 rustial_engine::AltitudeMode::RelativeToGround => 1,
4341 rustial_engine::AltitudeMode::Absolute => 2,
4342 });
4343 if let Some(color) = column.color {
4344 h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
4345 h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
4346 h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
4347 h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
4348 }
4349 }
4350 h
4351}
4352
4353fn point_set_fingerprint(points: &rustial_engine::PointInstanceSet) -> u64 {
4354 let mut h = points.points.len() as u64;
4355 for point in &points.points {
4356 h = h.wrapping_mul(31).wrapping_add(point.position.lat.to_bits());
4357 h = h.wrapping_mul(31).wrapping_add(point.position.lon.to_bits());
4358 h = h.wrapping_mul(31).wrapping_add(point.position.alt.to_bits());
4359 h = h.wrapping_mul(31).wrapping_add(point.radius.to_bits());
4360 h = h.wrapping_mul(31).wrapping_add(point.intensity.to_bits() as u64);
4361 h = h.wrapping_mul(31).wrapping_add(point.pick_id);
4362 h = h.wrapping_mul(31).wrapping_add(match point.altitude_mode {
4363 rustial_engine::AltitudeMode::ClampToGround => 0,
4364 rustial_engine::AltitudeMode::RelativeToGround => 1,
4365 rustial_engine::AltitudeMode::Absolute => 2,
4366 });
4367 if let Some(color) = point.color {
4368 h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
4369 h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
4370 h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
4371 h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
4372 }
4373 }
4374 h
4375}
4376
4377fn visualization_overlay_intersects_scene_viewport(
4378 overlay: &VisualizationOverlay,
4379 state: &MapState,
4380) -> bool {
4381 let scene_origin = state.scene_world_origin();
4382 let Some(bounds) = visualization_overlay_world_bounds(overlay, state, scene_origin) else {
4383 return false;
4384 };
4385 bounds.intersects(state.scene_viewport_bounds())
4386}
4387
4388fn visualization_overlay_world_bounds(
4389 overlay: &VisualizationOverlay,
4390 state: &MapState,
4391 scene_origin: DVec3,
4392) -> Option<rustial_math::WorldBounds> {
4393 match overlay {
4394 VisualizationOverlay::GridScalar { grid, .. }
4395 | VisualizationOverlay::GridExtrusion { grid, .. } => Some(grid_world_bounds(grid, state, scene_origin)),
4396 VisualizationOverlay::Columns { columns, .. } => column_world_bounds(columns, state, scene_origin),
4397 VisualizationOverlay::Points { points, .. } => point_world_bounds(points, state, scene_origin),
4398 }
4399}
4400
4401fn grid_world_bounds(
4402 grid: &rustial_engine::GeoGrid,
4403 state: &MapState,
4404 scene_origin: DVec3,
4405) -> rustial_math::WorldBounds {
4406 let corners = [
4407 grid_corner_coord(grid, 0, 0, state),
4408 grid_corner_coord(grid, 0, grid.cols, state),
4409 grid_corner_coord(grid, grid.rows, 0, state),
4410 grid_corner_coord(grid, grid.rows, grid.cols, state),
4411 ];
4412 let projected: Vec<_> = corners
4413 .iter()
4414 .map(|coord| state.camera().projection().project(coord))
4415 .collect();
4416 let mut bounds = rustial_math::WorldBounds::new(
4417 rustial_math::WorldCoord::new(
4418 projected[0].position.x - scene_origin.x,
4419 projected[0].position.y - scene_origin.y,
4420 projected[0].position.z - scene_origin.z,
4421 ),
4422 rustial_math::WorldCoord::new(
4423 projected[0].position.x - scene_origin.x,
4424 projected[0].position.y - scene_origin.y,
4425 projected[0].position.z - scene_origin.z,
4426 ),
4427 );
4428 for projected in projected.into_iter().skip(1) {
4429 bounds.extend_point(&rustial_math::WorldCoord::new(
4430 projected.position.x - scene_origin.x,
4431 projected.position.y - scene_origin.y,
4432 projected.position.z - scene_origin.z,
4433 ));
4434 }
4435 bounds
4436}
4437
4438fn point_world_bounds(
4439 points: &rustial_engine::PointInstanceSet,
4440 state: &MapState,
4441 scene_origin: DVec3,
4442) -> Option<rustial_math::WorldBounds> {
4443 let mut bounds: Option<rustial_math::WorldBounds> = None;
4444 for point in &points.points {
4445 let projected = state.camera().projection().project(&point.position);
4446 let radius = point.radius;
4447 let center_z = resolve_point_altitude(point, state) - scene_origin.z;
4448 let point_bounds = rustial_math::WorldBounds::new(
4449 rustial_math::WorldCoord::new(
4450 projected.position.x - scene_origin.x - radius,
4451 projected.position.y - scene_origin.y - radius,
4452 center_z - radius,
4453 ),
4454 rustial_math::WorldCoord::new(
4455 projected.position.x - scene_origin.x + radius,
4456 projected.position.y - scene_origin.y + radius,
4457 center_z + radius,
4458 ),
4459 );
4460 if let Some(existing) = bounds.as_mut() {
4461 existing.extend(&point_bounds);
4462 } else {
4463 bounds = Some(point_bounds);
4464 }
4465 }
4466 bounds
4467}
4468
4469fn column_world_bounds(
4470 columns: &rustial_engine::ColumnInstanceSet,
4471 state: &MapState,
4472 scene_origin: DVec3,
4473) -> Option<rustial_math::WorldBounds> {
4474 let mut bounds: Option<rustial_math::WorldBounds> = None;
4475 for column in &columns.columns {
4476 let projected = state.camera().projection().project(&column.position);
4477 let base_z = resolve_column_base_altitude(column, state) - scene_origin.z;
4478 let half_width = column.width * 0.5;
4479 let column_bounds = rustial_math::WorldBounds::new(
4480 rustial_math::WorldCoord::new(
4481 projected.position.x - scene_origin.x - half_width,
4482 projected.position.y - scene_origin.y - half_width,
4483 base_z,
4484 ),
4485 rustial_math::WorldCoord::new(
4486 projected.position.x - scene_origin.x + half_width,
4487 projected.position.y - scene_origin.y + half_width,
4488 base_z + column.height,
4489 ),
4490 );
4491 if let Some(existing) = bounds.as_mut() {
4492 existing.extend(&column_bounds);
4493 } else {
4494 bounds = Some(column_bounds);
4495 }
4496 }
4497 bounds
4498}
4499
4500fn build_shared_terrain_grid(resolution: usize) -> (Vec<TerrainGridVertex>, Vec<u32>) {
4501 let res = resolution.max(2);
4502 let mut vertices = Vec::with_capacity(res * res);
4503 let mut indices = Vec::with_capacity((res - 1) * (res - 1) * 6);
4504
4505 for row in 0..res {
4506 for col in 0..res {
4507 let u = col as f32 / (res - 1) as f32;
4508 let v = row as f32 / (res - 1) as f32;
4509 vertices.push(TerrainGridVertex { uv: [u, v], skirt: 0.0 });
4510 }
4511 }
4512
4513 for row in 0..(res - 1) {
4514 for col in 0..(res - 1) {
4515 let tl = (row * res + col) as u32;
4516 let tr = tl + 1;
4517 let bl = ((row + 1) * res + col) as u32;
4518 let br = bl + 1;
4519 indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
4520 }
4521 }
4522
4523 let edges: [Vec<usize>; 4] = [
4524 (0..res).collect(),
4525 ((res - 1) * res..res * res).collect(),
4526 (0..res).map(|r| r * res).collect(),
4527 (0..res).map(|r| r * res + res - 1).collect(),
4528 ];
4529
4530 for edge in &edges {
4531 for i in 0..edge.len() - 1 {
4532 let a = edge[i] as u32;
4533 let b = edge[i + 1] as u32;
4534 let uv_a = vertices[edge[i]].uv;
4535 let uv_b = vertices[edge[i + 1]].uv;
4536 let base_a = vertices.len() as u32;
4537 let base_b = base_a + 1;
4538 vertices.push(TerrainGridVertex { uv: uv_a, skirt: 1.0 });
4539 vertices.push(TerrainGridVertex { uv: uv_b, skirt: 1.0 });
4540 indices.extend_from_slice(&[a, base_a, b, b, base_a, base_b]);
4541 }
4542 }
4543
4544 (vertices, indices)
4545}
4546
4547fn build_terrain_tile_uniform(
4548 mesh: &TerrainMeshData,
4549 elevation: &rustial_engine::TerrainElevationTexture,
4550 state: &MapState,
4551 scene_origin: DVec3,
4552) -> TerrainTileUniform {
4553 let nw = rustial_math::tile_to_geo(&mesh.tile);
4554 let se = rustial_math::tile_xy_to_geo(
4555 mesh.tile.zoom,
4556 mesh.tile.x as f64 + 1.0,
4557 mesh.tile.y as f64 + 1.0,
4558 );
4559 let projection_kind = match state.camera().projection() {
4560 rustial_engine::CameraProjection::WebMercator => 0.0,
4561 rustial_engine::CameraProjection::Equirectangular => 1.0,
4562 _ => 0.0,
4563 };
4564 let skirt = rustial_engine::skirt_height(
4565 mesh.tile.zoom,
4566 mesh.vertical_exaggeration as f64,
4567 ) as f32;
4568 let skirt_base = (elevation.min_elev * mesh.vertical_exaggeration - skirt)
4569 .max(-skirt * 3.0);
4570 let elev_region = if mesh.tile != mesh.elevation_source_tile {
4571 rustial_engine::elevation_region_in_texture_space(
4572 mesh.elevation_region,
4573 elevation.width,
4574 elevation.height,
4575 )
4576 } else {
4577 mesh.elevation_region
4578 };
4579 TerrainTileUniform {
4580 geo_bounds: [nw.lat as f32, nw.lon as f32, se.lat as f32, se.lon as f32],
4581 scene_origin: [
4582 scene_origin.x as f32,
4583 scene_origin.y as f32,
4584 scene_origin.z as f32,
4585 projection_kind,
4586 ],
4587 elev_params: [
4588 mesh.vertical_exaggeration,
4589 skirt_base,
4590 elevation.min_elev,
4591 elevation.max_elev,
4592 ],
4593 elev_region: [
4594 elev_region.u_min,
4595 elev_region.v_min,
4596 elev_region.u_max,
4597 elev_region.v_max,
4598 ],
4599 }
4600}
4601
4602fn build_model_vertices(mesh: &rustial_engine::ModelMesh) -> Vec<ModelVertex> {
4603 debug_assert_eq!(mesh.positions.len(), mesh.normals.len());
4604 debug_assert_eq!(mesh.positions.len(), mesh.uvs.len());
4605
4606 mesh.positions
4607 .iter()
4608 .zip(mesh.normals.iter())
4609 .zip(mesh.uvs.iter())
4610 .map(|((pos, normal), uv)| ModelVertex {
4611 position: *pos,
4612 normal: *normal,
4613 uv: *uv,
4614 })
4615 .collect()
4616}
4617
4618#[cfg(test)]
4619mod tests {
4620 use super::*;
4621 use rustial_engine::{ColorRamp, ColorStop, ColumnInstance, ColumnInstanceSet, GeoCoord, GeoGrid, VisualizationOverlay};
4622
4623 fn visible_tile_with_fade(fade_opacity: f32) -> VisibleTile {
4624 let id = TileId::new(3, 4, 2);
4625 VisibleTile {
4626 target: id,
4627 actual: id,
4628 data: None,
4629 fade_opacity,
4630 }
4631 }
4632
4633 fn test_ramp() -> ColorRamp {
4634 ColorRamp::new(vec![
4635 ColorStop { value: 0.0, color: [0.0, 0.0, 1.0, 0.5] },
4636 ColorStop { value: 1.0, color: [1.0, 0.0, 0.0, 0.8] },
4637 ])
4638 }
4639
4640 #[test]
4641 fn tile_batch_cache_key_changes_when_fade_opacity_changes() {
4642 let a = [visible_tile_with_fade(0.25)];
4643 let b = [visible_tile_with_fade(0.75)];
4644
4645 let key_a = TileBatchCacheKey::new(&a, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
4646 let key_b = TileBatchCacheKey::new(&b, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
4647
4648 assert_ne!(key_a, key_b, "tile batch cache key must include fade-sensitive inputs");
4649 }
4650
4651 #[test]
4652 fn tile_batch_cache_key_stays_equal_when_fade_opacity_matches() {
4653 let a = [visible_tile_with_fade(1.0)];
4654 let b = [visible_tile_with_fade(1.0)];
4655
4656 let key_a = TileBatchCacheKey::new(&a, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
4657 let key_b = TileBatchCacheKey::new(&b, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
4658
4659 assert_eq!(key_a, key_b);
4660 }
4661
4662 #[test]
4663 fn diff_column_instance_ranges_tracks_contiguous_changes() {
4664 let old = vec![
4665 ColumnInstanceData {
4666 base_position: [0.0, 0.0, 0.0],
4667 dimensions: [1.0, 2.0, 0.0, 0.0],
4668 color: [1.0, 0.0, 0.0, 1.0],
4669 },
4670 ColumnInstanceData {
4671 base_position: [1.0, 0.0, 0.0],
4672 dimensions: [1.0, 2.0, 0.0, 0.0],
4673 color: [0.0, 1.0, 0.0, 1.0],
4674 },
4675 ColumnInstanceData {
4676 base_position: [2.0, 0.0, 0.0],
4677 dimensions: [1.0, 2.0, 0.0, 0.0],
4678 color: [0.0, 0.0, 1.0, 1.0],
4679 },
4680 ColumnInstanceData {
4681 base_position: [3.0, 0.0, 0.0],
4682 dimensions: [1.0, 2.0, 0.0, 0.0],
4683 color: [1.0, 1.0, 0.0, 1.0],
4684 },
4685 ];
4686 let mut new = old.clone();
4687 new[1].dimensions[1] = 4.0;
4688 new[2].color = [1.0, 0.0, 1.0, 1.0];
4689
4690 assert_eq!(diff_column_instance_ranges(&old, &new), vec![1..3]);
4691 }
4692
4693 #[test]
4694 fn diff_column_instance_ranges_splits_disjoint_changes() {
4695 let old = vec![
4696 ColumnInstanceData {
4697 base_position: [0.0, 0.0, 0.0],
4698 dimensions: [1.0, 2.0, 0.0, 0.0],
4699 color: [1.0, 0.0, 0.0, 1.0],
4700 },
4701 ColumnInstanceData {
4702 base_position: [1.0, 0.0, 0.0],
4703 dimensions: [1.0, 2.0, 0.0, 0.0],
4704 color: [0.0, 1.0, 0.0, 1.0],
4705 },
4706 ColumnInstanceData {
4707 base_position: [2.0, 0.0, 0.0],
4708 dimensions: [1.0, 2.0, 0.0, 0.0],
4709 color: [0.0, 0.0, 1.0, 1.0],
4710 },
4711 ];
4712 let mut new = old.clone();
4713 new[0].dimensions[0] = 3.0;
4714 new[2].base_position[2] = 5.0;
4715
4716 assert_eq!(diff_column_instance_ranges(&old, &new), vec![0..1, 2..3]);
4717 }
4718
4719 #[test]
4720 fn visualization_overlay_visibility_rejects_far_grid() {
4721 let mut state = MapState::new();
4722 state.set_viewport(1280, 720);
4723 state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
4724 state.set_camera_distance(1_000.0);
4725 state.update_camera(1.0 / 60.0);
4726
4727 let overlay = VisualizationOverlay::GridScalar {
4728 layer_id: LayerId::next(),
4729 grid: GeoGrid::new(GeoCoord::from_lat_lon(70.0, 120.0), 2, 2, 50.0, 50.0),
4730 field: rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0; 4]),
4731 ramp: test_ramp(),
4732 };
4733
4734 assert!(!visualization_overlay_intersects_scene_viewport(&overlay, &state));
4735 }
4736
4737 #[test]
4738 fn visualization_overlay_visibility_accepts_near_columns() {
4739 let mut state = MapState::new();
4740 state.set_viewport(1280, 720);
4741 state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
4742 state.set_camera_distance(1_000.0);
4743 state.update_camera(1.0 / 60.0);
4744
4745 let overlay = VisualizationOverlay::Columns {
4746 layer_id: LayerId::next(),
4747 columns: ColumnInstanceSet::new(vec![
4748 ColumnInstance::new(GeoCoord::from_lat_lon(0.0, 0.0), 10.0, 5.0),
4749 ]),
4750 ramp: test_ramp(),
4751 };
4752
4753 assert!(visualization_overlay_intersects_scene_viewport(&overlay, &state));
4754 }
4755}