Headless windows & Data pipeline (#119)

* Implement headless rendering

* Simplify window traits

* Simplify and fix headless rendering

* Rename HttpClient

* Ignore wasm-pack output

* Refactor stages and context

* Rename runnable to eventloop and fix variable names

* Fix tests

* Add pipeline draft

* Start implementing pipeline steps

* Use pipeline in headless rendering

* Improve the style significantly. The colors are taken from the default OSM map style

* Start working on a specific headless API

* Refactor thread state and pipeline

* Refactor thread state and pipeline

* Fix web

* Remove mem::replace usage

* Improve pipeline types

* Simplify pipeline API

* Add comment

* Fix tests

* Remove dynamic dispatch for schedule

* Add Run Test config

* Revive old legacy webworker example

* Fix resize detection

* Cleanup dependencies and simplify bounds

* Conditionally enable create_png

* Add some comments and rename

* Run more tests in CI

* Fix tests for various platforms

* Install missing dependency
This commit is contained in:
Max Ammann 2022-06-01 19:01:05 +02:00 committed by GitHub
commit 8248d4b4d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1921 additions and 1414 deletions

View File

@ -7,4 +7,5 @@ rustflags = [
# Enables the possibility to import memory into wasm.
# Without --shared-memory it is not possible to use shared WebAssembly.Memory.
"-C", "link-args=--shared-memory --import-memory",
]
]
runner = 'wasm-bindgen-test-runner'

View File

@ -12,6 +12,7 @@ runs:
- name: Build
shell: bash
run: just build-android
# TODO: Additional clippy checks for different targets
- name: Check x86_64
shell: bash
run: |
@ -24,3 +25,8 @@ runs:
env "AR_aarch64-linux-android=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" \
env "CC_aarch64-linux-android=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" \
just check maplibre-android aarch64-linux-android
# FIXME: Requires cross-compilation
#- name: Test
# shell: bash
# # TODO: Additional test runs for different targets
# run: just test maplibre-android aarch64-linux-android

View File

@ -22,10 +22,14 @@ runs:
- name: Check x86_64 darwin
shell: bash
run: just check apple x86_64-apple-darwin
- name: Check aarch64 darwin
- name: Check x86_64 darwin
shell: bash
run: just check apple aarch64-apple-darwin
# TODO: Additional clippy checks for iOS
# TODO: Additional clippy checks for different targets (iOS)
run: just check apple x86_64-apple-darwin
- name: Test x86_64 darwin
shell: bash
# TODO: Additional test runs for different targets (Different targets might require emulation)
run: just test apple x86_64-apple-darwin
- name: Build Example
shell: bash
run: cd apple/xcode && xcodebuild -scheme "example (iOS)" -arch arm64 -sdk iphoneos build CODE_SIGNING_ALLOWED=NO

View File

@ -15,12 +15,12 @@ runs:
- name: Build
shell: bash
run: cargo build -p maplibre-demo
- name: Test
shell: bash
run: just test maplibre-demo x86_64-unknown-linux-gnu
- name: Check
shell: bash
run: just check maplibre-demo x86_64-unknown-linux-gnu
- name: Test x86_64 linux
shell: bash
run: just test maplibre-demo x86_64-unknown-linux-gnu
- uses: actions/upload-artifact@v2
with:
name: maplibre-rs

View File

@ -19,6 +19,12 @@ runs:
- name: Build
shell: bash
run: cd apple/xcode && xcodebuild -scheme "example (macOS)" build CODE_SIGNING_ALLOWED=NO MACOSX_DEPLOYMENT_TARGET=10.9 -derivedDataPath build
- name: Check x86_64 darwin
shell: bash
run: just check maplibre-demo x86_64-apple-darwin
- name: Test x86_64 darwin
shell: bash
run: just test maplibre-demo x86_64-apple-darwin
- uses: actions/upload-artifact@v3
with:
name: maplibre-x86_64-apple-darwin-demo

View File

@ -22,7 +22,10 @@ runs:
- name: Build
shell: bash
run: cargo build -p maplibre-demo --release --target x86_64-pc-windows-msvc
- name: Test
- name: Check x86_64 windows
shell: bash
run: just check maplibre-demo x86_64-pc-windows-msvc
- name: Test x86_64 windows
shell: bash
run: just test maplibre-demo x86_64-pc-windows-msvc
- uses: actions/upload-artifact@v3

18
.github/actions/tests/action.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: tests
description: Run tests
runs:
using: "composite"
steps:
- uses: extractions/setup-just@v1
- name: Install toolchain
shell: bash
run: just default-toolchain
- uses: Swatinem/rust-cache@v1
- name: Install Dependencies
shell: bash
run: sudo apt-get install -y libwayland-dev libxkbcommon-dev # Required for winit
- name: Test
shell: bash
# TODO: Additional test runs for different targets
run: just test maplibre x86_64-unknown-linux-gnu

View File

@ -18,3 +18,8 @@ runs:
- name: Check
shell: bash
run: just check web wasm32-unknown-unknown
- name: Test
shell: bash
run: |
cargo install wasm-bindgen-cli --version "0.2.80"
just web-test "web-webgl"

View File

@ -17,4 +17,9 @@ runs:
run: just web-demo build
- name: Check
shell: bash
run: just check web wasm32-unknown-unknown
run: just check web wasm32-unknown-unknown
- name: Test
shell: bash
run: |
cargo install wasm-bindgen-cli --version "0.2.80"
just web-test ""

View File

@ -17,6 +17,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/benchmarks
run-tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/tests
build-android:
runs-on: ubuntu-20.04
steps:
@ -57,7 +62,7 @@ jobs:
source: docs/book/.
destination: docs
key: ${{ secrets.SSH_KEY_MAXAMMANN_ORG }}
build-ios:
build-apple:
runs-on: macos-11
steps:
- uses: actions/checkout@v2

View File

@ -17,6 +17,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/benchmarks
run-tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/tests
build-android:
runs-on: ubuntu-20.04
steps:
@ -37,7 +42,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/docs
build-ios:
build-apple:
runs-on: macos-11
steps:
- uses: actions/checkout@v2

3
.idea/maplibre-rs.iml generated
View File

@ -20,9 +20,10 @@
<excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/docs/book" />
<excludeFolder url="file://$MODULE_DIR$/web/lib/.parcel-cache" />
<excludeFolder url="file://$MODULE_DIR$/web/lib/src/wasm-pack" />
<excludeFolder url="file://$MODULE_DIR$/maplibre-cache" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>

19
.idea/runConfigurations/Run_Tests.xml generated Normal file
View File

@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test --package maplibre" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View File

@ -45,12 +45,18 @@ web-lib TARGET: nightly-toolchain (web-install "lib")
web-demo TARGET: (web-install "demo")
cd web/demo && npm run {{TARGET}}
web-test FEATURES: nightly-toolchain
export RUSTUP_TOOLCHAIN=$NIGHTLY_TOOLCHAIN && cargo test -p web --features "{{FEATURES}}" --target wasm32-unknown-unknown -Z build-std=std,panic_abort
#profile-bench:
# cargo flamegraph --bench render -- --bench
build-android: print-android-env
build-android: nightly-toolchain print-android-env
export RUSTUP_TOOLCHAIN=$NIGHTLY_TOOLCHAIN && cd android/gradle && ./gradlew assembleDebug
test-android TARGET: nightly-toolchain print-android-env
export RUSTUP_TOOLCHAIN=$NIGHTLY_TOOLCHAIN && cargo test -p maplibre-android --target {{TARGET}} -Z build-std=std,panic_abort
# language=bash
print-android-env:
#!/usr/bin/env bash

View File

@ -14,7 +14,7 @@ trace = ["maplibre/trace", "tracing-subscriber", "tracing-tracy", "tracy-client"
[dependencies]
env_logger = "0.9"
maplibre = { path = "../maplibre", version = "0.0.2" }
maplibre = { path = "../maplibre", version = "0.0.2", features = ["headless"] }
maplibre-winit = { path = "../maplibre-winit", version = "0.0.1" }
tracing = { version = "0.1" }

View File

@ -1,8 +1,24 @@
use maplibre::benchmarking::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use maplibre::coords::{WorldTileCoords, ZoomLevel};
use maplibre::error::Error;
use maplibre::io::pipeline::Processable;
use maplibre::io::pipeline::{PipelineContext, PipelineProcessor};
use maplibre::io::scheduler::ScheduleMethod;
use maplibre::io::source_client::{HttpClient, HttpSourceClient};
use maplibre::io::tile_pipelines::build_vector_tile_pipeline;
use maplibre::io::tile_repository::StoredLayer;
use maplibre::io::{RawLayer, TileRequest, TileRequestID};
use maplibre::map_schedule::{EventuallyMapContext, InteractiveMapSchedule};
use maplibre::platform::http_client::ReqwestHttpClient;
use maplibre::platform::run_multithreaded;
use maplibre::platform::schedule_method::TokioScheduleMethod;
use maplibre::render::settings::{RendererSettings, TextureFormat};
use maplibre::render::ShaderVertex;
use maplibre::window::{EventLoop, MapWindow, MapWindowConfig, WindowSize};
use maplibre::MapBuilder;
use maplibre_winit::winit::{WinitEventLoop, WinitMapWindow, WinitMapWindowConfig, WinitWindow};
use std::any::Any;
use std::collections::HashSet;
#[cfg(feature = "trace")]
fn enable_tracing() {
@ -13,6 +29,27 @@ fn enable_tracing() {
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
}
pub struct HeadlessMapWindowConfig {
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
}
}
fn run_in_window() {
run_multithreaded(async {
@ -27,11 +64,99 @@ fn run_in_window() {
})
}
#[derive(Default)]
struct HeadlessPipelineProcessor {
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,
buffer,
feature_indices,
layer_data,
})
}
}
fn run_headless() {
run_multithreaded(async {
let mut map = MapBuilder::new()
.with_map_window_config(HeadlessMapWindowConfig {
size: WindowSize::new(1000, 1000).unwrap(),
})
.with_http_client(ReqwestHttpClient::new(None))
.with_schedule_method(TokioScheduleMethod::new())
.with_renderer_settings(RendererSettings {
texture_format: TextureFormat::Rgba8UnormSrgb,
..RendererSettings::default()
})
.build()
.initialize_headless()
.await;
let http_source_client: HttpSourceClient<ReqwestHttpClient> =
HttpSourceClient::new(ReqwestHttpClient::new(None));
let coords = WorldTileCoords::from((0, 0, ZoomLevel::default()));
let request_id = 0;
let data = http_source_client
.fetch(&coords)
.await
.unwrap()
.into_boxed_slice();
let processor = HeadlessPipelineProcessor::default();
let mut pipeline_context = PipelineContext::new(processor);
let pipeline = build_vector_tile_pipeline();
pipeline.process(
(
TileRequest {
coords,
layers: HashSet::from(["boundary".to_owned(), "water".to_owned()]),
},
request_id,
data,
),
&mut pipeline_context,
);
let mut processor = pipeline_context
.take_processor::<HeadlessPipelineProcessor>()
.unwrap();
while let Some(v) = processor.layers.pop() {
map.map_schedule_mut()
.map_context
.tile_repository
.put_tessellated_layer(v);
}
match map.map_schedule_mut().update_and_redraw() {
Ok(_) => {}
Err(Error::Render(e)) => {
eprintln!("{}", e);
if e.should_exit() {}
}
e => eprintln!("{:?}", e),
};
})
}
fn main() {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
#[cfg(feature = "trace")]
enable_tracing();
run_in_window()
//run_headless();
run_in_window();
}

View File

