Refactor headless module, introduce world

This commit is contained in:
Maximilian Ammann 2022-09-25 13:26:35 +02:00
parent f8b38dc8f2
commit 4879d9c0b4
35 changed files with 626 additions and 593 deletions

View File

@ -1,95 +1,8 @@
use std::ops::Div;
use cgmath::Angle;
use crate::render::Renderer;
use crate::style::Style;
use crate::window::WindowSize;
use crate::{
coords::{LatLon, ViewRegion, WorldCoords, Zoom, ZoomLevel, TILE_SIZE},
io::tile_repository::TileRepository,
render::camera::{Camera, Perspective, ViewProjection},
util::ChangeObserver,
};
/// Stores the camera configuration.
pub struct ViewState {
pub zoom: ChangeObserver<Zoom>,
pub camera: ChangeObserver<Camera>,
pub perspective: Perspective,
}
impl ViewState {
pub fn new<P: Into<cgmath::Rad<f64>>>(
window_size: &WindowSize,
position: WorldCoords,
zoom: Zoom,
pitch: f64,
fovy: P,
) -> Self {
let tile_center = TILE_SIZE / 2.0;
let fovy = fovy.into();
let height = tile_center / (fovy / 2.0).tan();
let camera = Camera::new(
(position.x, position.y, height),
cgmath::Deg(-90.0),
cgmath::Deg(pitch),
window_size.width(),
window_size.height(),
);
let perspective = Perspective::new(
window_size.width(),
window_size.height(),
cgmath::Deg(110.0),
// in tile.vertex.wgsl we are setting each layer's final `z` in ndc space to `z_index`.
// This means that regardless of the `znear` value all layers will be rendered as part
// of the near plane.
// These values have been selected experimentally:
// https://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
1024.0,
2048.0,
);
Self {
zoom: ChangeObserver::new(zoom),
camera: ChangeObserver::new(camera),
perspective,
}
}
pub fn create_view_region(&self) -> Option<ViewRegion> {
self.camera
.view_region_bounding_box(&self.view_projection().invert())
.map(|bounding_box| {
ViewRegion::new(bounding_box, 0, 32, *self.zoom, self.visible_level())
})
}
pub fn view_projection(&self) -> ViewProjection {
self.camera.calc_view_proj(&self.perspective)
}
pub fn visible_level(&self) -> ZoomLevel {
self.zoom.level()
}
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);
}
}
use crate::{render::Renderer, style::Style, world::World};
/// Stores the context of the map.
pub struct MapContext {
pub view_state: ViewState,
pub style: Style,
pub tile_repository: TileRepository,
pub world: World,
pub renderer: Renderer,
}

View File

