# MapLibre Rust Documentation

# Introduction

maplibre-rs is a portable and performant vector maps renderer.

# Supported Platforms

For development the following platforms are recommended:

- Linux X11/Wayland
- MacOS
- Latest Firefox Nightly/Chrome Canary with WebGPU (Because WebGPU is a living spec, sometimes a bleeding-edge browser release is required)

## Short-term Obstacles

Platform | Obstacles
--- | ---
Linux X11 |
Linux Wayland |
Windows |
MacOS |
Android | * Unable to get window size before resume
iOS | * Touches are crashing the app on real devices * Instanced indices drawing is not supported * Drawing zero-length indices is prohibited
Firefox | * Shared Memory is currently not working because it a parallel web worker corrupts memory
Chrome |
Safari |
Mobile Firefox |
Mobile Chrome |
Mobile Safari |

## Long-term Goals

WebGPU is not enabled by default for all platforms.

WebGPU Status:

- Firefox
- Chrome
- WebKit

Platform | Linux & Android | Graphics API | Note
--- | --- | --- | ---
Linux X11 | ✅ | Vulkan |
Linux Wayland | ✅ | Vulkan |
Windows | ❓ | ❓ |
MacOS | ✅ | ❓ |
Android | ✅ | Vulkan/OpenGL ES/Angle | Not tested, but should work on all devices if Angle is used. Vulkan is not yet supported widely.
iOS | ✅ | Metal | Not tested.
Firefox | ✅ | WebGL/WebGPU |
Chrome | ✅ | WebGL/WebGPU | WebGPU is significantly faster because WASM output is smaller.
Safari | 🛠️ | WebGL/WebGPU | Safari does not yet support Shared Array Buffer
Mobile Firefox | 🆗 | WebGL/WebGPU |
Mobile Chrome | 🆗 | WebGL | WebGPU is not implemented.
Mobile Safari | 🛠️ | WebGL | WebGPU is not implemented. Safari does not yet support Shared Array Buffer

✅ = First Class Support — 🆗= Best Effort Support — 🛠️ = Unsupported, but support in progress

# Developer Log

I'm regularly releasing blog posts on my blog.

# User Guide

# Development Guide

# Building

# Debugging

- log crate

## GPU Debugging

- For WebGL there is SpectorJS is enabled by default right now. For debugging on a desktop environment you can use RenderDoc.

## Frame Profiling

- tracing crate

# Development Documents

# Architecture

## Rendering Architecture

The big picture of wgpu is as follows:

A simplified version is shown below:

Notes:

- wgpu is able to create an interface through which we can reach any device with a GPU.

## OS Architecture

Notes:

- The ability to use shared memory or the atomic instruction set of WASM comes by enabling compilation features.
- `threads` support here does not introduce threads like we know them from Linux. It introduces
- support for atomics like specified in a working draft to WebAssembly. Threads are simulated using WebWorkers by the browser.

# Design

# Caching

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.

- On the web the browser is automatically caching raw tiles.
- On Linux, MacOs, iOS and Android we are utilizing reqwest-middleware-cache, which writes raw network requests to disk.

# Stencil Masking

The following diagram shows a method which has been used in the beginning of maplibre-rs. It is not used currently.

# Font Rendering

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:

1. Tessellate Fonts
2. SDF Font Rendering
3. GPU Text Rendering directly from Bezier Curves
4. Draw text using a Web Canvas and load them to GPU

There is a thesis which summarizes some methods here. A link collection about font related projects can be viewed here.

## Approaches

### Tessellate Fonts

There is ttf2mesh which generates meshes. I was already able to generate about 1k glyphs with ~40FPS.

### SDF Font Rendering

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.

### GPU Text Rendering from Bezier Curves

The solutions exist:

- By Will Dobbie with an implementation here
- Slug Library which is patented and probably therefore not usable

Here is the whitepaper of the Slug library. There is also a poster about it. There also exists an open implementation.

