use rustial_engine as rustial_math;
use rustial_engine::{
build_terrain_mesh,
prepare_hillshade_raster,
tile_bounds_world,
CameraMode,
CameraProjection,
CircleStyleLayer,
DecodedImage,
ElevationGrid,
Feature,
FeatureCollection,
FillStyleLayer,
FogConfig,
Geometry,
LineString,
LineStyleLayer,
MapState,
MapStyle,
Point,
Polygon,
StyleDocument,
StyleLayer,
StyleSource,
TerrainMeshData,
TileData,
TileError,
TileId,
TileResponse,
TileSelectionConfig,
TileSource,
VectorLayer,
VectorStyle,
VectorTileData,
VectorTileSource,
VisibleTile,
WebMercator,
WorldCoord,
};
use rustial_renderer_wgpu::{RenderParams, WgpuMapRenderer};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
const WIDTH: u32 = 64;
const HEIGHT: u32 = 64;
fn create_device() -> Option<(wgpu::Device, wgpu::Queue, wgpu::TextureFormat)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("headless_test_device"),
..Default::default()
}))
.ok()?;
Some((device, queue, wgpu::TextureFormat::Rgba8UnormSrgb))
}
fn build_visualization_state() -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(5_000.0);
state.set_camera_pitch(35_f64.to_radians());
state.update_camera(1.0 / 60.0);
state.set_grid_scalar(
"density",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
4,
4,
100.0,
100.0,
),
rustial_engine::ScalarField2D::from_data(4, 4, vec![1.0; 16]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
);
state.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.01),
2,
2,
100.0,
100.0,
),
rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.2, 0.8, 0.6],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.3, 0.1, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 25.0,
base_meters: 0.0,
},
);
state.set_instanced_columns(
"columns",
rustial_engine::ColumnInstanceSet::new(vec![
rustial_engine::ColumnInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.0, -0.01),
20.0,
10.0,
)
.with_pick_id(1),
rustial_engine::ColumnInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.005, -0.015),
30.0,
12.0,
)
.with_pick_id(2),
]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.2, 0.9, 0.3, 0.7],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.8, 0.1, 0.9],
},
]),
);
state.update();
state
}
fn build_large_point_cloud_state(count: usize, updated: bool) -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(40_000.0);
state.set_camera_pitch(28_f64.to_radians());
state.update_camera(1.0 / 60.0);
let side = (count as f64).sqrt().ceil() as usize;
let mut points = Vec::with_capacity(count);
for idx in 0..count {
let row = idx / side;
let col = idx % side;
let lat = (row as f64 - side as f64 * 0.5) * 0.0005;
let lon = (col as f64 - side as f64 * 0.5) * 0.0005;
let mut point = rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(lat, lon),
4.0 + (idx % 8) as f64,
)
.with_pick_id(idx as u64 + 1)
.with_intensity(((idx % 100) as f32) / 100.0);
if updated && (count / 4..count / 4 + 512).contains(&idx) {
point.radius += 4.0;
point.intensity = 1.0 - point.intensity;
}
points.push(point);
}
state.set_point_cloud(
"large-points",
rustial_engine::PointInstanceSet::new(points),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.2, 0.9, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.3, 0.1, 0.9],
},
]),
);
state.update();
state
}
fn apply_large_point_cloud_update(state: &mut MapState, count: usize) {
let side = (count as f64).sqrt().ceil() as usize;
let mut points = Vec::with_capacity(count);
for idx in 0..count {
let row = idx / side;
let col = idx % side;
let lat = (row as f64 - side as f64 * 0.5) * 0.0005;
let lon = (col as f64 - side as f64 * 0.5) * 0.0005;
let mut point = rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(lat, lon),
4.0 + (idx % 8) as f64,
)
.with_pick_id(idx as u64 + 1)
.with_intensity(((idx % 100) as f32) / 100.0);
if (count / 4..count / 4 + 512).contains(&idx) {
point.radius += 4.0;
point.intensity = 1.0 - point.intensity;
}
points.push(point);
}
state.set_point_cloud(
"large-points",
rustial_engine::PointInstanceSet::new(points),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.2, 0.9, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.3, 0.1, 0.9],
},
]),
);
state.update();
}
fn build_point_cloud_state(updated: bool) -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(5_000.0);
state.set_camera_pitch(20_f64.to_radians());
state.update_camera(1.0 / 60.0);
state.set_point_cloud(
"points",
rustial_engine::PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
if updated { 18.0 } else { 10.0 },
)
.with_pick_id(1)
.with_intensity(if updated { 0.9 } else { 0.2 }),
rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.002, -0.002),
8.0,
)
.with_pick_id(2)
.with_color([0.3, 0.9, 0.4, 0.8]),
]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.2, 0.9, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.3, 0.1, 0.9],
},
]),
);
state.update();
state
}
fn apply_large_grid_scalar_update(state: &mut MapState, size: usize) {
let mut field = rustial_engine::ScalarField2D::from_data(
size,
size,
(0..size * size).map(|idx| (idx % 256) as f32).collect(),
);
field.update_values(
(0..size * size)
.map(|idx| 255.0 - (idx % 256) as f32)
.collect(),
);
state.set_grid_scalar(
"large-density",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
size,
size,
50.0,
50.0,
),
field,
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.3],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.9],
},
]),
);
state.update();
}
fn apply_large_column_update(state: &mut MapState, count: usize) {
let side = (count as f64).sqrt().ceil() as usize;
let mut columns = Vec::with_capacity(count);
for idx in 0..count {
let row = idx / side;
let col = idx % side;
let lat = (row as f64 - side as f64 * 0.5) * 0.0005;
let lon = (col as f64 - side as f64 * 0.5) * 0.0005;
let mut column = rustial_engine::ColumnInstance::new(
rustial_math::GeoCoord::from_lat_lon(lat, lon),
10.0 + (idx % 25) as f64,
8.0,
)
.with_pick_id(idx as u64 + 1);
if (count / 4..count / 4 + 512).contains(&idx) {
column.height += 15.0;
}
columns.push(column);
}
state.set_instanced_columns(
"large-columns",
rustial_engine::ColumnInstanceSet::new(columns),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.6, 0.9, 0.6],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.8, 0.2, 0.9],
},
]),
);
state.update();
}
fn build_large_grid_scalar_state(size: usize, updated: bool) -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(20_000.0);
state.set_camera_pitch(25_f64.to_radians());
state.update_camera(1.0 / 60.0);
let mut data = Vec::with_capacity(size * size);
for row in 0..size {
for col in 0..size {
let base = ((row + col) % 256) as f32;
data.push(if updated { 255.0 - base } else { base });
}
}
let mut field = rustial_engine::ScalarField2D::from_data(size, size, data);
if updated {
let updated_values = field
.data
.iter()
.map(|value| 255.0 - value)
.collect::<Vec<_>>();
field.update_values(updated_values);
}
state.set_grid_scalar(
"large-density",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
size,
size,
50.0,
50.0,
),
field,
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.3],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.9],
},
]),
);
state.update();
state
}
fn build_large_column_state(count: usize, updated: bool) -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(40_000.0);
state.set_camera_pitch(30_f64.to_radians());
state.update_camera(1.0 / 60.0);
let side = (count as f64).sqrt().ceil() as usize;
let mut columns = Vec::with_capacity(count);
for idx in 0..count {
let row = idx / side;
let col = idx % side;
let lat = (row as f64 - side as f64 * 0.5) * 0.0005;
let lon = (col as f64 - side as f64 * 0.5) * 0.0005;
let mut column = rustial_engine::ColumnInstance::new(
rustial_math::GeoCoord::from_lat_lon(lat, lon),
10.0 + (idx % 25) as f64,
8.0,
)
.with_pick_id(idx as u64 + 1);
if updated && (count / 4..count / 4 + 512).contains(&idx) {
column.height += 15.0;
}
columns.push(column);
}
state.set_instanced_columns(
"large-columns",
rustial_engine::ColumnInstanceSet::new(columns),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.6, 0.9, 0.6],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.8, 0.2, 0.9],
},
]),
);
state.update();
state
}
fn build_grid_scalar_state() -> MapState {
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(5_000.0);
state.set_camera_pitch(20_f64.to_radians());
state.update_camera(1.0 / 60.0);
state.set_grid_scalar(
"density",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
4,
4,
100.0,
100.0,
),
rustial_engine::ScalarField2D::from_data(
4,
4,
vec![
0.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0, 3.0, 4.0, 5.0, 6.0,
],
),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.1, 0.8, 0.4],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.2, 0.1, 0.9],
},
]),
);
state.update();
state
}
fn render_frame(include_tile: bool) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.update();
let visible_tiles = if include_tile {
let id = TileId::new(0, 0, 0);
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for pixel in pixel_data.chunks_exact_mut(4) {
pixel[0] = 255; pixel[1] = 0; pixel[2] = 0; pixel[3] = 255; }
vec![VisibleTile {
target: id,
actual: id,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]
} else {
Vec::new()
};
renderer.render(&state, &device, &queue, &color_view, &visible_tiles);
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
fn render_terrain_frame(include_hillshade: bool) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_terrain_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let tile = TileId::new(5, 16, 16);
let bounds = tile_bounds_world(&tile);
let center_world = WorldCoord::new(
(bounds.min.position.x + bounds.max.position.x) * 0.5,
(bounds.min.position.y + bounds.max.position.y) * 0.5,
0.0,
);
let center_geo = WebMercator::unproject(¢er_world);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(center_geo);
state.set_camera_distance(1_700_000.0);
state.set_camera_pitch(65_f64.to_radians());
state.set_camera_yaw(20_f64.to_radians());
state.update_camera(1.0 / 60.0);
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for (i, pixel) in pixel_data.chunks_exact_mut(4).enumerate() {
let x = (i % 256) as u8;
let y = (i / 256) as u8;
pixel[0] = 92u8.saturating_add(x / 6);
pixel[1] = 118u8.saturating_add(y / 5);
pixel[2] = 84u8.saturating_add((x ^ y) & 31);
pixel[3] = 255;
}
state.set_visible_tiles(vec![VisibleTile {
target: tile,
actual: tile,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]);
let elevation = ElevationGrid::from_data(
tile,
4,
4,
vec![
0.0, 20_000.0, 55_000.0, 90_000.0, 10_000.0, 40_000.0, 85_000.0, 130_000.0, 25_000.0,
60_000.0, 120_000.0, 165_000.0, 40_000.0, 80_000.0, 145_000.0, 210_000.0,
],
)?;
let terrain_mesh: TerrainMeshData = build_terrain_mesh(
&tile,
&elevation,
CameraProjection::WebMercator,
8,
1.0,
0.0,
1,
);
state.set_terrain_meshes(vec![terrain_mesh]);
if include_hillshade {
state.push_layer(Box::new(rustial_engine::HillshadeLayer::new("hillshade")));
state.set_hillshade_rasters(vec![prepare_hillshade_raster(&elevation, 1.0, 1)]);
}
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_terrain_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_terrain_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
fn render_projected_tile_frame(
projection: CameraProjection,
mode: CameraMode,
include_tile: bool,
) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_projected_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let tile = TileId::new(3, 4, 2);
let bounds = tile_bounds_world(&tile);
let center_world = WorldCoord::new(
(bounds.min.position.x + bounds.max.position.x) * 0.5,
(bounds.min.position.y + bounds.max.position.y) * 0.5,
0.0,
);
let center_geo = WebMercator::unproject(¢er_world);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_projection(projection);
state.set_camera_mode(mode);
state.set_camera_target(center_geo);
state.set_camera_distance(5_000_000.0);
state.update();
let visible_tiles = if include_tile {
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for pixel in pixel_data.chunks_exact_mut(4) {
pixel[0] = 0;
pixel[1] = 200;
pixel[2] = 255;
pixel[3] = 255;
}
vec![VisibleTile {
target: tile,
actual: tile,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]
} else {
Vec::new()
};
renderer.render(&state, &device, &queue, &color_view, &visible_tiles);
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_projected_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_projected_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
#[test]
fn engine_wgpu_integration_produces_non_empty_output() {
let Some(pixels) = render_frame(true) else {
eprintln!("Skipping headless renderer test: no suitable adapter/device available");
return;
};
assert_eq!(pixels.len(), (WIDTH * HEIGHT * 4) as usize);
assert!(pixels.iter().any(|&b| b != 0));
}
#[test]
fn visual_regression_tile_differs_from_empty_frame() {
let Some(empty_pixels) = render_frame(false) else {
eprintln!("Skipping visual regression test: no suitable adapter/device available");
return;
};
let Some(tile_pixels) = render_frame(true) else {
eprintln!("Skipping visual regression test: no suitable adapter/device available");
return;
};
assert_eq!(empty_pixels.len(), tile_pixels.len());
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(total_diff > 10_000, "pixel diff too small: {total_diff}");
}
#[test]
fn terrain_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_frame(false) else {
eprintln!("Skipping terrain visual regression test: no suitable adapter/device available");
return;
};
let Some(terrain_pixels) = render_terrain_frame(false) else {
eprintln!("Skipping terrain visual regression test: no suitable adapter/device available");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(terrain_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 200,
"terrain pixel diff too small: {total_diff}"
);
}
#[test]
fn hillshade_visual_regression_differs_from_terrain_only_frame() {
let Some(terrain_pixels) = render_terrain_frame(false) else {
eprintln!(
"Skipping hillshade visual regression test: no suitable adapter/device available"
);
return;
};
let Some(hillshade_pixels) = render_terrain_frame(true) else {
eprintln!(
"Skipping hillshade visual regression test: no suitable adapter/device available"
);
return;
};
let total_diff: u64 = terrain_pixels
.iter()
.zip(hillshade_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(total_diff < u64::MAX, "hillshade pixel diff: {total_diff}");
}
#[test]
fn equirectangular_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_projected_tile_frame(
CameraProjection::Equirectangular,
CameraMode::Perspective,
false,
) else {
eprintln!(
"Skipping equirectangular visual regression test: no suitable adapter/device available"
);
return;
};
let Some(tile_pixels) = render_projected_tile_frame(
CameraProjection::Equirectangular,
CameraMode::Perspective,
true,
) else {
eprintln!(
"Skipping equirectangular visual regression test: no suitable adapter/device available"
);
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"equirectangular raster pixel diff too small: {total_diff}"
);
}
#[test]
fn orthographic_equirectangular_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_projected_tile_frame(
CameraProjection::Equirectangular,
CameraMode::Orthographic,
false,
) else {
eprintln!("Skipping orthographic equirectangular visual regression test: no suitable adapter/device available");
return;
};
let Some(tile_pixels) = render_projected_tile_frame(
CameraProjection::Equirectangular,
CameraMode::Orthographic,
true,
) else {
eprintln!("Skipping orthographic equirectangular visual regression test: no suitable adapter/device available");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"orthographic equirectangular raster pixel diff too small: {total_diff}"
);
}
#[test]
fn orthographic_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_projected_tile_frame(
CameraProjection::WebMercator,
CameraMode::Orthographic,
false,
) else {
eprintln!(
"Skipping orthographic visual regression test: no suitable adapter/device available"
);
return;
};
let Some(tile_pixels) = render_projected_tile_frame(
CameraProjection::WebMercator,
CameraMode::Orthographic,
true,
) else {
eprintln!(
"Skipping orthographic visual regression test: no suitable adapter/device available"
);
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"orthographic raster pixel diff too small: {total_diff}"
);
}
fn render_pitched_multi_frame(frame_count: usize) -> Option<Vec<Vec<u8>>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_multiframe_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let tile = TileId::new(5, 16, 16);
let bounds = tile_bounds_world(&tile);
let center_world = WorldCoord::new(
(bounds.min.position.x + bounds.max.position.x) * 0.5,
(bounds.min.position.y + bounds.max.position.y) * 0.5,
0.0,
);
let center_geo = WebMercator::unproject(¢er_world);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(center_geo);
state.set_camera_distance(1_700_000.0);
state.set_camera_pitch(65_f64.to_radians());
state.set_camera_yaw(20_f64.to_radians());
state.update_camera(1.0 / 60.0);
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for (i, pixel) in pixel_data.chunks_exact_mut(4).enumerate() {
let x = (i % 256) as u8;
let y = (i / 256) as u8;
pixel[0] = 92u8.saturating_add(x / 6);
pixel[1] = 118u8.saturating_add(y / 5);
pixel[2] = 84u8.saturating_add((x ^ y) & 31);
pixel[3] = 255;
}
state.set_visible_tiles(vec![VisibleTile {
target: tile,
actual: tile,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]);
let elevation = ElevationGrid::from_data(
tile,
4,
4,
vec![
0.0, 20_000.0, 55_000.0, 90_000.0, 10_000.0, 40_000.0, 85_000.0, 130_000.0, 25_000.0,
60_000.0, 120_000.0, 165_000.0, 40_000.0, 80_000.0, 145_000.0, 210_000.0,
],
)?;
let terrain_mesh: TerrainMeshData = build_terrain_mesh(
&tile,
&elevation,
CameraProjection::WebMercator,
8,
1.0,
0.0,
1,
);
state.set_terrain_meshes(vec![terrain_mesh]);
state.push_layer(Box::new(rustial_engine::HillshadeLayer::new("hillshade")));
state.set_hillshade_rasters(vec![prepare_hillshade_raster(&elevation, 1.0, 1)]);
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let mut frames = Vec::with_capacity(frame_count);
for _ in 0..frame_count {
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_multiframe_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_multiframe_copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
frames.push(slice.get_mapped_range().to_vec());
readback.unmap();
}
Some(frames)
}
#[test]
fn retained_buffers_produce_identical_output_across_frames() {
let Some(frames) = render_pitched_multi_frame(4) else {
eprintln!("Skipping retained-buffer test: no suitable adapter/device available");
return;
};
assert!(frames.len() == 4, "expected 4 frames, got {}", frames.len());
let first = &frames[0];
assert!(first.iter().any(|&b| b != 0), "frame 0 is all zeros");
for (i, frame) in frames.iter().enumerate().skip(1) {
assert_eq!(first.len(), frame.len(), "frame {i} has different length");
let mismatches: usize = first
.iter()
.zip(frame.iter())
.filter(|(a, b)| a != b)
.count();
assert_eq!(
mismatches, 0,
"frame {i} differs from frame 0 in {mismatches} bytes -- \
retained buffer caches produced different output"
);
}
}
#[test]
fn visualization_steady_state_reuses_retained_resources() {
let Some((device, queue, format)) = create_device() else {
eprintln!("Skipping visualization steady-state test: no suitable adapter/device available");
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("visualization_steady_state_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let state = build_visualization_state();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let first = renderer.visualization_perf_stats();
assert!(first.grid_scalar_rebuilds > 0);
assert!(first.grid_extrusion_rebuilds > 0);
assert!(first.column_rebuilds > 0);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let second = renderer.visualization_perf_stats();
assert_eq!(second.grid_scalar_rebuilds, 0);
assert_eq!(second.grid_scalar_value_updates, 0);
assert_eq!(second.grid_extrusion_rebuilds, 0);
assert_eq!(second.grid_extrusion_value_updates, 0);
assert_eq!(second.column_rebuilds, 0);
assert_eq!(second.column_partial_writes, 0);
assert_eq!(second.column_partial_write_ranges, 0);
}
#[test]
fn grid_scalar_value_update_uses_texture_update_without_rebuild() {
let Some((device, queue, format)) = create_device() else {
eprintln!("Skipping grid scalar value-update test: no suitable adapter/device available");
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("grid_scalar_value_update_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = build_grid_scalar_state();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let mut field = rustial_engine::ScalarField2D::from_data(
4,
4,
vec![
0.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0, 3.0, 4.0, 5.0, 6.0,
],
);
field.update_values(vec![
6.0, 5.0, 4.0, 3.0, 5.0, 4.0, 3.0, 2.0, 4.0, 3.0, 2.0, 1.0, 3.0, 2.0, 1.0, 0.0,
]);
state.set_grid_scalar(
"density",
rustial_engine::GeoGrid::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
4,
4,
100.0,
100.0,
),
field,
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.1, 0.8, 0.4],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.2, 0.1, 0.9],
},
]),
);
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let stats = renderer.visualization_perf_stats();
assert_eq!(stats.grid_scalar_rebuilds, 0);
assert_eq!(stats.grid_scalar_value_updates, 1);
}
#[test]
fn large_grid_scalar_value_update_records_single_retained_upload() {
let Some((device, queue, format)) = create_device() else {
eprintln!(
"Skipping large GridScalar performance test: no suitable adapter/device available"
);
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("large_grid_scalar_perf_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = build_large_grid_scalar_state(256, false);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
apply_large_grid_scalar_update(&mut state, 256);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let stats = renderer.visualization_perf_stats();
assert_eq!(stats.grid_scalar_rebuilds, 0);
assert_eq!(stats.grid_scalar_value_updates, 1);
}
#[test]
fn large_column_update_records_partial_retained_writes() {
let Some((device, queue, format)) = create_device() else {
eprintln!("Skipping large column performance test: no suitable adapter/device available");
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("large_column_perf_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = build_large_column_state(10_000, false);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
apply_large_column_update(&mut state, 10_000);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let stats = renderer.visualization_perf_stats();
assert_eq!(stats.column_rebuilds, 0);
assert_eq!(stats.column_partial_writes, 1);
assert!(stats.column_partial_write_ranges >= 1);
}
#[test]
fn point_cloud_render_produces_retained_overlay_output() {
let Some((device, queue, format)) = create_device() else {
eprintln!("Skipping point-cloud WGPU test: no suitable adapter/device available");
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("point_cloud_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let state = build_point_cloud_state(false);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("point_cloud_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("point_cloud_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv()
.ok()
.and_then(Result::ok)
.expect("point cloud readback");
let data = slice.get_mapped_range().to_vec();
readback.unmap();
assert!(data.iter().any(|&b| b != 0));
}
#[test]
fn point_cloud_update_preserves_overlay_identity() {
let Some((device, queue, format)) = create_device() else {
eprintln!(
"Skipping point-cloud WGPU retained-update test: no suitable adapter/device available"
);
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("point_cloud_retained_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = build_point_cloud_state(false);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
state.set_point_cloud(
"points",
rustial_engine::PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.0, 0.0),
18.0,
)
.with_pick_id(1)
.with_intensity(0.9),
rustial_engine::PointInstance::new(
rustial_math::GeoCoord::from_lat_lon(0.002, -0.002),
8.0,
)
.with_pick_id(2)
.with_color([0.3, 0.9, 0.4, 0.8]),
]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.1, 0.2, 0.9, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [0.9, 0.3, 0.1, 0.9],
},
]),
);
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let stats = renderer.visualization_perf_stats();
assert_eq!(stats.point_cloud_rebuilds, 0);
assert!(stats.point_cloud_partial_writes <= 1);
}
#[test]
fn large_point_cloud_update_records_partial_retained_writes() {
let Some((device, queue, format)) = create_device() else {
eprintln!(
"Skipping large point-cloud performance test: no suitable adapter/device available"
);
return;
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("large_point_cloud_perf_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = build_large_point_cloud_state(10_000, false);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
apply_large_point_cloud_update(&mut state, 10_000);
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.background_color().unwrap_or([0.86, 0.91, 0.98, 1.0]),
});
let stats = renderer.visualization_perf_stats();
assert_eq!(stats.point_cloud_rebuilds, 0);
assert_eq!(stats.point_cloud_partial_writes, 1);
assert!(stats.point_cloud_partial_write_ranges >= 1);
}
fn render_globe_tile_frame(mode: CameraMode, include_tile: bool) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_globe_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let tile = TileId::new(3, 4, 2);
let bounds = tile_bounds_world(&tile);
let center_world = WorldCoord::new(
(bounds.min.position.x + bounds.max.position.x) * 0.5,
(bounds.min.position.y + bounds.max.position.y) * 0.5,
0.0,
);
let center_geo = WebMercator::unproject(¢er_world);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_projection(CameraProjection::Globe);
state.set_camera_mode(mode);
state.set_camera_target(center_geo);
let dist = match mode {
CameraMode::Orthographic => 8_000_000.0,
_ => 15_000_000.0,
};
state.set_camera_distance(dist);
state.update();
let visible_tiles = if include_tile {
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for pixel in pixel_data.chunks_exact_mut(4) {
pixel[0] = 0;
pixel[1] = 200;
pixel[2] = 255;
pixel[3] = 255;
}
vec![VisibleTile {
target: tile,
actual: tile,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]
} else {
Vec::new()
};
renderer.render(&state, &device, &queue, &color_view, &visible_tiles);
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_globe_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_globe_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
fn render_vertical_perspective_tile_frame(mode: CameraMode, include_tile: bool) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_vp_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let tile = TileId::new(3, 4, 2);
let bounds = tile_bounds_world(&tile);
let center_world = WorldCoord::new(
(bounds.min.position.x + bounds.max.position.x) * 0.5,
(bounds.min.position.y + bounds.max.position.y) * 0.5,
0.0,
);
let center_geo = WebMercator::unproject(¢er_world);
let vp_height = 8_000_000.0; let projection = CameraProjection::vertical_perspective(center_geo, vp_height);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_projection(projection);
state.set_camera_mode(mode);
state.set_camera_target(center_geo);
state.set_camera_distance(5_000_000.0);
state.update();
let visible_tiles = if include_tile {
let mut pixel_data = vec![0u8; 256 * 256 * 4];
for pixel in pixel_data.chunks_exact_mut(4) {
pixel[0] = 255;
pixel[1] = 128;
pixel[2] = 0;
pixel[3] = 255;
}
vec![VisibleTile {
target: tile,
actual: tile,
data: Some(TileData::Raster(DecodedImage {
width: 256,
height: 256,
data: Arc::new(pixel_data),
})),
fade_opacity: 1.0,
}]
} else {
Vec::new()
};
renderer.render(&state, &device, &queue, &color_view, &visible_tiles);
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_vp_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_vp_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
#[test]
fn globe_perspective_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_globe_tile_frame(CameraMode::Perspective, false) else {
eprintln!("Skipping globe perspective visual regression test: no suitable adapter/device available");
return;
};
let Some(tile_pixels) = render_globe_tile_frame(CameraMode::Perspective, true) else {
eprintln!("Skipping globe perspective visual regression test: no suitable adapter/device available");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"globe perspective raster pixel diff too small: {total_diff}"
);
}
#[test]
fn globe_orthographic_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_globe_tile_frame(CameraMode::Orthographic, false) else {
eprintln!("Skipping globe orthographic visual regression test: no suitable adapter/device available");
return;
};
let Some(tile_pixels) = render_globe_tile_frame(CameraMode::Orthographic, true) else {
eprintln!("Skipping globe orthographic visual regression test: no suitable adapter/device available");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"globe orthographic raster pixel diff too small: {total_diff}"
);
}
#[test]
fn vertical_perspective_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) = render_vertical_perspective_tile_frame(CameraMode::Perspective, false)
else {
eprintln!(
"Skipping VP perspective visual regression test: no suitable adapter/device available"
);
return;
};
let Some(tile_pixels) = render_vertical_perspective_tile_frame(CameraMode::Perspective, true)
else {
eprintln!(
"Skipping VP perspective visual regression test: no suitable adapter/device available"
);
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"VP perspective raster pixel diff too small: {total_diff}"
);
}
#[test]
fn vertical_perspective_orthographic_tile_visual_regression_differs_from_empty_frame() {
let Some(empty_pixels) =
render_vertical_perspective_tile_frame(CameraMode::Orthographic, false)
else {
eprintln!(
"Skipping VP orthographic visual regression test: no suitable adapter/device available"
);
return;
};
let Some(tile_pixels) = render_vertical_perspective_tile_frame(CameraMode::Orthographic, true)
else {
eprintln!(
"Skipping VP orthographic visual regression test: no suitable adapter/device available"
);
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(tile_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 10_000,
"VP orthographic raster pixel diff too small: {total_diff}"
);
}
#[test]
fn globe_output_differs_from_equirectangular_output() {
let Some(eq_pixels) = render_projected_tile_frame(
CameraProjection::Equirectangular,
CameraMode::Perspective,
true,
) else {
eprintln!("Skipping globe-vs-equirect comparison: no suitable adapter");
return;
};
let Some(globe_pixels) = render_globe_tile_frame(CameraMode::Perspective, true) else {
eprintln!("Skipping globe-vs-equirect comparison: no suitable adapter");
return;
};
let total_diff: u64 = eq_pixels
.iter()
.zip(globe_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 5_000,
"Globe and Equirectangular raster output are suspiciously similar: {total_diff}"
);
}
fn render_vector_frame(layers: Vec<Box<dyn rustial_engine::Layer>>) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_vector_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(500.0);
state.set_camera_pitch(0.0); state.update_camera(1.0 / 60.0);
for layer in layers {
state.push_layer(layer);
}
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: [1.0, 1.0, 1.0, 1.0],
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_vector_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_vector_copy_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
fn make_polygon_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![Feature {
geometry: Geometry::Polygon(Polygon {
exterior: vec![
gc(-0.002, -0.002),
gc(-0.002, 0.002),
gc(0.002, 0.002),
gc(0.002, -0.002),
gc(-0.002, -0.002),
],
interiors: vec![],
}),
properties: HashMap::new(),
}],
}
}
fn make_line_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![Feature {
geometry: Geometry::LineString(LineString {
coords: vec![gc(-0.002, -0.002), gc(0.0, 0.002), gc(0.002, -0.002)],
}),
properties: HashMap::new(),
}],
}
}
fn make_point_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![
Feature {
geometry: Geometry::Point(Point {
coord: gc(0.0, 0.0),
}),
properties: HashMap::new(),
},
Feature {
geometry: Geometry::Point(Point {
coord: gc(0.001, 0.001),
}),
properties: HashMap::new(),
},
Feature {
geometry: Geometry::Point(Point {
coord: gc(-0.001, -0.001),
}),
properties: HashMap::new(),
},
],
}
}
fn pixel_diff_from_white(pixels: &[u8]) -> u64 {
pixels
.chunks_exact(4)
.map(|px| {
let dr = (px[0] as i16 - 255).unsigned_abs() as u64;
let dg = (px[1] as i16 - 255).unsigned_abs() as u64;
let db = (px[2] as i16 - 255).unsigned_abs() as u64;
dr + dg + db
})
.sum()
}
#[test]
fn vector_fill_pipeline_renders_non_trivial_output() {
let features = make_polygon_features();
let style = VectorStyle::fill(
[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0], 2.0,
);
let layer = VectorLayer::new("test-fill", features, style);
let Some(pixels) = render_vector_frame(vec![Box::new(layer)]) else {
eprintln!("Skipping fill pipeline test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Fill pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn vector_line_pipeline_renders_non_trivial_output() {
let features = make_line_features();
let style = VectorStyle::line(
[0.0, 0.0, 1.0, 1.0], 4.0,
);
let layer = VectorLayer::new("test-line", features, style);
let Some(pixels) = render_vector_frame(vec![Box::new(layer)]) else {
eprintln!("Skipping line pipeline test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Line pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn vector_circle_pipeline_renders_non_trivial_output() {
let features = make_point_features();
let style = VectorStyle::circle(
[0.0, 1.0, 0.0, 1.0], 10.0,
[0.0, 0.0, 0.0, 1.0], 2.0,
);
let layer = VectorLayer::new("test-circle", features, style);
let Some(pixels) = render_vector_frame(vec![Box::new(layer)]) else {
eprintln!("Skipping circle pipeline test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Circle pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn vector_fill_extrusion_pipeline_renders_non_trivial_output() {
let (device, queue, format) = match create_device() {
Some(d) => d,
None => {
eprintln!("Skipping fill-extrusion pipeline test: no suitable adapter");
return;
}
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_extrusion_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(500.0);
state.set_camera_pitch(45_f64.to_radians()); state.update_camera(1.0 / 60.0);
let features = make_polygon_features();
let style = VectorStyle::fill_extrusion(
[0.8, 0.2, 0.2, 1.0], 0.0, 50.0, );
state.push_layer(Box::new(VectorLayer::new(
"test-extrusion",
features,
style,
)));
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: [1.0, 1.0, 1.0, 1.0],
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_extrusion_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_extrusion_copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
if rx.recv().ok().and_then(|r| r.ok()).is_none() {
eprintln!("Skipping fill-extrusion test: buffer map failed");
return;
}
let pixels = slice.get_mapped_range().to_vec();
readback.unmap();
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Fill-extrusion pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn vector_heatmap_pipeline_executes_without_error() {
let features = make_point_features();
let style = VectorStyle::heatmap([1.0, 0.0, 0.0, 1.0], 200.0, 1.0);
let layer = VectorLayer::new("test-heatmap", features, style);
let Some(pixels) = render_vector_frame(vec![Box::new(layer)]) else {
eprintln!("Skipping heatmap pipeline test: no suitable adapter");
return;
};
assert_eq!(
pixels.len(),
(WIDTH * HEIGHT * 4) as usize,
"Heatmap pipeline returned wrong buffer size"
);
}
#[test]
fn vector_symbol_pipeline_renders_non_trivial_output() {
let features = make_point_features();
let style = VectorStyle::symbol(
[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0], 16.0, );
let layer = VectorLayer::new("test-symbol", features, style);
let Some(pixels) = render_vector_frame(vec![Box::new(layer)]) else {
eprintln!("Skipping symbol pipeline test: no suitable adapter");
return;
};
assert_eq!(
pixels.len(),
(WIDTH * HEIGHT * 4) as usize,
"Symbol pipeline returned wrong buffer size"
);
}
#[test]
fn combined_vector_pipelines_render_non_trivial_output() {
let fill_layer = VectorLayer::new(
"fill",
make_polygon_features(),
VectorStyle::fill([1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0], 2.0),
);
let line_layer = VectorLayer::new(
"line",
make_line_features(),
VectorStyle::line([0.0, 0.0, 1.0, 1.0], 4.0),
);
let circle_layer = VectorLayer::new(
"circle",
make_point_features(),
VectorStyle::circle([0.0, 1.0, 0.0, 1.0], 8.0, [0.0, 0.0, 0.0, 1.0], 1.0),
);
let Some(pixels) = render_vector_frame(vec![
Box::new(fill_layer),
Box::new(line_layer),
Box::new(circle_layer),
]) else {
eprintln!("Skipping combined vector test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 1_000,
"Combined vector pipelines produced nearly blank output (diff={diff})"
);
}
#[test]
fn vector_pipelines_differ_from_empty_frame() {
let Some(empty_pixels) = render_vector_frame(vec![]) else {
eprintln!("Skipping vector-vs-empty test: no suitable adapter");
return;
};
let fill_layer = VectorLayer::new(
"fill",
make_polygon_features(),
VectorStyle::fill([1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0], 2.0),
);
let Some(vector_pixels) = render_vector_frame(vec![Box::new(fill_layer)]) else {
eprintln!("Skipping vector-vs-empty test: no suitable adapter");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(vector_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 1_000,
"Vector frame and empty frame are suspiciously similar (diff={total_diff})"
);
}
struct StaticVectorTileSource {
queued: Mutex<Vec<TileId>>,
response: TileResponse,
}
impl StaticVectorTileSource {
fn new(layers: HashMap<String, FeatureCollection>) -> Self {
Self {
queued: Mutex::new(Vec::new()),
response: TileResponse::from_data(TileData::Vector(VectorTileData { layers })),
}
}
}
impl TileSource for StaticVectorTileSource {
fn request(&self, id: TileId) {
self.queued.lock().expect("lock").push(id);
}
fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
let queued = std::mem::take(&mut *self.queued.lock().expect("lock"));
queued
.into_iter()
.map(|id| (id, Ok(self.response.clone())))
.collect()
}
}
fn make_streamed_polygon_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![Feature {
geometry: Geometry::Polygon(Polygon {
exterior: vec![
gc(-0.002, -0.002),
gc(-0.002, 0.002),
gc(0.002, 0.002),
gc(0.002, -0.002),
gc(-0.002, -0.002),
],
interiors: vec![],
}),
properties: HashMap::new(),
}],
}
}
fn make_streamed_line_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![Feature {
geometry: Geometry::LineString(LineString {
coords: vec![gc(-0.002, -0.002), gc(0.0, 0.002), gc(0.002, -0.002)],
}),
properties: HashMap::new(),
}],
}
}
fn make_streamed_point_features() -> FeatureCollection {
let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
FeatureCollection {
features: vec![
Feature {
geometry: Geometry::Point(Point {
coord: gc(0.0, 0.0),
}),
properties: HashMap::new(),
},
Feature {
geometry: Geometry::Point(Point {
coord: gc(0.001, 0.001),
}),
properties: HashMap::new(),
},
Feature {
geometry: Geometry::Point(Point {
coord: gc(-0.001, -0.001),
}),
properties: HashMap::new(),
},
],
}
}
fn render_streamed_vector_frame(
source_layers: HashMap<String, FeatureCollection>,
style_layers: Vec<StyleLayer>,
) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_streamed_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut document = StyleDocument::new();
document
.add_source(
"test-streamed",
StyleSource::VectorTile(
VectorTileSource::streamed(move || {
Box::new(StaticVectorTileSource::new(source_layers.clone()))
})
.with_selection(TileSelectionConfig {
visible_tile_budget: 4,
..Default::default()
}),
),
)
.expect("source added");
for layer in style_layers {
document.add_layer(layer).expect("layer added");
}
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(500.0);
state.set_camera_pitch(0.0);
state.update_camera(1.0 / 60.0);
state
.set_style(MapStyle::from_document(document))
.expect("style applied");
state.update();
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: [1.0, 1.0, 1.0, 1.0],
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_streamed_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_streamed_copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
#[test]
fn streamed_fill_pipeline_renders_through_style_document() {
let mut source_layers = HashMap::new();
source_layers.insert("buildings".to_string(), make_streamed_polygon_features());
let mut fill = FillStyleLayer::new("test-fill", "test-streamed");
fill.source_layer = Some("buildings".to_string());
fill.fill_color = [1.0, 0.0, 0.0, 1.0].into();
let Some(pixels) = render_streamed_vector_frame(source_layers, vec![StyleLayer::Fill(fill)])
else {
eprintln!("Skipping streamed fill test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Streamed fill pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn streamed_line_pipeline_renders_through_style_document() {
let mut source_layers = HashMap::new();
source_layers.insert("roads".to_string(), make_streamed_line_features());
let mut line = LineStyleLayer::new("test-line", "test-streamed");
line.source_layer = Some("roads".to_string());
line.color = [0.0, 0.0, 1.0, 1.0].into(); line.width = 4.0.into();
let Some(pixels) = render_streamed_vector_frame(source_layers, vec![StyleLayer::Line(line)])
else {
eprintln!("Skipping streamed line test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Streamed line pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn streamed_circle_pipeline_renders_through_style_document() {
let mut source_layers = HashMap::new();
source_layers.insert("poi".to_string(), make_streamed_point_features());
let mut circle = CircleStyleLayer::new("test-circle", "test-streamed");
circle.source_layer = Some("poi".to_string());
circle.color = [0.0, 1.0, 0.0, 1.0].into(); circle.radius = 10.0.into();
let Some(pixels) =
render_streamed_vector_frame(source_layers, vec![StyleLayer::Circle(circle)])
else {
eprintln!("Skipping streamed circle test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 500,
"Streamed circle pipeline produced nearly blank output (diff={diff})"
);
}
#[test]
fn streamed_combined_pipelines_render_through_style_document() {
let mut source_layers = HashMap::new();
source_layers.insert("buildings".to_string(), make_streamed_polygon_features());
source_layers.insert("roads".to_string(), make_streamed_line_features());
source_layers.insert("poi".to_string(), make_streamed_point_features());
let mut fill = FillStyleLayer::new("fill-layer", "test-streamed");
fill.source_layer = Some("buildings".to_string());
fill.fill_color = [1.0, 0.0, 0.0, 1.0].into();
let mut line = LineStyleLayer::new("line-layer", "test-streamed");
line.source_layer = Some("roads".to_string());
line.color = [0.0, 0.0, 1.0, 1.0].into();
line.width = 4.0.into();
let mut circle = CircleStyleLayer::new("circle-layer", "test-streamed");
circle.source_layer = Some("poi".to_string());
circle.color = [0.0, 1.0, 0.0, 1.0].into();
circle.radius = 8.0.into();
let Some(pixels) = render_streamed_vector_frame(
source_layers,
vec![
StyleLayer::Fill(fill),
StyleLayer::Line(line),
StyleLayer::Circle(circle),
],
) else {
eprintln!("Skipping streamed combined test: no suitable adapter");
return;
};
let diff = pixel_diff_from_white(&pixels);
assert!(
diff > 1_000,
"Streamed combined pipelines produced nearly blank output (diff={diff})"
);
}
#[test]
fn streamed_source_layer_selection_renders_correct_layer() {
let mut source_layers = HashMap::new();
source_layers.insert("buildings".to_string(), make_streamed_polygon_features());
source_layers.insert("roads".to_string(), make_streamed_line_features());
let mut line = LineStyleLayer::new("road-lines", "test-streamed");
line.source_layer = Some("roads".to_string());
line.color = [1.0, 0.0, 0.0, 1.0].into();
line.width = 6.0.into();
let Some(road_pixels) =
render_streamed_vector_frame(source_layers.clone(), vec![StyleLayer::Line(line)])
else {
eprintln!("Skipping source-layer selection test: no suitable adapter");
return;
};
let mut fill = FillStyleLayer::new("building-fills", "test-streamed");
fill.source_layer = Some("buildings".to_string());
fill.fill_color = [0.0, 0.0, 1.0, 1.0].into();
let Some(building_pixels) =
render_streamed_vector_frame(source_layers, vec![StyleLayer::Fill(fill)])
else {
eprintln!("Skipping source-layer selection test: no suitable adapter");
return;
};
let road_diff = pixel_diff_from_white(&road_pixels);
let building_diff = pixel_diff_from_white(&building_pixels);
assert!(
road_diff > 500,
"Road layer produced nearly blank output (diff={road_diff})"
);
assert!(
building_diff > 500,
"Building layer produced nearly blank output (diff={building_diff})"
);
let cross_diff: u64 = road_pixels
.iter()
.zip(building_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
cross_diff > 500,
"Road-only and building-only renders are suspiciously similar (diff={cross_diff})"
);
}
#[test]
fn streamed_source_reload_updates_rendered_output() {
let (device, queue, format) = match create_device() {
Some(d) => d,
None => {
eprintln!("Skipping streamed reload test: no suitable adapter");
return;
}
};
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_reload_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let initial_layers: HashMap<String, FeatureCollection> = {
let mut m = HashMap::new();
m.insert("buildings".to_string(), make_streamed_polygon_features());
m
};
let initial_clone = initial_layers.clone();
let mut document = StyleDocument::new();
document
.add_source(
"test-streamed",
StyleSource::VectorTile(
VectorTileSource::streamed(move || {
Box::new(StaticVectorTileSource::new(initial_clone.clone()))
})
.with_selection(TileSelectionConfig {
visible_tile_budget: 4,
..Default::default()
}),
),
)
.expect("source added");
let mut fill = FillStyleLayer::new("fill", "test-streamed");
fill.source_layer = Some("buildings".to_string());
fill.fill_color = [1.0, 0.0, 0.0, 1.0].into(); document
.add_layer(StyleLayer::Fill(fill))
.expect("layer added");
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(500.0);
state.set_camera_pitch(0.0);
state.update_camera(1.0 / 60.0);
state
.set_style(MapStyle::from_document(document))
.expect("style applied");
state.update();
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: [1.0, 1.0, 1.0, 1.0],
});
let first_pixels = {
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("reload_readback_1"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("reload_copy_1"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range().to_vec();
readback.unmap();
data
};
let reload_layers = initial_layers;
state
.reload_style_source(
"test-streamed",
StyleSource::VectorTile(
VectorTileSource::streamed(move || {
Box::new(StaticVectorTileSource::new(reload_layers.clone()))
})
.with_selection(TileSelectionConfig {
visible_tile_budget: 4,
..Default::default()
}),
),
)
.expect("source reloaded");
state.update();
state.update();
let first_diff = pixel_diff_from_white(&first_pixels);
assert!(
first_diff > 500,
"First streamed render produced nearly blank output (diff={first_diff})"
);
assert!(
!state.vector_meshes().is_empty(),
"After source reload, vector_meshes should be non-empty"
);
}
#[test]
fn streamed_pipeline_output_differs_from_empty_frame() {
let Some(empty_pixels) = render_streamed_vector_frame(HashMap::new(), vec![]) else {
eprintln!("Skipping streamed-vs-empty test: no suitable adapter");
return;
};
let mut source_layers = HashMap::new();
source_layers.insert("buildings".to_string(), make_streamed_polygon_features());
let mut fill = FillStyleLayer::new("fill", "test-streamed");
fill.source_layer = Some("buildings".to_string());
fill.fill_color = [1.0, 0.0, 0.0, 1.0].into();
let Some(fill_pixels) =
render_streamed_vector_frame(source_layers, vec![StyleLayer::Fill(fill)])
else {
eprintln!("Skipping streamed-vs-empty test: no suitable adapter");
return;
};
let total_diff: u64 = empty_pixels
.iter()
.zip(fill_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
total_diff > 1_000,
"Streamed fill frame and empty frame are suspiciously similar (diff={total_diff})"
);
}
fn render_fog_frame(pitch_rad: f64, fog: Option<FogConfig>) -> Option<Vec<u8>> {
let (device, queue, format) = create_device()?;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("headless_fog_color"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuMapRenderer::new(&device, &queue, format, WIDTH, HEIGHT);
let mut state = MapState::new();
state.set_viewport(WIDTH, HEIGHT);
state.set_camera_target(rustial_math::GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(500.0);
state.set_camera_pitch(pitch_rad);
state.update_camera(1.0 / 60.0);
if let Some(fog_config) = fog {
state.set_fog(Some(fog_config));
}
let features = make_polygon_features();
let style = VectorStyle::fill([1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0], 2.0);
state.push_layer(Box::new(VectorLayer::new("fog-fill", features, style)));
state.update();
renderer.render_full(&RenderParams {
state: &state,
device: &device,
queue: &queue,
color_view: &color_view,
visible_tiles: state.visible_tiles(),
vector_meshes: state.vector_meshes(),
model_instances: state.model_instances(),
clear_color: state.computed_fog().clear_color,
});
let bytes_per_row = WIDTH * 4;
let buffer_size = (bytes_per_row * HEIGHT) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("headless_fog_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("headless_fog_copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(HEIGHT),
},
},
wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
#[test]
fn fog_color_override_affects_clear_color() {
let Some(default_pixels) = render_fog_frame(0.8, None) else {
eprintln!("Skipping fog colour test: no suitable adapter");
return;
};
let Some(green_fog_pixels) = render_fog_frame(
0.8,
Some(FogConfig {
color: Some([0.0, 1.0, 0.0, 1.0]),
density: Some(0.9),
..Default::default()
}),
) else {
eprintln!("Skipping fog colour test: no suitable adapter");
return;
};
let cross_diff: u64 = default_pixels
.iter()
.zip(green_fog_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
cross_diff > 500,
"Fog colour override should produce visibly different output (diff={cross_diff})"
);
}
#[test]
fn fog_density_override_changes_output() {
let Some(no_fog_pixels) = render_fog_frame(
0.8,
Some(FogConfig {
density: Some(0.0),
..Default::default()
}),
) else {
eprintln!("Skipping fog density test: no suitable adapter");
return;
};
let Some(full_fog_pixels) = render_fog_frame(
0.8,
Some(FogConfig {
density: Some(1.0),
..Default::default()
}),
) else {
eprintln!("Skipping fog density test: no suitable adapter");
return;
};
let cross_diff: u64 = no_fog_pixels
.iter()
.zip(full_fog_pixels.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
.sum();
assert!(
cross_diff > 100,
"Fog density override should produce visibly different output (diff={cross_diff})"
);
}