@ -1,12 +1,14 @@
use crate::io::scheduler::Scheduler;
use crate::io::source_client::HttpClient;
use crate::io::{
apc::AsyncProcedureCall,
transferables::{
DefaultTessellatedLayer, DefaultTileTessellated, DefaultUnavailableLayer, Transferables,
use crate::{
io::{
apc::AsyncProcedureCall,
scheduler::Scheduler,
source_client::{HttpClient, HttpSourceClient, SourceClient},
transferables::{
DefaultTessellatedLayer, DefaultTileTessellated, DefaultUnavailableLayer, Transferables,
},
},
window::MapWindowConfig,
};
use crate::window::MapWindowConfig;
pub trait Environment: 'static {
type MapWindowConfig: MapWindowConfig;
@ -19,10 +21,10 @@ pub trait Environment: 'static {
}
pub struct Kernel<E: Environment> {
map_window_config: E::MapWindowConfig,
apc: E::AsyncProcedureCall,
scheduler: E::Scheduler,
http_client: E::HttpClient,
pub map_window_config: E::MapWindowConfig,
pub apc: E::AsyncProcedureCall,
pub scheduler: E::Scheduler,
pub source_client: SourceClient<E::HttpClient>,
}
pub struct KernelBuilder<E: Environment> {
@ -64,9 +66,9 @@ impl<E: Environment> KernelBuilder<E> {
pub fn build(self) -> Kernel<E> {
Kernel {
scheduler: self.scheduler.unwrap(), // TODO: Remove unwrap
apc: self.apc.unwrap(), // TODO: Remove unwrap
http_client: self.http_client.unwrap(), // TODO: Remove unwrap
scheduler: self.scheduler.unwrap(), // TODO: Remove unwrap
apc: self.apc.unwrap(), // TODO: Remove unwrap
source_client: SourceClient::new(HttpSourceClient::new(self.http_client.unwrap())), // TODO: Remove unwrap
map_window_config: self.map_window_config.unwrap(), // TODO: Remove unwrap
}
}

View File

@ -4,45 +4,17 @@ use std::{fmt, fmt::Formatter, sync::mpsc::SendError};
use lyon::tessellation::TessellationError;
#[derive(Debug)]
pub enum RenderError {
Surface(wgpu::SurfaceError),
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
RenderError::Surface(e) => write!(f, "{}", e),
}
}
}
impl RenderError {
pub fn should_exit(&self) -> bool {
match self {
RenderError::Surface(e) => match e {
wgpu::SurfaceError::OutOfMemory => true,
_ => false,
},
}
}
}
use crate::render::{error::RenderError, graph::RenderGraphError};
/// Enumeration of errors which can happen during the operation of the library.
#[derive(Debug)]
pub enum Error {
Schedule,
Scheduler,
Network(String),
Tesselation(TessellationError),
Render(RenderError),
}
impl From<wgpu::SurfaceError> for Error {
fn from(e: wgpu::SurfaceError) -> Self {
Error::Render(RenderError::Surface(e))
}
}
impl From<TessellationError> for Error {
fn from(e: TessellationError) -> Self {
Error::Tesselation(e)
@ -51,6 +23,6 @@ impl From<TessellationError> for Error {
impl<T> From<SendError<T>> for Error {
fn from(_e: SendError<T>) -> Self {
Error::Schedule
Error::Scheduler
}
}

View File

@ -1,355 +0,0 @@
use std::{
collections::HashSet,
fs::File,
future::Future,
io::Write,
iter,
marker::PhantomData,
ops::{Deref, Range},
sync::Arc,
};
use tokio::{runtime::Handle, task};
use wgpu::{BufferAsyncError, BufferSlice};
use crate::environment::Environment;
use crate::io::scheduler::Scheduler;
use crate::io::source_client::HttpClient;
use crate::render::Renderer;
use crate::style::Style;
use crate::window::{MapWindow, MapWindowConfig, WindowSize};
use crate::{
context::{MapContext, ViewState},
coords::{LatLon, ViewRegion, WorldCoords, WorldTileCoords, Zoom, TILE_SIZE},
error::Error,
headless::utils::HeadlessPipelineProcessor,
io::{
apc::{AsyncProcedureCall, SchedulerAsyncProcedureCall},
pipeline::{PipelineContext, Processable},
source_client::HttpSourceClient,
tile_pipelines::build_vector_tile_pipeline,
tile_repository::{StoredLayer, TileRepository},
transferables::{DefaultTransferables, Transferables},
TileRequest,
},
render::{
camera::ViewProjection,
create_default_render_graph, draw_graph,
eventually::Eventually,
graph::{Node, NodeRunError, RenderContext, RenderGraphContext, SlotInfo},
register_default_render_stages,
resource::{BufferDimensions, BufferedTextureHead, Head, IndexEntry, TrackedRenderPass},
stages::RenderStageLabel,
RenderState,
},
schedule::{Schedule, Stage},
};
pub struct HeadlessMapWindowConfig {
pub size: WindowSize,
}
impl MapWindowConfig for HeadlessMapWindowConfig {
type MapWindow = HeadlessMapWindow;
fn create(&self) -> Self::MapWindow {
Self::MapWindow { size: self.size }
}
}
pub struct HeadlessMapWindow {
size: WindowSize,
}
impl MapWindow for HeadlessMapWindow {
fn size(&self) -> WindowSize {
self.size
}
}
pub struct HeadlessEnvironment<
S: Scheduler,
HC: HttpClient,
T: Transferables,
APC: AsyncProcedureCall<T, HC>,
> {
phantom_s: PhantomData<S>,
phantom_hc: PhantomData<HC>,
phantom_t: PhantomData<T>,
phantom_apc: PhantomData<APC>,
}
impl<S: Scheduler, HC: HttpClient, T: Transferables, APC: AsyncProcedureCall<T, HC>> Environment
for HeadlessEnvironment<S, HC, T, APC>
{
type MapWindowConfig = HeadlessMapWindowConfig;
type AsyncProcedureCall = APC;
type Scheduler = S;
type HttpClient = HC;
type Transferables = T;
}
pub struct HeadlessMap<E: Environment> {
pub map_schedule: HeadlessMapSchedule<E>,
pub window: <E::MapWindowConfig as MapWindowConfig>::MapWindow,
}
impl<E: Environment> HeadlessMap<E> {
pub fn map_schedule_mut(&mut self) -> &mut HeadlessMapSchedule<E> {
&mut self.map_schedule
}
}
/// Stores the state of the map, dispatches tile fetching and caching, tessellation and drawing.
pub struct HeadlessMapSchedule<E: Environment> {
map_window_config: E::MapWindowConfig,
pub map_context: MapContext,
schedule: Schedule,
scheduler: E::Scheduler,
http_client: E::HttpClient,
}
impl<E: Environment> HeadlessMapSchedule<E> {
pub fn new(
map_window_config: E::MapWindowConfig,
window_size: WindowSize,
renderer: Renderer,
scheduler: E::Scheduler,
http_client: E::HttpClient,
style: Style,
) -> Self {
let view_state = ViewState::new(
&window_size,
WorldCoords::from((TILE_SIZE / 2., TILE_SIZE / 2.)),
Zoom::default(),
0.0,
cgmath::Deg(110.0),
);
let tile_repository = TileRepository::new();
let mut schedule = Schedule::default();
let mut graph = create_default_render_graph().unwrap(); // TODO: remove unwrap
let draw_graph = graph.get_sub_graph_mut(draw_graph::NAME).unwrap(); // TODO: remove unwrap
draw_graph.add_node(draw_graph::node::COPY, CopySurfaceBufferNode::default());
draw_graph
.add_node_edge(draw_graph::node::MAIN_PASS, draw_graph::node::COPY)
.unwrap(); // TODO: remove unwrap
register_default_render_stages(graph, &mut schedule);
schedule.add_stage(
RenderStageLabel::Cleanup,
WriteSurfaceBufferStage::default(),
);
Self {
map_window_config,
map_context: MapContext {
view_state,
style,
tile_repository,
renderer,
},
schedule,
scheduler,
http_client,
}
}
#[tracing::instrument(name = "update_and_redraw", skip_all)]
pub fn update_and_redraw(&mut self) -> Result<(), Error> {
self.schedule.run(&mut self.map_context);
Ok(())
}
pub fn schedule(&self) -> &Schedule {
&self.schedule
}
pub fn scheduler(&self) -> &E::Scheduler {
&self.scheduler
}
pub fn http_client(&self) -> &E::HttpClient {
&self.http_client
}
pub async fn fetch_process(&mut self, coords: &WorldTileCoords) -> Option<()> {
let source_layers: HashSet<String> = self
.map_context
.style
.layers
.iter()
.filter_map(|layer| layer.source_layer.clone())
.collect();
let http_source_client: HttpSourceClient<E::HttpClient> =
HttpSourceClient::new(self.http_client.clone());
let data = http_source_client
.fetch(&coords)
.await
.unwrap() // TODO: remove unwrap
.into_boxed_slice();
let mut pipeline_context = PipelineContext::new(HeadlessPipelineProcessor::default());
let pipeline = build_vector_tile_pipeline();
let request = TileRequest {
coords: WorldTileCoords::default(),
layers: source_layers,
};
pipeline.process((request, data), &mut pipeline_context);
let mut processor = pipeline_context
.take_processor::<HeadlessPipelineProcessor>()
.unwrap(); // TODO: remove unwrap
if let Eventually::Initialized(pool) = self.map_context.renderer.state.buffer_pool_mut() {
pool.clear();
}
self.map_context.tile_repository.clear();
while let Some(layer) = processor.layers.pop() {
self.map_context
.tile_repository
.create_tile(&layer.get_coords());
self.map_context
.tile_repository
.put_tessellated_layer(layer);
}
Some(())
}
}
/// Node which copies the contents of the GPU-side texture in [`BufferedTextureHead`] to an
/// unmapped GPU-side buffer. This buffer will be mapped in
/// [`crate::render::stages::write_surface_buffer_stage::WriteSurfaceBufferStage`].
#[derive(Default)]
pub struct CopySurfaceBufferNode {}
impl CopySurfaceBufferNode {
pub fn new() -> Self {
Self {}
}
}
impl Node for CopySurfaceBufferNode {
fn input(&self) -> Vec<SlotInfo> {
vec![]
}
fn update(&mut self, _state: &mut RenderState) {}
fn run(
&self,
_graph: &mut RenderGraphContext,
RenderContext {
command_encoder, ..
}: &mut RenderContext,
state: &RenderState,
) -> Result<(), NodeRunError> {
let surface = state.surface();
match surface.head() {
Head::Headed(_) => {}
Head::Headless(buffered_texture) => {
let size = surface.size();
command_encoder.copy_texture_to_buffer(
buffered_texture.texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &buffered_texture.output_buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(
std::num::NonZeroU32::new(
buffered_texture.buffer_dimensions.padded_bytes_per_row as u32,
)
.unwrap(), // TODO: remove unwrap
),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: size.width() as u32,
height: size.height() as u32,
depth_or_array_layers: 1,
},
);
}
}
Ok(())
}
}
/// Stage which writes the current contents of the GPU/CPU buffer in [`BufferedTextureHead`]
/// to disk as PNG.
#[derive(Default)]
pub struct WriteSurfaceBufferStage {
frame: u64,
}
impl Stage for WriteSurfaceBufferStage {
fn run(
&mut self,
MapContext {
renderer: Renderer { state, device, .. },
..
}: &mut MapContext,
) {
let surface = state.surface();
match surface.head() {
Head::Headed(_) => {}
Head::Headless(buffered_texture) => {
let buffered_texture: Arc<BufferedTextureHead> = buffered_texture.clone();
let device = device.clone();
let current_frame = self.frame;
task::block_in_place(|| {
Handle::current().block_on(async {
buffered_texture
.create_png(&device, format!("frame_{}.png", current_frame).as_str())
.await;
})
});
self.frame += 1;
}
}
}
}
pub mod utils {
use crate::{
coords::WorldTileCoords,
io::{pipeline::PipelineProcessor, tile_repository::StoredLayer, RawLayer},
render::ShaderVertex,
tessellation::{IndexDataType, OverAlignedVertexBuffer},
};
#[derive(Default)]
pub struct HeadlessPipelineProcessor {
pub layers: Vec<StoredLayer>,
}
impl PipelineProcessor for HeadlessPipelineProcessor {
fn layer_tesselation_finished(
&mut self,
coords: &WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
feature_indices: Vec<u32>,
layer_data: RawLayer,
) {
self.layers.push(StoredLayer::TessellatedLayer {
coords: *coords,
layer_name: layer_data.name,
buffer,
feature_indices,
})
}
}
}

View File

@ -0,0 +1,16 @@
use crate::environment::Environment;
use crate::headless::window::HeadlessMapWindowConfig;
use crate::io::apc::SchedulerAsyncProcedureCall;
use crate::io::transferables::DefaultTransferables;
use crate::platform::http_client::ReqwestHttpClient;
use crate::platform::scheduler::TokioScheduler;
pub struct HeadlessEnvironment;
impl Environment for HeadlessEnvironment {
type MapWindowConfig = HeadlessMapWindowConfig;
type AsyncProcedureCall = SchedulerAsyncProcedureCall<Self::HttpClient, Self::Scheduler>;
type Scheduler = TokioScheduler;
type HttpClient = ReqwestHttpClient;
type Transferables = DefaultTransferables;
}

View File

@ -0,0 +1,63 @@
use crate::render::graph::{Node, NodeRunError, RenderContext, RenderGraphContext, SlotInfo};
use crate::render::resource::Head;
use crate::render::RenderState;
/// Node which copies the contents of the GPU-side texture in [`BufferedTextureHead`] to an
/// unmapped GPU-side buffer. This buffer will be mapped in
/// [`crate::render::stages::write_surface_buffer_stage::WriteSurfaceBufferStage`].
#[derive(Default)]
pub struct CopySurfaceBufferNode {}
impl CopySurfaceBufferNode {
pub fn new() -> Self {
Self {}
}
}
impl Node for CopySurfaceBufferNode {
fn input(&self) -> Vec<SlotInfo> {
vec![]
}
fn update(&mut self, _state: &mut RenderState) {}
fn run(
&self,
_graph: &mut RenderGraphContext,
RenderContext {
command_encoder, ..
}: &mut RenderContext,
state: &RenderState,
) -> Result<(), NodeRunError> {
let surface = state.surface();
match surface.head() {
Head::Headed(_) => {}
Head::Headless(buffered_texture) => {
let size = surface.size();
command_encoder.copy_texture_to_buffer(
buffered_texture.texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &buffered_texture.output_buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(
std::num::NonZeroU32::new(
buffered_texture.buffer_dimensions.padded_bytes_per_row as u32,
)
.unwrap(), // TODO: remove unwrap
),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: size.width() as u32,
height: size.height() as u32,
depth_or_array_layers: 1,
},
);
}
}
Ok(())
}
}

View File

@ -0,0 +1,145 @@
use crate::io::pipeline::Processable;
use crate::{
context::MapContext,
coords::{WorldCoords, WorldTileCoords, Zoom, TILE_SIZE},
environment::Kernel,
error::Error,
headless::{
environment::HeadlessEnvironment, graph_node::CopySurfaceBufferNode,
stage::WriteSurfaceBufferStage,
},
io::{
pipeline::{PipelineContext, PipelineProcessor},
tile_pipelines::build_vector_tile_pipeline,
tile_repository::{StoredLayer, StoredTile, TileStatus},
RawLayer, TileRequest,
},
render::{
create_default_render_graph, draw_graph, error::RenderError, eventually::Eventually,
register_default_render_stages, resource::Head, stages::RenderStageLabel, Renderer,
ShaderVertex,
},
schedule::{Schedule, Stage},
style::Style,
tessellation::{IndexDataType, OverAlignedVertexBuffer},
window::WindowSize,
world::World,
};
use std::collections::HashSet;
pub struct HeadlessMap {
window_size: WindowSize,
kernel: Kernel<HeadlessEnvironment>,
map_context: MapContext,
schedule: Schedule,
}
impl HeadlessMap {
pub fn new(
style: Style,
window_size: WindowSize,
renderer: Renderer,
kernel: Kernel<HeadlessEnvironment>,
) -> Result<Self, Error> {
let world = World::new(
window_size,
WorldCoords::from((TILE_SIZE / 2., TILE_SIZE / 2.)),
Zoom::default(),
cgmath::Deg(110.0),
);
let mut schedule = Schedule::default();
let mut graph = create_default_render_graph()?;
let draw_graph = graph
.get_sub_graph_mut(draw_graph::NAME)
.expect("Subgraph does not exist");
draw_graph.add_node(draw_graph::node::COPY, CopySurfaceBufferNode::default());
draw_graph
.add_node_edge(draw_graph::node::MAIN_PASS, draw_graph::node::COPY)
.unwrap(); // TODO: remove unwrap
register_default_render_stages(graph, &mut schedule);
schedule.add_stage(
RenderStageLabel::Cleanup,
WriteSurfaceBufferStage::default(),
);
Ok(Self {
window_size,
kernel,
map_context: MapContext {
style,
world,
renderer,
},
schedule,
})
}
pub async fn render_tile(&mut self, tile: StoredTile) -> Result<(), Error> {
let context = &mut self.map_context;
if let Eventually::Initialized(pool) = context.renderer.state.buffer_pool_mut() {
pool.clear();
}
context.world.tile_repository.put_tile(tile);
self.schedule.run(&mut self.map_context);
Ok(())
}
pub async fn fetch_tile(
&self,
coords: WorldTileCoords,
source_layers: HashSet<String>,
) -> Result<StoredTile, Error> {
let source_client = &self.kernel.source_client;
let data = source_client.fetch(&coords).await?.into_boxed_slice();
let mut pipeline_context = PipelineContext::new(HeadlessPipelineProcessor::default());
let pipeline = build_vector_tile_pipeline();
pipeline.process(
(
TileRequest {
coords: WorldTileCoords::default(),
layers: source_layers,
},
data,
),
&mut pipeline_context,
);
let mut processor = pipeline_context
.take_processor::<HeadlessPipelineProcessor>()
.expect("Unable to get processor");
Ok(StoredTile::success(coords, processor.layers))
}
}
#[derive(Default)]
pub struct HeadlessPipelineProcessor {
pub layers: Vec<StoredLayer>,
}
impl PipelineProcessor for HeadlessPipelineProcessor {
fn layer_tesselation_finished(
&mut self,
coords: &WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
feature_indices: Vec<u32>,
layer_data: RawLayer,
) {
self.layers.push(StoredLayer::TessellatedLayer {
coords: *coords,
layer_name: layer_data.name,
buffer,
feature_indices,
})
}
}

View File

@ -0,0 +1,6 @@
mod environment;
mod graph_node;
mod stage;
mod window;
pub mod map;

View File

@ -0,0 +1,50 @@
use std::sync::Arc;
use tokio::{runtime::Handle, task};
use crate::{
context::MapContext,
render::{
resource::{BufferedTextureHead, Head},
Renderer,
},
schedule::Stage,
};
/// Stage which writes the current contents of the GPU/CPU buffer in [`BufferedTextureHead`]
/// to disk as PNG.
#[derive(Default)]
pub struct WriteSurfaceBufferStage {
frame: u64,
}
impl Stage for WriteSurfaceBufferStage {
fn run(
&mut self,
MapContext {
renderer: Renderer { state, device, .. },
..
}: &mut MapContext,
) {
let surface = state.surface();
match surface.head() {
Head::Headed(_) => {}
Head::Headless(buffered_texture) => {
let buffered_texture: Arc<BufferedTextureHead> = buffered_texture.clone();
let device = device.clone();
let current_frame = self.frame;
task::block_in_place(|| {
Handle::current().block_on(async {
buffered_texture
.create_png(&device, format!("frame_{}.png", current_frame).as_str())
.await;
})
});
self.frame += 1;
}
}
}
}

View File

@ -0,0 +1,23 @@
use crate::window::{MapWindow, MapWindowConfig, WindowSize};
pub struct HeadlessMapWindowConfig {
pub size: WindowSize,
}
impl MapWindowConfig for HeadlessMapWindowConfig {
type MapWindow = HeadlessMapWindow;
fn create(&self) -> Self::MapWindow {
Self::MapWindow { size: self.size }
}
}
pub struct HeadlessMapWindow {
size: WindowSize,
}
impl MapWindow for HeadlessMapWindow {
fn size(&self) -> WindowSize {
self.size
}
}

View File

@ -10,12 +10,11 @@ use std::{
use serde::{Deserialize, Serialize};
use crate::io::scheduler::Scheduler;
use crate::io::source_client::HttpClient;
use crate::{
coords::WorldTileCoords,
io::{
source_client::{HttpSourceClient, SourceClient},
scheduler::Scheduler,
source_client::{HttpClient, HttpSourceClient, SourceClient},
transferables::{DefaultTransferables, Transferables},
TileRequest,
},
@ -112,7 +111,7 @@ impl<HC: HttpClient, S: Scheduler> AsyncProcedureCall<DefaultTransferables, HC>
input,
SchedulerContext {
sender,
source_client: SourceClient::Http(HttpSourceClient::new(client)),
source_client: SourceClient::new(HttpSourceClient::new(client)),
},
)
.await;

View File

@ -31,6 +31,6 @@ impl Scheduler for NopScheduler {
where
T: Future<Output = ()> + 'static,
{
Err(Error::Schedule)
Err(Error::Scheduler)
}
}

View File

@ -32,25 +32,23 @@ where
/// Defines the different types of HTTP clients such as basic HTTP and Mbtiles.
/// More types might be coming such as S3 and other cloud http clients.
#[derive(Clone)]
pub enum SourceClient<HC>
pub struct SourceClient<HC>
where
HC: HttpClient,
{
Http(HttpSourceClient<HC>),
Mbtiles {
// TODO
},
http: HttpSourceClient<HC>, // TODO: mbtiles: Mbtiles
}
impl<HC> SourceClient<HC>
where
HC: HttpClient,
{
pub fn new(http: HttpSourceClient<HC>) -> Self {
Self { http }
}
pub async fn fetch(&self, coords: &WorldTileCoords) -> Result<Vec<u8>, Error> {
match self {
SourceClient::Http(client) => client.fetch(coords).await,
SourceClient::Mbtiles { .. } => unimplemented!(),
}
self.http.fetch(coords).await
}
}

View File

@ -50,17 +50,27 @@ pub enum TileStatus {
/// Stores multiple [StoredLayers](StoredLayer).
pub struct StoredTile {
coords: WorldTileCoords,
layers: Vec<StoredLayer>,
status: TileStatus,
}
impl StoredTile {
pub fn new() -> Self {
pub fn new(coords: WorldTileCoords) -> Self {
Self {
coords,
layers: vec![],
status: TileStatus::Pending,
}
}
pub fn success(coords: WorldTileCoords, layers: Vec<StoredLayer>) -> Self {
Self {
coords,
layers,
status: TileStatus::Success,
}
}
}
/// Stores and provides access to a quad tree of cached tiles with world tile coords.
@ -102,6 +112,12 @@ impl TileRepository {
}
}
pub fn put_tile(&mut self, tile: StoredTile) {
if let Some(key) = tile.coords.build_quad_key() {
self.tree.insert(key, tile);
}
}
/// Returns the list of tessellated layers at the given world tile coords. None if tile is
/// missing from the cache.
pub fn iter_tessellated_layers_at(
@ -115,11 +131,11 @@ impl TileRepository {
}
/// Create a new tile.
pub fn create_tile(&mut self, coords: &WorldTileCoords) -> bool {
pub fn create_tile(&mut self, coords: WorldTileCoords) -> bool {
if let Some(entry) = coords.build_quad_key().map(|key| self.tree.entry(key)) {
match entry {
btree_map::Entry::Vacant(entry) => {
entry.insert(StoredTile::new());
entry.insert(StoredTile::new(coords));
}
_ => {}
}

View File

@ -43,6 +43,7 @@ pub mod benchmarking;
// Internal modules
pub(crate) mod tessellation;
mod world;
// Export tile format
pub use geozero::mvt::tile;

View File

@ -1,22 +1,25 @@
use std::{cell::RefCell, marker::PhantomData, mem, rc::Rc};
use crate::environment::Environment;
use crate::render::settings::{RendererSettings, WgpuSettings};
use crate::render::Renderer;
use crate::window::{HeadedMapWindow, MapWindowConfig, WindowSize};
use crate::{
context::{MapContext, ViewState},
context::MapContext,
coords::{LatLon, WorldCoords, Zoom, TILE_SIZE},
environment::Environment,
error::Error,
io::{
scheduler::Scheduler,
source_client::{HttpClient, HttpSourceClient},
tile_repository::TileRepository,
},
render::{create_default_render_graph, register_default_render_stages},
render::{
create_default_render_graph, register_default_render_stages,
settings::{RendererSettings, WgpuSettings},
Renderer,
},
schedule::{Schedule, Stage},
stages::register_stages,
style::Style,
window::{HeadedMapWindow, MapWindowConfig, WindowSize},
world::{ViewState, World},
};
/// Stores the state of the map, dispatches tile fetching and caching, tessellation and drawing.
@ -34,26 +37,15 @@ pub struct InteractiveMapSchedule<E: Environment> {
}
impl<E: Environment> InteractiveMapSchedule<E> {
pub fn new(
map_window_config: E::MapWindowConfig,
window_size: WindowSize,
renderer: Option<Renderer>,
scheduler: E::Scheduler, // TODO: unused
apc: E::AsyncProcedureCall,
http_client: E::HttpClient,
style: Style,
wgpu_settings: WgpuSettings,
renderer_settings: RendererSettings,
) -> Self {
let zoom = style.zoom.map(|zoom| Zoom::new(zoom)).unwrap_or_default();
let position = style
.center
.map(|center| WorldCoords::from_lat_lon(LatLon::new(center[0], center[1]), zoom))
.unwrap_or_default();
let pitch = style.pitch.unwrap_or_default();
let view_state = ViewState::new(&window_size, position, zoom, pitch, cgmath::Deg(110.0));
pub fn new(window_size: WindowSize, renderer: Option<Renderer>, style: Style) -> Self {
let center = style.center.unwrap_or_default();
let world = World::new_at(
window_size,
LatLon::new(center[0], center[1]),
style.zoom.map(|zoom| Zoom::new(zoom)).unwrap_or_default(),
cgmath::Deg::<f64>(style.pitch.unwrap_or_default()),
);
let tile_repository = TileRepository::new();
let mut schedule = Schedule::default();
let apc = Rc::new(RefCell::new(apc));
@ -70,16 +62,14 @@ impl<E: Environment> InteractiveMapSchedule<E> {
map_window_config,
map_context: match renderer {
None => EventuallyMapContext::Premature(PrematureMapContext {
view_state,
world,
style,
tile_repository,
wgpu_settings,
renderer_settings,
}),
Some(renderer) => EventuallyMapContext::Full(MapContext {
view_state,
world,
style,
tile_repository,
renderer,
}),
},
@ -169,10 +159,9 @@ where
}
pub struct PrematureMapContext {
view_state: ViewState,
style: Style,
world: World,
tile_repository: TileRepository,
style: Style,
wgpu_settings: WgpuSettings,
renderer_settings: RendererSettings,

View File

@ -3,8 +3,7 @@ use reqwest::{Client, StatusCode};
use reqwest_middleware::ClientWithMiddleware;
use reqwest_middleware_cache::{managers::CACacheManager, Cache, CacheMode};
use crate::error::Error;
use crate::io::source_client::HttpClient;
use crate::{error::Error, io::source_client::HttpClient};
#[derive(Clone)]
pub struct ReqwestHttpClient {

View File

@ -1,7 +1,6 @@
use std::future::Future;
use crate::error::Error;
use crate::io::scheduler::Scheduler;
use crate::{error::Error, io::scheduler::Scheduler};
/// Multi-threading with Tokio.
pub struct TokioScheduler;

View File

@ -1,8 +1,12 @@
use crate::environment::Environment;
use crate::render::settings::{RendererSettings, WgpuSettings};
use crate::render::Renderer;
use crate::style::Style;
use crate::window::{HeadedMapWindow, MapWindow, MapWindowConfig};
use crate::{
environment::Environment,
render::{
settings::{RendererSettings, WgpuSettings},
Renderer,
},
style::Style,
window::{HeadedMapWindow, MapWindow, MapWindowConfig},
};
pub struct RenderBuilder {
wgpu_settings: Option<WgpuSettings>,

View File

@ -0,0 +1,36 @@
use std::fmt;
use crate::{error::Error, render::graph::RenderGraphError};
#[derive(Debug)]
pub enum RenderError {
Surface(wgpu::SurfaceError),
Graph(RenderGraphError),
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RenderError::Surface(e) => write!(f, "{}", e),
RenderError::Graph(e) => write!(f, "{}", e),
}
}
}
impl RenderError {
pub fn should_exit(&self) -> bool {
match self {
RenderError::Surface(e) => match e {
wgpu::SurfaceError::OutOfMemory => true,
_ => false,
},
_ => true,
}
}
}
impl From<wgpu::SurfaceError> for Error {
fn from(e: wgpu::SurfaceError) -> Self {
Error::Render(RenderError::Surface(e))
}
}

View File

@ -50,17 +50,20 @@ mod tile_view_pattern;
// Public API
pub mod builder;
pub mod camera;
pub mod error;
pub mod eventually;
pub mod settings;
pub use shaders::ShaderVertex;
pub use stages::register_default_render_stages;
use crate::render::{
graph::{EmptyNode, RenderGraph, RenderGraphError},
main_pass::{MainPassDriverNode, MainPassNode},
use crate::{
render::{
graph::{EmptyNode, RenderGraph, RenderGraphError},
main_pass::{MainPassDriverNode, MainPassNode},
},
window::{HeadedMapWindow, MapWindow},
};
use crate::window::{HeadedMapWindow, MapWindow};
const INDEX_FORMAT: wgpu::IndexFormat = wgpu::IndexFormat::Uint32; // Must match IndexDataType
@ -409,10 +412,11 @@ impl Renderer {
#[cfg(test)]
mod tests {
use crate::render::settings::RendererSettings;
use crate::render::RenderState;
use crate::window::{MapWindow, MapWindowConfig, WindowSize};
use crate::{MapWindow, MapWindowConfig, WindowSize};
use crate::{
render::{settings::RendererSettings, RenderState},
window::{MapWindow, MapWindowConfig, WindowSize},
MapWindow, MapWindowConfig, WindowSize,
};
pub struct HeadlessMapWindowConfig {
size: WindowSize,

View File

@ -1,13 +1,12 @@
//! Specifies the instructions which are going to be sent to the GPU. Render commands can be concatenated
//! into a new render command which executes multiple instruction sets.
use crate::render::RenderState;
use crate::render::{
eventually::Eventually::Initialized,
render_phase::{PhaseItem, RenderCommand, RenderCommandResult},
resource::{Globals, IndexEntry, TrackedRenderPass},
tile_view_pattern::{TileInView, TileShape},
INDEX_FORMAT,
RenderState, INDEX_FORMAT,
};
impl PhaseItem for TileInView {

View File

@ -1,5 +1,4 @@
use crate::render::resource::TrackedRenderPass;
use crate::render::RenderState;
use crate::render::{resource::TrackedRenderPass, RenderState};
/// A draw function which is used to draw a specific [`PhaseItem`].
///

View File

@ -3,10 +3,9 @@
use std::{mem::size_of, sync::Arc};
use crate::window::{MapWindow, WindowSize};
use crate::{
render::{eventually::HasChanged, resource::texture::TextureView, settings::RendererSettings},
window::HeadedMapWindow,
window::{HeadedMapWindow, MapWindow, WindowSize},
};
pub struct BufferDimensions {

View File

@ -1,9 +1,11 @@
//! Extracts data from the current state.
use crate::render::{RenderState, Renderer};
use crate::{
context::MapContext, coords::ViewRegion, render::eventually::Eventually::Initialized,
context::MapContext,
coords::ViewRegion,
render::{eventually::Eventually::Initialized, RenderState, Renderer},
schedule::Stage,
world::World,
};
#[derive(Default)]
@ -13,7 +15,7 @@ impl Stage for ExtractStage {
fn run(
&mut self,
MapContext {
view_state,
world: World { view_state, .. },
renderer:
Renderer {
state:

View File

@ -2,11 +2,11 @@
use log::error;
use crate::render::Renderer;
use crate::{
context::MapContext,
render::{
eventually::Eventually::Initialized, graph::RenderGraph, graph_runner::RenderGraphRunner,
Renderer,
},
schedule::Stage,
};

View File

@ -1,7 +1,10 @@
//! Sorts items of the [RenderPhases](RenderPhase).
use crate::render::Renderer;
use crate::{context::MapContext, render::render_phase::RenderPhase, schedule::Stage};
use crate::{
context::MapContext,
render::{render_phase::RenderPhase, Renderer},
schedule::Stage,
};
#[derive(Default)]
pub struct PhaseSortStage;

View File

@ -1,10 +1,10 @@
//! Queues [PhaseItems](crate::render::render_phase::PhaseItem) for rendering.
use crate::render::{RenderState, Renderer};
use crate::{
context::MapContext,
render::{
eventually::Eventually::Initialized, resource::IndexEntry, tile_view_pattern::TileInView,
RenderState, Renderer,
},
schedule::Stage,
};
@ -17,7 +17,6 @@ impl Stage for QueueStage {
fn run(
&mut self,
MapContext {
view_state: _,
renderer:
Renderer {
state:

View File

@ -2,7 +2,6 @@
use std::mem::size_of;
use crate::render::Renderer;
use crate::{
context::MapContext,
render::{
@ -11,6 +10,7 @@ use crate::{
shaders::{Shader, ShaderTileMetadata},
tile_pipeline::TilePipeline,
tile_view_pattern::{TileViewPattern, DEFAULT_TILE_VIEW_SIZE},
Renderer,
},
schedule::Stage,
};

View File

@ -2,8 +2,6 @@
use std::iter;
use crate::render::{RenderState, Renderer};
use crate::style::Style;
use crate::{
context::MapContext,
coords::ViewRegion,
@ -12,8 +10,11 @@ use crate::{
camera::ViewProjection,
eventually::Eventually::Initialized,
shaders::{ShaderCamera, ShaderFeatureStyle, ShaderGlobals, ShaderLayerMetadata, Vec4f32},
RenderState, Renderer,
},
schedule::Stage,
style::Style,
world::World,
};
#[derive(Default)]
@ -24,9 +25,12 @@ impl Stage for UploadStage {
fn run(
&mut self,
MapContext {
view_state,
world:
World {
tile_repository,
view_state,
},
style,
tile_repository,
renderer: Renderer { queue, state, .. },
..
}: &mut MapContext,
@ -41,8 +45,7 @@ impl Stage for UploadStage {
bytemuck::cast_slice(&[ShaderGlobals::new(ShaderCamera::new(
view_proj.downcast().into(),
view_state
.camera
.position
.camera_position()
.to_homogeneous()
.cast::<f32>()
.unwrap() // TODO: Remove unwrap

View File

@ -10,16 +10,15 @@ use std::{
use geozero::{mvt::tile, GeozeroDatasource};
use request_stage::RequestStage;
use crate::environment::Environment;
use crate::io::source_client::HttpClient;
use crate::{
coords::{WorldCoords, WorldTileCoords, Zoom, ZoomLevel},
environment::Environment,
error::Error,
io::{
apc::{AsyncProcedureCall, Context, Message},
geometry_index::{GeometryIndex, IndexedGeometry, TileIndex},
pipeline::{PipelineContext, PipelineProcessor, Processable},
source_client::HttpSourceClient,
source_client::{HttpClient, HttpSourceClient},
tile_pipelines::build_vector_tile_pipeline,
transferables::{
DefaultTessellatedLayer, DefaultTileTessellated, DefaultTransferables,

View File

@ -2,15 +2,16 @@
use std::{borrow::BorrowMut, cell::RefCell, ops::Deref, rc::Rc};
use crate::environment::Environment;
use crate::{
context::MapContext,
environment::Environment,
io::{
apc::{AsyncProcedureCall, Message},
tile_repository::StoredLayer,
transferables::{TessellatedLayer, TileTessellated, UnavailableLayer},
},
schedule::Stage,
world::World,
};
pub struct PopulateTileStore<E: Environment> {
@ -27,7 +28,10 @@ impl<E: Environment> Stage for PopulateTileStore<E> {
fn run(
&mut self,
MapContext {
tile_repository, ..
world: World {
tile_repository, ..
},
..
}: &mut MapContext,
) {
if let Ok(mut apc) = self.apc.deref().try_borrow_mut() {

View File

@ -12,11 +12,10 @@ use std::{
str::FromStr,
};
use crate::environment::Environment;
use crate::style::Style;
use crate::{
context::MapContext,
coords::{ViewRegion, WorldTileCoords, ZoomLevel},
environment::Environment,
error::Error,
io::{
apc::{AsyncProcedureCall, AsyncProcedureFuture, Context, Input, Message},
@ -29,6 +28,8 @@ use crate::{
},
schedule::Stage,
stages::HeadedPipelineProcessor,
style::Style,
world::World,
};
pub struct RequestStage<E: Environment> {
@ -52,23 +53,25 @@ impl<E: Environment> Stage for RequestStage<E> {
fn run(
&mut self,
MapContext {
view_state,
world:
World {
tile_repository,
view_state,
},
style,
tile_repository,
..
}: &mut MapContext,
) {
let view_region = view_state.create_view_region();
if view_state.camera.did_change(0.05) || view_state.zoom.did_change(0.05) {
if view_state.did_camera_change() || view_state.did_zoom_change() {
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.request_tiles_in_view(tile_repository, style, view_region);
}
}
view_state.camera.update_reference();
view_state.zoom.update_reference();
view_state.update_references();
}
}
@ -133,7 +136,7 @@ impl<E: Environment> RequestStage<E> {
for coords in view_region.iter() {
if coords.build_quad_key().is_some() {
// TODO: Make tesselation depend on style?
self.request_tile(tile_repository, &coords, &source_layers)
self.request_tile(tile_repository, coords, &source_layers)
.unwrap(); // TODO: Remove unwrap
}
}
@ -142,7 +145,7 @@ impl<E: Environment> RequestStage<E> {
fn request_tile(
&self,
tile_repository: &mut TileRepository,
coords: &WorldTileCoords,
coords: WorldTileCoords,
layers: &HashSet<String>,
) -> Result<(), Error> {
/* if !tile_repository.is_layers_missing(coords, layers) {
@ -155,7 +158,7 @@ impl<E: Environment> RequestStage<E> {
tracing::info!("new tile request: {}", &coords);
self.apc.deref().borrow().schedule(
Input::TileRequest(TileRequest {
coords: *coords,
coords,
layers: layers.clone(),
}),
schedule::<

View File

@ -2,10 +2,10 @@
use std::{cell::RefCell, rc::Rc};
use crate::environment::Environment;
use crate::map_schedule::InteractiveMapSchedule;
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use crate::{environment::Environment, map_schedule::InteractiveMapSchedule};
/// Window of a certain [`WindowSize`]. This can either be a proper window or a headless one.
pub trait MapWindow {
fn size(&self) -> WindowSize;

143
maplibre/src/world.rs Normal file
View File

@ -0,0 +1,143 @@
use cgmath::{Angle, Point3};
use crate::{
coords::{LatLon, ViewRegion, WorldCoords, Zoom, ZoomLevel, TILE_SIZE},
io::tile_repository::TileRepository,
render::camera::{Camera, Perspective, ViewProjection},
util::ChangeObserver,
window::WindowSize,
};
pub struct World {
pub view_state: ViewState,
pub tile_repository: TileRepository,
}
impl World {
pub fn new_at<P: Into<cgmath::Deg<f64>>>(
window_size: WindowSize,
initial_center: LatLon,
initial_zoom: Zoom,
pitch: P,
) -> Self {
Self::new(
window_size,
WorldCoords::from_lat_lon(initial_center, initial_zoom),
initial_zoom,
pitch,
)
}
pub fn new<P: Into<cgmath::Deg<f64>>>(
window_size: WindowSize,
initial_center: WorldCoords,
initial_zoom: Zoom,
pitch: P,
) -> Self {
let position = initial_center;
let view_state = ViewState::new(
window_size,
position,
initial_zoom,
pitch,
cgmath::Deg(110.0),
);
let tile_repository = TileRepository::new();
World {
view_state,
tile_repository,
}
}
}
/// Stores the camera configuration.
pub struct ViewState {
zoom: ChangeObserver<Zoom>,
camera: ChangeObserver<Camera>,
perspective: Perspective,
}
impl ViewState {
pub fn new<F: Into<cgmath::Rad<f64>>, P: Into<cgmath::Deg<f64>>>(
window_size: WindowSize,
position: WorldCoords,
zoom: Zoom,
pitch: P,
fovy: F,
) -> Self {
let tile_center = TILE_SIZE / 2.0;
let fovy = fovy.into();
let height = tile_center / (fovy / 2.0).tan();
let camera = Camera::new(
(position.x, position.y, height),
cgmath::Deg(-90.0),
pitch.into(),
window_size.width(),
window_size.height(),
);
let perspective = Perspective::new(
window_size.width(),
window_size.height(),
cgmath::Deg(110.0),
// in tile.vertex.wgsl we are setting each layer's final `z` in ndc space to `z_index`.
// This means that regardless of the `znear` value all layers will be rendered as part
// of the near plane.
// These values have been selected experimentally:
// https://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
1024.0,
2048.0,
);
Self {
zoom: ChangeObserver::new(zoom),
camera: ChangeObserver::new(camera),
perspective,
}
}
pub fn create_view_region(&self) -> Option<ViewRegion> {
self.camera
.view_region_bounding_box(&self.view_projection().invert())
.map(|bounding_box| {
ViewRegion::new(bounding_box, 0, 32, *self.zoom, self.visible_level())
})
}
pub fn view_projection(&self) -> ViewProjection {
self.camera.calc_view_proj(&self.perspective)
}
pub fn visible_level(&self) -> ZoomLevel {
self.zoom.level()
}
pub fn zoom(&self) -> Zoom {
*self.zoom
}
pub fn camera_position(&self) -> Point3<f64> {
self.camera.position
}
pub fn did_zoom_change(&self) -> bool {
self.zoom.did_change(0.05)
}
pub fn did_camera_change(&self) -> bool {
self.camera.did_change(0.05)
}
pub fn update_references(&mut self) {
self.camera.update_reference();
self.zoom.update_reference();
}
pub fn update_zoom(&mut self, new_zoom: Zoom) {
*self.zoom = new_zoom;
log::info!("zoom: {}", new_zoom);
}
}