@ -1,14 +1,14 @@
use instant::Instant;
use maplibre::error::Error;
use maplibre::io::scheduler::ScheduleMethod;
use maplibre::io::source_client::HTTPClient;
use maplibre::io::source_client::HttpClient;
use std::borrow::BorrowMut;
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent};
use winit::event_loop::ControlFlow;
use crate::input::{InputController, UpdateState};
use maplibre::map_schedule::MapSchedule;
use maplibre::window::{MapWindow, MapWindowConfig, Runnable};
use maplibre::map_schedule::InteractiveMapSchedule;
use maplibre::window::{EventLoop, HeadedMapWindow, MapWindow, MapWindowConfig};
use winit::event::Event;
#[cfg(target_arch = "wasm32")]
@ -47,10 +47,6 @@ impl WinitMapWindowConfig {
}
}
impl MapWindowConfig for WinitMapWindowConfig {
type MapWindow = WinitMapWindow;
}
pub struct WinitMapWindow {
window: WinitWindow,
event_loop: Option<WinitEventLoop>,
@ -69,13 +65,17 @@ impl WinitMapWindow {
///* Input (Mouse/Keyboard)
///* Platform Events like suspend/resume
///* Render a new frame
impl<MWC, SM, HC> Runnable<MWC, SM, HC> for WinitMapWindow
impl<MWC, SM, HC> EventLoop<MWC, SM, HC> for WinitMapWindow
where
MWC: MapWindowConfig<MapWindow = WinitMapWindow>,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
fn run(mut self, mut map_state: MapSchedule<MWC, SM, HC>, max_frames: Option<u64>) {
fn run(
mut self,
mut map_schedule: InteractiveMapSchedule<MWC, SM, HC>,
max_frames: Option<u64>,
) {
let mut last_render_time = Instant::now();
let mut current_frame: u64 = 0;
@ -85,13 +85,13 @@ where
.unwrap()
.run(move |event, _, control_flow| {
#[cfg(target_os = "android")]
if !map_state.is_initialized() && event == Event::Resumed {
if !map_schedule.is_initialized() && event == Event::Resumed {
use tokio::runtime::Handle;
use tokio::task;
let state = task::block_in_place(|| {
Handle::current().block_on(async {
map_state.late_init().await;
map_schedule.late_init().await;
})
});
return;
@ -122,10 +122,10 @@ where
..
} => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(physical_size) => {
map_state.resize(physical_size.width, physical_size.height);
map_schedule.resize(physical_size.width, physical_size.height);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
map_state.resize(new_inner_size.width, new_inner_size.height);
map_schedule.resize(new_inner_size.width, new_inner_size.height);
}
_ => {}
}
@ -136,11 +136,9 @@ where
let dt = now - last_render_time;
last_render_time = now;
{
input_controller.update_state(map_state.view_state_mut(), dt);
}
input_controller.update_state(map_schedule.view_state_mut(), dt);
match map_state.update_and_redraw() {
match map_schedule.update_and_redraw() {
Ok(_) => {}
Err(Error::Render(e)) => {
eprintln!("{}", e);
@ -161,10 +159,10 @@ where
}
}
Event::Suspended => {
map_state.suspend();
map_schedule.suspend();
}
Event::Resumed => {
map_state.resume(&self);
map_schedule.resume(&self);
}
Event::MainEventsCleared => {
// RedrawRequested will only trigger once, unless we manually

View File

@ -10,26 +10,9 @@ use super::WinitMapWindow;
use super::WinitWindow;
use super::WinitMapWindowConfig;
use maplibre::window::{MapWindow, WindowSize};
use maplibre::window::{HeadedMapWindow, MapWindow, MapWindowConfig, WindowSize};
impl MapWindow for WinitMapWindow {
type EventLoop = WinitEventLoop;
type Window = WinitWindow;
type MapWindowConfig = WinitMapWindowConfig;
fn create(map_window_config: &Self::MapWindowConfig) -> Self {
let event_loop = WinitEventLoop::new();
let window = WindowBuilder::new()
.with_title(&map_window_config.title)
.build(&event_loop)
.unwrap();
Self {
window,
event_loop: Some(event_loop),
}
}
fn size(&self) -> WindowSize {
let size = self.window.inner_size();
#[cfg(target_os = "android")]
@ -43,8 +26,28 @@ impl MapWindow for WinitMapWindow {
WindowSize::new(size.width, size.height).expect("failed to get window dimensions.");
window_size
}
}
impl HeadedMapWindow for WinitMapWindow {
type RawWindow = WinitWindow;
fn inner(&self) -> &Self::Window {
fn inner(&self) -> &Self::RawWindow {
&self.window
}
}
impl MapWindowConfig for WinitMapWindowConfig {
type MapWindow = WinitMapWindow;
fn create(&self) -> Self::MapWindow {
let event_loop = WinitEventLoop::new();
let window = WindowBuilder::new()
.with_title(&self.title)
.build(&event_loop)
.unwrap();
Self::MapWindow {
window,
event_loop: Some(event_loop),
}
}
}

View File

@ -4,38 +4,43 @@ use super::WinitEventLoop;
use super::WinitMapWindow;
use super::WinitMapWindowConfig;
use super::WinitWindow;
use maplibre::window::HeadedMapWindow;
use maplibre::window::MapWindowConfig;
use maplibre::window::{MapWindow, WindowSize};
use winit::platform::web::WindowBuilderExtWebSys;
impl MapWindow for WinitMapWindow {
type EventLoop = WinitEventLoop;
type Window = WinitWindow;
type MapWindowConfig = WinitMapWindowConfig;
impl MapWindowConfig for WinitMapWindowConfig {
type MapWindow = WinitMapWindow;
fn create(map_window_config: &Self::MapWindowConfig) -> Self {
fn create(&self) -> Self::MapWindow {
let event_loop = WinitEventLoop::new();
let window: winit::window::Window = WindowBuilder::new()
.with_canvas(Some(get_canvas(&map_window_config.canvas_id)))
.with_canvas(Some(get_canvas(&self.canvas_id)))
.build(&event_loop)
.unwrap();
let size = get_body_size().unwrap();
window.set_inner_size(size);
Self {
Self::MapWindow {
window,
event_loop: Some(event_loop),
}
}
}
impl MapWindow for WinitMapWindow {
fn size(&self) -> WindowSize {
let size = self.window.inner_size();
WindowSize::new(size.width, size.height).expect("failed to get window dimensions.")
}
}
impl HeadedMapWindow for WinitMapWindow {
type RawWindow = WinitWindow;
fn inner(&self) -> &Self::Window {
fn inner(&self) -> &Self::RawWindow {
&self.window
}
}

View File

@ -14,6 +14,7 @@ web-webgl = ["wgpu/webgl"]
trace = [ "tracing-subscriber", "tracing-tracy", "tracy-client"]
no-thread-safe-futures = []
embed-static-tiles = ["maplibre-build-tools/sqlite"]
headless = ["png"]
[target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "linux", target_os = "android", target_os = "windows"))'.dependencies]
@ -33,24 +34,25 @@ reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"
async-trait = "0.1"
instant = { version = "0.1", features = ["wasm-bindgen"] } # FIXME: Untrusted dependency
raw-window-handle = "0.4"
# Tracing
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", optional = true }
# Maths
cgmath = "0.18"
# Geo
geo = { version = "0.19" }
geo-types = { version = "0.7", features = ["use-rstar_0_9"] }
rstar = { version = "0.9" }
prost = "0.10.1"
geozero = { version = "0.9.4", default-features = false, features = ["with-mvt", "with-geo"]}
tile-grid = "0.3"
# Rendering
wgpu = { version = "0.12" }
lyon = { version = "0.17", features = [] }
raw-window-handle = "0.4"
# cached = "0.32"
@ -61,16 +63,24 @@ log = "0.4"
bytemuck = "1.2.0"
bytemuck_derive = "1.0"
# Static tiles inclusion
include_dir = "0.7.2"
# JSON
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Colors
csscolorparser = { version = "0.5", features = ["serde", "cint"]}
cint = "0.2"
# Required by bevy renderer
thiserror = "1"
downcast-rs = "1.2"
smallvec = "1.8"
# Headless
png = { version = "0.17", optional = true }
[build-dependencies]
maplibre-build-tools = { path = "../maplibre-build-tools", version = "0.1.0", features = ["sqlite"] }

View File

@ -1,7 +1,5 @@
use crate::coords::{Zoom, ZoomLevel, TILE_SIZE};
use crate::io::shared_thread_state::SharedThreadState;
use crate::io::tile_cache::TileCache;
use crate::io::TessellateMessage;
use crate::io::tile_repository::TileRepository;
use crate::render::camera::{Camera, Perspective, ViewProjection};
use crate::util::ChangeObserver;
use crate::{Renderer, ScheduleMethod, Style, WindowSize};
@ -57,14 +55,11 @@ impl ViewState {
}
}
/// Stores the context of the map.
pub struct MapContext {
pub view_state: ViewState,
pub style: Style,
pub tile_cache: TileCache,
pub tile_repository: TileRepository,
pub renderer: Renderer,
pub scheduler: Box<dyn ScheduleMethod>,
pub message_receiver: mpsc::Receiver<TessellateMessage>,
pub shared_thread_state: SharedThreadState,
}

View File

@ -1,96 +1,21 @@
//! Handles IO related processing as well as multithreading.
use crate::coords::WorldTileCoords;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use crate::render::ShaderVertex;
use geozero::mvt::tile;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use std::collections::HashSet;
use std::fmt;
pub mod scheduler;
pub mod source_client;
pub mod static_tile_fetcher;
pub mod geometry_index;
pub mod shared_thread_state;
pub mod tile_cache;
pub mod pipeline;
pub mod tile_pipelines;
pub mod tile_repository;
pub mod tile_request_state;
/// Contains a `Tile` if the fetch was successful otherwise `Unavailable`.
pub enum TileFetchResult {
Unavailable {
coords: WorldTileCoords,
},
Tile {
coords: WorldTileCoords,
data: Box<[u8]>,
},
}
impl fmt::Debug for TileFetchResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TileFetchResult({})",
match self {
TileFetchResult::Unavailable { coords, .. } => coords,
TileFetchResult::Tile { coords, .. } => coords,
}
)
}
}
/// [crate::io::TileTessellateMessage] or [crate::io::LayerTessellateMessage] tessellation message.
pub enum TessellateMessage {
Tile(TileTessellateMessage),
Layer(LayerTessellateMessage),
}
/// The result of the tessellation of a tile.
pub struct TileTessellateMessage {
pub request_id: TileRequestID,
pub coords: WorldTileCoords,
}
/// `TessellatedLayer` contains the result of the tessellation for a specific layer, otherwise
/// `UnavailableLayer` if the layer doesn't exist.
pub enum LayerTessellateMessage {
UnavailableLayer {
coords: WorldTileCoords,
layer_name: String,
},
TessellatedLayer {
coords: WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
/// Holds for each feature the count of indices.
feature_indices: Vec<u32>,
layer_data: tile::Layer,
},
}
impl fmt::Debug for LayerTessellateMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LayerTessellateMessage{}", self.get_coords())
}
}
impl LayerTessellateMessage {
pub fn get_coords(&self) -> WorldTileCoords {
match self {
LayerTessellateMessage::UnavailableLayer { coords, .. } => *coords,
LayerTessellateMessage::TessellatedLayer { coords, .. } => *coords,
}
}
pub fn layer_name(&self) -> &str {
match self {
LayerTessellateMessage::UnavailableLayer { layer_name, .. } => layer_name.as_str(),
LayerTessellateMessage::TessellatedLayer { layer_data, .. } => &layer_data.name,
}
}
}
pub use geozero::mvt::tile::Layer as RawLayer;
/// A request for a tile at the given coordinates and in the given layers.
#[derive(Clone)]

243
maplibre/src/io/pipeline.rs Normal file
View File

@ -0,0 +1,243 @@
use crate::coords::WorldTileCoords;
use crate::io::geometry_index::IndexedGeometry;
use crate::io::TileRequestID;
use crate::render::ShaderVertex;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use downcast_rs::{impl_downcast, Downcast};
use geozero::mvt::tile;
use std::any::Any;
use std::marker::PhantomData;
use std::process::Output;
use std::sync::mpsc;
/// Processes events which happen during the pipeline execution
pub trait PipelineProcessor: Downcast {
fn tile_finished(&mut self, request_id: TileRequestID, coords: &WorldTileCoords) {}
fn layer_unavailable(&mut self, coords: &WorldTileCoords, layer_name: &str) {}
fn layer_tesselation_finished(
&mut self,
coords: &WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
feature_indices: Vec<u32>,
layer_data: tile::Layer,
) {
}
fn layer_indexing_finished(
&mut self,
coords: &WorldTileCoords,
geometries: Vec<IndexedGeometry<f64>>,
) {
}
}
impl_downcast!(PipelineProcessor);
/// Context which is available to each step within a [`DataPipeline`]
pub struct PipelineContext {
processor: Box<dyn PipelineProcessor>,
}
impl PipelineContext {
pub fn new<P>(processor: P) -> Self
where
P: PipelineProcessor,
{
Self {
processor: Box::new(processor),
}
}
pub fn take_processor<P>(self) -> Option<Box<P>>
where
P: PipelineProcessor,
{
self.processor.into_any().downcast::<P>().ok()
}
pub fn processor_mut(&mut self) -> &mut dyn PipelineProcessor {
self.processor.as_mut()
}
}
pub trait Processable {
type Input;
type Output;
fn process(&self, input: Self::Input, context: &mut PipelineContext) -> Self::Output;
}
/// A pipeline which consists of multiple steps. Steps are [`Processable`] workloads. Later steps
/// depend on previous ones.
pub struct DataPipeline<P, N>
where
P: Processable,
N: Processable<Input = P::Output>,
{
step: P,
next_step: N,
}
impl<P, N> DataPipeline<P, N>
where
P: Processable,
N: Processable<Input = P::Output>,
{
pub fn new(step: P, next_step: N) -> Self {
Self { step, next_step }
}
}
impl<P, N> Processable for DataPipeline<P, N>
where
P: Processable,
N: Processable<Input = P::Output>,
{
type Input = P::Input;
type Output = N::Output;
fn process(&self, input: Self::Input, context: &mut PipelineContext) -> Self::Output {
let output = self.step.process(input, context);
self.next_step.process(output, context)
}
}
/// Marks the end of a [`DataPipeline`]
pub struct PipelineEnd<I> {
phantom: PhantomData<I>,
}
impl<I> Default for PipelineEnd<I> {
fn default() -> Self {
Self {
phantom: PhantomData::default(),
}
}
}
impl<I> Processable for PipelineEnd<I> {
type Input = I;
type Output = I;
fn process(&self, input: Self::Input, _context: &mut PipelineContext) -> Self::Output {
input
}
}
impl<I, O> Processable for &fn(input: I, context: &mut PipelineContext) -> O {
type Input = I;
type Output = O;
fn process(&self, input: Self::Input, context: &mut PipelineContext) -> Self::Output {
(self)(input, context)
}
}
impl<I, O> Processable for fn(input: I, context: &mut PipelineContext) -> O {
type Input = I;
type Output = O;
fn process(&self, input: Self::Input, context: &mut PipelineContext) -> Self::Output {
(self)(input, context)
}
}
// TODO: Implementing Processable directly on Fn is not possible for some strange reason:
// https://github.com/rust-lang/rust/issues/25041
pub struct ClosureProcessable<F, I, O>
where
F: Fn(I, &mut PipelineContext) -> O,
{
func: F,
phantom_i: PhantomData<I>,
}
impl<F, I, O> From<F> for ClosureProcessable<F, I, O>
where
F: Fn(I, &mut PipelineContext) -> O,
{
fn from(func: F) -> Self {
ClosureProcessable {
func,
phantom_i: PhantomData::default(),
}
}
}
impl<F, I, O> Processable for ClosureProcessable<F, I, O>
where
F: Fn(I, &mut PipelineContext) -> O,
{
type Input = I;
type Output = O;
fn process(&self, input: Self::Input, context: &mut PipelineContext) -> Self::Output {
(self.func)(input, context)
}
}
#[cfg(test)]
mod tests {
use crate::io::pipeline::{
ClosureProcessable, DataPipeline, PipelineContext, PipelineEnd, PipelineProcessor,
Processable,
};
use std::sync::mpsc;
pub struct DummyPipelineProcessor;
impl PipelineProcessor for DummyPipelineProcessor {}
fn add_one(input: u32, context: &mut PipelineContext) -> u8 {
input as u8 + 1
}
fn add_two(input: u8, context: &mut PipelineContext) -> u32 {
input as u32 + 2
}
#[test]
fn test_fn_pointer() {
let mut context = PipelineContext::new(DummyPipelineProcessor);
let output: u32 = DataPipeline::new(
add_two as fn(u8, &mut PipelineContext) -> u32,
PipelineEnd::default(),
)
.process(5u8, &mut context);
assert_eq!(output, 7);
let output: u32 = DataPipeline::new(
add_one as fn(u32, &mut PipelineContext) -> u8,
DataPipeline::new(
add_two as fn(u8, &mut PipelineContext) -> u32,
PipelineEnd::default(),
),
)
.process(5u32, &mut context);
assert_eq!(output, 8);
}
#[test]
fn test_closure() {
let mut context = PipelineContext::new(DummyPipelineProcessor);
let mut outer_value = 3;
// using from()
let closure = ClosureProcessable::from(|input: u8, context: &mut PipelineContext| -> u32 {
return input as u32 + 2 + outer_value;
});
let output: u32 =
DataPipeline::new(closure, PipelineEnd::default()).process(5u8, &mut context);
assert_eq!(output, 10);
// with into()
let output: u32 = DataPipeline::<ClosureProcessable<_, u8, u32>, _>::new(
(|input: u8, context: &mut PipelineContext| -> u32 {
return input as u32 + 2 + outer_value;
})
.into(),
PipelineEnd::<u32>::default(),
)
.process(5u8, &mut context);
assert_eq!(output, 10);
}
}

View File

@ -5,8 +5,6 @@ use std::pin::Pin;
use crate::error::Error;
use crate::io::shared_thread_state::SharedThreadState;
/// Async/await scheduler.
pub struct Scheduler<SM>
where
@ -26,30 +24,23 @@ where
pub fn schedule_method(&self) -> &SM {
&self.schedule_method
}
pub fn take(self) -> SM {
self.schedule_method
}
}
/// Can schedule a task from a future factory and a shared state.
// Should be object safe in order to be able to have a dyn object in MapContext
pub trait ScheduleMethod: 'static {
#[cfg(not(feature = "no-thread-safe-futures"))]
fn schedule(
fn schedule<T>(
&self,
shared_thread_state: SharedThreadState,
future_factory: Box<
(dyn (FnOnce(SharedThreadState) -> Pin<Box<dyn Future<Output = ()> + Send>>) + Send),
>,
) -> Result<(), Error>;
future_factory: impl (FnOnce() -> T) + Send + 'static,
) -> Result<(), Error>
where
T: Future<Output = ()> + Send + 'static;
#[cfg(feature = "no-thread-safe-futures")]
fn schedule(
fn schedule<T>(
&self,
shared_thread_state: SharedThreadState,
future_factory: Box<
(dyn (FnOnce(SharedThreadState) -> Pin<Box<dyn Future<Output = ()>>>) + Send),
>,
) -> Result<(), Error>;
future_factory: impl (FnOnce() -> T) + Send + 'static,
) -> Result<(), Error>
where
T: Future<Output = ()> + 'static;
}

View File

@ -1,170 +0,0 @@
//! Shared thread state.
use crate::coords::{WorldCoords, WorldTileCoords, Zoom, ZoomLevel};
use crate::error::Error;
use crate::io::geometry_index::{GeometryIndex, IndexProcessor, IndexedGeometry, TileIndex};
use crate::io::tile_request_state::TileRequestState;
use crate::io::{
LayerTessellateMessage, TessellateMessage, TileRequest, TileRequestID, TileTessellateMessage,
};
use std::collections::HashSet;
use crate::tessellation::zero_tessellator::ZeroTessellator;
use geozero::GeozeroDatasource;
use prost::Message;
use std::sync::{mpsc, Arc, Mutex};
/// Stores and provides access to the thread safe data shared between the schedulers.
#[derive(Clone)]
pub struct SharedThreadState {
pub tile_request_state: Arc<Mutex<TileRequestState>>,
pub message_sender: mpsc::Sender<TessellateMessage>,
pub geometry_index: Arc<Mutex<GeometryIndex>>,
}
impl SharedThreadState {
fn get_tile_request(&self, request_id: TileRequestID) -> Option<TileRequest> {
self.tile_request_state
.lock()
.ok()
.and_then(|tile_request_state| tile_request_state.get_tile_request(request_id).cloned())
}
#[tracing::instrument(skip_all)]
pub fn process_tile(&self, request_id: TileRequestID, data: Box<[u8]>) -> Result<(), Error> {
if let Some(tile_request) = self.get_tile_request(request_id) {
let coords = tile_request.coords;
tracing::info!("parsing tile {} with {}bytes", &coords, data.len());
let _span_ = tracing::span!(tracing::Level::TRACE, "parse_tile_bytes").entered();
let mut tile = geozero::mvt::Tile::decode(data.as_ref()).expect("failed to load tile");
let index = IndexProcessor::new();
for layer in &mut tile.layers {
let cloned_layer = layer.clone();
let layer_name: &str = &cloned_layer.name;
if !tile_request.layers.contains(layer_name) {
continue;
}
tracing::info!("layer {} at {} ready", layer_name, &coords);
let mut tessellator = ZeroTessellator::default();
if let Err(e) = layer.process(&mut tessellator) {
self.message_sender.send(TessellateMessage::Layer(
LayerTessellateMessage::UnavailableLayer {
coords,
layer_name: layer_name.to_owned(),
},
))?;
tracing::error!(
"layer {} at {} tesselation failed {:?}",
layer_name,
&coords,
e
);
} else {
self.message_sender.send(TessellateMessage::Layer(
LayerTessellateMessage::TessellatedLayer {
coords,
buffer: tessellator.buffer.into(),
feature_indices: tessellator.feature_indices,
layer_data: cloned_layer,
},
))?;
}
// TODO
// layer.process(&mut index).unwrap();
}
let available_layers: HashSet<_> = tile
.layers
.iter()
.map(|layer| layer.name.clone())
.collect::<HashSet<_>>();
for missing_layer in tile_request.layers.difference(&available_layers) {
self.message_sender.send(TessellateMessage::Layer(
LayerTessellateMessage::UnavailableLayer {
coords,
layer_name: missing_layer.to_owned(),
},
))?;
tracing::info!(
"requested layer {} at {} not found in tile",
missing_layer,
&coords
);
}
tracing::info!("tile tessellated at {} finished", &tile_request.coords);
self.message_sender
.send(TessellateMessage::Tile(TileTessellateMessage {
request_id,
coords: tile_request.coords,
}))?;
if let Ok(mut geometry_index) = self.geometry_index.lock() {
geometry_index.index_tile(
&coords,
TileIndex::Linear {
list: index.get_geometries(),
},
)
}
}
Ok(())
}
pub fn tile_unavailable(
&self,
coords: &WorldTileCoords,
request_id: TileRequestID,
) -> Result<(), Error> {
if let Some(tile_request) = self.get_tile_request(request_id) {
for to_load in &tile_request.layers {
tracing::warn!("layer {} at {} unavailable", to_load, coords);
self.message_sender.send(TessellateMessage::Layer(
LayerTessellateMessage::UnavailableLayer {
coords: tile_request.coords,
layer_name: to_load.to_string(),
},
))?;
}
}
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn query_point(
&self,
world_coords: &WorldCoords,
z: ZoomLevel,
zoom: Zoom,
) -> Option<Vec<IndexedGeometry<f64>>> {
if let Ok(geometry_index) = self.geometry_index.lock() {
geometry_index
.query_point(world_coords, z, zoom)
.map(|geometries| {
geometries
.iter()
.cloned()
.cloned()
.collect::<Vec<IndexedGeometry<f64>>>()
})
} else {
unimplemented!()
}
}
}

View File

@ -16,7 +16,7 @@ pub type HTTPClientFactory<HC> = dyn Fn() -> HC;
/// the future "no-thread-safe-futures". Tokio futures are thread-safe.
#[cfg_attr(feature = "no-thread-safe-futures", async_trait(?Send))]
#[cfg_attr(not(feature = "no-thread-safe-futures"), async_trait)]
pub trait HTTPClient: Clone + Sync + Send + 'static {
pub trait HttpClient: Clone + Sync + Send + 'static {
async fn fetch(&self, url: &str) -> Result<Vec<u8>, Error>;
}
@ -25,7 +25,7 @@ pub trait HTTPClient: Clone + Sync + Send + 'static {
#[derive(Clone)]
pub struct HttpSourceClient<HC>
where
HC: HTTPClient,
HC: HttpClient,
{
inner_client: HC,
}
@ -35,7 +35,7 @@ where
#[derive(Clone)]
pub enum SourceClient<HC>
where
HC: HTTPClient,
HC: HttpClient,
{
Http(HttpSourceClient<HC>),
Mbtiles {
@ -45,7 +45,7 @@ where
impl<HC> SourceClient<HC>
where
HC: HTTPClient,
HC: HttpClient,
{
pub async fn fetch(&self, coords: &WorldTileCoords) -> Result<Vec<u8>, Error> {
match self {
@ -57,7 +57,7 @@ where
impl<HC> HttpSourceClient<HC>
where
HC: HTTPClient,
HC: HttpClient,
{
pub fn new(http_client: HC) -> Self {
Self {

View File

@ -0,0 +1,156 @@
use crate::io::geometry_index::IndexProcessor;
use crate::io::pipeline::{DataPipeline, PipelineContext, PipelineEnd, Processable};
use crate::io::{TileRequest, TileRequestID};
use crate::tessellation::zero_tessellator::ZeroTessellator;
use crate::tessellation::IndexDataType;
use geozero::GeozeroDatasource;
use prost::Message;
use std::collections::HashSet;
pub struct ParseTile;
impl Processable for ParseTile {
type Input = (TileRequest, TileRequestID, Box<[u8]>);
type Output = (TileRequest, TileRequestID, geozero::mvt::Tile);
// TODO (perf): Maybe force inline
fn process(
&self,
(tile_request, request_id, data): Self::Input,
_context: &mut PipelineContext,
) -> Self::Output {
let tile = geozero::mvt::Tile::decode(data.as_ref()).expect("failed to load tile");
(tile_request, request_id, tile)
}
}
pub struct IndexLayer;
impl Processable for IndexLayer {
type Input = (TileRequest, TileRequestID, geozero::mvt::Tile);
type Output = (TileRequest, TileRequestID, geozero::mvt::Tile);
// TODO (perf): Maybe force inline
fn process(
&self,
(tile_request, request_id, tile): Self::Input,
context: &mut PipelineContext,
) -> Self::Output {
let index = IndexProcessor::new();
context
.processor_mut()
.layer_indexing_finished(&tile_request.coords, index.get_geometries());
(tile_request, request_id, tile)
}
}
pub struct TessellateLayer;
impl Processable for TessellateLayer {
type Input = (TileRequest, TileRequestID, geozero::mvt::Tile);
type Output = (TileRequest, TileRequestID, geozero::mvt::Tile);
// TODO (perf): Maybe force inline
fn process(
&self,
(tile_request, request_id, mut tile): Self::Input,
context: &mut PipelineContext,
) -> Self::Output {
let coords = &tile_request.coords;
for layer in &mut tile.layers {
let cloned_layer = layer.clone();
let layer_name: &str = &cloned_layer.name;
if !tile_request.layers.contains(layer_name) {
continue;
}
tracing::info!("layer {} at {} ready", layer_name, coords);
let mut tessellator = ZeroTessellator::<IndexDataType>::default();
if let Err(e) = layer.process(&mut tessellator) {
context
.processor_mut()
.layer_unavailable(coords, layer_name);
tracing::error!(
"layer {} at {} tesselation failed {:?}",
layer_name,
&coords,
e
);
} else {
context.processor_mut().layer_tesselation_finished(
coords,
tessellator.buffer.into(),
tessellator.feature_indices,
cloned_layer,
)
}
}
let available_layers: HashSet<_> = tile
.layers
.iter()
.map(|layer| layer.name.clone())
.collect::<HashSet<_>>();
for missing_layer in tile_request.layers.difference(&available_layers) {
context
.processor_mut()
.layer_unavailable(coords, missing_layer);
tracing::info!(
"requested layer {} at {} not found in tile",
missing_layer,
&coords
);
}
tracing::info!("tile tessellated at {} finished", &tile_request.coords);
context
.processor_mut()
.tile_finished(request_id, &tile_request.coords);
(tile_request, request_id, tile)
}
}
pub fn build_vector_tile_pipeline() -> impl Processable<Input = <ParseTile as Processable>::Input> {
DataPipeline::new(
ParseTile,
DataPipeline::new(TessellateLayer, PipelineEnd::default()),
)
}
#[cfg(test)]
mod tests {
use super::build_vector_tile_pipeline;
use crate::coords::ZoomLevel;
use crate::io::pipeline::{PipelineContext, PipelineProcessor, Processable};
use crate::io::TileRequest;
pub struct DummyPipelineProcessor;
impl PipelineProcessor for DummyPipelineProcessor {}
#[test] // TODO: Add proper tile byte array
#[ignore]
fn test() {
let mut context = PipelineContext::new(DummyPipelineProcessor);
let pipeline = build_vector_tile_pipeline();
let output = pipeline.process(
(
TileRequest {
coords: (0, 0, ZoomLevel::default()).into(),
layers: Default::default(),
},
0,
Box::new([0]),
),
&mut context,
);
}
}

View File

@ -1,18 +1,49 @@
//! Tile cache.
use crate::coords::{Quadkey, WorldTileCoords};
use crate::io::LayerTessellateMessage;
use crate::render::ShaderVertex;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use geozero::mvt::tile;
use std::collections::{btree_map, BTreeMap, HashSet};
/// Stores the multiple [crate::io::LayerTessellateMessage] of a cached tile.
pub struct CachedTile {
layers: Vec<LayerTessellateMessage>,
/// A layer which is stored for future use.
pub enum StoredLayer {
UnavailableLayer {
coords: WorldTileCoords,
layer_name: String,
},
TessellatedLayer {
coords: WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
/// Holds for each feature the count of indices.
feature_indices: Vec<u32>,
layer_data: tile::Layer,
},
}
impl CachedTile {
pub fn new(first_layer: LayerTessellateMessage) -> Self {
impl StoredLayer {
pub fn get_coords(&self) -> WorldTileCoords {
match self {
StoredLayer::UnavailableLayer { coords, .. } => *coords,
StoredLayer::TessellatedLayer { coords, .. } => *coords,
}
}
pub fn layer_name(&self) -> &str {
match self {
StoredLayer::UnavailableLayer { layer_name, .. } => layer_name.as_str(),
StoredLayer::TessellatedLayer { layer_data, .. } => &layer_data.name,
}
}
}
/// Stores multiple [StoredLayers](StoredLayer).
pub struct StoredTile {
layers: Vec<StoredLayer>,
}
impl StoredTile {
pub fn new(first_layer: StoredLayer) -> Self {
Self {
layers: vec![first_layer],
}
@ -21,34 +52,34 @@ impl CachedTile {
/// Stores and provides access to a quad tree of cached tiles with world tile coords.
#[derive(Default)]
pub struct TileCache {
cache: BTreeMap<Quadkey, CachedTile>,
pub struct TileRepository {
tree: BTreeMap<Quadkey, StoredTile>,
}
impl TileCache {
impl TileRepository {
pub fn new() -> Self {
Self {
cache: BTreeMap::new(),
tree: BTreeMap::new(),
}
}
/// Inserts a tessellated layer into the quad tree at its world tile coords.
/// If the space is vacant, the tessellated layer is inserted into a new
/// [crate::io::tile_cache::CachedTile].
/// [crate::io::tile_repository::CachedTile].
/// If the space is occupied, the tessellated layer is added to the current
/// [crate::io::tile_cache::CachedTile].
pub fn put_tessellated_layer(&mut self, message: LayerTessellateMessage) {
if let Some(entry) = message
/// [crate::io::tile_repository::CachedTile].
pub fn put_tessellated_layer(&mut self, layer: StoredLayer) {
if let Some(entry) = layer
.get_coords()
.build_quad_key()
.map(|key| self.cache.entry(key))
.map(|key| self.tree.entry(key))
{
match entry {
btree_map::Entry::Vacant(entry) => {
entry.insert(CachedTile::new(message));
entry.insert(StoredTile::new(layer));
}
btree_map::Entry::Occupied(mut entry) => {
entry.get_mut().layers.push(message);
entry.get_mut().layers.push(layer);
}
}
}
@ -59,10 +90,10 @@ impl TileCache {
pub fn iter_tessellated_layers_at(
&self,
coords: &WorldTileCoords,
) -> Option<impl Iterator<Item = &LayerTessellateMessage> + '_> {
) -> Option<impl Iterator<Item = &StoredLayer> + '_> {
coords
.build_quad_key()
.and_then(|key| self.cache.get(&key))
.and_then(|key| self.tree.get(&key))
.map(|results| results.layers.iter())
}
@ -73,7 +104,7 @@ impl TileCache {
coords: &WorldTileCoords,
layers: &mut HashSet<String>,
) {
if let Some(cached_tile) = coords.build_quad_key().and_then(|key| self.cache.get(&key)) {
if let Some(cached_tile) = coords.build_quad_key().and_then(|key| self.tree.get(&key)) {
let tessellated_set: HashSet<String> = cached_tile
.layers
.iter()
@ -86,7 +117,7 @@ impl TileCache {
/// Checks if a layer is missing from the given layers set at the given coords.
pub fn is_layers_missing(&self, coords: &WorldTileCoords, layers: &HashSet<String>) -> bool {
if let Some(cached_tile) = coords.build_quad_key().and_then(|key| self.cache.get(&key)) {
if let Some(cached_tile) = coords.build_quad_key().and_then(|key| self.tree.get(&key)) {
let tessellated_set: HashSet<&str> = cached_tile
.layers
.iter()

View File

@ -17,12 +17,12 @@
//! ```
use crate::io::scheduler::{ScheduleMethod, Scheduler};
use crate::io::source_client::HTTPClient;
use crate::map_schedule::MapSchedule;
use crate::io::source_client::HttpClient;
use crate::map_schedule::{InteractiveMapSchedule, SimpleMapSchedule};
use crate::render::settings::{RendererSettings, WgpuSettings};
use crate::render::{RenderState, Renderer};
use crate::style::Style;
use crate::window::{MapWindow, MapWindowConfig, Runnable, WindowSize};
use crate::window::{EventLoop, HeadedMapWindow, MapWindow, MapWindowConfig, WindowSize};
pub mod context;
pub mod coords;
@ -37,43 +37,51 @@ pub mod style;
pub mod window;
// Exposed because of doc-strings
pub mod schedule;
// Exposed because of SharedThreadState
pub mod stages;
// Used for benchmarking
pub mod benchmarking;
// Internal modules
pub(crate) mod stages;
pub(crate) mod tessellation;
pub(crate) mod util;
pub mod util;
/// Map's configuration and execution.
pub struct Map<W, SM, HC>
/// The [`Map`] defines the public interface of the map renderer.
// DO NOT IMPLEMENT INTERNALS ON THIS STRUCT.
pub struct Map<MWC, SM, HC>
where
W: MapWindow,
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
map_state: MapSchedule<W::MapWindowConfig, SM, HC>,
window: W,
map_schedule: InteractiveMapSchedule<MWC, SM, HC>,
window: MWC::MapWindow,
}
impl<W, SM, HC> Map<W, SM, HC>
impl<MWC, SM, HC> Map<MWC, SM, HC>
where
W: MapWindow + Runnable<W::MapWindowConfig, SM, HC>,
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
/// Starts the [`crate::map_state::MapState`] Runnable with the configured event loop.
pub fn run(self) {
/// Starts the [`crate::map_schedule::MapState`] Runnable with the configured event loop.
pub fn run(self)
where
MWC::MapWindow: EventLoop<MWC, SM, HC>,
{
self.run_with_optionally_max_frames(None);
}
/// Starts the [`crate::map_state::MapState`] Runnable with the configured event loop.
/// Starts the [`crate::map_schedule::MapState`] Runnable with the configured event loop.
///
/// # Arguments
///
/// * `max_frames` - Maximum number of frames per second.
pub fn run_with_max_frames(self, max_frames: u64) {
pub fn run_with_max_frames(self, max_frames: u64)
where
MWC::MapWindow: EventLoop<MWC, SM, HC>,
{
self.run_with_optionally_max_frames(Some(max_frames));
}
@ -82,20 +90,49 @@ where
/// # Arguments
///
/// * `max_frames` - Optional maximum number of frames per second.
pub fn run_with_optionally_max_frames(self, max_frames: Option<u64>) {
self.window.run(self.map_state, max_frames);
pub fn run_with_optionally_max_frames(self, max_frames: Option<u64>)
where
MWC::MapWindow: EventLoop<MWC, SM, HC>,
{
self.window.run(self.map_schedule, max_frames);
}
pub fn map_schedule(&self) -> &InteractiveMapSchedule<MWC, SM, HC> {
&self.map_schedule
}
pub fn map_schedule_mut(&mut self) -> &mut InteractiveMapSchedule<MWC, SM, HC> {
&mut self.map_schedule
}
}
pub struct HeadlessMap<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HttpClient,
{
map_schedule: SimpleMapSchedule<MWC, SM, HC>,
window: MWC::MapWindow,
}
impl<MWC, SM, HC> HeadlessMap<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HttpClient,
{
pub fn map_schedule_mut(&mut self) -> &mut SimpleMapSchedule<MWC, SM, HC> {
&mut self.map_schedule
}
}
/// Stores the map configuration before the map's state has been fully initialized.
///
/// FIXME: We could maybe remove this class, and store the render_state in an Optional in [`crate::map_state::MapState`].
/// FIXME: I think we can find a workaround so that this class doesn't exist.
pub struct UninitializedMap<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
scheduler: Scheduler<SM>,
http_client: HC,
@ -110,12 +147,16 @@ impl<MWC, SM, HC> UninitializedMap<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
/// Initializes the whole rendering pipeline for the given configuration.
/// Returns the initialized map, ready to be run.
pub async fn initialize(self) -> Map<MWC::MapWindow, SM, HC> {
let window = MWC::MapWindow::create(&self.map_window_config);
pub async fn initialize(self) -> Map<MWC, SM, HC>
where
MWC: MapWindowConfig,
<MWC as MapWindowConfig>::MapWindow: HeadedMapWindow,
{
let window = self.map_window_config.create();
let window_size = window.size();
#[cfg(target_os = "android")]
@ -129,7 +170,7 @@ where
.await
.ok();
Map {
map_state: MapSchedule::new(
map_schedule: InteractiveMapSchedule::new(
self.map_window_config,
window_size,
renderer,
@ -142,6 +183,30 @@ where
window,
}
}
pub async fn initialize_headless(self) -> HeadlessMap<MWC, SM, HC> {
let window = self.map_window_config.create();
let window_size = window.size();
let renderer = Renderer::initialize_headless(
&window,
self.wgpu_settings.clone(),
self.renderer_settings.clone(),
)
.await
.expect("Failed to initialize renderer");
HeadlessMap {
map_schedule: SimpleMapSchedule::new(
self.map_window_config,
window_size,
renderer,
self.scheduler,
self.http_client,
self.style,
),
window,
}
}
}
pub struct MapBuilder<MWC, SM, HC>
@ -162,7 +227,7 @@ impl<MWC, SM, HC> MapBuilder<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
pub fn new() -> Self {
Self {

View File

@ -1,35 +1,27 @@
//! Stores the state of the map such as `[crate::coords::Zoom]`, `[crate::camera::Camera]`, `[crate::style::Style]`, `[crate::io::tile_cache::TileCache]` and more.
use crate::context::{MapContext, ViewState};
use crate::error::Error;
use crate::io::geometry_index::GeometryIndex;
use crate::io::scheduler::Scheduler;
use crate::io::shared_thread_state::SharedThreadState;
use crate::io::source_client::{HTTPClient, HttpSourceClient, SourceClient};
use crate::io::tile_cache::TileCache;
use crate::io::source_client::{HttpClient, HttpSourceClient, SourceClient};
use crate::io::tile_repository::TileRepository;
use crate::io::tile_request_state::TileRequestState;
use crate::io::TessellateMessage;
use crate::render::register_render_stages;
use crate::schedule::{Schedule, Stage};
use crate::stages::register_stages;
use crate::style::Style;
use crate::{
MapWindow, MapWindowConfig, Renderer, RendererSettings, ScheduleMethod, WgpuSettings,
WindowSize,
HeadedMapWindow, MapWindow, MapWindowConfig, Renderer, RendererSettings, ScheduleMethod,
WgpuSettings, WindowSize,
};
use std::marker::PhantomData;
use std::mem;
use std::sync::{mpsc, Arc, Mutex};
pub struct PrematureMapContext {
pub view_state: ViewState,
pub style: Style,
view_state: ViewState,
style: Style,
pub tile_cache: TileCache,
pub scheduler: Box<dyn ScheduleMethod>,
pub message_receiver: mpsc::Receiver<TessellateMessage>,
pub shared_thread_state: SharedThreadState,
tile_repository: TileRepository,
wgpu_settings: WgpuSettings,
renderer_settings: RendererSettings,
@ -38,53 +30,43 @@ pub struct PrematureMapContext {
pub enum EventuallyMapContext {
Full(MapContext),
Premature(PrematureMapContext),
Empty,
_Uninitialized,
}
impl EventuallyMapContext {
pub fn make_full(&mut self, renderer: Renderer) {
let context = mem::replace(self, EventuallyMapContext::Empty);
let context = mem::replace(self, EventuallyMapContext::_Uninitialized);
match context {
EventuallyMapContext::Full(_) => {}
EventuallyMapContext::Premature(PrematureMapContext {
view_state,
style,
tile_cache,
scheduler,
message_receiver,
shared_thread_state,
wgpu_settings,
renderer_settings,
tile_repository,
..
}) => {
mem::replace(
self,
EventuallyMapContext::Full(MapContext {
view_state,
style,
tile_cache,
renderer,
scheduler,
message_receiver,
shared_thread_state,
}),
);
*self = EventuallyMapContext::Full(MapContext {
view_state,
style,
tile_repository,
renderer,
});
}
EventuallyMapContext::Empty => {}
EventuallyMapContext::_Uninitialized => {}
}
}
}
/// Stores the state of the map, dispatches tile fetching and caching, tessellation and drawing.
pub struct MapSchedule<MWC, SM, HC>
pub struct InteractiveMapSchedule<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
map_window_config: MWC,
map_context: EventuallyMapContext,
pub map_context: EventuallyMapContext,
schedule: Schedule,
@ -94,11 +76,11 @@ where
suspended: bool,
}
impl<MWC, SM, HC> MapSchedule<MWC, SM, HC>
impl<MWC, SM, HC> InteractiveMapSchedule<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
pub fn new(
map_window_config: MWC,
@ -111,42 +93,29 @@ where
renderer_settings: RendererSettings,
) -> Self {
let view_state = ViewState::new(&window_size);
let tile_cache = TileCache::new();
let tile_repository = TileRepository::new();
let mut schedule = Schedule::default();
let client: SourceClient<HC> = SourceClient::Http(HttpSourceClient::new(http_client));
register_stages(&mut schedule, client);
register_render_stages(&mut schedule);
let (message_sender, message_receiver) = mpsc::channel();
let http_source_client: HttpSourceClient<HC> = HttpSourceClient::new(http_client);
register_stages(&mut schedule, http_source_client, Box::new(scheduler));
register_render_stages(&mut schedule, false).unwrap();
let scheduler = Box::new(scheduler.take());
let shared_thread_state = SharedThreadState {
tile_request_state: Arc::new(Mutex::new(TileRequestState::new())),
message_sender,
geometry_index: Arc::new(Mutex::new(GeometryIndex::new())),
};
Self {
map_window_config,
map_context: match renderer {
None => EventuallyMapContext::Premature(PrematureMapContext {
view_state,
style,
tile_cache,
scheduler,
shared_thread_state,
tile_repository,
wgpu_settings,
message_receiver,
renderer_settings,
}),
Some(renderer) => EventuallyMapContext::Full(MapContext {
view_state,
style,
tile_cache,
tile_repository,
renderer,
scheduler,
shared_thread_state,
message_receiver,
}),
},
schedule,
@ -183,13 +152,15 @@ where
self.suspended = true;
}
pub fn resume<MW>(&mut self, window: &MW)
pub fn resume(&mut self, window: &MWC::MapWindow)
where
MW: MapWindow,
<MWC as MapWindowConfig>::MapWindow: HeadedMapWindow,
{
if let EventuallyMapContext::Full(map_context) = &mut self.map_context {
let mut renderer = &mut map_context.renderer;
renderer.surface.recreate(window, &renderer.instance);
renderer
.state
.recreate_surface::<MWC::MapWindow>(window, &renderer.instance);
self.suspended = false;
}
}
@ -201,28 +172,26 @@ where
}
}
pub async fn late_init(&mut self) -> bool {
pub async fn late_init(&mut self) -> bool
where
<MWC as MapWindowConfig>::MapWindow: HeadedMapWindow,
{
match &self.map_context {
EventuallyMapContext::Full(_) => false,
EventuallyMapContext::Premature(PrematureMapContext {
view_state,
style,
tile_cache,
scheduler,
message_receiver,
shared_thread_state,
wgpu_settings,
renderer_settings,
..
}) => {
let window = MWC::MapWindow::create(&self.map_window_config);
let window = self.map_window_config.create();
let renderer =
Renderer::initialize(&window, wgpu_settings.clone(), renderer_settings.clone())
.await
.unwrap();
&self.map_context.make_full(renderer);
self.map_context.make_full(renderer);
true
}
EventuallyMapContext::Empty => false,
EventuallyMapContext::_Uninitialized => false,
}
}
@ -234,3 +203,71 @@ where
}
}
}
/// Stores the state of the map, dispatches tile fetching and caching, tessellation and drawing.
pub struct SimpleMapSchedule<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HttpClient,
{
map_window_config: MWC,
pub map_context: MapContext,
schedule: Schedule,
scheduler: Scheduler<SM>,
http_client: HC,
}
impl<MWC, SM, HC> SimpleMapSchedule<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HttpClient,
{
pub fn new(
map_window_config: MWC,
window_size: WindowSize,
renderer: Renderer,
scheduler: Scheduler<SM>,
http_client: HC,
style: Style,
) -> Self {
let view_state = ViewState::new(&window_size);
let tile_repository = TileRepository::new();
let mut schedule = Schedule::default();
register_render_stages(&mut schedule, true).unwrap();
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) -> &Scheduler<SM> {
&self.scheduler
}
pub fn http_client(&self) -> &HC {
&self.http_client
}
}

View File

@ -1,5 +1,5 @@
use crate::error::Error;
use crate::HTTPClient;
use crate::HttpClient;
use async_trait::async_trait;
use reqwest::{Client, StatusCode};
use reqwest_middleware::ClientWithMiddleware;
@ -41,7 +41,7 @@ impl ReqwestHttpClient {
}
#[async_trait]
impl HTTPClient for ReqwestHttpClient {
impl HttpClient for ReqwestHttpClient {
async fn fetch(&self, url: &str) -> Result<Vec<u8>, Error> {
let response = self.client.get(url).send().await?;
match response.error_for_status() {

View File

@ -1,5 +1,4 @@
use crate::error::Error;
use crate::io::shared_thread_state::SharedThreadState;
use crate::ScheduleMethod;
use std::future::Future;
use std::pin::Pin;
@ -14,16 +13,11 @@ impl TokioScheduleMethod {
}
impl ScheduleMethod for TokioScheduleMethod {
fn schedule(
&self,
shared_thread_state: SharedThreadState,
future_factory: Box<
(dyn (FnOnce(SharedThreadState) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>)
+ Send
+ 'static),
>,
) -> Result<(), Error> {
tokio::task::spawn((future_factory)(shared_thread_state));
fn schedule<T>(&self, future_factory: impl FnOnce() -> T + Send + 'static) -> Result<(), Error>
where
T: Future<Output = ()> + Send + 'static,
{
tokio::task::spawn((future_factory)());
Ok(())
}
}

View File

@ -0,0 +1,68 @@
//! 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`].
use crate::render::graph::{Node, NodeRunError, RenderContext, RenderGraphContext, SlotInfo};
use crate::render::render_commands::{DrawMasks, DrawTiles};
use crate::render::render_phase::{PhaseItem, RenderCommand};
use crate::render::resource::{Head, TrackedRenderPass};
use crate::render::util::FloatOrd;
use crate::render::Eventually::Initialized;
use crate::render::RenderState;
use std::ops::{Deref, Range};
#[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> {
match state.surface.head() {
Head::Headed(_) => {}
Head::Headless(buffered_texture) => {
let size = state.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(),
),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: size.width() as u32,
height: size.height() as u32,
depth_or_array_layers: 1,
},
);
}
}
Ok(())
}
}

View File

@ -27,7 +27,8 @@ use super::EdgeExistence;
///
/// ## Example
/// Here is a simple render graph example with two nodes connected by a node edge.
/// ```ignore
/// ```
/// #
/// # use maplibre::render::graph::{Node, NodeRunError, RenderContext, RenderGraph, RenderGraphContext};
/// # use maplibre::render::{RenderState};
/// # struct MyNode;
@ -574,10 +575,8 @@ mod tests {
Edge, Node, NodeId, NodeRunError, RenderGraph, RenderGraphContext, RenderGraphError,
SlotInfo,
};
use crate::render::{
graph::{RenderContext, SlotType},
RenderState,
};
use crate::render::graph::{RenderContext, SlotType};
use crate::RenderState;
use std::collections::HashSet;
#[derive(Debug)]
@ -610,9 +609,9 @@ mod tests {
fn run(
&self,
_: &mut RenderGraphContext,
_: &mut RenderContext,
_: &RenderState,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
state: &RenderState,
) -> Result<(), NodeRunError> {
Ok(())
}
@ -683,9 +682,9 @@ mod tests {
impl Node for MyNode {
fn run(
&self,
_: &mut RenderGraphContext,
_: &mut RenderContext,
_: &RenderState,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
state: &RenderState,
) -> Result<(), NodeRunError> {
Ok(())
}

View File

@ -7,12 +7,21 @@ use crate::render::graph::{Node, NodeRunError, RenderContext, RenderGraphContext
use crate::render::render_commands::{DrawMasks, DrawTiles};
use crate::render::render_phase::{PhaseItem, RenderCommand};
use crate::render::resource::TrackedRenderPass;
use crate::render::stages::draw_graph;
use crate::render::util::FloatOrd;
use crate::render::Eventually::Initialized;
use crate::render::RenderState;
use crate::render::{draw_graph, main_graph, RenderState};
use std::ops::{Deref, Range};
pub mod graph {
// Labels for input nodes
pub mod input {}
// Labels for non-input nodes
pub mod node {
pub const MAIN_PASS_DEPENDENCIES: &str = "main_pass_dependencies";
pub const MAIN_PASS_DRIVER: &str = "main_pass_driver";
}
}
pub struct MainPassNode {}
impl MainPassNode {

View File

@ -27,18 +27,23 @@ use crate::render::shaders::{ShaderFeatureStyle, ShaderLayerMetadata};
use crate::render::tile_view_pattern::{TileInView, TileShape, TileViewPattern};
use crate::render::util::Eventually;
use crate::tessellation::IndexDataType;
use crate::MapWindow;
use crate::{HeadedMapWindow, MapWindow, MapWindowConfig};
use log::info;
use std::sync::Arc;
#[cfg(feature = "headless")]
// Exposed because it should be addable conditionally
pub mod copy_surface_to_buffer_node;
pub mod graph;
pub mod resource;
pub mod stages;
// Rendering internals
mod graph;
mod graph_runner;
mod main_pass;
mod render_commands;
mod render_phase;
mod resource;
mod shaders;
mod stages;
mod tile_pipeline;
mod tile_view_pattern;
mod util;
@ -52,7 +57,6 @@ pub use stages::register_render_stages;
pub const INDEX_FORMAT: wgpu::IndexFormat = wgpu::IndexFormat::Uint32; // Must match IndexDataType
#[derive(Default)]
pub struct RenderState {
render_target: Eventually<TextureView>,
@ -76,13 +80,40 @@ pub struct RenderState {
depth_texture: Eventually<Texture>,
multisampling_texture: Eventually<Option<Texture>>,
surface: Surface,
mask_phase: RenderPhase<TileInView>,
tile_phase: RenderPhase<(IndexEntry, TileShape)>,
}
impl RenderState {
pub fn new(surface: Surface) -> Self {
Self {
render_target: Default::default(),
buffer_pool: Default::default(),
tile_view_pattern: Default::default(),
tile_pipeline: Default::default(),
mask_pipeline: Default::default(),
globals_bind_group: Default::default(),
depth_texture: Default::default(),
multisampling_texture: Default::default(),
surface,
mask_phase: Default::default(),
tile_phase: Default::default(),
}
}
pub fn recreate_surface<MW>(&mut self, window: &MW, instance: &wgpu::Instance)
where
MW: MapWindow + HeadedMapWindow,
{
self.surface.recreate::<MW>(window, instance);
}
}
pub struct Renderer {
pub instance: wgpu::Instance,
pub device: wgpu::Device,
pub device: Arc<wgpu::Device>, // TODO: Arc is needed for headless rendering. Is there a simpler solution?
pub queue: wgpu::Queue,
pub adapter_info: wgpu::AdapterInfo,
@ -90,7 +121,6 @@ pub struct Renderer {
pub settings: RendererSettings,
pub state: RenderState,
pub surface: Surface,
}
impl Renderer {
@ -102,22 +132,15 @@ impl Renderer {
settings: RendererSettings,
) -> Result<Self, wgpu::RequestDeviceError>
where
MW: MapWindow,
MW: MapWindow + HeadedMapWindow,
{
let instance = wgpu::Instance::new(wgpu_settings.backends.unwrap_or(wgpu::Backends::all()));
let maybe_surface = match &settings.surface_type {
SurfaceType::Headless => None,
SurfaceType::Headed => Some(Surface::from_window(&instance, window, &settings)),
};
let surface = Surface::from_window(&instance, window, &settings);
let compatible_surface = if let Some(surface) = &maybe_surface {
match &surface.head() {
Head::Headed(window_head) => Some(window_head.surface()),
Head::Headless(_) => None,
}
} else {
None
let compatible_surface = match &surface.head() {
Head::Headed(window_head) => Some(window_head.surface()),
Head::Headless(_) => None,
};
let (device, queue, adapter_info) = Self::request_device(
@ -131,11 +154,6 @@ impl Renderer {
)
.await?;
let surface = maybe_surface.unwrap_or_else(|| match &settings.surface_type {
SurfaceType::Headless => Surface::from_image(&device, window, &settings),
SurfaceType::Headed => Surface::from_window(&instance, window, &settings),
});
match surface.head() {
Head::Headed(window) => window.configure(&device),
Head::Headless(_) => {}
@ -143,18 +161,51 @@ impl Renderer {
Ok(Self {
instance,
device,
device: Arc::new(device),
queue,
adapter_info,
wgpu_settings,
settings,
state: Default::default(),
surface,
state: RenderState::new(surface),
})
}
pub async fn initialize_headless<MW>(
window: &MW,
wgpu_settings: WgpuSettings,
settings: RendererSettings,
) -> Result<Self, wgpu::RequestDeviceError>
where
MW: MapWindow,
{
let instance = wgpu::Instance::new(wgpu_settings.backends.unwrap_or(wgpu::Backends::all()));
let (device, queue, adapter_info) = Self::request_device(
&instance,
&wgpu_settings,
&wgpu::RequestAdapterOptions {
power_preference: wgpu_settings.power_preference,
force_fallback_adapter: false,
compatible_surface: None,
},
)
.await?;
let surface = Surface::from_image(&device, window, &settings);
Ok(Self {
instance,
device: Arc::new(device),
queue,
adapter_info,
wgpu_settings,
settings,
state: RenderState::new(surface),
})
}
pub fn resize(&mut self, width: u32, height: u32) {
self.surface.resize(width, height)
self.state.surface.resize(width, height)
}
/// Requests a device
@ -323,7 +374,7 @@ impl Renderer {
&self.state
}
pub fn surface(&self) -> &Surface {
&self.surface
&self.state.surface
}
}
@ -331,10 +382,34 @@ impl Renderer {
mod tests {
use crate::render::graph::RenderGraph;
use crate::render::graph_runner::RenderGraphRunner;
use crate::render::RenderState;
use crate::render::resource::Surface;
use crate::{MapWindow, MapWindowConfig, RenderState, Renderer, RendererSettings, WindowSize};
pub struct HeadlessMapWindowConfig {
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
}
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
#[ignore] // FIXME: We do not have a GPU in CI
async fn test_render() {
let graph = RenderGraph::default();
@ -362,8 +437,41 @@ mod tests {
.ok()
.unwrap();
let render_state = RenderState::default();
let render_state = RenderState::new(Surface::from_image(
&device,
&HeadlessMapWindow {
size: WindowSize::new(100, 100).unwrap(),
},
&RendererSettings::default(),
));
RenderGraphRunner::run(&graph, &device, &queue, &render_state);
RenderGraphRunner::run(&graph, &device, &queue, &render_state).unwrap();
}
}
// Contributors to the RenderGraph should use the following label conventions:
// 1. Graph modules should have a NAME, input module, and node module (where relevant)
// 2. The "main_graph" graph is the root.
// 3. "sub graph" modules should be nested beneath their parent graph module
pub mod main_graph {
// Labels for input nodes
pub mod input {}
// Labels for non-input nodes
pub mod node {
pub const MAIN_PASS_DEPENDENCIES: &str = "main_pass_dependencies";
pub const MAIN_PASS_DRIVER: &str = "main_pass_driver";
}
}
/// Labels for the "draw" graph
pub mod draw_graph {
pub const NAME: &str = "draw";
// Labels for input nodes
pub mod input {}
// Labels for non-input nodes
pub mod node {
pub const MAIN_PASS: &str = "main_pass";
#[cfg(feature = "headless")]
pub const COPY: &str = "copy";
}
}

View File

@ -44,7 +44,7 @@ pub struct BufferPool<Q, B, V, I, TM, FM> {
}
#[derive(Debug)]
enum BackingBufferType {
pub enum BackingBufferType {
Vertices,
Indices,
Metadata,
@ -578,12 +578,12 @@ impl RingIndex {
#[cfg(test)]
mod tests {
use crate::coords::ZoomLevel;
use crate::style::layer::StyleLayer;
use lyon::tessellation::VertexBuffers;
use crate::render::resource::buffer_pool::{
BackingBufferDescriptor, BackingBufferType, BufferPool, Queue,
};
use crate::render::resource::buffer_pool::BackingBufferType;
use crate::render::resource::{BackingBufferDescriptor, BufferPool, Queue};
#[derive(Debug)]
struct TestBuffer {
@ -639,7 +639,7 @@ mod tests {
for _ in 0..2 {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer.clone(),
&data48bytes_aligned,
2,
@ -653,7 +653,7 @@ mod tests {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer.clone(),
&data24bytes_aligned,
2,
@ -667,7 +667,7 @@ mod tests {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer.clone(),
&data24bytes_aligned,
2,
@ -679,7 +679,7 @@ mod tests {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer.clone(),
&data24bytes_aligned,
2,
@ -690,7 +690,7 @@ mod tests {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer.clone(),
&data24bytes_aligned,
2,
@ -701,7 +701,7 @@ mod tests {
pool.allocate_layer_geometry(
&queue,
(0, 0, 0).into(),
(0, 0, ZoomLevel::default()).into(),
style_layer,
&data24bytes_aligned,
2,

View File

@ -4,14 +4,18 @@
use crate::render::resource::texture::TextureView;
use crate::render::settings::RendererSettings;
use crate::render::util::HasChanged;
use crate::{MapWindow, WindowSize};
use crate::window::HeadedMapWindow;
use crate::{MapWindow, MapWindowConfig, WindowSize};
use std::fs::File;
use std::io::Write;
use std::mem::size_of;
use std::sync::Arc;
struct BufferDimensions {
width: usize,
height: usize,
unpadded_bytes_per_row: usize,
padded_bytes_per_row: usize,
pub struct BufferDimensions {
pub width: usize,
pub height: usize,
pub unpadded_bytes_per_row: usize,
pub padded_bytes_per_row: usize,
}
impl BufferDimensions {
@ -42,7 +46,7 @@ impl WindowHead {
pub fn recreate_surface<MW>(&mut self, window: &MW, instance: &wgpu::Instance)
where
MW: MapWindow,
MW: MapWindow + HeadedMapWindow,
{
self.surface = unsafe { instance.create_surface(window.inner()) };
}
@ -52,14 +56,63 @@ impl WindowHead {
}
pub struct BufferedTextureHead {
texture: wgpu::Texture,
output_buffer: wgpu::Buffer,
buffer_dimensions: BufferDimensions,
pub texture: wgpu::Texture,
pub output_buffer: wgpu::Buffer,
pub buffer_dimensions: BufferDimensions,
}
#[cfg(feature = "headless")]
impl BufferedTextureHead {
pub async fn create_png<'a>(
&self,
device: &wgpu::Device,
png_output_path: &str,
// device: &wgpu::Device,
) {
// Note that we're not calling `.await` here.
let buffer_slice = self.output_buffer.slice(..);
let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read);
// Poll the device in a blocking manner so that our future resolves.
// In an actual application, `device.poll(...)` should
// be called in an event loop or on another thread.
device.poll(wgpu::Maintain::Wait);
if let Ok(()) = buffer_future.await {
let padded_buffer = buffer_slice.get_mapped_range();
let mut png_encoder = png::Encoder::new(
File::create(png_output_path).unwrap(),
self.buffer_dimensions.width as u32,
self.buffer_dimensions.height as u32,
);
png_encoder.set_depth(png::BitDepth::Eight);
png_encoder.set_color(png::ColorType::Rgba);
let mut png_writer = png_encoder
.write_header()
.unwrap()
.into_stream_writer_with_size(self.buffer_dimensions.unpadded_bytes_per_row)
.unwrap();
// from the padded_buffer we write just the unpadded bytes into the image
for chunk in padded_buffer.chunks(self.buffer_dimensions.padded_bytes_per_row) {
png_writer
.write_all(&chunk[..self.buffer_dimensions.unpadded_bytes_per_row])
.unwrap();
}
png_writer.finish().unwrap();
// With the current interface, we have to make sure all mapped views are
// dropped before we unmap the buffer.
drop(padded_buffer);
self.output_buffer.unmap();
}
}
}
pub enum Head {
Headed(WindowHead),
Headless(BufferedTextureHead),
Headless(Arc<BufferedTextureHead>),
}
pub struct Surface {
@ -74,7 +127,7 @@ impl Surface {
settings: &RendererSettings,
) -> Self
where
MW: MapWindow,
MW: MapWindow + HeadedMapWindow,
{
let size = window.size();
let surface_config = wgpu::SurfaceConfiguration {
@ -97,6 +150,7 @@ impl Surface {
}
}
// TODO: Give better name
pub fn from_image<MW>(device: &wgpu::Device, window: &MW, settings: &RendererSettings) -> Self
where
MW: MapWindow,
@ -111,7 +165,7 @@ impl Surface {
BufferDimensions::new(size.width() as usize, size.height() as usize);
// The output buffer lets us retrieve the data as an array
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
label: Some("BufferedTextureHead buffer"),
size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
@ -133,11 +187,11 @@ impl Surface {
Self {
size,
head: Head::Headless(BufferedTextureHead {
head: Head::Headless(Arc::new(BufferedTextureHead {
texture,
output_buffer,
buffer_dimensions,
}),
})),
}
}
@ -149,7 +203,7 @@ impl Surface {
let frame = match surface.get_current_texture() {
Ok(view) => view,
Err(wgpu::SurfaceError::Outdated) => {
tracing::trace!("surface outdated");
log::warn!("surface outdated");
window.configure(device);
surface
.get_current_texture()
@ -159,7 +213,8 @@ impl Surface {
};
frame.into()
}
Head::Headless(BufferedTextureHead { texture, .. }) => texture
Head::Headless(arc) => arc
.texture
.create_view(&wgpu::TextureViewDescriptor::default())
.into(),
}
@ -193,11 +248,13 @@ impl Surface {
pub fn recreate<MW>(&mut self, window: &MW, instance: &wgpu::Instance)
where
MW: MapWindow,
MW: MapWindow + HeadedMapWindow,
{
match &mut self.head {
Head::Headed(head) => {
head.recreate_surface(window, instance);
Head::Headed(window_head) => {
if window_head.has_changed(&(self.size.width(), self.size.height())) {
window_head.recreate_surface(window, instance);
}
}
Head::Headless(_) => {}
}
@ -216,6 +273,6 @@ impl HasChanged for WindowHead {
type Criteria = (u32, u32);
fn has_changed(&self, criteria: &Self::Criteria) -> bool {
self.surface_config.height != criteria.0 || self.surface_config.width != criteria.1
self.surface_config.width != criteria.0 || self.surface_config.height != criteria.1
}
}

View File

@ -202,7 +202,7 @@ impl<'a> TrackedRenderPass<'a> {
/// Push a new debug group over the internal stack. Subsequent render commands and debug
/// markers are grouped into this new group, until [`pop_debug_group`] is called.
///
/// ```ignore
/// ```
/// # fn example(mut pass: maplibre::render::resource::TrackedRenderPass<'static>) {
/// pass.push_debug_group("Render the car");
/// // [setup pipeline etc...]

View File

@ -4,6 +4,10 @@ use crate::platform::COLOR_TEXTURE_FORMAT;
use std::borrow::Cow;
pub use wgpu::Backends;
pub use wgpu::Features;
pub use wgpu::Limits;
pub use wgpu::PowerPreference;
pub use wgpu::TextureFormat;
/// Provides configuration for renderer initialization. Use [`Device::features`](crate::renderer::Device::features),
/// [`Device::limits`](crate::renderer::Device::limits), and the [`WgpuAdapterInfo`](crate::render_resource::WgpuAdapterInfo)
@ -11,17 +15,17 @@ pub use wgpu::Backends;
#[derive(Clone)]
pub struct WgpuSettings {
pub device_label: Option<Cow<'static, str>>,
pub backends: Option<wgpu::Backends>,
pub power_preference: wgpu::PowerPreference,
pub backends: Option<Backends>,
pub power_preference: PowerPreference,
/// The features to ensure are enabled regardless of what the adapter/backend supports.
/// Setting these explicitly may cause renderer initialization to fail.
pub features: wgpu::Features,
pub features: Features,
/// The features to ensure are disabled regardless of what the adapter/backend supports
pub disabled_features: Option<wgpu::Features>,
pub disabled_features: Option<Features>,
/// The imposed limits.
pub limits: wgpu::Limits,
pub limits: Limits,
/// The constraints on limits allowed regardless of what the adapter/backend supports
pub constrained_limits: Option<wgpu::Limits>,
pub constrained_limits: Option<Limits>,
/// Whether a trace is recorded an stored in the current working directory
pub record_trace: bool,
@ -32,12 +36,12 @@ impl Default for WgpuSettings {
let backends = Some(wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::all()));
let limits = if cfg!(feature = "web-webgl") {
wgpu::Limits {
Limits {
max_texture_dimension_2d: 4096,
..wgpu::Limits::downlevel_webgl2_defaults()
..Limits::downlevel_webgl2_defaults()
}
} else if cfg!(target_os = "android") {
wgpu::Limits {
Limits {
max_storage_textures_per_shader_stage: 4,
max_compute_workgroups_per_dimension: 0,
max_compute_workgroup_size_z: 0,
@ -45,19 +49,19 @@ impl Default for WgpuSettings {
max_compute_workgroup_size_x: 0,
max_compute_workgroup_storage_size: 0,
max_compute_invocations_per_workgroup: 0,
..wgpu::Limits::downlevel_defaults()
..Limits::downlevel_defaults()
}
} else {
wgpu::Limits {
..wgpu::Limits::default()
Limits {
..Limits::default()
}
};
Self {
device_label: Default::default(),
backends,
power_preference: wgpu::PowerPreference::HighPerformance,
features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
power_preference: PowerPreference::HighPerformance,
features: Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
disabled_features: None,
limits,
constrained_limits: None,
@ -101,8 +105,7 @@ impl Default for Msaa {
#[derive(Clone)]
pub struct RendererSettings {
pub msaa: Msaa,
pub texture_format: wgpu::TextureFormat,
pub surface_type: SurfaceType,
pub texture_format: TextureFormat,
}
impl Default for RendererSettings {
@ -110,7 +113,6 @@ impl Default for RendererSettings {
Self {
msaa: Msaa::default(),
texture_format: COLOR_TEXTURE_FORMAT,
surface_type: SurfaceType::Headed,
}
}
}

View File

@ -0,0 +1,61 @@
//! Extracts data from the current state.
use crate::context::MapContext;
use crate::coords::{ViewRegion, Zoom};
use crate::io::tile_repository::TileRepository;
use crate::render::camera::ViewProjection;
use crate::render::render_phase::RenderPhase;
use crate::render::resource::IndexEntry;
use crate::render::shaders::{
ShaderCamera, ShaderFeatureStyle, ShaderGlobals, ShaderLayerMetadata, Vec4f32,
};
use crate::render::tile_view_pattern::TileInView;
use crate::render::util::Eventually::Initialized;
use crate::schedule::Stage;
use crate::{RenderState, Renderer, Style};
use std::iter;
#[derive(Default)]
pub struct ExtractStage;
impl Stage for ExtractStage {
fn run(
&mut self,
MapContext {
view_state,
renderer:
Renderer {
state:
RenderState {
mask_phase,
tile_phase,
tile_view_pattern,
buffer_pool,
..
},
..
},
..
}: &mut MapContext,
) {
if let (Initialized(tile_view_pattern), Initialized(buffer_pool)) =
(tile_view_pattern, &buffer_pool)
{
let visible_level = view_state.visible_level();
let view_proj = view_state.view_projection();
let view_region = view_state
.camera
.view_region_bounding_box(&view_proj.invert())
.map(|bounding_box| {
ViewRegion::new(bounding_box, 0, *view_state.zoom, visible_level)
});
if let Some(view_region) = &view_region {
let zoom = view_state.zoom();
tile_view_pattern.update_pattern(view_region, buffer_pool, zoom);
}
}
}
}

View File

@ -1,10 +1,5 @@
//! Executes the [`RenderGraph`] current render graph.
// Plugins that contribute to the RenderGraph should use the following label conventions:
// 1. Graph modules should have a NAME, input module, and node module (where relevant)
// 2. The "top level" graph is the plugin module root. Just add things like `pub mod node` directly under the plugin module
// 3. "sub graph" modules should be nested beneath their parent graph module
use crate::context::MapContext;
use crate::render::graph::{EmptyNode, RenderGraph};
use crate::render::graph_runner::RenderGraphRunner;
@ -14,42 +9,13 @@ use crate::schedule::Stage;
use crate::Renderer;
use log::error;
pub mod node {
pub const MAIN_PASS_DEPENDENCIES: &str = "main_pass_dependencies";
pub const MAIN_PASS_DRIVER: &str = "main_pass_driver";
}
pub mod draw_graph {
pub const NAME: &str = "draw";
pub mod input {}
pub mod node {
pub const MAIN_PASS: &str = "main_pass";
}
}
/// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame.
pub struct GraphRunnerStage {
graph: RenderGraph,
}
impl Default for GraphRunnerStage {
fn default() -> Self {
let pass_node = MainPassNode::new();
let mut graph = RenderGraph::default();
let mut draw_graph = RenderGraph::default();
draw_graph.add_node(draw_graph::node::MAIN_PASS, pass_node);
let input_node_id = draw_graph.set_input(vec![]);
draw_graph
.add_node_edge(input_node_id, draw_graph::node::MAIN_PASS)
.unwrap();
graph.add_sub_graph(draw_graph::NAME, draw_graph);
graph.add_node(node::MAIN_PASS_DEPENDENCIES, EmptyNode);
graph.add_node(node::MAIN_PASS_DRIVER, MainPassDriverNode);
graph
.add_node_edge(node::MAIN_PASS_DEPENDENCIES, node::MAIN_PASS_DRIVER)
.unwrap();
impl GraphRunnerStage {
pub fn new(graph: RenderGraph) -> Self {
Self { graph }
}
}

View File

@ -1,21 +1,28 @@
//! Rendering specific [Stages](Stage)
use crate::context::MapContext;
use crate::multi_stage;
use crate::render::graph::{EmptyNode, RenderGraph, RenderGraphError};
use crate::render::main_pass::{MainPassDriverNode, MainPassNode};
use crate::render::stages::extract_stage::ExtractStage;
use crate::render::stages::phase_sort_stage::PhaseSortStage;
use crate::render::stages::queue_stage::QueueStage;
use crate::render::{draw_graph, main_graph};
use crate::schedule::{MultiStage, Schedule, Stage, StageLabel};
use graph_runner_stage::GraphRunnerStage;
use resource_stage::ResourceStage;
use upload_stage::UploadStage;
mod extract_stage;
mod graph_runner_stage;
mod phase_sort_stage;
mod queue_stage;
mod resource_stage;
mod upload_stage;
use crate::multi_stage;
use crate::render::stages::phase_sort_stage::PhaseSortStage;
use crate::render::stages::queue_stage::QueueStage;
pub use graph_runner_stage::{draw_graph, node};
#[cfg(feature = "headless")]
// Exposed because it should be addable conditionally
pub mod write_surface_buffer_stage;
/// The labels of the default App rendering stages.
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
@ -46,11 +53,52 @@ impl StageLabel for RenderStageLabel {
}
}
multi_stage!(PrepareStage, upload: UploadStage, resource: ResourceStage);
multi_stage!(
PrepareStage,
resource: ResourceStage,
extract: ExtractStage,
upload: UploadStage
);
pub fn register_render_stages(
schedule: &mut Schedule,
headless: bool,
) -> Result<(), RenderGraphError> {
let mut graph = RenderGraph::default();
let mut draw_graph = RenderGraph::default();
draw_graph.add_node(draw_graph::node::MAIN_PASS, MainPassNode::new());
let input_node_id = draw_graph.set_input(vec![]);
draw_graph.add_node_edge(input_node_id, draw_graph::node::MAIN_PASS)?;
#[cfg(feature = "headless")]
if headless {
use crate::render::copy_surface_to_buffer_node::CopySurfaceBufferNode;
draw_graph.add_node(draw_graph::node::COPY, CopySurfaceBufferNode::default());
draw_graph.add_node_edge(draw_graph::node::MAIN_PASS, draw_graph::node::COPY)?;
}
graph.add_sub_graph(draw_graph::NAME, draw_graph);
graph.add_node(main_graph::node::MAIN_PASS_DEPENDENCIES, EmptyNode);
graph.add_node(main_graph::node::MAIN_PASS_DRIVER, MainPassDriverNode);
graph.add_node_edge(
main_graph::node::MAIN_PASS_DEPENDENCIES,
main_graph::node::MAIN_PASS_DRIVER,
)?;
pub fn register_render_stages(schedule: &mut Schedule) {
schedule.add_stage(RenderStageLabel::Prepare, PrepareStage::default());
schedule.add_stage(RenderStageLabel::Queue, QueueStage::default());
schedule.add_stage(RenderStageLabel::PhaseSort, PhaseSortStage::default());
schedule.add_stage(RenderStageLabel::Render, GraphRunnerStage::default());
schedule.add_stage(RenderStageLabel::Render, GraphRunnerStage::new(graph));
#[cfg(feature = "headless")]
if headless {
use crate::render::stages::write_surface_buffer_stage::WriteSurfaceBufferStage;
schedule.add_stage(
RenderStageLabel::Cleanup,
WriteSurfaceBufferStage::default(),
);
}
Ok(())
}

View File

@ -2,8 +2,7 @@
use crate::context::MapContext;
use crate::coords::{ViewRegion, Zoom};
use crate::io::tile_cache::TileCache;
use crate::io::LayerTessellateMessage;
use crate::io::tile_repository::TileRepository;
use crate::render::camera::ViewProjection;
use crate::render::render_phase::RenderPhase;
use crate::render::resource::IndexEntry;

View File

@ -2,8 +2,7 @@
use crate::context::MapContext;
use crate::coords::{ViewRegion, Zoom};
use crate::io::tile_cache::TileCache;
use crate::io::LayerTessellateMessage;
use crate::io::tile_repository::TileRepository;
use crate::render::camera::ViewProjection;
use crate::render::resource::IndexEntry;
use crate::render::shaders::{
@ -23,15 +22,27 @@ impl Stage for QueueStage {
fn run(
&mut self,
MapContext {
renderer: Renderer { state, .. },
view_state,
renderer:
Renderer {
state:
RenderState {
mask_phase,
tile_phase,
tile_view_pattern,
buffer_pool,
..
},
..
},
..
}: &mut MapContext,
) {
state.mask_phase.items.clear();
state.tile_phase.items.clear();
mask_phase.items.clear();
tile_phase.items.clear();
if let (Initialized(tile_view_pattern), Initialized(buffer_pool)) =
(&state.tile_view_pattern, &state.buffer_pool)
(tile_view_pattern, &buffer_pool)
{
let index = buffer_pool.index();
@ -43,7 +54,7 @@ impl Stage for QueueStage {
let shape_to_render = fallback.as_ref().unwrap_or(shape);
// Draw mask
state.mask_phase.add(tile_in_view.clone());
mask_phase.add(tile_in_view.clone());
if let Some(entries) = index.get_layers(&shape_to_render.coords) {
let mut layers_to_render: Vec<&IndexEntry> = Vec::from_iter(entries);
@ -51,9 +62,7 @@ impl Stage for QueueStage {
for entry in layers_to_render {
// Draw tile
state
.tile_phase
.add((entry.clone(), shape_to_render.clone()))
tile_phase.add((entry.clone(), shape_to_render.clone()))
}
} else {
tracing::trace!("No layers found at {}", &shape_to_render.coords);

View File

@ -28,13 +28,14 @@ impl Stage for ResourceStage {
Renderer {
settings,
device,
surface,
state,
..
},
..
}: &mut MapContext,
) {
let surface = &mut state.surface;
let size = surface.size();
surface.reconfigure(device);

View File

@ -2,8 +2,7 @@
use crate::context::MapContext;
use crate::coords::{ViewRegion, Zoom};
use crate::io::tile_cache::TileCache;
use crate::io::LayerTessellateMessage;
use crate::io::tile_repository::{StoredLayer, TileRepository};
use crate::render::camera::ViewProjection;
use crate::render::resource::IndexEntry;
use crate::render::shaders::{
@ -26,16 +25,8 @@ impl Stage for UploadStage {
MapContext {
view_state,
style,
tile_cache,
renderer:
Renderer {
settings: _,
device: _,
queue,
surface: _,
state,
..
},
tile_repository,
renderer: Renderer { queue, state, .. },
..
}: &mut MapContext,
) {
@ -67,10 +58,8 @@ impl Stage for UploadStage {
.map(|bounding_box| ViewRegion::new(bounding_box, 0, *view_state.zoom, visible_level));
if let Some(view_region) = &view_region {
let zoom = view_state.zoom();
self.upload_tile_geometry(state, queue, tile_cache, style, view_region);
self.update_tile_view_pattern(state, queue, view_region, &view_proj, zoom);
self.upload_tile_geometry(state, queue, tile_repository, style, view_region);
self.upload_tile_view_pattern(state, queue, &view_proj);
self.update_metadata();
}
}
@ -99,7 +88,7 @@ impl UploadStage {
/*let source_layer = entry.style_layer.source_layer.as_ref().unwrap();
if let Some(result) = scheduler
.get_tile_cache()
.get_tile_repository()
.iter_tessellated_layers_at(&world_coords)
.unwrap()
.find(|layer| source_layer.as_str() == layer.layer_name())
@ -147,22 +136,15 @@ impl UploadStage {
}
#[tracing::instrument(skip_all)]
pub fn update_tile_view_pattern(
pub fn upload_tile_view_pattern(
&self,
RenderState {
tile_view_pattern,
buffer_pool,
..
tile_view_pattern, ..
}: &mut RenderState,
queue: &wgpu::Queue,
view_region: &ViewRegion,
view_proj: &ViewProjection,
zoom: Zoom,
) {
if let (Initialized(tile_view_pattern), Initialized(buffer_pool)) =
(tile_view_pattern, buffer_pool)
{
tile_view_pattern.update_pattern(view_region, buffer_pool, zoom);
if let Initialized(tile_view_pattern) = tile_view_pattern {
tile_view_pattern.upload_pattern(queue, view_proj);
}
}
@ -172,7 +154,7 @@ impl UploadStage {
&self,
RenderState { buffer_pool, .. }: &mut RenderState,
queue: &wgpu::Queue,
tile_cache: &TileCache,
tile_repository: &TileRepository,
style: &Style,
view_region: &ViewRegion,
) {
@ -182,7 +164,7 @@ impl UploadStage {
let loaded_layers = buffer_pool
.get_loaded_layers_at(&world_coords)
.unwrap_or_default();
if let Some(available_layers) = tile_cache
if let Some(available_layers) = tile_repository
.iter_tessellated_layers_at(&world_coords)
.map(|layers| {
layers
@ -204,10 +186,10 @@ impl UploadStage {
.map(|color| color.into());
match message {
LayerTessellateMessage::UnavailableLayer { coords: _, .. } => {
StoredLayer::UnavailableLayer { coords: _, .. } => {
/*self.buffer_pool.mark_layer_unavailable(*coords);*/
}
LayerTessellateMessage::TessellatedLayer {
StoredLayer::TessellatedLayer {
coords,
feature_indices,
layer_data,

View File

@ -0,0 +1,60 @@
//! Stage which writes the current contents of the GPU/CPU buffer in [`BufferedTextureHead`]
//! to disk as PNG.
use crate::context::MapContext;
use crate::coords::{ViewRegion, Zoom};
use crate::io::tile_repository::TileRepository;
use crate::render::camera::ViewProjection;
use crate::render::render_phase::RenderPhase;
use crate::render::resource::{BufferDimensions, BufferedTextureHead, Head, IndexEntry};
use crate::render::shaders::{
ShaderCamera, ShaderFeatureStyle, ShaderGlobals, ShaderLayerMetadata, Vec4f32,
};
use crate::render::tile_view_pattern::TileInView;
use crate::render::util::Eventually::Initialized;
use crate::schedule::Stage;
use crate::{RenderState, Renderer, Style};
use std::fs::File;
use std::future::Future;
use std::io::Write;
use std::iter;
use std::ops::Deref;
use std::sync::Arc;
use tokio::runtime::Handle;
use tokio::task;
use wgpu::{BufferAsyncError, BufferSlice};
#[derive(Default)]
pub struct WriteSurfaceBufferStage {
frame: u64,
}
impl Stage for WriteSurfaceBufferStage {
fn run(
&mut self,
MapContext {
renderer: Renderer { state, device, .. },
..
}: &mut MapContext,
) {
match state.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

@ -79,7 +79,7 @@ where
};
if should_replace {
mem::replace(self, Eventually::Initialized(f()));
*self = Eventually::Initialized(f());
}
}
}
@ -87,7 +87,7 @@ impl<T> Eventually<T> {
#[tracing::instrument(name = "initialize", skip_all)]
pub fn initialize(&mut self, f: impl FnOnce() -> T) {
if let Eventually::Uninitialized = self {
mem::replace(self, Eventually::Initialized(f()));
*self = Eventually::Initialized(f());
}
}

View File

@ -0,0 +1,80 @@
use crate::coords::{WorldCoords, WorldTileCoords, Zoom};
use crate::error::Error;
use crate::io::geometry_index::{GeometryIndex, IndexedGeometry};
use crate::io::pipeline::PipelineContext;
use crate::io::pipeline::Processable;
use crate::io::tile_pipelines::build_vector_tile_pipeline;
use crate::io::tile_repository::StoredLayer;
use crate::io::tile_request_state::TileRequestState;
use crate::io::{TileRequest, TileRequestID};
use crate::render::ShaderVertex;
use crate::stages::HeadedPipelineProcessor;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use geozero::mvt::tile;
use std::fmt;
use std::sync::{mpsc, Arc, Mutex};
pub type MessageSender = mpsc::Sender<TessellateMessage>;
pub type MessageReceiver = mpsc::Receiver<TessellateMessage>;
/// [crate::io::TileTessellateMessage] or [crate::io::LayerTessellateMessage] tessellation message.
pub enum TessellateMessage {
Tile(TileTessellateMessage),
Layer(LayerTessellateMessage),
}
/// The result of the tessellation of a tile.
pub struct TileTessellateMessage {
pub request_id: TileRequestID,
pub coords: WorldTileCoords,
}
/// `TessellatedLayer` contains the result of the tessellation for a specific layer, otherwise
/// `UnavailableLayer` if the layer doesn't exist.
pub enum LayerTessellateMessage {
UnavailableLayer {
coords: WorldTileCoords,
layer_name: String,
},
TessellatedLayer {
coords: WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
/// Holds for each feature the count of indices.
feature_indices: Vec<u32>,
layer_data: tile::Layer,
},
}
impl Into<StoredLayer> for LayerTessellateMessage {
fn into(self) -> StoredLayer {
match self {
LayerTessellateMessage::UnavailableLayer { coords, layer_name } => {
StoredLayer::UnavailableLayer { coords, layer_name }
}
LayerTessellateMessage::TessellatedLayer {
coords,
buffer,
feature_indices,
layer_data,
} => StoredLayer::TessellatedLayer {
coords,
buffer,
feature_indices,
layer_data,
},
}
}
}
impl fmt::Debug for LayerTessellateMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"LayerTessellateMessage{}",
match self {
LayerTessellateMessage::UnavailableLayer { coords, .. } => coords,
LayerTessellateMessage::TessellatedLayer { coords, .. } => coords,
}
)
}
}

View File

@ -1,15 +1,188 @@
//! [Stages](Stage) for requesting and preparing data
use crate::io::source_client::SourceClient;
use crate::coords::ZoomLevel;
use crate::coords::{WorldCoords, WorldTileCoords, Zoom};
use crate::error::Error;
use crate::io::geometry_index::GeometryIndex;
use crate::io::geometry_index::{IndexProcessor, IndexedGeometry, TileIndex};
use crate::io::pipeline::Processable;
use crate::io::pipeline::{PipelineContext, PipelineProcessor};
use crate::io::source_client::{HttpSourceClient, SourceClient};
use crate::io::tile_pipelines::build_vector_tile_pipeline;
use crate::io::tile_repository::StoredLayer;
use crate::io::tile_request_state::TileRequestState;
use crate::io::{TileRequest, TileRequestID};
use crate::render::ShaderVertex;
use crate::schedule::Schedule;
use crate::stages::message::{
LayerTessellateMessage, MessageReceiver, MessageSender, TessellateMessage,
TileTessellateMessage,
};
use crate::stages::populate_tile_store_stage::PopulateTileStore;
use crate::HTTPClient;
use crate::tessellation::zero_tessellator::ZeroTessellator;
use crate::tessellation::{IndexDataType, OverAlignedVertexBuffer};
use crate::{HttpClient, ScheduleMethod, Scheduler};
use geozero::mvt::tile;
use geozero::GeozeroDatasource;
use prost::Message;
use request_stage::RequestStage;
use std::collections::HashSet;
use std::fmt;
use std::sync::{mpsc, Arc, Mutex};
mod message;
mod populate_tile_store_stage;
mod request_stage;
pub fn register_stages<HC: HTTPClient>(schedule: &mut Schedule, source_client: SourceClient<HC>) {
schedule.add_stage("request", RequestStage::new(source_client));
schedule.add_stage("populate_tile_store", PopulateTileStore::default());
/// Register stages required for requesting and preparing new tiles.
pub fn register_stages<HC: HttpClient, SM: ScheduleMethod>(
schedule: &mut Schedule,
http_source_client: HttpSourceClient<HC>,
scheduler: Box<Scheduler<SM>>,
) {
let (message_sender, message_receiver): (MessageSender, MessageReceiver) = mpsc::channel();
let shared_thread_state = SharedThreadState {
tile_request_state: Arc::new(Mutex::new(TileRequestState::new())),
message_sender,
geometry_index: Arc::new(Mutex::new(GeometryIndex::new())),
};
schedule.add_stage(
"request",
RequestStage::new(shared_thread_state.clone(), http_source_client, *scheduler),
);
schedule.add_stage(
"populate_tile_store",
PopulateTileStore::new(shared_thread_state, message_receiver),
);
}
pub struct HeadedPipelineProcessor {
state: SharedThreadState,
}
impl PipelineProcessor for HeadedPipelineProcessor {
fn tile_finished(&mut self, request_id: TileRequestID, coords: &WorldTileCoords) {
self.state
.message_sender
.send(TessellateMessage::Tile(TileTessellateMessage {
request_id,
coords: *coords,
}))
.unwrap();
}
fn layer_unavailable(&mut self, coords: &WorldTileCoords, layer_name: &str) {
self.state
.message_sender
.send(TessellateMessage::Layer(
LayerTessellateMessage::UnavailableLayer {
coords: *coords,
layer_name: layer_name.to_owned(),
},
))
.unwrap();
}
fn layer_tesselation_finished(
&mut self,
coords: &WorldTileCoords,
buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
feature_indices: Vec<u32>,
layer_data: tile::Layer,
) {
self.state
.message_sender
.send(TessellateMessage::Layer(
LayerTessellateMessage::TessellatedLayer {
coords: *coords,
buffer,
feature_indices,
layer_data,
},
))
.unwrap();
}
fn layer_indexing_finished(
&mut self,
coords: &WorldTileCoords,
geometries: Vec<IndexedGeometry<f64>>,
) {
if let Ok(mut geometry_index) = self.state.geometry_index.lock() {
geometry_index.index_tile(&coords, TileIndex::Linear { list: geometries })
}
}
}
/// Stores and provides access to the thread safe data shared between the schedulers.
#[derive(Clone)]
pub struct SharedThreadState {
pub tile_request_state: Arc<Mutex<TileRequestState>>,
pub message_sender: mpsc::Sender<TessellateMessage>,
pub geometry_index: Arc<Mutex<GeometryIndex>>,
}
impl SharedThreadState {
fn get_tile_request(&self, request_id: TileRequestID) -> Option<TileRequest> {
self.tile_request_state
.lock()
.ok()
.and_then(|tile_request_state| tile_request_state.get_tile_request(request_id).cloned())
}
#[tracing::instrument(skip_all)]
pub fn process_tile(&self, request_id: TileRequestID, data: Box<[u8]>) -> Result<(), Error> {
if let Some(tile_request) = self.get_tile_request(request_id) {
let mut pipeline_context = PipelineContext::new(HeadedPipelineProcessor {
state: self.clone(),
});
let pipeline = build_vector_tile_pipeline();
pipeline.process((tile_request, request_id, data), &mut pipeline_context);
}
Ok(())
}
pub fn tile_unavailable(
&self,
coords: &WorldTileCoords,
request_id: TileRequestID,
) -> Result<(), Error> {
if let Some(tile_request) = self.get_tile_request(request_id) {
for to_load in &tile_request.layers {
tracing::warn!("layer {} at {} unavailable", to_load, coords);
self.message_sender.send(TessellateMessage::Layer(
LayerTessellateMessage::UnavailableLayer {
coords: tile_request.coords,
layer_name: to_load.to_string(),
},
))?;
}
}
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn query_point(
&self,
world_coords: &WorldCoords,
z: ZoomLevel,
zoom: Zoom,
) -> Option<Vec<IndexedGeometry<f64>>> {
if let Ok(geometry_index) = self.geometry_index.lock() {
geometry_index
.query_point(world_coords, z, zoom)
.map(|geometries| {
geometries
.iter()
.cloned()
.cloned()
.collect::<Vec<IndexedGeometry<f64>>>()
})
} else {
unimplemented!()
}
}
}

View File

@ -1,35 +1,46 @@
//! Receives data from async threads and populates the [`crate::io::tile_cache::TileCache`].
//! Receives data from async threads and populates the [`crate::io::tile_repository::TileRepository`].
use super::{MessageReceiver, SharedThreadState, TessellateMessage, TileTessellateMessage};
use crate::context::MapContext;
use crate::io::{TessellateMessage, TileTessellateMessage};
use crate::io::tile_repository::StoredLayer;
use crate::schedule::Stage;
use std::sync::mpsc;
#[derive(Default)]
pub struct PopulateTileStore {}
pub struct PopulateTileStore {
shared_thread_state: SharedThreadState,
message_receiver: MessageReceiver,
}
impl PopulateTileStore {
pub fn new(shared_thread_state: SharedThreadState, message_receiver: MessageReceiver) -> Self {
Self {
shared_thread_state,
message_receiver,
}
}
}
impl Stage for PopulateTileStore {
fn run(
&mut self,
MapContext {
tile_cache,
shared_thread_state,
message_receiver,
..
tile_repository, ..
}: &mut MapContext,
) {
if let Ok(result) = message_receiver.try_recv() {
if let Ok(result) = self.message_receiver.try_recv() {
match result {
TessellateMessage::Layer(layer_result) => {
let layer: StoredLayer = layer_result.into();
tracing::trace!(
"Layer {} at {} reached main thread",
layer_result.layer_name(),
layer_result.get_coords()
layer.layer_name(),
layer.get_coords()
);
tile_cache.put_tessellated_layer(layer_result);
tile_repository.put_tessellated_layer(layer);
}
TessellateMessage::Tile(TileTessellateMessage { request_id, coords }) => loop {
if let Ok(mut tile_request_state) =
shared_thread_state.tile_request_state.try_lock()
self.shared_thread_state.tile_request_state.try_lock()
{
tile_request_state.finish_tile_request(request_id);
tracing::trace!("Tile at {} finished loading", coords);

View File

@ -3,46 +3,55 @@
use crate::context::MapContext;
use crate::coords::{ViewRegion, WorldTileCoords};
use crate::error::Error;
use crate::io::shared_thread_state::SharedThreadState;
use crate::io::source_client::SourceClient;
use crate::io::tile_cache::TileCache;
use crate::io::source_client::{HttpSourceClient, SourceClient};
use crate::io::tile_repository::TileRepository;
use crate::io::TileRequest;
use crate::schedule::Stage;
use crate::{HTTPClient, ScheduleMethod, Style};
use crate::stages::SharedThreadState;
use crate::{HttpClient, ScheduleMethod, Scheduler, Style};
use std::collections::HashSet;
pub struct RequestStage<HC>
pub struct RequestStage<SM, HC>
where
HC: HTTPClient,
SM: ScheduleMethod,
HC: HttpClient,
{
pub source_client: SourceClient<HC>,
pub try_failed: bool,
shared_thread_state: SharedThreadState,
scheduler: Scheduler<SM>,
http_source_client: HttpSourceClient<HC>,
try_failed: bool,
}
impl<HC> RequestStage<HC>
impl<SM, HC> RequestStage<SM, HC>
where
HC: HTTPClient,
SM: ScheduleMethod,
HC: HttpClient,
{
pub fn new(source_client: SourceClient<HC>) -> Self {
pub fn new(
shared_thread_state: SharedThreadState,
http_source_client: HttpSourceClient<HC>,
scheduler: Scheduler<SM>,
) -> Self {
Self {
source_client,
shared_thread_state,
scheduler,
http_source_client,
try_failed: false,
}
}
}
impl<HC> Stage for RequestStage<HC>
impl<SM, HC> Stage for RequestStage<SM, HC>
where
HC: HTTPClient,
SM: ScheduleMethod,
HC: HttpClient,
{
fn run(
&mut self,
MapContext {
view_state,
style,
tile_cache,
scheduler,
shared_thread_state,
tile_repository,
..
}: &mut MapContext,
) {
@ -59,13 +68,7 @@ where
{
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(
tile_cache,
style,
shared_thread_state,
scheduler,
view_region,
);
self.try_failed = self.request_tiles_in_view(tile_repository, style, view_region);
}
}
@ -74,18 +77,17 @@ where
}
}
impl<HC> RequestStage<HC>
impl<SM, HC> RequestStage<SM, HC>
where
HC: HTTPClient,
SM: ScheduleMethod,
HC: HttpClient,
{
/// Request tiles which are currently in view.
#[tracing::instrument(skip_all)]
fn request_tiles_in_view(
&self,
tile_cache: &TileCache,
tile_repository: &TileRepository,
style: &Style,
shared_thread_state: &SharedThreadState,
scheduler: &Box<dyn ScheduleMethod>,
view_region: &ViewRegion,
) -> bool {
let mut try_failed = false;
@ -99,13 +101,7 @@ where
if coords.build_quad_key().is_some() {
// TODO: Make tesselation depend on style?
try_failed = self
.try_request_tile(
tile_cache,
shared_thread_state,
scheduler,
&coords,
&source_layers,
)
.try_request_tile(tile_repository, &coords, &source_layers)
.unwrap();
}
}
@ -114,17 +110,15 @@ where
fn try_request_tile(
&self,
tile_cache: &TileCache,
shared_thread_state: &SharedThreadState,
scheduler: &Box<dyn ScheduleMethod>,
tile_repository: &TileRepository,
coords: &WorldTileCoords,
layers: &HashSet<String>,
) -> Result<bool, Error> {
if !tile_cache.is_layers_missing(coords, layers) {
if !tile_repository.is_layers_missing(coords, layers) {
return Ok(false);
}
if let Ok(mut tile_request_state) = shared_thread_state.tile_request_state.try_lock() {
if let Ok(mut tile_request_state) = self.shared_thread_state.tile_request_state.try_lock() {
if let Some(request_id) = tile_request_state.start_tile_request(TileRequest {
coords: *coords,
layers: layers.clone(),
@ -141,26 +135,25 @@ where
);
}*/
let client = self.source_client.clone();
let client = SourceClient::Http(self.http_source_client.clone());
let coords = *coords;
scheduler
.schedule(
shared_thread_state.clone(),
Box::new(move |state: SharedThreadState| {
Box::pin(async move {
match client.fetch(&coords).await {
Ok(data) => state
.process_tile(request_id, data.into_boxed_slice())
.unwrap(),
Err(e) => {
log::error!("{:?}", &e);
state.tile_unavailable(&coords, request_id).unwrap()
}
let state = self.shared_thread_state.clone();
self.scheduler
.schedule_method()
.schedule(Box::new(move || {
Box::pin(async move {
match client.fetch(&coords).await {
Ok(data) => state
.process_tile(request_id, data.into_boxed_slice())
.unwrap(),
Err(e) => {
log::error!("{:?}", &e);
state.tile_unavailable(&coords, request_id).unwrap()
}
})
}),
)
}
})
}))
.unwrap();
}

View File

@ -33,7 +33,7 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("lightgreen").unwrap()),
line_color: Some(Color::from_str("#c8facc").unwrap()),
})),
source: None,
source_layer: Some("park".to_string()),
@ -46,7 +46,7 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("lightgreen").unwrap()),
line_color: Some(Color::from_str("#e0dfdf").unwrap()),
})),
source: None,
source_layer: Some("landuse".to_string()),
@ -59,20 +59,20 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("lightgreen").unwrap()),
line_color: Some(Color::from_str("#aedfa3").unwrap()),
})),
source: None,
source_layer: Some("landcover".to_string()),
},
StyleLayer {
index: 3,
id: "1transportation".to_string(),
id: "transportation".to_string(),
typ: "line".to_string(),
maxzoom: None,
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("violet").unwrap()),
line_color: Some(Color::from_str("#ffffff").unwrap()),
})),
source: None,
source_layer: Some("transportation".to_string()),
@ -85,7 +85,7 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("grey").unwrap()),
line_color: Some(Color::from_str("#d9d0c9").unwrap()),
})),
source: None,
source_layer: Some("building".to_string()),
@ -98,7 +98,7 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("blue").unwrap()),
line_color: Some(Color::from_str("#aad3df").unwrap()),
})),
source: None,
source_layer: Some("water".to_string()),
@ -111,7 +111,7 @@ impl Default for Style {
minzoom: None,
metadata: None,
paint: Some(LayerPaint::Line(LinePaint {
line_color: Some(Color::from_str("blue").unwrap()),
line_color: Some(Color::from_str("#aad3df").unwrap()),
})),
source: None,
source_layer: Some("waterway".to_string()),

View File

@ -4,7 +4,7 @@ use std::time::Duration;
/// Measures the frames per second.
///
/// # Example
/// ```ignore
/// ```
/// use maplibre::util::FPSMeter;
///
/// let mut meter = FPSMeter::new();

View File

@ -51,7 +51,7 @@ where
///
/// # Example
///
/// ```ignore
/// ```
/// # use maplibre::define_label;
/// define_label!(MyNewLabelTrait);
/// ```

View File

@ -1,31 +1,38 @@
//! Utilities for the window system.
use crate::{HTTPClient, MapSchedule, ScheduleMethod};
use crate::{HttpClient, InteractiveMapSchedule, ScheduleMethod};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
/// Window with an optional [carte::window::WindowSize].
/// Window of a certain [`WindowSize`]. This can either be a proper window or a headless one.
pub trait MapWindow {
type EventLoop;
type Window: raw_window_handle::HasRawWindowHandle; // FIXME: Not true for headless
type MapWindowConfig: MapWindowConfig<MapWindow = Self>;
fn create(map_window_config: &Self::MapWindowConfig) -> Self;
fn size(&self) -> WindowSize;
fn inner(&self) -> &Self::Window;
}
/// Window which references a physical `RawWindow`. This is only implemented by headed windows and
/// not by headless windows.
pub trait HeadedMapWindow: MapWindow {
type RawWindow: HasRawWindowHandle;
fn inner(&self) -> &Self::RawWindow;
}
/// A configuration for a window which determines the corresponding implementation of a
/// [`MapWindow`] and is able to create it.
pub trait MapWindowConfig: 'static {
type MapWindow: MapWindow<MapWindowConfig = Self>;
type MapWindow: MapWindow;
fn create(&self) -> Self::MapWindow;
}
pub trait Runnable<MWC, SM, HC>
/// The event loop is responsible for processing events and propagating them to the map renderer.
/// Only non-headless windows use an [`EventLoop`].
pub trait EventLoop<MWC, SM, HC>
where
MWC: MapWindowConfig,
SM: ScheduleMethod,
HC: HTTPClient,
HC: HttpClient,
{
fn run(self, map_state: MapSchedule<MWC, SM, HC>, max_frames: Option<u64>);
fn run(self, map_schedule: InteractiveMapSchedule<MWC, SM, HC>, max_frames: Option<u64>);
}
/// Window size with a width and an height in pixels.

View File

@ -35,3 +35,6 @@ wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
console_log = { version = "0.2", features = ["color"] }
tracing-wasm = { version = "0.2", optional = true } # FIXME: Low quality dependency
[dev-dependencies]
wasm-bindgen-test = "0.3"

View File

@ -28,7 +28,6 @@
"../lib": {
"name": "maplibre-rs",
"version": "0.0.1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"spectorjs": "^0.9.27",
@ -36,10 +35,9 @@
},
"devDependencies": {
"@chialab/esbuild-plugin-env": "^0.15.3",
"@chialab/esbuild-plugin-meta-url": "^0.15.15",
"@chialab/esbuild-plugin-meta-url": "^0.15.28",
"esbuild": "^0.14.38",
"esbuild-plugin-inline-worker": "^0.1.1",
"patch-package": "^6.4.7",
"ts-loader": "^9.2.8",
"typescript": "^4.5.4",
"wasm-pack": "^0.10.2"
@ -5892,10 +5890,9 @@
"version": "file:../lib",
"requires": {
"@chialab/esbuild-plugin-env": "^0.15.3",
"@chialab/esbuild-plugin-meta-url": "^0.15.15",
"@chialab/esbuild-plugin-meta-url": "^0.15.28",
"esbuild": "^0.14.38",
"esbuild-plugin-inline-worker": "^0.1.1",
"patch-package": "^6.4.7",
"spectorjs": "^0.9.27",
"ts-loader": "^9.2.8",
"typescript": "^4.5.4",

View File

@ -7,7 +7,6 @@
"": {
"name": "maplibre-rs",
"version": "0.0.1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"spectorjs": "^0.9.27",
@ -18,7 +17,6 @@
"@chialab/esbuild-plugin-meta-url": "^0.15.28",
"esbuild": "^0.14.38",
"esbuild-plugin-inline-worker": "^0.1.1",
"patch-package": "^6.4.7",
"ts-loader": "^9.2.8",
"typescript": "^4.5.4",
"wasm-pack": "^0.10.2"
@ -288,12 +286,6 @@
"license": "Apache-2.0",
"peer": true
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"node_modules/acorn": {
"version": "8.7.0",
"dev": true,
@ -315,18 +307,6 @@
"acorn": "^8"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/axios": {
"version": "0.21.4",
"dev": true,
@ -424,20 +404,6 @@
"license": "CC-BY-4.0",
"peer": true
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"dev": true,
@ -455,27 +421,6 @@
"node": ">=6.0"
}
},
"node_modules/ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"node_modules/commondir": {
"version": "1.0.1",
"dev": true,
@ -486,31 +431,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"dev": true,
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
},
"engines": {
"node": ">=4.8"
}
},
"node_modules/cross-spawn/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@ -614,15 +534,6 @@
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
@ -726,15 +637,6 @@
"node": ">=8"
}
},
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"dev": true,
@ -754,20 +656,6 @@
}
}
},
"node_modules/fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"dev": true,
@ -814,15 +702,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"dev": true,
@ -837,33 +716,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
"integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
"dev": true,
"dependencies": {
"ci-info": "^2.0.0"
},
"bin": {
"is-ci": "bin.js"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"dev": true,
@ -872,24 +724,6 @@
"node": ">=0.12.0"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"node_modules/jest-worker": {
"version": "27.5.1",
"dev": true,
@ -934,24 +768,6 @@
"license": "MIT",
"peer": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/loader-runner": {
"version": "4.3.0",
"dev": true,
@ -1055,12 +871,6 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/minipass": {
"version": "3.1.6",
"dev": true,
@ -1101,12 +911,6 @@
"license": "MIT",
"peer": true
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/node-releases": {
"version": "2.0.3",
"dev": true,
@ -1121,31 +925,6 @@
"wrappy": "1"
}
},
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"dev": true,
@ -1179,54 +958,6 @@
"node": ">=6"
}
},
"node_modules/patch-package": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz",
"integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==",
"dev": true,
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^2.4.2",
"cross-spawn": "^6.0.5",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^7.0.1",
"is-ci": "^2.0.0",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.0",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^5.6.0",
"slash": "^2.0.0",
"tmp": "^0.0.33"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/patch-package/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"dev": true,
@ -1243,15 +974,6 @@
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"dev": true,
@ -1355,36 +1077,6 @@
"randombytes": "^2.1.0"
}
},
"node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"dev": true,
"dependencies": {
"shebang-regex": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"dev": true,
@ -1408,18 +1100,6 @@
"version": "0.9.27",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
@ -1560,18 +1240,6 @@
"node": ">= 8"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"dev": true,
@ -1677,15 +1345,6 @@
"node": ">=4.2.0"
}
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"dev": true,
@ -1829,18 +1488,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"dev": true,
@ -2076,12 +1723,6 @@
"dev": true,
"peer": true
},
"@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"acorn": {
"version": "8.7.0",
"dev": true,
@ -2093,15 +1734,6 @@
"peer": true,
"requires": {}
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"axios": {
"version": "0.21.4",
"dev": true,
@ -2159,17 +1791,6 @@
"dev": true,
"peer": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"chownr": {
"version": "2.0.0",
"dev": true
@ -2179,27 +1800,6 @@
"dev": true,
"peer": true
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"commondir": {
"version": "1.0.1",
"dev": true
@ -2208,27 +1808,6 @@
"version": "0.0.1",
"dev": true
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@ -2297,12 +1876,6 @@
"dev": true,
"peer": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"eslint-scope": {
"version": "5.1.1",
"dev": true,
@ -2371,30 +1944,10 @@
"path-exists": "^4.0.0"
}
},
"find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"requires": {
"micromatch": "^4.0.2"
}
},
"follow-redirects": {
"version": "1.14.9",
"dev": true
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"dev": true,
@ -2427,12 +1980,6 @@
"version": "4.2.10",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"dev": true,
@ -2445,40 +1992,10 @@
"version": "2.0.4",
"dev": true
},
"is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
"integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
"dev": true,
"requires": {
"ci-info": "^2.0.0"
}
},
"is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true
},
"is-number": {
"version": "7.0.0",
"dev": true
},
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"requires": {
"is-docker": "^2.0.0"
}
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"jest-worker": {
"version": "27.5.1",
"dev": true,
@ -2509,24 +2026,6 @@
"dev": true,
"peer": true
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
},
"klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.11"
}
},
"loader-runner": {
"version": "4.3.0",
"dev": true,
@ -2592,12 +2091,6 @@
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"minipass": {
"version": "3.1.6",
"dev": true,
@ -2622,12 +2115,6 @@
"dev": true,
"peer": true
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-releases": {
"version": "2.0.3",
"dev": true,
@ -2640,22 +2127,6 @@
"wrappy": "1"
}
},
"open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"requires": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
"p-limit": {
"version": "2.3.0",
"dev": true,
@ -2674,44 +2145,6 @@
"version": "2.2.0",
"dev": true
},
"patch-package": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz",
"integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==",
"dev": true,
"requires": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^2.4.2",
"cross-spawn": "^6.0.5",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^7.0.1",
"is-ci": "^2.0.0",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.0",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^5.6.0",
"slash": "^2.0.0",
"tmp": "^0.0.33"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"path-exists": {
"version": "4.0.0",
"dev": true
@ -2720,12 +2153,6 @@
"version": "1.0.1",
"dev": true
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"picocolors": {
"version": "1.0.0",
"dev": true,
@ -2782,27 +2209,6 @@
"randombytes": "^2.1.0"
}
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"dev": true,
"requires": {
"shebang-regex": "^1.0.0"
}
},
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true
},
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"dev": true,
@ -2820,15 +2226,6 @@
"spectorjs": {
"version": "0.9.27"
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"tapable": {
"version": "2.2.1",
"dev": true
@ -2914,15 +2311,6 @@
}
}
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
}
},
"to-regex-range": {
"version": "5.0.1",
"dev": true,
@ -2983,12 +2371,6 @@
"version": "4.6.3",
"dev": true
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"uri-js": {
"version": "4.4.1",
"dev": true,
@ -3086,15 +2468,6 @@
"dev": true,
"peer": true
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"dev": true

View File

@ -65,3 +65,15 @@ pub async fn run(scheduler_ptr: *mut Scheduler<WebWorkerPoolScheduleMethod>) {
// std::mem::forget(scheduler);
}
#[cfg(test)]
/// See https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/browsers.html
mod tests {
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1, 1);
}
}

View File

@ -1,5 +1,5 @@
use js_sys::{ArrayBuffer, Uint8Array};
use maplibre::io::source_client::HTTPClient;
use maplibre::io::source_client::HttpClient;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
@ -61,7 +61,7 @@ impl Clone for WHATWGFetchHttpClient {
}
#[async_trait(?Send)]
impl HTTPClient for WHATWGFetchHttpClient {
impl HttpClient for WHATWGFetchHttpClient {
async fn fetch(&self, url: &str) -> Result<Vec<u8>, Error> {
self.fetch_bytes(url)
.await

View File

@ -5,9 +5,9 @@ use wasm_bindgen::prelude::*;
use maplibre::coords::TileCoords;
use maplibre::io::scheduler::Scheduler;
use maplibre::io::shared_thread_state::SharedThreadState;
use maplibre::io::TileRequestID;
use maplibre::stages::SharedThreadState;
#[wasm_bindgen]
extern "C" {

View File

@ -8,7 +8,6 @@ use web_sys::Worker;
use maplibre::error::Error;
use maplibre::io::scheduler::ScheduleMethod;
use maplibre::io::shared_thread_state::SharedThreadState;
use super::pool::WorkerPool;
@ -35,19 +34,17 @@ impl WebWorkerPoolScheduleMethod {
}
impl ScheduleMethod for WebWorkerPoolScheduleMethod {
fn schedule(
fn schedule<T>(
&self,
shared_thread_state: SharedThreadState,
future_factory: Box<
(dyn (FnOnce(SharedThreadState) -> Pin<Box<dyn Future<Output = ()> + 'static>>)
+ Send
+ 'static),
>,
) -> Result<(), Error> {
future_factory: impl (FnOnce() -> T) + Send + 'static,
) -> Result<(), Error>
where
T: Future<Output = ()> + 'static,
{
self.pool
.run(move || {
wasm_bindgen_futures::future_to_promise(async move {
future_factory(shared_thread_state).await;
future_factory().await;
Ok(JsValue::undefined())
})
})