### Draw text using a Web Canvas

This approach has the downside that we can not dynamically scale rendered fonts according to the current zoom level.

## Other Approaches

- 16x AA font rendering using coverage masks

# Library Packaging

# Apple

On Apple maplibre-rs is packaged as:

- Multiple .xcarchive packages which include a framework. Each for a different architecture and platform.
- A single .xcframework package which contains multiple frameworks of different architectures and platforms.
- A swift package which just references the .xcframework package and makes distributing easier.

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.

## XCode Project description

### Library Entry

xcode/maplibre-rs/maplibre.swift ADDED Viewed

| @@ -0,0 +1,5 @@
1 | + public class MapLibre {
2 | + public static func start() {
3 | + maplibre_apple_main();
4 | + }
5 | + }

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.

### Framework

{diff/maplibre-rs → xcode}/maplibre-rs.xcodeproj/project.pbxproj RENAMED Viewed

29 33 |
30 34 | /* Begin PBXFrameworksBuildPhase section */
31 | - 0BE452E128132EA6003BD2A5 /* Frameworks */ = {
35 | + 0B85D5682812903700906D21 /* Frameworks */ = {
32 36 | isa = PBXFrameworksBuildPhase;
33 37 | buildActionMask = 2147483647;
34 38 | files = (
39 | + 0B85D5A42812991100906D21 /* libmaplibre_apple.a in Frameworks */,
35 40 | );
36 41 | runOnlyForDeploymentPostprocessing = 0;
37 42 | };
38 | - 0BE452F128132EB5003BD2A5 /* Frameworks */ = {
43 | + 0B85D57B281290F800906D21 /* Frameworks */ = {
39 44 | isa = PBXFrameworksBuildPhase;
40 45 | buildActionMask = 2147483647;
41 46 | files = (
47 | + 0B85D596281291A400906D21 /* maplibre_rs.framework in Frameworks */,
42 48 | );
43 49 | runOnlyForDeploymentPostprocessing = 0;
44 50 | };
45 | - 0BE452F628132EB5003BD2A5 /* Frameworks */ = {
51 | + 0B85D580281290F800906D21 /* Frameworks */ = {
46 52 | isa = PBXFrameworksBuildPhase;
47 53 | buildActionMask = 2147483647;
48 54 | files = (
55 | + 0B85D599281291A700906D21 /* maplibre_rs.framework in Frameworks */,
49 56 | );
50 57 | runOnlyForDeploymentPostprocessing = 0;
51 58 | };
59 | /* End PBXFrameworksBuildPhase section */

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:

    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.

A file can be added to the frameworks and library link phase in XCode.

### Cargo Build Phase

{diff/maplibre-rs → xcode}/maplibre-rs.xcodeproj/project.pbxproj RENAMED Viewed

114 131 | /* Begin PBXNativeTarget section */
115 | - 0BE452E328132EA6003BD2A5 /* maplibre-rs */ = {
132 | + 0B85D56A2812903700906D21 /* maplibre-rs */ = {
116 133 | isa = PBXNativeTarget;
117 | - buildConfigurationList = 0BE452E928132EA6003BD2A5 /* Build configuration list for PBXNativeTarget "maplibre-rs" */;
134 | + buildConfigurationList = 0B85D5702812903700906D21 /* Build configuration list for PBXNativeTarget "maplibre-rs" */;
118 135 | buildPhases = (
119 | - 0BE452DF28132EA6003BD2A5 /* Headers */,
120 | - 0BE452E028132EA6003BD2A5 /* Sources */,
121 | - 0BE452E128132EA6003BD2A5 /* Frameworks */,
122 | - 0BE452E228132EA6003BD2A5 /* Resources */,
136 | + 0B85D5662812903700906D21 /* Headers */,
137 | + 0B85D5672812903700906D21 /* Sources */,
138 | + 0B85D5682812903700906D21 /* Frameworks */,
139 | + 0B85D5692812903700906D21 /* Resources */,
140 | + 0BE452D72812EFC1003BD2A5 /* Cargo Build */,
229 248 | );
230 249 | runOnlyForDeploymentPostprocessing = 0;
231 250 | };
232 251 | /* End PBXResourcesBuildPhase section */
233 252 |
253 | + /* Begin PBXShellScriptBuildPhase section */
254 | + 0BE452D72812EFC1003BD2A5 /* Cargo Build */ = {
255 | + isa = PBXShellScriptBuildPhase;
256 | + buildActionMask = 2147483647;
257 | + files = (
258 | + );
259 | + inputFileListPaths = (
260 | + );
261 | + inputPaths = (
262 | + );
263 | + name = "Cargo Build";
264 | + outputFileListPaths = (
265 | + );
266 | + outputPaths = (
267 | + );
268 | + runOnlyForDeploymentPostprocessing = 0;
269 | + shellPath = /bin/bash;

270

+ shellScript = ". \"$HOME/.cargo/env\"\n\narch=\"unknown\"\nvendor=\"apple\"\nos_type=\"unknown\"\nenvironment_type=\"\"\n\nmode=\"\"\n\necho \"ARCH: $ARCHS\"\n\nif [[ $CONFIGURATION == \"Release\" ]]\nthen\n mode=\"--release\"\nfi\n\nif [[ $ARCHS == \"x86_64\" ]]\nthen\n arch=\"x86_64\"\nelif [[ $ARCHS == \"arm64\" ]]\nthen\n arch=\"aarch64\"\nfi\n\nif [[ $SDK_NAME == *\"iphoneos\"* ]]\nthen\n os_type=\"ios\"\nelif [[ $SDK_NAME == *\"macos\"* ]]\nthen\n os_type=\"darwin\"\nelif [[ $SDK_NAME == *\"iphonesimulator\"* ]]\nthen\n os_type=\"ios\"\n environment_type=\"sim\"\nfi\n\n\ntriplet=\"$arch-$vendor-$os_type\"\n\nif [ -n \"$environment_type\" ]\nthen\n triplet=\"$triplet-$environment_type\"\nfi\n\necho \"$mode\"\necho \"$triplet\"\n\nenv -i zsh -c \"cargo build -p maplibre-apple $mode --target $triplet --lib\"\n\n";

271 | + };
272 | + /* End PBXShellScriptBuildPhase section */

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).

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`.

    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"

### Build Settings

{diff/maplibre-rs → xcode}/maplibre-rs.xcodeproj/project.pbxproj RENAMED Viewed

260 299 | /* End PBXSourcesBuildPhase section */
275 | - 0BE452EA28132EA6003BD2A5 /* Debug */ = {
314 | + 0B85D5712812903700906D21 /* Debug */ = {
276 315 | isa = XCBuildConfiguration;
277 316 | buildSettings = {
317 | + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
278 318 | ALWAYS_SEARCH_USER_PATHS = NO;
319 | + BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
279 320 | CLANG_ANALYZER_NONNULL = YES;
280 321 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
281 322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
| @@ -304,11 +345,13 @@
304 345 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
305 346 | CLANG_WARN_UNREACHABLE_CODE = YES;
306 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
307 | - CODE_SIGN_STYLE = Automatic;
348 | + CODE_SIGN_IDENTITY = "Apple Development";
349 | + CODE_SIGN_STYLE = Manual;
308 350 | COPY_PHASE_STRIP = NO;
309 351 | CURRENT_PROJECT_VERSION = 1;
310 352 | DEBUG_INFORMATION_FORMAT = dwarf;
311 353 | DEFINES_MODULE = YES;
354 | + DEVELOPMENT_TEAM = "";
312 355 | DYLIB_COMPATIBILITY_VERSION = 1;
313 356 | DYLIB_CURRENT_VERSION = 1;
314 357 | DYLIB_INSTALL_NAME_BASE = "@rpath";
| @@ -337,14 +380,28 @@
337 380 | "@executable_path/Frameworks",
338 381 | "@loader_path/Frameworks",
339 382 | );
383 | + LIBRARY_SEARCH_PATHS = (
384 | + "$(inherited)",
385 | + "$(PROJECT_DIR)",
386 | + );
387 | + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-ios/debug";
388 | + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=x86_64]" = "$(PROJECT_DIR)/libs/x86_64-apple-ios/debug";
389 | + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-ios-sim/debug";
390 | + "LIBRARY_SEARCH_PATHS[sdk=macosx*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-darwin/debug";
391 | + "LIBRARY_SEARCH_PATHS[sdk=macosx*][arch=x86_64]" = "$(PROJECT_DIR)/libs/x86_64-apple-darwin/debug";
392 | + MACH_O_TYPE = staticlib;
340 393 | MARKETING_VERSION = 1.0;
341 394 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
342 395 | MTL_FAST_MATH = YES;
343 396 | ONLY_ACTIVE_ARCH = YES;
344 397 | PRODUCT_BUNDLE_IDENTIFIER = "org.maplibre.maplibre-rs";
345 398 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
399 | + PROVISIONING_PROFILE_SPECIFIER = "";
400 | + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
346 401 | SDKROOT = iphoneos;
347 | - SKIP_INSTALL = YES;
402 | + SKIP_INSTALL = NO;
403 | + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
404 | + SUPPORTS_MACCATALYST = YES;
348 405 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
349 406 | SWIFT_EMIT_LOC_STRINGS = YES;
350 407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
| @@ -355,10 +412,12 @@
355 412 | };
356 413 | name = Debug;
357 414 | };
358 | - 0BE452EB28132EA6003BD2A5 /* Release */ = {
415 | + 0B85D5722812903700906D21 /* Release */ = {
359 416 | isa = XCBuildConfiguration;
360 417 | buildSettings = {
418 | + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
361 419 | ALWAYS_SEARCH_USER_PATHS = NO;
420 | + BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
362 421 | CLANG_ANALYZER_NONNULL = YES;
363 422 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
364 423 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
| @@ -387,11 +446,13 @@
387 446 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
388 447 | CLANG_WARN_UNREACHABLE_CODE = YES;
389 448 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
390 | - CODE_SIGN_STYLE = Automatic;
449 | + CODE_SIGN_IDENTITY = "Apple Development";
450 | + CODE_SIGN_STYLE = Manual;
391 451 | COPY_PHASE_STRIP = NO;
392 452 | CURRENT_PROJECT_VERSION = 1;
393 453 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
394 454 | DEFINES_MODULE = YES;
455 | + DEVELOPMENT_TEAM = "";
395 456 | DYLIB_COMPATIBILITY_VERSION = 1;
396 457 | DYLIB_CURRENT_VERSION = 1;
397 458 | DYLIB_INSTALL_NAME_BASE = "@rpath";
| @@ -414,13 +475,28 @@
414 475 | "@executable_path/Frameworks",
415 476 | "@loader_path/Frameworks",
416 477 | );
478 | + LIBRARY_SEARCH_PATHS = (
479 | + "$(inherited)",
480 | + "$(PROJECT_DIR)",
481 | + );
482 | + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-ios/release";
483 | + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=x86_64]" = "$(PROJECT_DIR)/libs/x86_64-apple-ios/release";
484 | + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-ios-sim/release";
485 | + "LIBRARY_SEARCH_PATHS[sdk=macosx*][arch=arm64]" = "$(PROJECT_DIR)/libs/aarch64-apple-darwin/release";
486 | + "LIBRARY_SEARCH_PATHS[sdk=macosx*][arch=x86_64]" = "$(PROJECT_DIR)/libs/x86_64-apple-darwin/release";
487 | + MACH_O_TYPE = staticlib;
417 488 | MARKETING_VERSION = 1.0;
418 489 | MTL_ENABLE_DEBUG_INFO = NO;
419 490 | MTL_FAST_MATH = YES;
491 | + ONLY_ACTIVE_ARCH = YES;
420 492 | PRODUCT_BUNDLE_IDENTIFIER = "org.maplibre.maplibre-rs";
421 493 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
494 | + PROVISIONING_PROFILE_SPECIFIER = "";
495 | + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
422 496 | SDKROOT = iphoneos;
423 | - SKIP_INSTALL = YES;
497 | + SKIP_INSTALL = NO;
498 | + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
499 | + SUPPORTS_MACCATALYST = YES;
424 500 | SWIFT_COMPILATION_MODE = wholemodule;
425 501 | SWIFT_EMIT_LOC_STRINGS = YES;
426 502 | SWIFT_OPTIMIZATION_LEVEL = "-O";
| @@ -432,7 +508,7 @@
432 508 | };
433 509 | name = Release;
510 | };

Explanations for the settings:

- `BUILD_LIBRARY_FOR_DISTRIBUTION`: Define that this is a library (effect unknown to me)
- `CODE_SIGN_STYLE`: The framework is not signed
- `DEVELOPMENT_TEAM`: No development team is set
- `LIBRARY_SEARCH_PATHS[sdk=x][arch=y]`: We set the path for the `libmaplibre_apple.a` lies
- `MACH_O_TYPE` / `SKIP_INSTALL`: If this is not set to `staticlib` and `NO`, then the `libmaplibre_apple.a` binary is not included in the final framework xcarchive.
- `SUPPORTED_PLATFORMS`: Explicitly says that this library works on any platform.
- `SUPPORTS_MACCATALYST`: Explicitly says that this library works on Mac Catalyst.

The same settings are done for Release and Debug.

## xcframework packaging

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:

- macOS-arm64 and macOS-x86_64

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:

1. Create a fat binary using `lipo -create binA binB -output binfat`
2. Copy for example the arm64 framework and add the `.swiftmodule` definitions from the x86_64 framework

## Single UIApplication

Right now `winit` only allows the usage of a `UIApplication`. This means the application needs to run in fullscreen. Tracking Issue

## Example App

The following settings are important for the example application within the XCode project.

### Info Plist for Applications

{diff/maplibre-rs → xcode}/maplibre-rs.xcodeproj/project.pbxproj RENAMED Viewed

435 | - 0BE4530328132EB5003BD2A5 /* Debug */ = {
511 | + 0B85D58D281290F800906D21 /* Debug */ = {
436 512 | isa = XCBuildConfiguration;
437 513 | buildSettings = {
438 514 | ALWAYS_SEARCH_USER_PATHS = NO;
| @@ -489,7 +565,7 @@
489 565 | GCC_WARN_UNUSED_FUNCTION = YES;
490 566 | GCC_WARN_UNUSED_VARIABLE = YES;
491 567 | GENERATE_INFOPLIST_FILE = YES;
492 | - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
568 | + INFOPLIST_FILE = "example--iOS--Info.plist";
493 569 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
494 570 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
495 571 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
| @@ -514,7 +590,7 @@
514 590 | };
515 591 | name = Debug;
516 592 | };
517 | - 0BE4530428132EB5003BD2A5 /* Release */ = {
593 | + 0B85D58E281290F800906D21 /* Release */ = {
518 594 | isa = XCBuildConfiguration;
519 595 | buildSettings = {
520 596 | ALWAYS_SEARCH_USER_PATHS = NO;
| @@ -565,7 +641,7 @@
565 641 | GCC_WARN_UNUSED_FUNCTION = YES;
566 642 | GCC_WARN_UNUSED_VARIABLE = YES;
567 643 | GENERATE_INFOPLIST_FILE = YES;
568 | - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
644 | + INFOPLIST_FILE = "example--iOS--Info.plist";
569 645 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
570 646 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
571 647 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
| @@ -590,7 +666,7 @@

- The `INFOPLIST_KEY_UIApplicationSceneManifest_Generation` needs to be unset. Else the application screen is just black.

### Files & Assets

{diff/maplibre-rs → xcode}/maplibre-rs.xcodeproj/project.pbxproj RENAMED Viewed

| @@ -7,93 +7,110 @@
7 7 | objects = {
8 8 |
9 9 | /* Begin PBXBuildFile section */
10 | - 0BE452E828132EA6003BD2A5 /* maplibre_rs.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BE452E728132EA6003BD2A5 /* maplibre_rs.h */; settings = {ATTRIBUTES = (Public, ); }; };
11 | - 0BE452FC28132EB5003BD2A5 /* exampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE452ED28132EB5003BD2A5 /* exampleApp.swift */; };
12 | - 0BE452FD28132EB5003BD2A5 /* exampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE452ED28132EB5003BD2A5 /* exampleApp.swift */; };
13 | - 0BE452FE28132EB5003BD2A5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE452EE28132EB5003BD2A5 /* ContentView.swift */; };
14 | - 0BE452FF28132EB5003BD2A5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE452EE28132EB5003BD2A5 /* ContentView.swift */; };
15 | - 0BE4530028132EB5003BD2A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BE452EF28132EB5003BD2A5 /* Assets.xcassets */; };
16 | - 0BE4530128132EB5003BD2A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BE452EF28132EB5003BD2A5 /* Assets.xcassets */; };
10 | + 0B85D56F2812903700906D21 /* maplibre_rs.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B85D56E2812903700906D21 /* maplibre_rs.h */; settings = {ATTRIBUTES = (Public, ); }; };
11 | + 0B85D586281290F800906D21 /* exampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B85D577281290F800906D21 /* exampleApp.swift */; };
12 | + 0B85D587281290F800906D21 /* exampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B85D577281290F800906D21 /* exampleApp.swift */; };
13 | + 0B85D58A281290F800906D21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B85D579281290F800906D21 /* Assets.xcassets */; };
14 | + 0B85D58B281290F800906D21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B85D579281290F800906D21 /* Assets.xcassets */; };
15 | + 0B85D5952812913700906D21 /* maplibre.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B85D5942812913700906D21 /* maplibre.swift */; };
16 | + 0B85D596281291A400906D21 /* maplibre_rs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B85D56B2812903700906D21 /* maplibre_rs.framework */; };
17 | + 0B85D599281291A700906D21 /* maplibre_rs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B85D56B2812903700906D21 /* maplibre_rs.framework */; };
18 | + 0B85D5A42812991100906D21 /* libmaplibre_apple.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B85D5A32812987B00906D21 /* libmaplibre_apple.a */; };
17 19 | /* End PBXBuildFile section */
18 20 |
19 21 | /* Begin PBXFileReference section */
20 | - 0BE452E428132EA6003BD2A5 /* maplibre_rs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = maplibre_rs.framework; sourceTree = BUILT_PRODUCTS_DIR; };
21 | - 0BE452E728132EA6003BD2A5 /* maplibre_rs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = maplibre_rs.h; sourceTree = "<group>"; };
22 | - 0BE452ED28132EB5003BD2A5 /* exampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = exampleApp.swift; sourceTree = "<group>"; };
23 | - 0BE452EE28132EB5003BD2A5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
24 | - 0BE452EF28132EB5003BD2A5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
25 | - 0BE452F428132EB5003BD2A5 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
26 | - 0BE452F928132EB5003BD2A5 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
27 | - 0BE452FB28132EB5003BD2A5 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = "<group>"; };
22 | + 0B85D56B2812903700906D21 /* maplibre_rs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = maplibre_rs.framework; sourceTree = BUILT_PRODUCTS_DIR; };
23 | + 0B85D56E2812903700906D21 /* maplibre_rs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = maplibre_rs.h; sourceTree = "<group>"; };
24 | + 0B85D577281290F800906D21 /* exampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = exampleApp.swift; sourceTree = "<group>"; };
25 | + 0B85D579281290F800906D21 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
26 | + 0B85D57E281290F800906D21 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
27 | + 0B85D583281290F800906D21 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
28 | + 0B85D585281290F800906D21 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = "<group>"; };
29 | + 0B85D5942812913700906D21 /* maplibre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = maplibre.swift; sourceTree = "<group>"; };
30 | + 0B85D5A32812987B00906D21 /* libmaplibre_apple.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libmaplibre_apple.a; sourceTree = SOURCE_ROOT; };
31 | + 0BE452D62812EEA8003BD2A5 /* example--iOS--Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "example--iOS--Info.plist"; sourceTree = "<group>"; };
32 | /* End PBXFileReference section */

- The example/demo application within the XCode project references the `maplibre_rs.framework`. Some default files have been removed.

### MacOS Entitlements

xcode/macOS/macOS.entitlements ADDED Viewed

| @@ -0,0 +1,12 @@
1 | + <?xml version="1.0" encoding="UTF-8"?>
2 | + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 | + <plist version="1.0">
4 | + <dict>
5 | + <key>com.apple.security.app-sandbox</key>
6 | + <true/>
7 | + <key>com.apple.security.files.user-selected.read-only</key>
8 | + <true/>
9 | + <key>com.apple.security.network.client</key>
10 | + <true/>
11 | + </dict>
12 | + </plist>

- On macOS one needs to allow network access via `com.apple.security.network.client`

# Android

## Gradle Project Setup

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.

## JNI

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.

## Single NativeActivity

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

# Web

This document describes issues and challenges when packaging maplibre-rs as a npm package.

## Required Formats

### ESM

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);

### IIFE (immediately-invoked function expression)

> 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.

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

> 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.

### CJS/CommonJS

> 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.

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 output

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.

## Required Features

- WASM Bundling: Make the WASM binary available to users of the maplibre-rs library
- WebWorker Bundling: Make the WebWorker available to users of the maplibre-rs library. This could also be achived by inlining.
- WebWorker Inlining: Inline the WebWorker bundle in the library bundle as a string.
- Predictable Paths: Without predictable paths, it's difficult for users to reference the wasm file directly from the `node_modules` directory if requried.

## Bundler Feature Comparison

Bundler | ESM | IIFE | CJS | UMD | WebWorker Inlining | Web Worker Bundling | WASM Bundling | Predictable Paths | Inlining Environment Variables
--- | --- | --- | --- | --- | --- | --- | --- | --- | ---
Babel 1) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅
TypeScript 1) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌
Webpack | ❌ 4) | ❓ | ❌ | ❓ | ❌ 2) | ✅ | ✅ | ❓ | ✅
Parcel | ✅ | ❌ | ✅ | ❌ | 🛠️ 3) | ✅ | ✅ | ❌ 5) | ✅
ESBuild | ✅ | ✅ | ✅ | ❌ | ✅ 6) | ❓ | ✅ 6) | ✅ | ✅
Rollup | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ✅

Features in italics are required for maplibre-rs.

> 1. Technically not a bundler but can be used to emit ES modules
> 2. Was Supported in Webpack 4, but currently is not supported
> 3. https://github.com/parcel-bundler/parcel/issues/8004
> 4. As of the time of writing Webpack can not output ESM libraries
> 5. Plugins exist, but they don't work reliably
> 6. Plugins exist, and work reliably

### ESBuild

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.

- IIFI: The esbuild bundler translates to `new URL('index_bg.wasm', import.meta.url);` to

      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.

### Babel & TypeScript

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.

### WebPack

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.

- CJS: Webpack translates `new URL('index_bg.wasm', import.meta.url);` to something that is equivalent to `'./index_bg.wasm'` . It just expects that assets are resolvable from the current file.

Example scripts for `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

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.

- CJS: The Parcel bundler translates to `new URL('index_bg.wasm', import.meta.url);` to `new URL("index_bg.wasm", "file:" + __filename);` While depending on `file:` and `filename` works for NodeJS, it is unsupported in the browser.

Example scripts for `package.json`:

    {
      "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"
        ]
      }
    }

### Rollup

Not yet evaluated

# Appendix

## Goals

### Next Major Goals

- Improve buffer_pool eviction rules
- Use MPI: https://doc.rust-lang.org/book/ch16-02-message-passing.html
- Input-handling via events and functional pipelines
- Show old tiles until new tile is ready / Show mixed tiles, based on availability
- Use a simple style definition
  - type: background/fill/line
  - minzoom/maxzoom
  - source
  - source-layer
  - paint (fill-color)
- Map feeling:
  - Wrap world around in x direction
  - Limit panning in y direction
  - Nicer default map style

### Intermediate Goals

- Support multiple projections? PoC such that we are sure the renderer is acceptable

### Future Goals

- Very simple text rendering
- Cache tessellation results
  - We have three "caches": downloaded tiles, tessellated tiles, gpu tiles
- Handle missing tiles
- Support different tile raster addressing

## Future Ideas

- Use rust-gpu as shading language
- Focus on accessibility of maps: https://www.w3.org/WAI/RD/wiki/Accessible_Maps
- Display in AR: https://developer.apple.com/documentation/arkit/displaying_an_ar_experience_with_metal
- Use tracing framework: tracing

## Challenges:

- Accuracy of floating point numbers is very bad for big world view coordinates (Plot)
- Perils of World Space

### Create paths for tessellating streets

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.

# Related Resources

## Talks

- 2022-04-13-World-in-Vectors

## Related Projects

- Tangram Renderer
- Tilezen/Nextzen
- Protomaps: New Maps Stack
- Harp GL

## GIS

- Google Maps Projection
- Grid Calculation Examples
- Slippy map tilenames (also known as XYZ)
- TMS
- Mapbox Adaptive Projections
- Bing Map Tile System
- Bing: Understanding Scale and Resolution

## WebAssembly and WebWorkers

Specs:

- Thread/Atomics Proposal for WASM

Projects:

- Experiment with shared memory and the idea behind it
- Shared channel
- Bridge for async executors
- Rayon for WebAssembly
- wasm-mt: postMessage message passing

Articles:

- WebAssembly Threads (official)
- Multithreading Rust and Wasm 2018
- postMessage Performance
- A practical guide to WebAssembly memory

Examples:

- WASM in a WebWorker
- Building for Shared Memory
- Parallel Raytracing

## Rendering

Specs:

- WebGPU Spec
- WGSL Spec
- WGSL Struct Alignment
- Mismatches Stencil Test

Articles:

- Life of a Tile (MapLibre)

Tutorials:

- Stencil Testing
- Camera
- Writing an efficient Vulkan renderer

Examples:

- Stencil Mask Example
- WGPU Examples

## Maths

Articles:

- Magnificent Matrix

Examples:

- Dolly Camera

## Font Rendering

Specs:

- MapBox Glyphs Spec

Articles:

- Signed distance function

Projects:

- Mapbox fontnik
- TinySDK (JS)
- RustType
- SDF Render
- pbf_font_tools
- Multi-channel signed distance field generator
  - MSDF font atlas generator

## Tessellation

Projects:

- earcutr
- Line Tessellation in MapLibre

## Specifications

- TileJSON

## Render Graphs

- https://github.com/metal-by-example/simple-instancing/blob/master/MetalSimpleInstancing/Renderer.swift
- https://github.com/troughton/Substrate
- https://de.slideshare.net/DICEStudio/framegraph-extensible-rendering-architecture-in-frostbite
- http://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/
- http://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/

## Animation

- https://crates.io/crates/pareen
- https://crates.io/crates/keyframe