1use crate::gpu::terrain_vertex::TerrainVertex;
57use crate::gpu::tile_atlas::TileAtlas;
58use crate::gpu::vector_vertex::VectorVertex;
59use crate::gpu::vertex::TileVertex;
60use glam::DVec3;
61use rustial_engine::{
62 CameraProjection, PreparedHillshadeRaster, TerrainMeshData, TileId, VectorMeshData, VisibleTile,
63};
64use wgpu::util::DeviceExt;
65
66const MAX_FALLBACK_DEPTH: u8 = 8;
68
69pub(crate) fn find_terrain_texture_actual(
70 tile: TileId,
71 visible_tiles: &[VisibleTile],
72) -> Option<TileId> {
73 if let Some(vt) = visible_tiles
74 .iter()
75 .find(|vt| vt.target == tile && vt.data.is_some())
76 {
77 return Some(vt.actual);
78 }
79
80 let mut current = tile;
81 let mut depth = 0u8;
82 while depth < MAX_FALLBACK_DEPTH {
83 let Some(parent) = current.parent() else {
84 break;
85 };
86 if let Some(vt) = visible_tiles
87 .iter()
88 .find(|vt| vt.target == parent && vt.data.is_some())
89 {
90 return Some(vt.actual);
91 }
92 if visible_tiles
93 .iter()
94 .any(|vt| vt.actual == parent && vt.data.is_some())
95 {
96 return Some(parent);
97 }
98 current = parent;
99 depth += 1;
100 }
101
102 None
103}
104
105pub struct TileBatch {
115 pub vertex_buffer: wgpu::Buffer,
117 pub index_buffer: wgpu::Buffer,
119 pub index_count: u32,
121}
122
123pub struct TilePageBatches {
130 pub opaque: Option<TileBatch>,
132 pub translucent: Option<TileBatch>,
134}
135
136fn tile_opacity_for(vt: &VisibleTile) -> f32 {
137 let fallback_zoom_delta = if vt.actual.zoom > vt.target.zoom {
138 (vt.actual.zoom - vt.target.zoom) as f32
139 } else {
140 vt.target.zoom.saturating_sub(vt.actual.zoom) as f32
141 };
142 let base_opacity = if vt.target == vt.actual {
143 1.0_f32
144 } else {
145 (0.9_f32 - 0.12_f32 * fallback_zoom_delta).clamp(0.55_f32, 0.9_f32)
146 };
147 base_opacity * vt.fade_opacity
148}
149
150fn tile_requires_blended_pass(vt: &VisibleTile) -> bool {
151 tile_opacity_for(vt) < 0.999_999
152}
153
154fn build_tile_batch(
155 device: &wgpu::Device,
156 verts: Vec<TileVertex>,
157 idxs: Vec<u32>,
158) -> Option<TileBatch> {
159 if verts.is_empty() {
160 return None;
161 }
162
163 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
164 label: Some("tile_batch_vb"),
165 contents: bytemuck::cast_slice(&verts),
166 usage: wgpu::BufferUsages::VERTEX,
167 });
168 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
169 label: Some("tile_batch_ib"),
170 contents: bytemuck::cast_slice(&idxs),
171 usage: wgpu::BufferUsages::INDEX,
172 });
173 Some(TileBatch {
174 vertex_buffer,
175 index_buffer,
176 index_count: idxs.len() as u32,
177 })
178}
179
180pub fn build_tile_batches(
201 device: &wgpu::Device,
202 visible_tiles: &[VisibleTile],
203 atlas: &TileAtlas,
204 camera_origin: DVec3,
205 projection: CameraProjection,
206) -> Vec<TilePageBatches> {
207 if atlas.page_count() == 0 {
208 return Vec::new();
209 }
210
211 let page_count = atlas.page_count();
214 let mut page_verts_opaque: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
215 let mut page_idxs_opaque: Vec<Vec<u32>> = vec![Vec::new(); page_count];
216 let mut page_verts_translucent: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
217 let mut page_idxs_translucent: Vec<Vec<u32>> = vec![Vec::new(); page_count];
218
219 for vt in visible_tiles {
220 let region = match atlas.get(&vt.actual) {
221 Some(r) => r,
222 None => continue,
223 };
224
225 let geometry_tile = if vt.actual.zoom > vt.target.zoom {
230 vt.actual
231 } else {
232 vt.target
233 };
234 let [sw, se, ne, nw] = projected_tile_corners(geometry_tile, projection, camera_origin);
235
236 let texture_region = vt.texture_region();
237
238 let (page_verts, page_idxs) = if tile_requires_blended_pass(vt) {
239 (&mut page_verts_translucent, &mut page_idxs_translucent)
240 } else {
241 (&mut page_verts_opaque, &mut page_idxs_opaque)
242 };
243
244 let base = page_verts[region.page].len() as u32;
246 let tile_opacity = tile_opacity_for(vt);
247
248 page_verts[region.page].extend_from_slice(&[
252 TileVertex {
253 position: [sw.x as f32, sw.y as f32, sw.z as f32],
254 uv: region.remap_uv(texture_region.u_min, texture_region.v_max),
255 opacity: tile_opacity,
256 },
257 TileVertex {
258 position: [se.x as f32, se.y as f32, se.z as f32],
259 uv: region.remap_uv(texture_region.u_max, texture_region.v_max),
260 opacity: tile_opacity,
261 },
262 TileVertex {
263 position: [ne.x as f32, ne.y as f32, ne.z as f32],
264 uv: region.remap_uv(texture_region.u_max, texture_region.v_min),
265 opacity: tile_opacity,
266 },
267 TileVertex {
268 position: [nw.x as f32, nw.y as f32, nw.z as f32],
269 uv: region.remap_uv(texture_region.u_min, texture_region.v_min),
270 opacity: tile_opacity,
271 },
272 ]);
273
274 page_idxs[region.page].extend_from_slice(&[
276 base,
277 base + 1,
278 base + 2,
279 base,
280 base + 2,
281 base + 3,
282 ]);
283 }
284
285 page_verts_opaque
286 .into_iter()
287 .zip(page_idxs_opaque)
288 .zip(
289 page_verts_translucent
290 .into_iter()
291 .zip(page_idxs_translucent),
292 )
293 .map(
294 |((opaque_verts, opaque_idxs), (translucent_verts, translucent_idxs))| {
295 TilePageBatches {
296 opaque: build_tile_batch(device, opaque_verts, opaque_idxs),
297 translucent: build_tile_batch(device, translucent_verts, translucent_idxs),
298 }
299 },
300 )
301 .collect()
302}
303
304fn projected_tile_corners(
305 tile: TileId,
306 projection: CameraProjection,
307 camera_origin: DVec3,
308) -> [glam::DVec3; 4] {
309 let southwest =
310 glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 1.0)) - camera_origin;
311 let southeast =
312 glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 1.0)) - camera_origin;
313 let northeast =
314 glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 0.0)) - camera_origin;
315 let northwest =
316 glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 0.0)) - camera_origin;
317 [southwest, southeast, northeast, northwest]
318}
319
320pub struct TerrainBatch {
329 pub vertex_buffer: wgpu::Buffer,
331 pub index_buffer: wgpu::Buffer,
333 pub index_count: u32,
335}
336
337pub struct HillshadeBatch {
339 pub vertex_buffer: wgpu::Buffer,
341 pub index_buffer: wgpu::Buffer,
343 pub index_count: u32,
345}
346
347pub fn build_terrain_batches(
362 device: &wgpu::Device,
363 terrain_meshes: &[TerrainMeshData],
364 atlas: &TileAtlas,
365 camera_origin: DVec3,
366 visible_tiles: &[VisibleTile],
367) -> Vec<Option<TerrainBatch>> {
368 if atlas.page_count() == 0 || terrain_meshes.is_empty() {
369 return Vec::new();
370 }
371
372 let page_count = atlas.page_count();
373 let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
374 let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
375
376 for mesh in terrain_meshes {
377 let actual_tile =
378 find_terrain_texture_actual(mesh.tile, visible_tiles).unwrap_or(mesh.tile);
379 let texture_region = rustial_engine::VisibleTile {
380 target: mesh.tile,
381 actual: actual_tile,
382 data: None,
383 fade_opacity: 1.0,
384 }
385 .texture_region();
386
387 let region = match atlas.get(&actual_tile) {
388 Some(r) => r,
389 None => continue,
390 };
391
392 debug_assert_eq!(
393 mesh.positions.len(),
394 mesh.uvs.len(),
395 "TerrainMeshData positions/uvs length mismatch for tile {:?}",
396 mesh.tile,
397 );
398 debug_assert_eq!(
399 mesh.positions.len(),
400 mesh.normals.len(),
401 "TerrainMeshData positions/normals length mismatch for tile {:?}",
402 mesh.tile,
403 );
404
405 let vert_count = mesh.positions.len();
406 let base = page_verts[region.page].len() as u32;
407 page_verts[region.page].reserve(vert_count);
408
409 for i in 0..vert_count {
410 let pos = &mesh.positions[i];
411 let uv = &mesh.uvs[i];
412 let normal = &mesh.normals[i];
413
414 let mapped_u =
415 texture_region.u_min + (texture_region.u_max - texture_region.u_min) * uv[0];
416 let mapped_v =
417 texture_region.v_min + (texture_region.v_max - texture_region.v_min) * uv[1];
418
419 page_verts[region.page].push(TerrainVertex {
420 position: [
421 (pos[0] - camera_origin.x) as f32,
422 (pos[1] - camera_origin.y) as f32,
423 (pos[2] - camera_origin.z) as f32,
424 ],
425 uv: region.remap_uv(mapped_u, mapped_v),
426 normal: *normal,
427 });
428 }
429
430 page_idxs[region.page].reserve(mesh.indices.len());
431 for idx in &mesh.indices {
432 debug_assert!(
433 (*idx as usize) < vert_count,
434 "TerrainMeshData index {} out of bounds (vertex_count={}) for tile {:?}",
435 idx,
436 vert_count,
437 mesh.tile,
438 );
439 page_idxs[region.page].push(base + idx);
440 }
441 }
442
443 page_verts
444 .into_iter()
445 .zip(page_idxs)
446 .map(|(verts, idxs)| {
447 if verts.is_empty() {
448 return None;
449 }
450 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
451 label: Some("terrain_batch_vb"),
452 contents: bytemuck::cast_slice(&verts),
453 usage: wgpu::BufferUsages::VERTEX,
454 });
455 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
456 label: Some("terrain_batch_ib"),
457 contents: bytemuck::cast_slice(&idxs),
458 usage: wgpu::BufferUsages::INDEX,
459 });
460 Some(TerrainBatch {
461 vertex_buffer,
462 index_buffer,
463 index_count: idxs.len() as u32,
464 })
465 })
466 .collect()
467}
468
469pub fn build_hillshade_batches(
471 device: &wgpu::Device,
472 terrain_meshes: &[TerrainMeshData],
473 hillshade_rasters: &[PreparedHillshadeRaster],
474 atlas: &TileAtlas,
475 camera_origin: DVec3,
476) -> Vec<Option<HillshadeBatch>> {
477 if atlas.page_count() == 0 || terrain_meshes.is_empty() || hillshade_rasters.is_empty() {
478 return Vec::new();
479 }
480
481 let available: std::collections::HashSet<TileId> =
482 hillshade_rasters.iter().map(|r| r.tile).collect();
483 let page_count = atlas.page_count();
484 let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
485 let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
486
487 for mesh in terrain_meshes {
488 if !available.contains(&mesh.tile) {
489 continue;
490 }
491
492 let region = match atlas.get(&mesh.tile) {
493 Some(r) => r,
494 None => continue,
495 };
496
497 let vert_count = mesh.positions.len();
498 let base = page_verts[region.page].len() as u32;
499 page_verts[region.page].reserve(vert_count);
500
501 for i in 0..vert_count {
502 let pos = &mesh.positions[i];
503 let uv = &mesh.uvs[i];
504 let normal = &mesh.normals[i];
505
506 page_verts[region.page].push(TerrainVertex {
507 position: [
508 (pos[0] - camera_origin.x) as f32,
509 (pos[1] - camera_origin.y) as f32,
510 (pos[2] - camera_origin.z) as f32,
511 ],
512 uv: region.remap_uv(uv[0], uv[1]),
513 normal: *normal,
514 });
515 }
516
517 page_idxs[region.page].reserve(mesh.indices.len());
518 for idx in &mesh.indices {
519 page_idxs[region.page].push(base + idx);
520 }
521 }
522
523 page_verts
524 .into_iter()
525 .zip(page_idxs)
526 .map(|(verts, idxs)| {
527 if verts.is_empty() {
528 return None;
529 }
530 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
531 label: Some("hillshade_batch_vb"),
532 contents: bytemuck::cast_slice(&verts),
533 usage: wgpu::BufferUsages::VERTEX,
534 });
535 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
536 label: Some("hillshade_batch_ib"),
537 contents: bytemuck::cast_slice(&idxs),
538 usage: wgpu::BufferUsages::INDEX,
539 });
540 Some(HillshadeBatch {
541 vertex_buffer,
542 index_buffer,
543 index_count: idxs.len() as u32,
544 })
545 })
546 .collect()
547}
548
549pub struct VectorBatchEntry {
559 pub vertex_buffer: wgpu::Buffer,
561 pub index_buffer: wgpu::Buffer,
563 pub index_count: u32,
565}
566
567pub fn build_vector_batch(
584 device: &wgpu::Device,
585 mesh: &VectorMeshData,
586 camera_origin: DVec3,
587) -> Option<VectorBatchEntry> {
588 if mesh.indices.is_empty() {
589 return None;
590 }
591
592 debug_assert_eq!(
593 mesh.positions.len(),
594 mesh.colors.len(),
595 "VectorMeshData positions/colors length mismatch ({} vs {})",
596 mesh.positions.len(),
597 mesh.colors.len(),
598 );
599
600 let vertices: Vec<VectorVertex> = mesh
601 .positions
602 .iter()
603 .zip(mesh.colors.iter())
604 .map(|(pos, color)| VectorVertex {
605 position: [
606 (pos[0] - camera_origin.x) as f32,
607 (pos[1] - camera_origin.y) as f32,
608 (pos[2] - camera_origin.z) as f32,
609 ],
610 color: *color,
611 })
612 .collect();
613
614 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
615 label: Some("vector_batch_vb"),
616 contents: bytemuck::cast_slice(&vertices),
617 usage: wgpu::BufferUsages::VERTEX,
618 });
619 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
620 label: Some("vector_batch_ib"),
621 contents: bytemuck::cast_slice(&mesh.indices),
622 usage: wgpu::BufferUsages::INDEX,
623 });
624
625 Some(VectorBatchEntry {
626 vertex_buffer,
627 index_buffer,
628 index_count: mesh.indices.len() as u32,
629 })
630}
631
632pub struct FillExtrusionBatchEntry {
640 pub vertex_buffer: wgpu::Buffer,
642 pub index_buffer: wgpu::Buffer,
644 pub index_count: u32,
646}
647
648pub fn build_fill_extrusion_batch(
652 device: &wgpu::Device,
653 mesh: &VectorMeshData,
654 camera_origin: DVec3,
655) -> Option<FillExtrusionBatchEntry> {
656 use crate::gpu::fill_extrusion_vertex::FillExtrusionVertex;
657
658 if mesh.indices.is_empty() || mesh.normals.is_empty() {
659 return None;
660 }
661
662 debug_assert_eq!(
663 mesh.positions.len(),
664 mesh.normals.len(),
665 "FillExtrusion positions/normals length mismatch ({} vs {})",
666 mesh.positions.len(),
667 mesh.normals.len(),
668 );
669
670 let vertices: Vec<FillExtrusionVertex> = mesh
671 .positions
672 .iter()
673 .zip(mesh.normals.iter())
674 .zip(mesh.colors.iter())
675 .map(|((pos, normal), color)| FillExtrusionVertex {
676 position: [
677 (pos[0] - camera_origin.x) as f32,
678 (pos[1] - camera_origin.y) as f32,
679 (pos[2] - camera_origin.z) as f32,
680 ],
681 normal: *normal,
682 color: *color,
683 })
684 .collect();
685
686 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
687 label: Some("fill_extrusion_batch_vb"),
688 contents: bytemuck::cast_slice(&vertices),
689 usage: wgpu::BufferUsages::VERTEX,
690 });
691 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
692 label: Some("fill_extrusion_batch_ib"),
693 contents: bytemuck::cast_slice(&mesh.indices),
694 usage: wgpu::BufferUsages::INDEX,
695 });
696
697 Some(FillExtrusionBatchEntry {
698 vertex_buffer,
699 index_buffer,
700 index_count: mesh.indices.len() as u32,
701 })
702}
703
704pub struct FillBatchEntry {
710 pub vertex_buffer: wgpu::Buffer,
712 pub index_buffer: wgpu::Buffer,
714 pub index_count: u32,
716 pub fill_params_buffer: wgpu::Buffer,
718 pub bind_group: wgpu::BindGroup,
720}
721
722pub fn build_fill_batch(
726 device: &wgpu::Device,
727 mesh: &VectorMeshData,
728 camera_origin: DVec3,
729 uniform_buffer: &wgpu::Buffer,
730 fill_bind_group_layout: &wgpu::BindGroupLayout,
731) -> Option<FillBatchEntry> {
732 use crate::gpu::fill_vertex::FillVertex;
733 use crate::pipeline::fill_pipeline::FillParamsUniform;
734
735 if mesh.indices.is_empty() {
736 return None;
737 }
738
739 let vertices: Vec<FillVertex> = mesh
740 .positions
741 .iter()
742 .zip(mesh.colors.iter())
743 .map(|(pos, color)| FillVertex {
744 position: [
745 (pos[0] - camera_origin.x) as f32,
746 (pos[1] - camera_origin.y) as f32,
747 (pos[2] - camera_origin.z) as f32,
748 ],
749 color: *color,
750 })
751 .collect();
752
753 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
754 label: Some("fill_batch_vb"),
755 contents: bytemuck::cast_slice(&vertices),
756 usage: wgpu::BufferUsages::VERTEX,
757 });
758 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
759 label: Some("fill_batch_ib"),
760 contents: bytemuck::cast_slice(&mesh.indices),
761 usage: wgpu::BufferUsages::INDEX,
762 });
763
764 let fill_params = FillParamsUniform {
765 fill_translate: mesh.fill_translate,
766 fill_opacity: mesh.fill_opacity,
767 fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
768 outline_color: mesh.fill_outline_color,
769 };
770 let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
771 label: Some("fill_params_buf"),
772 contents: bytemuck::bytes_of(&fill_params),
773 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
774 });
775
776 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
777 label: Some("fill_batch_bg"),
778 layout: fill_bind_group_layout,
779 entries: &[
780 wgpu::BindGroupEntry {
781 binding: 0,
782 resource: uniform_buffer.as_entire_binding(),
783 },
784 wgpu::BindGroupEntry {
785 binding: 1,
786 resource: fill_params_buffer.as_entire_binding(),
787 },
788 ],
789 });
790
791 Some(FillBatchEntry {
792 vertex_buffer,
793 index_buffer,
794 index_count: mesh.indices.len() as u32,
795 fill_params_buffer,
796 bind_group,
797 })
798}
799
800pub struct FillPatternBatchEntry {
806 pub vertex_buffer: wgpu::Buffer,
808 pub index_buffer: wgpu::Buffer,
810 pub index_count: u32,
812 pub uniform_bind_group: wgpu::BindGroup,
814 pub texture_bind_group: wgpu::BindGroup,
816 pub _pattern_texture: wgpu::Texture,
818 pub _pattern_texture_view: wgpu::TextureView,
820 pub _fill_params_buffer: wgpu::Buffer,
822}
823
824#[allow(clippy::too_many_arguments)]
829pub fn build_fill_pattern_batch(
830 device: &wgpu::Device,
831 queue: &wgpu::Queue,
832 mesh: &VectorMeshData,
833 camera_origin: DVec3,
834 uniform_buffer: &wgpu::Buffer,
835 uniform_bgl: &wgpu::BindGroupLayout,
836 texture_bgl: &wgpu::BindGroupLayout,
837 pattern_sampler: &wgpu::Sampler,
838) -> Option<FillPatternBatchEntry> {
839 use crate::gpu::fill_pattern_vertex::FillPatternVertex;
840 use crate::pipeline::fill_pipeline::FillParamsUniform;
841
842 let pattern = mesh.fill_pattern.as_ref()?;
843 if mesh.indices.is_empty() || mesh.fill_pattern_uvs.is_empty() {
844 return None;
845 }
846
847 let vertices: Vec<FillPatternVertex> = (0..mesh.positions.len())
848 .map(|i| {
849 let pos = &mesh.positions[i];
850 FillPatternVertex {
851 position: [
852 (pos[0] - camera_origin.x) as f32,
853 (pos[1] - camera_origin.y) as f32,
854 (pos[2] - camera_origin.z) as f32,
855 ],
856 color: mesh.colors[i],
857 uv: if i < mesh.fill_pattern_uvs.len() {
858 mesh.fill_pattern_uvs[i]
859 } else {
860 [0.0, 0.0]
861 },
862 }
863 })
864 .collect();
865
866 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
867 label: Some("fill_pattern_batch_vb"),
868 contents: bytemuck::cast_slice(&vertices),
869 usage: wgpu::BufferUsages::VERTEX,
870 });
871 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
872 label: Some("fill_pattern_batch_ib"),
873 contents: bytemuck::cast_slice(&mesh.indices),
874 usage: wgpu::BufferUsages::INDEX,
875 });
876
877 let texture_size = wgpu::Extent3d {
879 width: pattern.width,
880 height: pattern.height,
881 depth_or_array_layers: 1,
882 };
883 let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
884 label: Some("fill_pattern_tex"),
885 size: texture_size,
886 mip_level_count: 1,
887 sample_count: 1,
888 dimension: wgpu::TextureDimension::D2,
889 format: wgpu::TextureFormat::Rgba8UnormSrgb,
890 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
891 view_formats: &[],
892 });
893
894 queue.write_texture(
895 wgpu::TexelCopyTextureInfo {
896 texture: &pattern_texture,
897 mip_level: 0,
898 origin: wgpu::Origin3d::ZERO,
899 aspect: wgpu::TextureAspect::All,
900 },
901 &pattern.data,
902 wgpu::TexelCopyBufferLayout {
903 offset: 0,
904 bytes_per_row: Some(pattern.width * 4),
905 rows_per_image: Some(pattern.height),
906 },
907 texture_size,
908 );
909
910 let pattern_texture_view = pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
911
912 let fill_params = FillParamsUniform {
914 fill_translate: mesh.fill_translate,
915 fill_opacity: mesh.fill_opacity,
916 fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
917 outline_color: mesh.fill_outline_color,
918 };
919 let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
920 label: Some("fill_pattern_params_buf"),
921 contents: bytemuck::bytes_of(&fill_params),
922 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
923 });
924
925 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
927 label: Some("fill_pattern_uniform_bg"),
928 layout: uniform_bgl,
929 entries: &[
930 wgpu::BindGroupEntry {
931 binding: 0,
932 resource: uniform_buffer.as_entire_binding(),
933 },
934 wgpu::BindGroupEntry {
935 binding: 1,
936 resource: fill_params_buffer.as_entire_binding(),
937 },
938 ],
939 });
940
941 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
943 label: Some("fill_pattern_texture_bg"),
944 layout: texture_bgl,
945 entries: &[
946 wgpu::BindGroupEntry {
947 binding: 0,
948 resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
949 },
950 wgpu::BindGroupEntry {
951 binding: 1,
952 resource: wgpu::BindingResource::Sampler(pattern_sampler),
953 },
954 ],
955 });
956
957 Some(FillPatternBatchEntry {
958 vertex_buffer,
959 index_buffer,
960 index_count: mesh.indices.len() as u32,
961 uniform_bind_group,
962 texture_bind_group,
963 _pattern_texture: pattern_texture,
964 _pattern_texture_view: pattern_texture_view,
965 _fill_params_buffer: fill_params_buffer,
966 })
967}
968
969pub struct LineBatchEntry {
975 pub vertex_buffer: wgpu::Buffer,
977 pub index_buffer: wgpu::Buffer,
979 pub index_count: u32,
981 pub line_params: [f32; 4],
983}
984
985pub fn build_line_batch(
989 device: &wgpu::Device,
990 mesh: &VectorMeshData,
991 camera_origin: DVec3,
992) -> Option<LineBatchEntry> {
993 use crate::gpu::line_vertex::LineVertex;
994
995 if mesh.indices.is_empty() || mesh.line_distances.is_empty() {
996 return None;
997 }
998
999 let vertices: Vec<LineVertex> = (0..mesh.positions.len())
1000 .map(|i| {
1001 let pos = &mesh.positions[i];
1002 LineVertex {
1003 position: [
1004 (pos[0] - camera_origin.x) as f32,
1005 (pos[1] - camera_origin.y) as f32,
1006 (pos[2] - camera_origin.z) as f32,
1007 ],
1008 color: mesh.colors[i],
1009 line_normal: mesh.line_normals[i],
1010 line_distance: mesh.line_distances[i],
1011 cap_join: if i < mesh.line_cap_joins.len() {
1012 mesh.line_cap_joins[i]
1013 } else {
1014 0.0
1015 },
1016 }
1017 })
1018 .collect();
1019
1020 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1021 label: Some("line_batch_vb"),
1022 contents: bytemuck::cast_slice(&vertices),
1023 usage: wgpu::BufferUsages::VERTEX,
1024 });
1025 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1026 label: Some("line_batch_ib"),
1027 contents: bytemuck::cast_slice(&mesh.indices),
1028 usage: wgpu::BufferUsages::INDEX,
1029 });
1030
1031 Some(LineBatchEntry {
1032 vertex_buffer,
1033 index_buffer,
1034 index_count: mesh.indices.len() as u32,
1035 line_params: mesh.line_params,
1036 })
1037}
1038
1039pub struct LinePatternBatchEntry {
1045 pub vertex_buffer: wgpu::Buffer,
1047 pub index_buffer: wgpu::Buffer,
1049 pub index_count: u32,
1051 pub uniform_bind_group: wgpu::BindGroup,
1053 pub texture_bind_group: wgpu::BindGroup,
1055 pub _pattern_texture: wgpu::Texture,
1057 pub _pattern_texture_view: wgpu::TextureView,
1059 pub line_params: [f32; 4],
1061}
1062
1063#[allow(clippy::too_many_arguments)]
1068pub fn build_line_pattern_batch(
1069 device: &wgpu::Device,
1070 queue: &wgpu::Queue,
1071 mesh: &VectorMeshData,
1072 camera_origin: DVec3,
1073 uniform_buffer: &wgpu::Buffer,
1074 uniform_bgl: &wgpu::BindGroupLayout,
1075 texture_bgl: &wgpu::BindGroupLayout,
1076 pattern_sampler: &wgpu::Sampler,
1077) -> Option<LinePatternBatchEntry> {
1078 use crate::gpu::line_pattern_vertex::LinePatternVertex;
1079
1080 let pattern = mesh.line_pattern.as_ref()?;
1081 if mesh.indices.is_empty() || mesh.line_pattern_uvs.is_empty() {
1082 return None;
1083 }
1084
1085 let vertices: Vec<LinePatternVertex> = (0..mesh.positions.len())
1086 .map(|i| {
1087 let pos = &mesh.positions[i];
1088 LinePatternVertex {
1089 position: [
1090 (pos[0] - camera_origin.x) as f32,
1091 (pos[1] - camera_origin.y) as f32,
1092 (pos[2] - camera_origin.z) as f32,
1093 ],
1094 color: mesh.colors[i],
1095 line_normal: if i < mesh.line_normals.len() {
1096 mesh.line_normals[i]
1097 } else {
1098 [0.0, 0.0]
1099 },
1100 line_distance: if i < mesh.line_distances.len() {
1101 mesh.line_distances[i]
1102 } else {
1103 0.0
1104 },
1105 cap_join: if i < mesh.line_cap_joins.len() {
1106 mesh.line_cap_joins[i]
1107 } else {
1108 0.0
1109 },
1110 uv: if i < mesh.line_pattern_uvs.len() {
1111 mesh.line_pattern_uvs[i]
1112 } else {
1113 [0.0, 0.0]
1114 },
1115 }
1116 })
1117 .collect();
1118
1119 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1120 label: Some("line_pattern_batch_vb"),
1121 contents: bytemuck::cast_slice(&vertices),
1122 usage: wgpu::BufferUsages::VERTEX,
1123 });
1124 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1125 label: Some("line_pattern_batch_ib"),
1126 contents: bytemuck::cast_slice(&mesh.indices),
1127 usage: wgpu::BufferUsages::INDEX,
1128 });
1129
1130 let texture_size = wgpu::Extent3d {
1132 width: pattern.width,
1133 height: pattern.height,
1134 depth_or_array_layers: 1,
1135 };
1136 let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
1137 label: Some("line_pattern_tex"),
1138 size: texture_size,
1139 mip_level_count: 1,
1140 sample_count: 1,
1141 dimension: wgpu::TextureDimension::D2,
1142 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1143 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1144 view_formats: &[],
1145 });
1146
1147 queue.write_texture(
1148 wgpu::TexelCopyTextureInfo {
1149 texture: &pattern_texture,
1150 mip_level: 0,
1151 origin: wgpu::Origin3d::ZERO,
1152 aspect: wgpu::TextureAspect::All,
1153 },
1154 &pattern.data,
1155 wgpu::TexelCopyBufferLayout {
1156 offset: 0,
1157 bytes_per_row: Some(pattern.width * 4),
1158 rows_per_image: Some(pattern.height),
1159 },
1160 texture_size,
1161 );
1162
1163 let pattern_texture_view = pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
1164
1165 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1167 label: Some("line_pattern_uniform_bg"),
1168 layout: uniform_bgl,
1169 entries: &[wgpu::BindGroupEntry {
1170 binding: 0,
1171 resource: uniform_buffer.as_entire_binding(),
1172 }],
1173 });
1174
1175 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1177 label: Some("line_pattern_texture_bg"),
1178 layout: texture_bgl,
1179 entries: &[
1180 wgpu::BindGroupEntry {
1181 binding: 0,
1182 resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
1183 },
1184 wgpu::BindGroupEntry {
1185 binding: 1,
1186 resource: wgpu::BindingResource::Sampler(pattern_sampler),
1187 },
1188 ],
1189 });
1190
1191 Some(LinePatternBatchEntry {
1192 vertex_buffer,
1193 index_buffer,
1194 index_count: mesh.indices.len() as u32,
1195 uniform_bind_group,
1196 texture_bind_group,
1197 _pattern_texture: pattern_texture,
1198 _pattern_texture_view: pattern_texture_view,
1199 line_params: mesh.line_params,
1200 })
1201}
1202
1203pub struct CircleBatchEntry {
1209 pub vertex_buffer: wgpu::Buffer,
1211 pub index_buffer: wgpu::Buffer,
1213 pub index_count: u32,
1215}
1216
1217pub fn build_circle_batch(
1222 device: &wgpu::Device,
1223 mesh: &VectorMeshData,
1224 camera_origin: DVec3,
1225) -> Option<CircleBatchEntry> {
1226 use crate::gpu::circle_vertex::CircleVertex;
1227
1228 if mesh.circle_instances.is_empty() {
1229 return None;
1230 }
1231
1232 let instance_count = mesh.circle_instances.len();
1233 let mut vertices: Vec<CircleVertex> = Vec::with_capacity(instance_count * 4);
1234 let mut indices: Vec<u32> = Vec::with_capacity(instance_count * 6);
1235
1236 let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
1237
1238 for (i, ci) in mesh.circle_instances.iter().enumerate() {
1239 let cx = (ci.center[0] - camera_origin.x) as f32;
1240 let cy = (ci.center[1] - camera_origin.y) as f32;
1241 let cz = (ci.center[2] - camera_origin.z) as f32;
1242 let params = [ci.radius, ci.stroke_width, ci.blur, 0.0];
1243
1244 for offset in &offsets {
1245 vertices.push(CircleVertex {
1246 position: [cx, cy, cz],
1247 quad_offset: *offset,
1248 color: ci.color,
1249 stroke_color: ci.stroke_color,
1250 params,
1251 });
1252 }
1253
1254 let base = (i as u32) * 4;
1255 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1256 }
1257
1258 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1259 label: Some("circle_batch_vb"),
1260 contents: bytemuck::cast_slice(&vertices),
1261 usage: wgpu::BufferUsages::VERTEX,
1262 });
1263 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1264 label: Some("circle_batch_ib"),
1265 contents: bytemuck::cast_slice(&indices),
1266 usage: wgpu::BufferUsages::INDEX,
1267 });
1268
1269 Some(CircleBatchEntry {
1270 vertex_buffer,
1271 index_buffer,
1272 index_count: indices.len() as u32,
1273 })
1274}
1275
1276pub struct HeatmapBatchEntry {
1282 pub vertex_buffer: wgpu::Buffer,
1284 pub index_buffer: wgpu::Buffer,
1286 pub index_count: u32,
1288}
1289
1290pub fn build_heatmap_batch(
1295 device: &wgpu::Device,
1296 mesh: &VectorMeshData,
1297 camera_origin: DVec3,
1298) -> Option<HeatmapBatchEntry> {
1299 use crate::gpu::heatmap_vertex::HeatmapVertex;
1300
1301 if mesh.heatmap_points.is_empty() {
1302 return None;
1303 }
1304
1305 let point_count = mesh.heatmap_points.len();
1306 let mut vertices: Vec<HeatmapVertex> = Vec::with_capacity(point_count * 4);
1307 let mut indices: Vec<u32> = Vec::with_capacity(point_count * 6);
1308
1309 let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
1310
1311 for (i, pt) in mesh.heatmap_points.iter().enumerate() {
1312 let cx = (pt[0] - camera_origin.x) as f32;
1313 let cy = (pt[1] - camera_origin.y) as f32;
1314 let cz = 0.0f32;
1315 let weight = pt[2] as f32;
1316 let radius = pt[3] as f32;
1317 let params = [weight, radius, mesh.heatmap_intensity, 0.0];
1318
1319 for offset in &offsets {
1320 vertices.push(HeatmapVertex {
1321 position: [cx, cy, cz],
1322 quad_offset: *offset,
1323 params,
1324 });
1325 }
1326
1327 let base = (i as u32) * 4;
1328 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1329 }
1330
1331 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1332 label: Some("heatmap_batch_vb"),
1333 contents: bytemuck::cast_slice(&vertices),
1334 usage: wgpu::BufferUsages::VERTEX,
1335 });
1336 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1337 label: Some("heatmap_batch_ib"),
1338 contents: bytemuck::cast_slice(&indices),
1339 usage: wgpu::BufferUsages::INDEX,
1340 });
1341
1342 Some(HeatmapBatchEntry {
1343 vertex_buffer,
1344 index_buffer,
1345 index_count: indices.len() as u32,
1346 })
1347}
1348
1349pub fn build_placeholder_batches(
1364 device: &wgpu::Device,
1365 placeholders: &[rustial_engine::LoadingPlaceholder],
1366 style: &rustial_engine::PlaceholderStyle,
1367 camera_origin: DVec3,
1368) -> Option<VectorBatchEntry> {
1369 if placeholders.is_empty() {
1370 return None;
1371 }
1372
1373 let quad_count = placeholders.len();
1374 let mut vertices: Vec<VectorVertex> = Vec::with_capacity(quad_count * 4);
1375 let mut indices: Vec<u32> = Vec::with_capacity(quad_count * 6);
1376
1377 for ph in placeholders {
1378 let opacity = style.shimmer_opacity(ph.animation_phase);
1379 let color = [
1380 style.background_color[0],
1381 style.background_color[1],
1382 style.background_color[2],
1383 style.background_color[3] * opacity,
1384 ];
1385
1386 let min = ph.bounds.min.position;
1387 let max = ph.bounds.max.position;
1388 let ox = camera_origin.x;
1389 let oy = camera_origin.y;
1390 let z: f32 = -0.01;
1392
1393 let base = vertices.len() as u32;
1394 vertices.push(VectorVertex {
1396 position: [(min.x - ox) as f32, (min.y - oy) as f32, z],
1397 color,
1398 });
1399 vertices.push(VectorVertex {
1400 position: [(max.x - ox) as f32, (min.y - oy) as f32, z],
1401 color,
1402 });
1403 vertices.push(VectorVertex {
1404 position: [(max.x - ox) as f32, (max.y - oy) as f32, z],
1405 color,
1406 });
1407 vertices.push(VectorVertex {
1408 position: [(min.x - ox) as f32, (max.y - oy) as f32, z],
1409 color,
1410 });
1411 indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1412 }
1413
1414 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1415 label: Some("placeholder_batch_vb"),
1416 contents: bytemuck::cast_slice(&vertices),
1417 usage: wgpu::BufferUsages::VERTEX,
1418 });
1419 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1420 label: Some("placeholder_batch_ib"),
1421 contents: bytemuck::cast_slice(&indices),
1422 usage: wgpu::BufferUsages::INDEX,
1423 });
1424
1425 Some(VectorBatchEntry {
1426 vertex_buffer,
1427 index_buffer,
1428 index_count: indices.len() as u32,
1429 })
1430}
1431
1432pub struct SymbolBatchEntry {
1438 pub vertex_buffer: wgpu::Buffer,
1440 pub index_buffer: wgpu::Buffer,
1442 pub index_count: u32,
1444}
1445
1446pub fn build_symbol_batch(
1454 device: &wgpu::Device,
1455 symbols: &[rustial_engine::symbols::PlacedSymbol],
1456 atlas: &rustial_engine::symbols::GlyphAtlas,
1457 camera_origin: DVec3,
1458 render_em_px: f32,
1459) -> Option<SymbolBatchEntry> {
1460 use crate::gpu::symbol_vertex::SymbolVertex;
1461
1462 let atlas_dims = atlas.dimensions();
1463 if atlas_dims[0] == 0 || atlas_dims[1] == 0 {
1464 return None;
1465 }
1466
1467 let atlas_w = atlas_dims[0] as f32;
1468 let atlas_h = atlas_dims[1] as f32;
1469
1470 let mut vertices: Vec<SymbolVertex> = Vec::new();
1471 let mut indices: Vec<u32> = Vec::new();
1472
1473 for symbol in symbols {
1474 if !symbol.visible || symbol.opacity <= 0.0 {
1475 continue;
1476 }
1477 if symbol.text.as_ref().is_none_or(|t| t.is_empty()) {
1478 continue;
1479 }
1480
1481 let ax = (symbol.world_anchor[0] - camera_origin.x) as f32;
1483 let ay = (symbol.world_anchor[1] - camera_origin.y) as f32;
1484 let az = (symbol.world_anchor[2] - camera_origin.z) as f32;
1485
1486 let scale = symbol.size_px / render_em_px.max(1.0);
1487
1488 let color = [
1489 symbol.fill_color[0],
1490 symbol.fill_color[1],
1491 symbol.fill_color[2],
1492 symbol.fill_color[3] * symbol.opacity,
1493 ];
1494 let halo_color = symbol.halo_color;
1495 let params = [1.0_f32, 1.0, 0.0, 0.0];
1496
1497 if !symbol.glyph_quads.is_empty() {
1498 for quad in &symbol.glyph_quads {
1500 let entry = match atlas.get(&symbol.font_stack, quad.codepoint) {
1501 Some(e) => e,
1502 None => continue,
1503 };
1504
1505 let gw = entry.size[0] as f32 * scale;
1506 let gh = entry.size[1] as f32 * scale;
1507 let bearing_x = entry.bearing_x as f32 * scale;
1508 let bearing_y = entry.bearing_y as f32 * scale;
1509
1510 let x0 = quad.x + bearing_x;
1513 let y0 = quad.y + bearing_y;
1514 let x1 = x0 + gw;
1515 let y1 = y0 - gh;
1516
1517 let u0 = entry.origin[0] as f32 / atlas_w;
1518 let v0 = entry.origin[1] as f32 / atlas_h;
1519 let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
1520 let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
1521
1522 let base = vertices.len() as u32;
1523 vertices.push(SymbolVertex {
1524 position: [ax, ay, az],
1525 glyph_offset: [x0, y0],
1526 tex_coord: [u0, v0],
1527 color,
1528 halo_color,
1529 params,
1530 });
1531 vertices.push(SymbolVertex {
1532 position: [ax, ay, az],
1533 glyph_offset: [x1, y0],
1534 tex_coord: [u1, v0],
1535 color,
1536 halo_color,
1537 params,
1538 });
1539 vertices.push(SymbolVertex {
1540 position: [ax, ay, az],
1541 glyph_offset: [x0, y1],
1542 tex_coord: [u0, v1],
1543 color,
1544 halo_color,
1545 params,
1546 });
1547 vertices.push(SymbolVertex {
1548 position: [ax, ay, az],
1549 glyph_offset: [x1, y1],
1550 tex_coord: [u1, v1],
1551 color,
1552 halo_color,
1553 params,
1554 });
1555 indices.extend_from_slice(&[
1556 base,
1557 base + 2,
1558 base + 1,
1559 base + 1,
1560 base + 2,
1561 base + 3,
1562 ]);
1563 }
1564 } else {
1565 let text = symbol.text.as_deref().unwrap_or("");
1567 let mut cursor_x: f32 = 0.0;
1568 for codepoint in text.chars() {
1569 let entry = match atlas.get(&symbol.font_stack, codepoint) {
1570 Some(e) => e,
1571 None => continue,
1572 };
1573
1574 let gw = entry.size[0] as f32 * scale;
1575 let gh = entry.size[1] as f32 * scale;
1576 let bearing_x = entry.bearing_x as f32 * scale;
1577 let bearing_y = entry.bearing_y as f32 * scale;
1578
1579 let x0 = cursor_x + bearing_x;
1580 let y0 = bearing_y;
1581 let x1 = x0 + gw;
1582 let y1 = y0 - gh;
1583
1584 let u0 = entry.origin[0] as f32 / atlas_w;
1585 let v0 = entry.origin[1] as f32 / atlas_h;
1586 let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
1587 let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
1588
1589 let base = vertices.len() as u32;
1590 vertices.push(SymbolVertex {
1591 position: [ax, ay, az],
1592 glyph_offset: [x0, y0],
1593 tex_coord: [u0, v0],
1594 color,
1595 halo_color,
1596 params,
1597 });
1598 vertices.push(SymbolVertex {
1599 position: [ax, ay, az],
1600 glyph_offset: [x1, y0],
1601 tex_coord: [u1, v0],
1602 color,
1603 halo_color,
1604 params,
1605 });
1606 vertices.push(SymbolVertex {
1607 position: [ax, ay, az],
1608 glyph_offset: [x0, y1],
1609 tex_coord: [u0, v1],
1610 color,
1611 halo_color,
1612 params,
1613 });
1614 vertices.push(SymbolVertex {
1615 position: [ax, ay, az],
1616 glyph_offset: [x1, y1],
1617 tex_coord: [u1, v1],
1618 color,
1619 halo_color,
1620 params,
1621 });
1622 indices.extend_from_slice(&[
1623 base,
1624 base + 2,
1625 base + 1,
1626 base + 1,
1627 base + 2,
1628 base + 3,
1629 ]);
1630
1631 cursor_x += entry.advance_x * scale;
1632 }
1633 }
1634 }
1635
1636 if vertices.is_empty() {
1637 return None;
1638 }
1639
1640 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1641 label: Some("symbol_batch_vb"),
1642 contents: bytemuck::cast_slice(&vertices),
1643 usage: wgpu::BufferUsages::VERTEX,
1644 });
1645 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1646 label: Some("symbol_batch_ib"),
1647 contents: bytemuck::cast_slice(&indices),
1648 usage: wgpu::BufferUsages::INDEX,
1649 });
1650
1651 Some(SymbolBatchEntry {
1652 vertex_buffer,
1653 index_buffer,
1654 index_count: indices.len() as u32,
1655 })
1656}
1657
1658#[cfg(test)]
1663mod tests {
1664 use super::*;
1665 use crate::gpu::tile_atlas::TileAtlas;
1666 use rustial_engine::{CameraProjection, DecodedImage};
1667 use std::sync::Arc;
1668
1669 fn create_test_device() -> Option<wgpu::Device> {
1670 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1671 backends: wgpu::Backends::all(),
1672 ..Default::default()
1673 });
1674
1675 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1676 power_preference: wgpu::PowerPreference::LowPower,
1677 compatible_surface: None,
1678 force_fallback_adapter: false,
1679 }))
1680 .ok()?;
1681
1682 let (device, _) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1683 label: Some("batch_test_device"),
1684 ..Default::default()
1685 }))
1686 .ok()?;
1687
1688 Some(device)
1689 }
1690
1691 fn test_image() -> DecodedImage {
1692 DecodedImage {
1693 width: 1,
1694 height: 1,
1695 data: Arc::new(vec![255, 255, 255, 255]),
1696 }
1697 }
1698
1699 #[test]
1700 fn visible_tile_texture_region_matches_expected_subrect() {
1701 let tile = VisibleTile {
1702 target: TileId::new(3, 4, 2),
1703 actual: TileId::new(1, 1, 0),
1704 data: None,
1705 fade_opacity: 1.0,
1706 };
1707
1708 let region = tile.texture_region();
1709 assert!((region.u_min - 0.0).abs() < 1e-6);
1710 assert!((region.v_min - 0.5).abs() < 1e-6);
1711 assert!((region.u_max - 0.25).abs() < 1e-6);
1712 assert!((region.v_max - 0.75).abs() < 1e-6);
1713 }
1714
1715 #[test]
1716 fn terrain_texture_lookup_falls_back_to_visible_ancestor() {
1717 let target = TileId::new(4, 8, 4);
1718 let parent = TileId::new(3, 4, 2);
1719 let visible = vec![VisibleTile {
1720 target: parent,
1721 actual: parent,
1722 data: Some(rustial_engine::TileData::Raster(
1723 rustial_engine::DecodedImage {
1724 width: 1,
1725 height: 1,
1726 data: vec![255, 255, 255, 255].into(),
1727 },
1728 )),
1729 fade_opacity: 1.0,
1730 }];
1731
1732 assert_eq!(find_terrain_texture_actual(target, &visible), Some(parent));
1733 }
1734
1735 #[test]
1736 fn projected_tile_corners_differ_between_planar_projections() {
1737 let tile = TileId::new(3, 4, 2);
1738 let merc = projected_tile_corners(tile, CameraProjection::WebMercator, DVec3::ZERO);
1739 let eq = projected_tile_corners(tile, CameraProjection::Equirectangular, DVec3::ZERO);
1740
1741 assert!((merc[0].y - eq[0].y).abs() > 1.0);
1742 }
1743
1744 #[test]
1745 fn exact_full_opacity_tile_uses_opaque_pass() {
1746 let tile = VisibleTile {
1747 target: TileId::new(3, 4, 2),
1748 actual: TileId::new(3, 4, 2),
1749 data: None,
1750 fade_opacity: 1.0,
1751 };
1752
1753 assert!(!tile_requires_blended_pass(&tile));
1754 }
1755
1756 #[test]
1757 fn fading_tile_uses_translucent_pass() {
1758 let tile = VisibleTile {
1759 target: TileId::new(3, 4, 2),
1760 actual: TileId::new(3, 4, 2),
1761 data: None,
1762 fade_opacity: 0.5,
1763 };
1764
1765 assert!(tile_requires_blended_pass(&tile));
1766 }
1767
1768 #[test]
1769 fn fallback_tile_uses_translucent_pass() {
1770 let tile = VisibleTile {
1771 target: TileId::new(4, 8, 4),
1772 actual: TileId::new(3, 4, 2),
1773 data: None,
1774 fade_opacity: 1.0,
1775 };
1776
1777 assert!(tile_requires_blended_pass(&tile));
1778 }
1779
1780 #[test]
1781 fn build_tile_batches_routes_exact_tile_to_opaque_batch() {
1782 let Some(device) = create_test_device() else {
1783 eprintln!("Skipping batch routing test: no suitable adapter/device available");
1784 return;
1785 };
1786
1787 let mut atlas = TileAtlas::new();
1788 let tile_id = TileId::new(0, 0, 0);
1789 atlas.insert(&device, tile_id, &test_image());
1790
1791 let visible = vec![VisibleTile {
1792 target: tile_id,
1793 actual: tile_id,
1794 data: None,
1795 fade_opacity: 1.0,
1796 }];
1797
1798 let batches = build_tile_batches(
1799 &device,
1800 &visible,
1801 &atlas,
1802 DVec3::ZERO,
1803 CameraProjection::WebMercator,
1804 );
1805
1806 assert_eq!(batches.len(), 1);
1807 assert!(batches[0].opaque.is_some());
1808 assert!(batches[0].translucent.is_none());
1809 }
1810
1811 #[test]
1812 fn build_tile_batches_routes_fading_crossfade_tiles_to_translucent_batch() {
1813 let Some(device) = create_test_device() else {
1814 eprintln!(
1815 "Skipping translucent batch routing test: no suitable adapter/device available"
1816 );
1817 return;
1818 };
1819
1820 let mut atlas = TileAtlas::new();
1821 let parent = TileId::new(0, 0, 0);
1822 let child = TileId::new(1, 0, 0);
1823 atlas.insert(&device, parent, &test_image());
1824 atlas.insert(&device, child, &test_image());
1825
1826 let visible = vec![
1827 VisibleTile {
1828 target: child,
1829 actual: child,
1830 data: None,
1831 fade_opacity: 0.5,
1832 },
1833 VisibleTile {
1834 target: child,
1835 actual: parent,
1836 data: None,
1837 fade_opacity: 0.5,
1838 },
1839 ];
1840
1841 let batches = build_tile_batches(
1842 &device,
1843 &visible,
1844 &atlas,
1845 DVec3::ZERO,
1846 CameraProjection::WebMercator,
1847 );
1848
1849 assert_eq!(batches.len(), 1);
1850 assert!(batches[0].opaque.is_none());
1851 assert!(batches[0].translucent.is_some());
1852 }
1853}