maplibre-rs is a portable and performant vector maps renderer.
For development the following platforms are recommended:
WebGPU is not enabled by default for all platforms.
WebGPU Status:
✅ = First Class Support — 🆗= Best Effort Support — 🛠️ = Unsupported, but support in progress
I'm regularly releasing blog posts on my blog.
The big picture of wgpu is as follows:
A simplified version is shown below:
Notes:
threads
The caching for maplibre-rs is handled on the networking layer. This means that data which is fetched over slow IO is cached in the format of the network requests. The maplibre-rs library is not introducing a separate serialization format for caching.
Instead, caching functionality of HTTP client libraries of the web platform are used. This has the advantage that we can honor HTTP headers which configure caching. This is very important for fetched tiles, as they can have an expiry date.
The following diagram shows a method which has been used in the beginning of maplibre-rs. It is not used currently.
There exists no universally perfect solution to font rendering. Depending on the runtime environment a method needs to be chosen. This StackOverflow post outlines some state-of-the-art methods. Some more approaches are described here.
From my perspective the following approaches could work potentially:
There is a thesis which summarizes some methods here. A link collection about font related projects can be viewed here.
There is ttf2mesh which generates meshes. I was already able to generate about 1k glyphs with ~40FPS.
There is a blogpost by Mapbox here. Some more implementation documents are available here. A good foundation for SDF fonts was created by Chlumsky with msdfgen.
The solutions exist:
Here is the whitepaper of the Slug library. There is also a poster about it. There also exists an open implementation.
This approach has the downside that we can not dynamically scale rendered fonts according to the current zoom level.
On Apple maplibre-rs is packaged as:
The following diffs are extracted from this diff. They should serve as documentation for the XCode project. This is required because XCode is a mess.
The swift code above is the main entry for the Swift API. From this entry file we can expose more API of maplibre-rs. Any C functions which are referenced in the XCode framework's header are available automatically in Swift.
The framework needs to link against the static library libmaplibre_apple.a, which has been generated by Cargo. In order to allow XCode to dynamically select the library based on the Library Search Path (Build Settings) one needs to add a relative file to XCode. The entry in the project.pbxproj should look like that:
libmaplibre_apple.a
Library Search Path
project.pbxproj
B085D5A32812987B00906D21 /* libmaplibre_apple.a */ = { isa = PBXFileReference; lastKnownFileType = archive.ar; path = libmaplibre_apple.a; sourceTree = SOURCE_ROOT; };
Note the path = libmaplibre_apple.a. This path does not link to a concrete file, but to a file which can be found during building.
path = libmaplibre_apple.a
A file can be added to the frameworks and library link phase in XCode.
In order to trigger Cargo builds when starting a XCode build we include a Cargo Build script. This build script needs to run before the linking phase (drag and drop it to the top).
Cargo Build
The following build script builds based on XCode environment variables the correct static library. We depend on the $ARCHS environment variable, as the others seem unreliable. Note that this can include multiple architectures, unless the build setting ONLY_ACTIVE_ARCH is set to YES.
$ARCHS
ONLY_ACTIVE_ARCH
YES
arch="unknown" vendor="apple" os_type="unknown" environment_type="" mode="" echo "ARCH: $ARCHS" if [[ $CONFIGURATION == "Release" ]] then mode="--release" fi if [[ $ARCHS == "x86_64" ]] then arch="x86_64" elif [[ $ARCHS == "arm64" ]] then arch="aarch64" fi if [[ $SDK_NAME == *"iphoneos"* ]] then os_type="ios" elif [[ $SDK_NAME == *"macos"* ]] then os_type="darwin" elif [[ $SDK_NAME == *"iphonesimulator"* ]] then os_type="ios" if [[ $ARCHS == "arm64" ]] then environment_type="sim" fi fi triplet="$arch-$vendor-$os_type" if [ -n "$environment_type" ] then triplet="$triplet-$environment_type" fi echo "Mode: $mode" echo "Triplet: $triplet" echo "Shell: $SHELL" cmd="export HOME=$HOME && . $HOME/.cargo/env && cargo build -p apple $mode --target $triplet --lib" echo "Command: $cmd" env -i /bin/bash -c "$cmd"
Explanations for the settings:
BUILD_LIBRARY_FOR_DISTRIBUTION
CODE_SIGN_STYLE
DEVELOPMENT_TEAM
LIBRARY_SEARCH_PATHS[sdk=x][arch=y]
MACH_O_TYPE
SKIP_INSTALL
staticlib
NO
SUPPORTED_PLATFORMS
SUPPORTS_MACCATALYST
The same settings are done for Release and Debug.
Creating a xcframework is usually quite straight forward. Just execute the following:
xargs xcodebuild -create-xcframework -framework ./a -framework ./b -output out.xcframework
Unfortunately, it is not possible to bundle some frameworks together like:
In order to package these architectures and platforms together a fat binary needs to be created using the lipo tool. This means from two frameworks we create a unified framework with a fat binary. There are two important steps:
lipo
lipo -create binA binB -output binfat
.swiftmodule
Right now winit only allows the usage of a UIApplication. This means the application needs to run in fullscreen. Tracking Issue
winit
UIApplication
The following settings are important for the example application within the XCode project.
INFOPLIST_KEY_UIApplicationSceneManifest_Generation
maplibre_rs.framework
com.apple.security.network.client
In order to package an Android .aar archive we use the rust-android-gradle. Except some customisations for the latest NDK toolchain release everything worked flawlessly.
.aar
There is no way right now to automatically generate JNI stubs for Rust. A manual example is available in the android crate of maplibre-rs.
Right now winit only allows the usage of a NativeActivity. This means the application needs to run in fullscreen. This native activity is referenced in the ´AndroidManifest.xml` by defining the name of the shared library. Tracking Issue
NativeActivity
This document describes issues and challenges when packaging maplibre-rs as a npm package.
The ESM module format is the standard nowadays which should be followed. If a JS bundler encounters an ESM module it can resolve WebAssembly files or WebWorkers dynamically. The following syntax is used to resolve referenced WebWorkers:
new Worker(new URL("./pool.worker.ts", import.meta.url), { type: 'module' });
Similarly, the following works:
new URL('index_bg.wasm', import.meta.url);
This format is used when including maplibre-rs in a <script> tag. The library is "written" onto the window/global object. This allows quick prototyping/playgrounds/experiments using maplibre-rs.
<script>
In order to support this we need to create a bundle which works on any modern browser. Additionally, a WASM file and WebWorker needs to be deployed at a predictable path, because there is no bundler active which manages assets. Users of these libraries have to specify where WASM or non-inlined WebWorkers are located.
Both assets could be inlined theoretically. This is common for WebWorkers, but not for WASM files.
UMD modules are needed when creating a library which should run in Node as well as browsers. This is not a usecase for maplibre-rs. If we support node, then we probably would ship a separate package called "maplibre-rs-node" which bundles to CJS directly.
Not needed for the browser build of maplibre-rs, possibly needed when supporting Node
With a CommonJS module its is not possible for bundlers to dynamically resolve WebWorkers or WASM files.
The import.meta.url token can not exist in a CommonJS module. Therefore, bundlers which encounter a CommonJS module have to use a different mechanism of resolving files.
import.meta.url
Generally, we do not need to support CommonJS, because we are not targeting Node with maplibre-rs. It's properly good to support it as a fallback though, for bundlers which can not deal with ESM modules yet. This is for example true for test runners like Jest which require that dependencies are available as CJS module.
wasm-pack can output multiple formats. The web and bundler outputs offer the most modular modules. Unfortunately, the function wasm_bindgen::module() is only supported in web and no-modules. We currently are using this in order to send loaded instances of WebAssembly.Module to WebWorkers. nodejs should not be used because MapLibre does not target Node. Therefore, we should stick to the web output format.
web
bundler
no-modules
WebAssembly.Module
nodejs
node_modules
Features in italics are required for maplibre-rs.
Technically not a bundler but can be used to emit ES modules Was Supported in Webpack 4, but currently is not supported https://github.com/parcel-bundler/parcel/issues/8004 As of the time of writing Webpack can not output ESM libraries Plugins exist, but they don't work reliably Plugins exist, and work reliably
ESBuild supports CJS, ESM and IIFI modules equally well. Plugins exist for WebWorker inlining and resolving assets through import.meta.url. The plugin quality seems to be superior compared to Parcel. It is also very fast compared to all other bundlers.
var __currentScriptUrl__ = document.currentScript && document.currentScript.src || document.baseURI; new URL("./assets/index_bg.wasm?emit=file", __currentScriptUrl__);
See config in web/lib/build.mjs for an example usage.
web/lib/build.mjs
Babel and TypeScript both can produce ESM modules, but they fail with transforming references within the source code like new URL("./pool.worker.ts", import.meta.url). There exist some Babel plugins, but none of them is stable. Therefore, we actually need a proper bundler which supports outputting ESM modules. The only stable solution to this is Parcel. Parcel also has good documentation around the bundling of WebWorkers.
new URL("./pool.worker.ts", import.meta.url)
WebPack supports older module formats like CommonJS or UMD very well. It falls short when bundling the format ESM format which is not yet stable. It also does not support inlining WebWorkers in version 5. The wasm-pack plugin for WebPack makes including Cargo projects easy.
'./index_bg.wasm'
Example scripts for package.json:
package.json
{ "scripts": { "webpack": "webpack --mode=development", "webpack-webgl": "npm run build -- --env webgl", "webpack-production": "webpack --mode=production", "webpack-webgl-production": "npm run production-build -- --env webgl" } }
Example config:
const path = require("path"); const webpack = require("webpack"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); let dist = path.join(__dirname, 'dist/maplibre-rs'); module.exports = (env) => ({ mode: "development", entry: "./src/index.ts", experiments: { syncWebAssembly: true, }, performance: { maxEntrypointSize: 400000, maxAssetSize: 400000000, }, output: { path: dist, filename: "maplibre-rs.js", library: { name: 'maplibre_rs', type: 'umd', }, }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader', options: {} } ] }, ], }, resolve: { extensions: ['.ts', '.js'], }, plugins: [ new webpack.DefinePlugin({ 'process.env.WEBGL': !!env.webgl }), new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, '../'), // Check https://rustwasm.github.io/wasm-pack/book/commands/build.html for // the available set of arguments. // // Optional space delimited arguments to appear before the wasm-pack // command. Default arguments are `--verbose`. //args: '--log-level warn', // Default arguments are `--typescript --target browser --mode normal`. extraArgs: ` --target web -- . -Z build-std=std,panic_abort ${env.webgl ? '--features web-webgl' : ''} ${env.tracing ? '--features trace' : ''}`, // Optional array of absolute paths to directories, changes to which // will trigger the build. // watchDirectories: [ // path.resolve(__dirname, "another-crate/src") // ], // The same as the `--out-dir` option for `wasm-pack` outDir: path.resolve(__dirname, 'src/wasm-pack'), // The same as the `--out-name` option for `wasm-pack` // outName: "index", // If defined, `forceWatch` will force activate/deactivate watch mode for // `.rs` files. // // The default (not set) aligns watch mode for `.rs` files to Webpack's // watch mode. // forceWatch: true, // If defined, `forceMode` will force the compilation mode for `wasm-pack` // // Possible values are `development` and `production`. // // the mode `development` makes `wasm-pack` build in `debug` mode. // the mode `production` makes `wasm-pack` build in `release` mode. // forceMode: "production", // Controls plugin output verbosity, either 'info' or 'error'. // Defaults to 'info'. // pluginLogLevel: 'info' }), ] });
Parcel supports CommonJS and ESM modules equally good. The documentation about import.meta.url is very good. In other bundlers documentations around this feature is missing. In the latest Parcel version inlining WebWorkers is not working.
new URL("index_bg.wasm", "file:" + __filename);
file:
filename
{ "scripts": { "parcel": "npm run clean && npm run wasm-pack && WEBGL=false parcel build --no-cache src/index.ts", "parcel-webgl": "npm run clean && FEATURES=web-webgl npm run wasm-pack && WEBGL=true parcel build --no-cache src/index.ts" } }
Example config in `package.json:
{ "module": "dist/parcel-esm/module.js", "main": "dist/parcel-cjs/main.js", "types": "dist/parcel/types.d.ts", "targets": { "main": { "distDir": "./dist/parcel-cjs", "context": "browser", "outputFormat": "commonjs" }, "module": { "distDir": "./dist/parcel-esm", "context": "browser", "outputFormat": "esmodule" } }, "@parcel/transformer-js": { "inlineFS": false, "inlineEnvironment": [ "WEBGL" ] } }
Not yet evaluated
Streets can have unusual shaped like shown here in Munich. OSM does not offer such data and therefore just renders an ordinary street contour like shown here. Because the data is probably not available this is a very hard challenge.
Specs:
Projects:
Articles:
Examples:
Tutorials: