Add Zoom type and ChangeObserver, also improve performance by tessellating less

This commit is contained in:
Maximilian Ammann 2022-04-06 17:18:21 +02:00
parent 56ab38b29e
commit 37a29539d7
9 changed files with 252 additions and 89 deletions

View File

@ -2,9 +2,10 @@
use std::fmt;
use std::fmt::Formatter;
use std::ops::Add;
use cgmath::num_traits::Pow;
use cgmath::{Matrix4, Point3, Vector3};
use cgmath::{AbsDiffEq, Matrix4, Point3, Vector3};
use style_spec::source::TileAddressingScheme;
@ -40,6 +41,68 @@ impl fmt::Debug for Quadkey {
}
}
#[derive(Copy, Clone, Debug)]
pub struct Zoom(f64);
impl Zoom {
pub fn new(zoom: f64) -> Self {
Zoom(zoom)
}
}
impl Default for Zoom {
fn default() -> Self {
Zoom(0.0)
}
}
impl fmt::Display for Zoom {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", (self.0 * 100.0).round() / 100.0)
}
}
impl std::ops::Add for Zoom {
type Output = Zoom;
fn add(self, rhs: Self) -> Self::Output {
Zoom(self.0 + rhs.0)
}
}
impl std::ops::Sub for Zoom {
type Output = Zoom;
fn sub(self, rhs: Self) -> Self::Output {
Zoom(self.0 - rhs.0)
}
}
impl Zoom {
pub fn scale_to_tile(&self, coords: &WorldTileCoords) -> f64 {
2.0_f64.powf(coords.z as f64 - self.0)
}
pub fn scale_to_zoom_level(&self, z: u8) -> f64 {
2.0_f64.powf(z as f64 - self.0)
}
pub fn scale_delta(&self, zoom: &Zoom) -> f64 {
2.0_f64.powf(zoom.0 - self.0)
}
pub fn level(&self) -> u8 {
self.0.floor() as u8
}
}
impl Eq for Zoom {}
impl PartialEq for Zoom {
fn eq(&self, other: &Self) -> bool {
self.0.abs_diff_eq(&other.0, 0.05)
}
}
/// Within each tile there is a separate coordinate system. Usually this coordinate system is
/// within [`crate::coords::EXTENT`]. Therefore, `x` and `y` must be within the bounds of
/// [`crate::coords::EXTENT`].
@ -146,7 +209,7 @@ impl WorldTileCoords {
}
#[tracing::instrument(skip_all)]
pub fn transform_for_zoom(&self, zoom: f64) -> Matrix4<f64> {
pub fn transform_for_zoom(&self, zoom: Zoom) -> Matrix4<f64> {
/*
For tile.z = zoom:
=> scale = 512
@ -155,7 +218,7 @@ impl WorldTileCoords {
If tile.z > zoom:
=> scale < 512
*/
let tile_scale = TILE_SIZE * 2.0.pow(zoom - self.z as f64);
let tile_scale = TILE_SIZE * Zoom::new(self.z as f64).scale_delta(&zoom);
let translate = Matrix4::from_translation(Vector3::new(
self.x as f64 * tile_scale,
@ -309,10 +372,6 @@ pub struct WorldCoords {
pub y: f64,
}
fn world_size_at_zoom(zoom: f64) -> f64 {
TILE_SIZE * 2.0.pow(zoom)
}
fn tiles_with_z(z: u8) -> f64 {
2.0.pow(z)
}
@ -322,8 +381,8 @@ impl WorldCoords {
Self { x, y }
}
pub fn into_world_tile(self, z: u8, zoom: f64) -> WorldTileCoords {
let tile_scale = 2.0.pow(z as f64 - zoom) / TILE_SIZE; // TODO: Deduplicate
pub fn into_world_tile(self, z: u8, zoom: Zoom) -> WorldTileCoords {
let tile_scale = zoom.scale_to_zoom_level(z) / TILE_SIZE; // TODO: Deduplicate
let x = self.x * tile_scale;
let y = self.y * tile_scale;
@ -371,7 +430,7 @@ pub struct ViewRegion {
}
impl ViewRegion {
pub fn new(view_region: Aabb2<f64>, padding: i32, zoom: f64, z: u8) -> Self {
pub fn new(view_region: Aabb2<f64>, padding: i32, zoom: Zoom, z: u8) -> Self {
let min_world: WorldCoords = WorldCoords::at_ground(view_region.min.x, view_region.min.y);
let min_world_tile: WorldTileCoords = min_world.into_world_tile(z, zoom);
let max_world: WorldCoords = WorldCoords::at_ground(view_region.max.x, view_region.max.y);
@ -439,6 +498,7 @@ impl fmt::Display for WorldCoords {
#[cfg(test)]
mod tests {
use cgmath::{Point2, Vector4};
use style_spec::source::TileAddressingScheme;
use crate::coords::{Quadkey, TileCoords, ViewRegion, WorldCoords, WorldTileCoords, EXTENT};
@ -447,7 +507,7 @@ mod tests {
const TOP_LEFT: Vector4<f64> = Vector4::new(0.0, 0.0, 0.0, 1.0);
const BOTTOM_RIGHT: Vector4<f64> = Vector4::new(EXTENT, EXTENT, 0.0, 1.0);
fn to_from_world(tile: (i32, i32, u8), zoom: f64) {
fn to_from_world(tile: (i32, i32, u8), zoom: Zoom) {
let tile = WorldTileCoords::from(tile);
let p1 = tile.transform_for_zoom(zoom) * TOP_LEFT;
let p2 = tile.transform_for_zoom(zoom) * BOTTOM_RIGHT;

View File

@ -63,8 +63,8 @@ impl QueryHandler {
let view_proj = state.camera.calc_view_proj(perspective);
let inverted_view_proj = view_proj.invert();
let z = state.visible_z();
let zoom = state.zoom;
let z = state.visible_z(); // FIXME: can be wrong, if tiles of different z are visible
let zoom = state.zoom();
if let Some(coordinates) = state
.camera

View File

@ -1,5 +1,6 @@
use super::UpdateState;
use crate::coords::Zoom;
use crate::render::render_state::RenderState;
use crate::Scheduler;
use cgmath::num_traits::Pow;
@ -8,20 +9,19 @@ use std::time::Duration;
pub struct ZoomHandler {
window_position: Option<Vector2<f64>>,
zoom_delta: f64,
zoom_delta: Option<Zoom>,
sensitivity: f64,
}
impl UpdateState for ZoomHandler {
fn update_state(&mut self, state: &mut RenderState, _scheduler: &Scheduler, _dt: Duration) {
if self.zoom_delta != 0.0 {
if let Some(zoom_delta) = self.zoom_delta {
if let Some(window_position) = self.window_position {
let current_zoom = state.zoom;
let next_zoom = current_zoom + self.zoom_delta;
let current_zoom = state.zoom();
let next_zoom = current_zoom + zoom_delta;
state.zoom = next_zoom;
self.zoom_delta = 0.0;
println!("zoom: {}", state.zoom);
state.update_zoom(next_zoom);
self.zoom_delta = None;
let perspective = &state.perspective;
let view_proj = state.camera.calc_view_proj(perspective);
@ -31,7 +31,7 @@ impl UpdateState for ZoomHandler {
.camera
.window_to_world_at_ground(&window_position, &inverted_view_proj)
{
let scale = 2.0.pow(next_zoom - current_zoom);
let scale = current_zoom.scale_delta(&next_zoom);
let delta = Vector3::new(
cursor_position.x * scale,
@ -50,7 +50,7 @@ impl ZoomHandler {
pub fn new(sensitivity: f64) -> Self {
Self {
window_position: None,
zoom_delta: 0.0,
zoom_delta: None,
sensitivity,
}
}
@ -64,14 +64,22 @@ impl ZoomHandler {
true
}
pub fn update_zoom(&mut self, delta: f64) {
self.zoom_delta = Some(self.zoom_delta.unwrap_or_default() + Zoom::new(delta));
}
pub fn process_scroll(&mut self, delta: &winit::event::MouseScrollDelta) {
self.zoom_delta += match delta {
winit::event::MouseScrollDelta::LineDelta(_horizontal, vertical) => *vertical as f64,
winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition {
y: scroll,
..
}) => *scroll / 100.0,
} * self.sensitivity;
self.update_zoom(
match delta {
winit::event::MouseScrollDelta::LineDelta(_horizontal, vertical) => {
*vertical as f64
}
winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition {
y: scroll,
..
}) => *scroll / 100.0,
} * self.sensitivity,
);
}
pub fn process_key_press(
@ -87,11 +95,11 @@ impl ZoomHandler {
match key {
winit::event::VirtualKeyCode::Plus | winit::event::VirtualKeyCode::I => {
self.zoom_delta += amount;
self.update_zoom(amount);
true
}
winit::event::VirtualKeyCode::Minus | winit::event::VirtualKeyCode::K => {
self.zoom_delta -= amount;
self.update_zoom(-amount);
true
}
_ => false,

View File

@ -9,7 +9,7 @@ use geozero::geo_types::GeoWriter;
use geozero::{ColumnValue, FeatureProcessor, GeomProcessor, PropertyProcessor};
use rstar::{Envelope, PointDistance, RTree, RTreeObject, AABB};
use crate::coords::{InnerCoords, Quadkey, WorldCoords, WorldTileCoords, EXTENT, TILE_SIZE};
use crate::coords::{InnerCoords, Quadkey, WorldCoords, WorldTileCoords, Zoom, EXTENT, TILE_SIZE};
use crate::util::math::bounds_from_points;
pub struct GeometryIndex {
@ -33,7 +33,7 @@ impl GeometryIndex {
&self,
world_coords: &WorldCoords,
z: u8,
zoom: f64,
zoom: Zoom,
) -> Option<Vec<&IndexedGeometry<f64>>> {
let world_tile_coords = world_coords.into_world_tile(z, zoom);
@ -41,7 +41,7 @@ impl GeometryIndex {
.build_quad_key()
.and_then(|key| self.index.get(&key))
{
let scale = 2.0f64.pow(z as f64 - zoom); // TODO deduplicate
let scale = zoom.scale_delta(&Zoom::new(z as f64)); // FIXME: can be wrong, if tiles of different z are visible
let delta_x = world_coords.x / TILE_SIZE * scale - world_tile_coords.x as f64;
let delta_y = world_coords.y / TILE_SIZE * scale - world_tile_coords.y as f64;

View File

@ -9,7 +9,7 @@ use std::sync::{Arc, Mutex};
use vector_tile::parse_tile_bytes;
/// Describes through which channels work-requests travel. It describes the flow of work.
use crate::coords::{WorldCoords, WorldTileCoords};
use crate::coords::{WorldCoords, WorldTileCoords, Zoom};
use crate::io::tile_cache::TileCache;
use crate::io::{
LayerTessellateMessage, TessellateMessage, TileFetchResult, TileRequest, TileRequestID,
@ -155,7 +155,7 @@ impl ThreadLocalState {
&self,
world_coords: &WorldCoords,
z: u8,
zoom: f64,
zoom: Zoom,
) -> Option<Vec<IndexedGeometry<f64>>> {
if let Ok(mut geometry_index) = self.geometry_index.lock() {
geometry_index
@ -256,7 +256,7 @@ impl ThreadLocalState {
}
}
tracing::info!("tile at {} finished", &coords);
tracing::info!("tile at {} finished", &tile_request.coords);
self.tessellate_result_sender
.send(TessellateMessage::Tile(TileTessellateMessage {
@ -330,9 +330,9 @@ impl Scheduler {
&mut self,
coords: &WorldTileCoords,
layers: &HashSet<String>,
) -> Result<(), SendError<TileRequest>> {
) -> Result<bool, SendError<TileRequest>> {
if !self.tile_cache.is_layers_missing(coords, layers) {
return Ok(());
return Ok(false);
}
if let Ok(mut tile_request_state) = self.tile_request_state.try_lock() {
@ -372,9 +372,11 @@ impl Scheduler {
self.schedule_method.schedule(self, future_fn).unwrap();
}
}
}
Ok(())
Ok(false)
} else {
Ok(true)
}
}
pub fn get_tile_cache(&self) -> &TileCache {

View File

@ -1,5 +1,5 @@
use cgmath::prelude::*;
use cgmath::{Matrix4, Point2, Point3, Vector2, Vector3, Vector4};
use cgmath::{AbsDiffEq, Matrix4, Point2, Point3, Vector2, Vector3, Vector4};
use crate::render::shaders::ShaderCamera;
use crate::util::math::{bounds_from_points, Aabb2, Aabb3, Plane};
@ -73,6 +73,15 @@ pub struct Camera {
pub height: f64,
}
impl Eq for Camera {}
impl PartialEq for Camera {
fn eq(&self, other: &Self) -> bool {
self.position.abs_diff_eq(&other.position, 0.05)
&& self.yaw.abs_diff_eq(&other.yaw, 0.05)
&& self.pitch.abs_diff_eq(&other.pitch, 0.05)
}
}
impl Camera {
pub fn new<
V: Into<cgmath::Point3<f64>>,

View File

@ -1,6 +1,7 @@
use std::collections::HashSet;
use std::default::Default;
use cgmath::AbsDiffEq;
use std::{cmp, iter};
use tracing;
@ -10,7 +11,7 @@ use winit::window::Window;
use style_spec::Style;
use crate::coords::{ViewRegion, TILE_SIZE};
use crate::coords::{ViewRegion, Zoom, TILE_SIZE};
use crate::io::scheduler::Scheduler;
use crate::io::LayerTessellateMessage;
use crate::platform::{COLOR_TEXTURE_FORMAT, MIN_BUFFER_SIZE};
@ -23,7 +24,7 @@ use crate::render::options::{
};
use crate::render::tile_view_pattern::{TileInView, TileViewPattern};
use crate::tessellation::IndexDataType;
use crate::util::FPSMeter;
use crate::util::{ChangeObserver, FPSMeter};
use super::piplines::*;
use super::shaders;
@ -66,14 +67,24 @@ pub struct RenderState {
tile_view_pattern: TileViewPattern<Queue, Buffer>,
pub camera: camera::Camera,
pub camera: ChangeObserver<camera::Camera>,
pub perspective: camera::Perspective,
pub zoom: f64,
zoom: ChangeObserver<Zoom>,
try_failed: bool,
style: Box<Style>,
}
impl RenderState {
pub fn zoom(&self) -> Zoom {
*self.zoom
}
pub fn update_zoom(&mut self, new_zoom: Zoom) {
*self.zoom = new_zoom;
log::info!("zoom: {}", new_zoom);
}
pub async fn new(window: &Window, style: Box<Style>) -> Self {
let sample_count = 4;
@ -292,7 +303,7 @@ impl RenderState {
sample_count,
globals_uniform_buffer,
fps_meter: FPSMeter::new(),
camera,
camera: ChangeObserver::new(camera),
perspective: projection,
suspended: false, // Initially the app is not suspended
buffer_pool: BufferPool::new(
@ -305,7 +316,8 @@ impl RenderState {
tile_view_buffer,
TILE_VIEW_BUFFER_SIZE,
)),
zoom: 0.0,
zoom: ChangeObserver::default(),
try_failed: false,
style,
}
}
@ -356,12 +368,13 @@ impl RenderState {
}
pub fn visible_z(&self) -> u8 {
self.zoom.floor() as u8
self.zoom.level()
}
/// Request tiles which are currently in view
#[tracing::instrument(skip_all)]
fn request_tiles_in_view(&self, view_region: &ViewRegion, scheduler: &mut Scheduler) {
fn request_tiles_in_view(&self, view_region: &ViewRegion, scheduler: &mut Scheduler) -> bool {
let mut try_failed = false;
let source_layers: HashSet<String> = self
.style
.layers
@ -372,9 +385,10 @@ impl RenderState {
for coords in view_region.iter() {
if coords.build_quad_key().is_some() {
// TODO: Make tesselation depend on style?
scheduler.try_request_tile(&coords, &source_layers).unwrap();
try_failed = scheduler.try_request_tile(&coords, &source_layers).unwrap();
}
}
try_failed
}
/// Update tile metadata for all required tiles on the GPU according to current zoom, camera and perspective
@ -383,16 +397,12 @@ impl RenderState {
#[tracing::instrument(skip_all)]
fn update_metadata(
&mut self,
scheduler: &mut Scheduler,
_scheduler: &mut Scheduler,
view_region: &ViewRegion,
view_proj: &ViewProjection,
) {
self.tile_view_pattern.update_pattern(
view_region,
scheduler.get_tile_cache(),
&self.buffer_pool,
self.zoom,
);
self.tile_view_pattern
.update_pattern(view_region, &self.buffer_pool, *self.zoom);
self.tile_view_pattern
.upload_pattern(&self.queue, view_proj);
@ -511,6 +521,12 @@ impl RenderState {
buffer,
..
} => {
let allocate_feature_metadata = tracing::span!(
tracing::Level::TRACE,
"allocate_feature_metadata"
);
let guard = allocate_feature_metadata.enter();
let feature_metadata = layer_data
.features()
.iter()
@ -522,6 +538,7 @@ impl RenderState {
.take(feature_indices[i] as usize)
})
.collect::<Vec<_>>();
drop(guard);
tracing::trace!("Allocating geometry at {}", &coords);
self.buffer_pool.allocate_layer_geometry(
@ -552,27 +569,36 @@ impl RenderState {
let view_region = self
.camera
.view_region_bounding_box(&view_proj.invert())
.map(|bounding_box| ViewRegion::new(bounding_box, 1, self.zoom, visible_z));
.map(|bounding_box| ViewRegion::new(bounding_box, 0, *self.zoom, visible_z));
drop(_guard);
if let Some(view_region) = &view_region {
self.upload_tile_geometry(&view_proj, view_region, scheduler);
self.update_metadata(scheduler, view_region, &view_proj);
self.request_tiles_in_view(view_region, scheduler);
}
// TODO: Could we draw inspiration from StagingBelt (https://docs.rs/wgpu/latest/wgpu/util/struct.StagingBelt.html)?
// TODO: What is StagingBelt for?
// Update globals
self.queue.write_buffer(
&self.globals_uniform_buffer,
0,
bytemuck::cast_slice(&[ShaderGlobals::new(
self.camera.create_camera_uniform(&self.perspective),
)]),
);
if self.camera.did_change() || self.zoom.did_change() || self.try_failed {
if let Some(view_region) = &view_region {
// FIXME: We also need to request tiles from layers above if we are over the maximum zoom level
self.try_failed = self.request_tiles_in_view(view_region, scheduler);
}
// Update globals
self.queue.write_buffer(
&self.globals_uniform_buffer,
0,
bytemuck::cast_slice(&[ShaderGlobals::new(
self.camera.create_camera_uniform(&self.perspective),
)]),
);
}
self.camera.finished_observing();
self.zoom.finished_observing();
}
#[tracing::instrument(skip_all)]

View File

@ -1,4 +1,4 @@
use crate::coords::{ViewRegion, WorldTileCoords};
use crate::coords::{ViewRegion, WorldTileCoords, Zoom};
use crate::io::tile_cache::TileCache;
use crate::render::buffer_pool::{BackingBufferDescriptor, BufferPool, Queue};
use crate::render::camera::ViewProjection;
@ -28,6 +28,18 @@ pub struct TileShape {
pub buffer_range: Range<wgpu::BufferAddress>,
}
impl TileShape {
fn new(coords: WorldTileCoords, zoom: Zoom, index: u64) -> Self {
const STRIDE: u64 = size_of::<ShaderTileMetadata>() as u64;
Self {
coords,
zoom_factor: zoom.scale_to_tile(&coords),
transform: coords.transform_for_zoom(zoom),
buffer_range: index as u64 * STRIDE..(index as u64 + 1) * STRIDE,
}
}
}
pub struct TileInView {
pub shape: TileShape,
@ -61,7 +73,6 @@ impl<Q: Queue<B>, B> TileViewPattern<Q, B> {
pub fn update_pattern(
&mut self,
view_region: &ViewRegion,
tile_cache: &TileCache,
buffer_pool: &BufferPool<
wgpu::Queue,
Buffer,
@ -70,43 +81,31 @@ impl<Q: Queue<B>, B> TileViewPattern<Q, B> {
ShaderLayerMetadata,
ShaderFeatureStyle,
>,
zoom: f64,
zoom: Zoom,
) {
self.in_view.clear();
let stride = size_of::<ShaderTileMetadata>() as u64;
let mut index = 0;
let pool_index = buffer_pool.index();
for coords in view_region.iter() {
if coords.build_quad_key().is_none() {
continue;
}
let shape = TileShape {
coords,
zoom_factor: 2.0_f64.powf(coords.z as f64 - zoom),
transform: coords.transform_for_zoom(zoom),
buffer_range: index as u64 * stride..(index as u64 + 1) * stride,
};
let shape = TileShape::new(coords, zoom, index);
index += 1;
let fallback = {
if !buffer_pool.index().has_tile(&coords) {
if let Some(fallback_coords) =
buffer_pool.index().get_tile_coords_fallback(&coords)
{
if !pool_index.has_tile(&coords) {
if let Some(fallback_coords) = pool_index.get_tile_coords_fallback(&coords) {
tracing::trace!(
"Could not find data at {coords}. Falling back to {fallback_coords}"
);
let shape = TileShape {
coords: fallback_coords,
zoom_factor: 2.0_f64.powf(fallback_coords.z as f64 - zoom),
transform: fallback_coords.transform_for_zoom(zoom),
buffer_range: index as u64 * stride..(index as u64 + 1) * stride,
};
let shape = TileShape::new(fallback_coords, zoom, index);
index += 1;
Some(shape)

View File

@ -5,6 +5,7 @@ pub mod math;
use crate::coords::WorldTileCoords;
pub use fps_meter::FPSMeter;
use std::ops::{Deref, DerefMut};
struct MinMaxBoundingBox {
min_x: i32,
@ -49,3 +50,61 @@ impl MinMaxBoundingBox {
}
}
}
pub struct ChangeObserver<T> {
inner: T,
last_value: Option<T>,
}
impl<T> ChangeObserver<T> {
pub fn new(value: T) -> Self {
Self {
inner: value,
last_value: None,
}
}
}
impl<T> ChangeObserver<T>
where
T: Clone + Eq,
{
pub fn finished_observing(&mut self) {
self.last_value = Some(self.inner.clone());
}
pub fn did_change(&self) -> bool {
if let Some(last_value) = &self.last_value {
if !last_value.eq(&self.inner) {
true
} else {
false
}
} else {
true
}
}
}
impl<T> Default for ChangeObserver<T>
where
T: Default,
{
fn default() -> Self {
ChangeObserver::new(T::default())
}
}
impl<T> Deref for ChangeObserver<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for ChangeObserver<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}