Support for WebGPU backend in iOS SDK, core (#4103)

This commit is contained in:
Birk Skyum
2026-02-28 23:25:19 +01:00
committed by GitHub
parent 57a14cf110
commit 10453c1ec4
23 changed files with 1366 additions and 72 deletions

View File

@@ -243,6 +243,48 @@ jobs:
- name: Build mbgl-core
run: cmake --build build-ios --target mbgl-core ios-sdk-static app
ios-build-webgpu-dawn:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
runs-on: macos-15
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
with:
submodules: recursive
persist-credentials: false
- name: Configure build with CMake
run: cmake --preset ios-webgpu-dawn
- name: Build mbgl-core and iOS SDK
continue-on-error: true
run: cmake --build build-ios-webgpu-dawn --target mbgl-core ios-sdk-static
ios-build-webgpu-wgpu:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
runs-on: macos-15
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
with:
submodules: recursive
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable
with:
toolchain: stable
- name: Add iOS Rust target
run: rustup target add aarch64-apple-ios
- name: Configure build with CMake
run: cmake --preset ios-webgpu-wgpu
- name: Build mbgl-core and iOS SDK
continue-on-error: true
run: cmake --build build-ios-webgpu-wgpu --target mbgl-core ios-sdk-static
ios-ci-result:
runs-on: ubuntu-latest
if: needs.pre_job.outputs.should_skip != 'true' && always()
@@ -250,6 +292,8 @@ jobs:
- pre_job
- ios-build
- ios-build-cmake
- ios-build-webgpu-dawn
- ios-build-webgpu-wgpu
steps:
- name: Mark result as failed
if: needs.ios-build.result != 'success' || needs.ios-build-cmake.result != 'success'

View File

@@ -1206,7 +1206,15 @@ if(TARGET mbgl-vendor-wgpu)
list(APPEND EXPORT_TARGETS mbgl-vendor-wgpu)
endif()
export(TARGETS ${EXPORT_TARGETS} FILE MapboxCoreTargets.cmake)
# The Xcode generator with Dawn component targets (no monolithic library) breaks export()
# because dawn_native/dawn_proc aren't in an export set. Skip export in that case.
set(_can_export TRUE)
if(TARGET mbgl-vendor-dawn AND NOT TARGET dawn::webgpu_dawn)
set(_can_export FALSE)
endif()
if(_can_export)
export(TARGETS ${EXPORT_TARGETS} FILE MapboxCoreTargets.cmake)
endif()
if(MLN_WITH_VULKAN)
include(${PROJECT_SOURCE_DIR}/vendor/vulkan.cmake)

View File

@@ -32,6 +32,26 @@
"MLN_WITH_METAL": "ON"
}
},
{
"name": "ios-webgpu-dawn",
"displayName": "iOS WebGPU (Dawn)",
"inherits": "ios",
"binaryDir": "${sourceDir}/build-ios-webgpu-dawn",
"cacheVariables": {
"MLN_WITH_WEBGPU": "ON",
"MLN_WEBGPU_IMPL_DAWN": "ON"
}
},
{
"name": "ios-webgpu-wgpu",
"displayName": "iOS WebGPU (wgpu-native)",
"inherits": "ios",
"binaryDir": "${sourceDir}/build-ios-webgpu-wgpu",
"cacheVariables": {
"MLN_WITH_WEBGPU": "ON",
"MLN_WEBGPU_IMPL_WGPU": "ON"
}
},
{
"name": "macos",
"hidden": true,

View File

@@ -15,6 +15,8 @@
- [Documentation](./platforms/ios/ios-documentation.md)
- [Release](./platforms/ios/release.md)
- [Development Apps](./platforms/ios/dev-apps.md)
- [Rendering Backends](./platforms/ios/ios-rendering-backends.md)
- [WebGPU](./platforms/ios/ios-webgpu.md)
- [macOS](./platforms/macos/README.md)
- [Linux](./platforms/linux/README.md)
- [Using Docker](./platforms/linux/using-docker.md)

View File

@@ -24,15 +24,15 @@ You can find an app that uses GLFW in [`platform/glfw`](https://github.com/mapli
## Rendering Backends
Originally the project only supported OpenGL 2.0. In 2023, the [renderer was modularized](https://github.com/maplibre/maplibre-native/blob/main/design-proposals/2022-10-27-rendering-modularization.md) allowing for the implementation of alternate rendering backends. The first alternate rendering backend that was implemented was [Metal](https://maplibre.org/news/2024-01-19-metal-support-for-maplibre-native-ios-is-here/), followed by [Vulkan](https://maplibre.org/news/2024-12-12-maplibre-android-vulkan/). In the future other rendering backends could be implemented such as WebGPU.
Originally the project only supported OpenGL 2.0. In 2023, the [renderer was modularized](https://github.com/maplibre/maplibre-native/blob/main/design-proposals/2022-10-27-rendering-modularization.md) allowing for the implementation of alternate rendering backends. The first alternate rendering backend that was implemented was [Metal](https://maplibre.org/news/2024-01-19-metal-support-for-maplibre-native-ios-is-here/), followed by [Vulkan](https://maplibre.org/news/2024-12-12-maplibre-android-vulkan/), and recently [WebGPU](https://maplibre.org/news/2025-10-04-maplibre-newsletter-september-2025/).
What platforms support which rendering backend can be found below.
| Platform | OpenGL ES 3.0 | Vulkan 1.0 | Metal | WebGPU |
| -------- | -------------- | ---------- | ------ | ------- |
| Android | ✅ | ✅ | ❌ | ❌ |
| iOS | ❌ | ❌ | ✅ | ❌ |
| Android | ✅ | ✅ | ❌ | ✅ |
| iOS | ❌ | ❌ | ✅ | ✅ |
| Linux | ✅ | ✅ | ❌ | ✅ |
| Windows | ✅ | ❌ | ❌ | ❓ |
| macOS | ❌ | ✅ | ✅[^1] | ✅ |

View File

@@ -0,0 +1,45 @@
# Rendering Backends
MapLibre Native for iOS supports multiple rendering backends. The backend is selected at build time through CMake presets.
## Available backends
| Preset | Backend | Status | Notes |
|---|---|---|---|
| `ios-metal` | Metal | Stable | Default, recommended for production |
| `ios-webgpu-dawn` | WebGPU (Dawn) | Experimental | Uses Metal internally, see [WebGPU](./ios-webgpu.md) |
| `ios-webgpu-wgpu` | WebGPU (wgpu-native) | Experimental | Uses Metal internally, see [WebGPU](./ios-webgpu.md) |
## Building with a specific backend
Each preset generates an Xcode project in its own build directory:
```bash
# Metal (default)
cmake --preset ios-metal -DDEVELOPMENT_TEAM_ID=YOUR_TEAM_ID
xed build-ios/MapLibre\ Native.xcodeproj
# WebGPU (Dawn)
cmake --preset ios-webgpu-dawn -DDEVELOPMENT_TEAM_ID=YOUR_TEAM_ID
xed build-ios-webgpu-dawn/MapLibre\ Native.xcodeproj
# WebGPU (wgpu-native)
cmake --preset ios-webgpu-wgpu -DDEVELOPMENT_TEAM_ID=YOUR_TEAM_ID
xed build-ios-webgpu-wgpu/MapLibre\ Native.xcodeproj
```
Then build the `app` target in Xcode, or from the command line:
```bash
cmake --build build-ios-webgpu-wgpu -- -sdk iphonesimulator -arch arm64
```
## How it works
Each preset passes different CMake variables that control which native rendering code gets compiled:
- `ios-metal`: `-DMLN_WITH_METAL=ON`
- `ios-webgpu-dawn`: `-DMLN_WITH_WEBGPU=ON -DMLN_WEBGPU_IMPL_DAWN=ON`
- `ios-webgpu-wgpu`: `-DMLN_WITH_WEBGPU=ON -DMLN_WEBGPU_IMPL_WGPU=ON`
Only one backend is active per build. The Objective-C layer is shared across backends, with renderer-specific code selected at compile time via `#if MLN_WEBGPU_IMPL_DAWN` / `#elif MLN_WEBGPU_IMPL_WGPU` conditionals in `MLNMapView+WebGPU.mm`.

View File

@@ -0,0 +1,65 @@
# WebGPU on iOS
The iOS platform supports WebGPU as a rendering backend alongside Metal. There are two WebGPU implementations: **Dawn** (Google) and **wgpu-native** (Rust). Both use Metal as the GPU backend on iOS.
## Building with Dawn
Dawn is fetched and built from source via CMake `FetchContent`, so no extra setup is needed. The first build takes a while because Dawn pulls in abseil, SPIRV-Tools, Tint, etc. Subsequent builds are incremental.
```bash
cmake --preset ios-webgpu-dawn
cmake --build build-ios-webgpu-dawn -- -sdk iphonesimulator -arch arm64
```
## Building with wgpu-native
wgpu-native requires a pre-built static library for the target architecture. The CMake build will attempt to build it from source if cargo is available, but cross-compilation for iOS needs some setup.
### 1. Install the Rust iOS targets
```bash
# For simulator (Apple Silicon Mac)
rustup target add aarch64-apple-ios-sim
# For device
rustup target add aarch64-apple-ios
```
### 2. Cross-compile wgpu-native
For the iOS Simulator:
```bash
cd vendor/wgpu-native
BINDGEN_EXTRA_CLANG_ARGS="--target=arm64-apple-ios -isysroot $(xcrun --sdk iphoneos --show-sdk-path)" \
cargo build --release --target aarch64-apple-ios-sim \
--no-default-features --features metal,wgsl,spirv,glsl
```
For a physical device:
```bash
cd vendor/wgpu-native
BINDGEN_EXTRA_CLANG_ARGS="--target=arm64-apple-ios -isysroot $(xcrun --sdk iphoneos --show-sdk-path)" \
cargo build --release --target aarch64-apple-ios \
--no-default-features --features metal,wgsl,spirv,glsl
```
The `BINDGEN_EXTRA_CLANG_ARGS` environment variable is required so that bindgen's macro fallback can find iOS system headers when generating FFI bindings.
This produces `vendor/wgpu-native/target/<target>/release/libwgpu_native.a`, which the CMake build picks up automatically.
### 3. Configure and build
```bash
cmake --preset ios-webgpu-wgpu
cmake --build build-ios-webgpu-wgpu -- -sdk iphonesimulator -arch arm64
```
If the pre-built library is not found and cargo is available, CMake will attempt to build wgpu-native automatically during configuration.
## Implementation notes
The native backend lives in `platform/ios/src/MLNMapView+WebGPU.mm`. It uses `#if MLN_WEBGPU_IMPL_DAWN` / `#elif MLN_WEBGPU_IMPL_WGPU` conditionals for the API differences between the two implementations, following the same pattern as the Android and desktop backends.
Both backends use a plain `UIView` with a `CAMetalLayer` (not `MTKView`) for the WebGPU surface. The rendering view has `userInteractionEnabled = NO` so touch events pass through to the parent `MLNMapView`.

View File

@@ -172,25 +172,35 @@ add_custom_target(mbgl-darwin-style-code
DEPENDS ${MLN_GENERATED_DARWIN_STYLE_SOURCE} ${MLN_GENERATED_DARWIN_STYLE_HEADERS}
)
add_library(
custom-layer-examples
EXCLUDE_FROM_ALL
# Custom layer examples use OpenGL ES / Metal APIs directly and are not
# available for WebGPU builds.
if(NOT MLN_WITH_WEBGPU)
set(_custom_layer_sources
"${CMAKE_CURRENT_LIST_DIR}/app/ExampleCustomDrawableStyleLayer.mm"
"${CMAKE_CURRENT_LIST_DIR}/app/CustomStyleLayerExample.m"
"${CMAKE_CURRENT_LIST_DIR}/app/PluginLayerExample.mm"
"${CMAKE_CURRENT_LIST_DIR}/app/PluginLayerExampleMetalRendering.mm"
)
)
if(MLN_WITH_METAL)
list(APPEND _custom_layer_sources "${CMAKE_CURRENT_LIST_DIR}/app/PluginLayerExampleMetalRendering.mm")
endif()
target_link_libraries(
add_library(
custom-layer-examples
EXCLUDE_FROM_ALL
${_custom_layer_sources}
)
target_link_libraries(
custom-layer-examples
PUBLIC ios-sdk-static
PRIVATE mbgl-compiler-options mbgl-core
)
)
target_include_directories(
target_include_directories(
custom-layer-examples
PUBLIC
"${CMAKE_CURRENT_LIST_DIR}/app"
PRIVATE
"${PROJECT_SOURCE_DIR}/src" # FIXME: should not use private headers
)
)
endif()

View File

@@ -48,10 +48,17 @@ add_custom_command(TARGET app POST_BUILD
COMMAND ls "$<TARGET_BUNDLE_DIR:app>"
)
target_link_libraries(
if(TARGET custom-layer-examples)
target_link_libraries(
app
custom-layer-examples # ios-sdk-static transitive dependency
)
)
else()
target_link_libraries(
app
ios-sdk-static
)
endif()
foreach(resource_file ${APP_RESOURCES})
set_source_files_properties(

View File

@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
@class MLNMapView;
@interface MBXSnapshotTestViewController : UIViewController
@property (nonatomic, weak) MLNMapView *sourceMapView;
@end

View File

@@ -0,0 +1,96 @@
#import "MBXSnapshotTestViewController.h"
#import "MLNMapView+Impl.h"
#import "MLNMapView_Private.h"
#import "Mapbox.h"
@interface MBXSnapshotTestViewController () <MLNMapViewDelegate>
@property (nonatomic) UIImageView *snapshotImageView;
@property (nonatomic, weak) id<MLNMapViewDelegate> originalDelegate;
@property (nonatomic, weak) UIView *originalSuperview;
@property (nonatomic) NSArray<NSLayoutConstraint *> *mapConstraints;
@end
@implementation MBXSnapshotTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Live Snapshot";
self.view.backgroundColor = [UIColor blackColor];
_snapshotImageView = [[UIImageView alloc] init];
_snapshotImageView.contentMode = UIViewContentModeScaleAspectFit;
_snapshotImageView.backgroundColor = [UIColor darkGrayColor];
_snapshotImageView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_snapshotImageView];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
if (_sourceMapView) {
_originalDelegate = _sourceMapView.delegate;
_originalSuperview = _sourceMapView.superview;
_sourceMapView.delegate = self;
_sourceMapView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_sourceMapView];
_mapConstraints = @[
[_snapshotImageView.topAnchor constraintEqualToAnchor:safe.topAnchor],
[_snapshotImageView.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor],
[_snapshotImageView.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor],
[_snapshotImageView.heightAnchor constraintEqualToAnchor:safe.heightAnchor multiplier:0.5],
[_sourceMapView.topAnchor constraintEqualToAnchor:_snapshotImageView.bottomAnchor],
[_sourceMapView.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor],
[_sourceMapView.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor],
[_sourceMapView.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor],
];
[NSLayoutConstraint activateConstraints:_mapConstraints];
} else {
[NSLayoutConstraint activateConstraints:@[
[_snapshotImageView.topAnchor constraintEqualToAnchor:safe.topAnchor],
[_snapshotImageView.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor],
[_snapshotImageView.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor],
[_snapshotImageView.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor],
]];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController && _sourceMapView && _originalSuperview) {
_sourceMapView.delegate = _originalDelegate;
if (_mapConstraints) {
[NSLayoutConstraint deactivateConstraints:_mapConstraints];
_mapConstraints = nil;
}
// Re-pin to all edges; original storyboard constraints were
// removed when the map view was reparented into this VC.
[_originalSuperview addSubview:_sourceMapView];
_sourceMapView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_sourceMapView.topAnchor constraintEqualToAnchor:_originalSuperview.topAnchor],
[_sourceMapView.leadingAnchor constraintEqualToAnchor:_originalSuperview.leadingAnchor],
[_sourceMapView.trailingAnchor constraintEqualToAnchor:_originalSuperview.trailingAnchor],
[_sourceMapView.bottomAnchor constraintEqualToAnchor:_originalSuperview.bottomAnchor],
]];
}
}
- (void)takeSnapshot {
if (!_sourceMapView) return;
UIImage *image = [_sourceMapView viewImpl]->snapshot();
if (image) {
self.snapshotImageView.image = image;
}
}
#pragma mark - MLNMapViewDelegate
- (void)mapViewDidFinishRenderingFrame:(MLNMapView *)mapView fullyRendered:(BOOL)fullyRendered {
[self takeSnapshot];
}
@end

View File

@@ -46,8 +46,8 @@
MLNMapCamera* mapCamera = [[MLNMapCamera alloc] init];
mapCamera.pitch = 20;
mapCamera.centerCoordinate = coordinates;
MLNMapSnapshotOptions* options = [[MLNMapSnapshotOptions alloc] initWithStyleURL:[NSURL URLWithString:@"https://tiles.openfreemap.org/styles/liberty"] camera:mapCamera size:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)];
options.zoomLevel = 10;
MLNMapSnapshotOptions* options = [[MLNMapSnapshotOptions alloc] initWithStyleURL:[NSURL URLWithString:@"https://demotiles.maplibre.org/style.json"] camera:mapCamera size:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)];
options.zoomLevel = 5;
options.showsAttribution = !hideAttribution;
// Create and start the snapshotter

View File

@@ -8,6 +8,7 @@
#import "MBXEmbeddedMapViewController.h"
#import "MBXOfflinePacksTableViewController.h"
#import "MBXOrnamentsViewController.h"
#import "MBXSnapshotTestViewController.h"
#import "MBXState.h"
#import "MBXStateManager.h"
#import "MBXUserLocationAnnotationView.h"
@@ -24,9 +25,13 @@
#import "MLNMapView_Experimental.h"
// Plug In Examples
#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL
#import "MLNPluginStyleLayer.h"
#import "PluginLayerExample.h"
#if MLN_RENDER_BACKEND_METAL
#import "PluginLayerExampleMetalRendering.h"
#endif
#endif
static const CLLocationCoordinate2D WorldTourDestinations[] = {
{.latitude = 38.8999418, .longitude = -77.033996},
@@ -129,6 +134,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
MBXSettingsMiscellaneousOrnamentsPlacement,
MBXSettingsMiscellaneousLatLngBoundsWithPadding,
MBXSettingsMiscellaneousCycleTileLOD,
MBXSettingsMiscellaneousLiveSnapshot,
MBXSettingsMiscellaneousPrintLogFile,
MBXSettingsMiscellaneousDeleteLogFile
};
@@ -273,8 +279,12 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
// This will add the plug-in layers. This is a demo of how
// extensible layers for the style can be added to the map view
- (void)addPluginLayers {
#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL
[self.mapView addPluginLayerType:[PluginLayerExample class]];
#if MLN_RENDER_BACKEND_METAL
[self.mapView addPluginLayerType:[PluginLayerExampleMetalRendering class]];
#endif
#endif
}
- (void)viewDidLoad {
@@ -513,7 +523,8 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
[NSString
stringWithFormat:@"Turn %@ Content Insets", (_contentInsetsEnabled ? @"Off" : @"On")],
@"View Route Simulation", @"Ornaments Placement", @"Lat Long bounds with padding",
[NSString stringWithFormat:@"Cycle tile LOD mode: %@", [self getTileLodModeName]]
[NSString stringWithFormat:@"Cycle tile LOD mode: %@", [self getTileLodModeName]],
@"Live Snapshot"
]];
break;
@@ -862,6 +873,12 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
case MBXSettingsMiscellaneousCycleTileLOD:
[self cycleTileLodMode];
break;
case MBXSettingsMiscellaneousLiveSnapshot: {
MBXSnapshotTestViewController *vc = [[MBXSnapshotTestViewController alloc] init];
vc.sourceMapView = self.mapView;
[self.navigationController pushViewController:vc animated:YES];
break;
}
default:
NSAssert(NO, @"All miscellaneous setting rows should be implemented");
break;
@@ -1863,6 +1880,7 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
}
- (void)addCustomDrawableLayer {
#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL
// Create a CustomLayer that uses the Drawable/Builder toolkit to generate and render geometry
ExampleCustomDrawableStyleLayer *layer =
[[ExampleCustomDrawableStyleLayer alloc] initWithIdentifier:@"custom-drawable-layer"];
@@ -1870,6 +1888,7 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
if (layer) {
[self.mapView.style addLayer:layer];
}
#endif
}
- (void)removeSource:(NSString *)ident {
@@ -1925,10 +1944,12 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
}
- (void)styleAddCustomTriangleLayer {
#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL
if (CustomStyleLayerExample *layer =
[[CustomStyleLayerExample alloc] initWithIdentifier:@"mbx-custom"]) {
[self.mapView.style addLayer:layer];
}
#endif
}
- (void)stylePolygonWithDDS {
@@ -2505,6 +2526,9 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
self.styleURLs = [NSMutableArray array];
/// Style that does not require an `apiKey` nor any further configuration
[self.styleNames addObject:@"OpenFreeMap Liberty"];
[self.styleURLs addObject:[NSURL URLWithString:@"https://tiles.openfreemap.org/styles/liberty"]];
[self.styleNames addObject:@"MapLibre Basic"];
[self.styleURLs addObject:[NSURL URLWithString:@"https://demotiles.maplibre.org/style.json"]];

View File

@@ -24,10 +24,12 @@ file(GLOB_RECURSE IOS_SDK_RESOURCE_FILES
)
if(NOT MLN_WITH_OPENGL)
list(FILTER IOS_SDK_SOURCE_FILES EXCLUDE REGEX ".*(OpenGL|gl).*")
elseif(NOT MLN_WITH_METAL)
list(FILTER IOS_SDK_SOURCE_FILES EXCLUDE REGEX ".*Metal.*")
if(MLN_WITH_METAL)
list(FILTER IOS_SDK_SOURCE_FILES EXCLUDE REGEX ".*(OpenGL|gl|WebGPU).*")
elseif(MLN_WITH_WEBGPU)
list(FILTER IOS_SDK_SOURCE_FILES EXCLUDE REGEX ".*(OpenGL|gl|Metal).*")
else()
list(FILTER IOS_SDK_SOURCE_FILES EXCLUDE REGEX ".*(Metal|WebGPU).*")
endif()
set(FRAMEWORK_BUNDLE_DIR ${CMAKE_CURRENT_BINARY_DIR}/Mapbox.bundle)
@@ -92,6 +94,8 @@ set_target_properties(ios-sdk-static PROPERTIES
if(MLN_WITH_METAL)
message(STATUS "Configuring Metal renderer backend")
elseif(MLN_WITH_WEBGPU)
message(STATUS "Configuring WebGPU renderer backend")
endif()
target_include_directories(
@@ -105,16 +109,28 @@ include("${CMAKE_CURRENT_LIST_DIR}/vendor/calloutview.cmake")
target_link_libraries(
ios-sdk-static
PUBLIC mbgl-core
PRIVATE mbgl-compiler-options mbgl-vendor-calloutview mbgl-vendor-metal-cpp mbgl-vendor-polylabel
PRIVATE mbgl-compiler-options mbgl-vendor-calloutview mbgl-vendor-polylabel
"-framework CoreText"
"-framework CoreImage"
"-framework CoreGraphics"
"-framework QuartzCore"
"-framework UIKit"
"-framework MetalKit"
"-framework ImageIO"
)
if(MLN_WITH_METAL)
target_link_libraries(ios-sdk-static PRIVATE mbgl-vendor-metal-cpp "-framework MetalKit")
elseif(MLN_WITH_WEBGPU)
target_link_libraries(ios-sdk-static PRIVATE "-framework Metal")
if(MLN_WEBGPU_IMPL_DAWN AND TARGET mbgl-vendor-dawn)
target_compile_definitions(ios-sdk-static PRIVATE MLN_WEBGPU_IMPL_DAWN=1)
target_link_libraries(ios-sdk-static PRIVATE mbgl-vendor-dawn)
elseif(MLN_WEBGPU_IMPL_WGPU AND TARGET mbgl-vendor-wgpu)
target_compile_definitions(ios-sdk-static PRIVATE MLN_WEBGPU_IMPL_WGPU=1)
target_link_libraries(ios-sdk-static PRIVATE mbgl-vendor-wgpu)
endif()
endif()
target_link_options(ios-sdk-static INTERFACE -ObjC)
add_dependencies(ios-sdk-static mbgl-darwin-style-code)

View File

@@ -5,15 +5,24 @@
#if MLN_RENDER_BACKEND_METAL
#import "MLNMapView+Metal.h"
#else // MLN_RENDER_BACKEND_OPENGL
#elif MLN_RENDER_BACKEND_WEBGPU
#import "MLNMapView+WebGPU.h"
#elif MLN_RENDER_BACKEND_OPENGL
#import "MLNMapView+OpenGL.h"
#else
#error "No render backend defined"
#endif
std::unique_ptr<MLNMapViewImpl> MLNMapViewImpl::Create(MLNMapView* nativeView) {
#if MLN_RENDER_BACKEND_METAL
return std::make_unique<MLNMapViewMetalImpl>(nativeView);
#else // MLN_RENDER_BACKEND_OPENGL
#elif MLN_RENDER_BACKEND_WEBGPU
return std::make_unique<MLNMapViewWebGPUImpl>(nativeView);
#elif MLN_RENDER_BACKEND_OPENGL
return std::make_unique<MLNMapViewOpenGLImpl>(nativeView);
#else
#error "No render backend defined"
return nullptr;
#endif
}

View File

@@ -0,0 +1,50 @@
#import "MLNMapView+Impl.h"
#import "MLNMapView_Private.h"
#include <mbgl/gfx/renderable.hpp>
#include <mbgl/webgpu/renderer_backend.hpp>
/// Adapter responsible for bridging calls from mbgl to MLNMapView and Cocoa (WebGPU backend).
class MLNMapViewWebGPUImpl final : public MLNMapViewImpl,
public mbgl::webgpu::RendererBackend,
public mbgl::gfx::Renderable {
public:
MLNMapViewWebGPUImpl(MLNMapView*);
~MLNMapViewWebGPUImpl() override;
// Implementation of mbgl::gfx::RendererBackend
public:
mbgl::gfx::Renderable& getDefaultRenderable() override { return *this; }
// webgpu::RendererBackend overrides
void* getCurrentTextureView() override;
void* getDepthStencilView() override;
mbgl::Size getFramebufferSize() const override;
void presentSurface();
void markNeedsPresent();
// Implementation of MLNMapViewImpl
public:
mbgl::gfx::RendererBackend& getRendererBackend() override { return *this; }
void setOpaque(bool) override;
void display() override;
void setPresentsWithTransaction(bool) override;
void createView() override;
UIView* getView() override;
void deleteView() override;
UIImage* snapshot() override;
void layoutChanged() override;
MLNBackendResource* getObject() override;
private:
void activate() override;
void deactivate() override;
void createSurface();
void configureSurface(uint32_t width, uint32_t height);
void createDepthStencilTexture(uint32_t width, uint32_t height);
class Impl;
std::unique_ptr<Impl> impl;
};

View File

@@ -0,0 +1,802 @@
#import "MLNDisplayUtils.h"
#import "MLNFoundation_Private.h"
#import "MLNLoggingConfiguration_Private.h"
#import "MLNMapView+WebGPU.h"
#import <mbgl/util/logging.hpp>
#import <mbgl/webgpu/renderable_resource.hpp>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#include <webgpu/webgpu.h>
#if MLN_WEBGPU_IMPL_DAWN
#include <dawn/dawn_proc.h>
#include <dawn/native/DawnNative.h>
#include <dawn/native/MetalBackend.h>
#include <webgpu/webgpu_cpp.h>
#elif MLN_WEBGPU_IMPL_WGPU
#include <webgpu/webgpu_cpp.h>
#include <wgpu.h>
#endif
#include <cassert>
#include <cstring>
#if MLN_WEBGPU_IMPL_WGPU
#include <chrono>
#include <thread>
#endif
@interface MLNWebGPUView : UIView
@end
@implementation MLNWebGPUView
+ (Class)layerClass {
return [CAMetalLayer class];
}
@end
class MLNMapViewWebGPURenderableResource final : public mbgl::webgpu::RenderableResource {
public:
MLNMapViewWebGPURenderableResource(MLNMapViewWebGPUImpl& backend_) : backend(backend_) {}
void bind() override {}
void swap() override { backend.markNeedsPresent(); }
const mbgl::webgpu::RendererBackend& getBackend() const override { return backend; }
const WGPUCommandEncoder& getCommandEncoder() const override {
assert(false);
static WGPUCommandEncoder dummy = nullptr;
return dummy;
}
WGPURenderPassEncoder getRenderPassEncoder() const override {
assert(false);
return nullptr;
}
WGPUTextureView getColorTextureView() override {
return static_cast<WGPUTextureView>(backend.getCurrentTextureView());
}
WGPUTextureView getDepthStencilTextureView() override {
return static_cast<WGPUTextureView>(backend.getDepthStencilView());
}
private:
MLNMapViewWebGPUImpl& backend;
};
class MLNMapViewWebGPUImpl::Impl {
public:
#if MLN_WEBGPU_IMPL_DAWN
std::unique_ptr<dawn::native::Instance> instance;
WGPUAdapter adapter = nullptr;
#elif MLN_WEBGPU_IMPL_WGPU
wgpu::Instance instance;
wgpu::Adapter adapter;
#endif
wgpu::Device device;
wgpu::Queue queue;
WGPUSurface surface = nullptr;
WGPUTextureFormat surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
bool surfaceConfigured = false;
WGPUTexture depthStencilTexture = nullptr;
WGPUTextureView depthStencilView = nullptr;
WGPUTexture currentTexture = nullptr;
WGPUTextureView currentTextureView = nullptr;
bool needsPresent = false;
mbgl::Size framebufferSize;
MLNWebGPUView* webGPUView = nil;
bool presentsWithTransaction = false;
NSUInteger activationCount = 0;
};
MLNMapViewWebGPUImpl::MLNMapViewWebGPUImpl(MLNMapView* nativeView_)
: MLNMapViewImpl(nativeView_),
mbgl::webgpu::RendererBackend(mbgl::gfx::ContextMode::Unique),
mbgl::gfx::Renderable({0, 0}, std::make_unique<MLNMapViewWebGPURenderableResource>(*this)),
impl(std::make_unique<Impl>()) {
#if MLN_WEBGPU_IMPL_DAWN
// Required when not using the monolithic Dawn library.
dawnProcSetProcs(&dawn::native::GetProcs());
WGPUInstanceFeatureName instanceFeatures[] = {WGPUInstanceFeatureName_TimedWaitAny};
WGPUInstanceLimits instanceLimits = WGPU_INSTANCE_LIMITS_INIT;
instanceLimits.timedWaitAnyMaxCount = 64;
WGPUInstanceDescriptor instanceDesc = WGPU_INSTANCE_DESCRIPTOR_INIT;
instanceDesc.requiredFeatureCount = 1;
instanceDesc.requiredFeatures = instanceFeatures;
instanceDesc.requiredLimits = &instanceLimits;
impl->instance = std::make_unique<dawn::native::Instance>(&instanceDesc);
wgpu::RequestAdapterOptions metalOpts = {};
metalOpts.backendType = wgpu::BackendType::Metal;
std::vector<dawn::native::Adapter> adapters = impl->instance->EnumerateAdapters(&metalOpts);
if (adapters.empty()) {
adapters = impl->instance->EnumerateAdapters();
}
if (adapters.empty()) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: No adapters found");
return;
}
dawn::native::Adapter& selectedAdapter = adapters[0];
bool hasDepth32FloatStencil8 = false;
WGPUSupportedFeatures features = {};
wgpuAdapterGetFeatures(selectedAdapter.Get(), &features);
if (features.featureCount > 0 && features.features) {
for (size_t i = 0; i < features.featureCount; ++i) {
if (static_cast<wgpu::FeatureName>(features.features[i]) ==
wgpu::FeatureName::Depth32FloatStencil8) {
hasDepth32FloatStencil8 = true;
break;
}
}
wgpuSupportedFeaturesFreeMembers(features);
}
#elif MLN_WEBGPU_IMPL_WGPU
wgpu::InstanceDescriptor instanceDesc = {};
impl->instance = wgpu::createInstance(instanceDesc);
if (!impl->instance) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: Failed to create instance");
return;
}
wgpu::RequestAdapterOptions adapterOpts = {};
adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance;
adapterOpts.backendType = wgpu::BackendType::Metal;
impl->adapter = impl->instance.requestAdapter(adapterOpts);
if (!impl->adapter) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: No adapter found");
return;
}
WGPUSupportedFeatures features = {};
bool hasDepth32FloatStencil8 = false;
wgpuAdapterGetFeatures(static_cast<WGPUAdapter>(impl->adapter), &features);
if (features.featureCount > 0 && features.features) {
for (size_t i = 0; i < features.featureCount; ++i) {
if (static_cast<wgpu::FeatureName>(features.features[i]) ==
wgpu::FeatureName::Depth32FloatStencil8) {
hasDepth32FloatStencil8 = true;
break;
}
}
wgpuSupportedFeaturesFreeMembers(features);
}
#endif
std::vector<wgpu::FeatureName> requiredFeatures;
wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Depth24PlusStencil8;
if (hasDepth32FloatStencil8) {
requiredFeatures.push_back(wgpu::FeatureName::Depth32FloatStencil8);
depthStencilFormat = wgpu::TextureFormat::Depth32FloatStencil8;
}
wgpu::DeviceDescriptor deviceDesc = {};
#if MLN_WEBGPU_IMPL_DAWN
deviceDesc.label = "MapLibre iOS WebGPU Device";
deviceDesc.SetUncapturedErrorCallback([](const wgpu::Device&, wgpu::ErrorType type,
wgpu::StringView message) {
NSLog(@"[WebGPU] Device error (type=%d): %.*s", (int)type, (int)message.length, message.data);
});
deviceDesc.requiredFeatures = requiredFeatures.data();
#elif MLN_WEBGPU_IMPL_WGPU
deviceDesc.requiredFeatures = reinterpret_cast<const WGPUFeatureName*>(requiredFeatures.data());
#endif
deviceDesc.requiredFeatureCount = requiredFeatures.size();
#if MLN_WEBGPU_IMPL_DAWN
WGPUDevice rawDevice = selectedAdapter.CreateDevice(&deviceDesc);
if (!rawDevice) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: Failed to create device");
return;
}
impl->device = wgpu::Device::Acquire(rawDevice);
impl->queue = impl->device.GetQueue();
impl->adapter = selectedAdapter.Get();
wgpuAdapterAddRef(impl->adapter);
setDevice(impl->device.Get());
setQueue(impl->queue.Get());
setInstance(impl->instance->Get());
#elif MLN_WEBGPU_IMPL_WGPU
impl->device = impl->adapter.requestDevice(deviceDesc);
if (!impl->device) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: Failed to create device");
return;
}
impl->queue = impl->device.getQueue();
setDevice(static_cast<WGPUDevice>(impl->device));
setQueue(static_cast<WGPUQueue>(impl->queue));
setInstance(static_cast<WGPUInstance>(impl->instance));
#endif
setDepthStencilFormat(depthStencilFormat);
}
MLNMapViewWebGPUImpl::~MLNMapViewWebGPUImpl() {
if (!impl) return;
if (impl->currentTextureView) {
wgpuTextureViewRelease(impl->currentTextureView);
impl->currentTextureView = nullptr;
}
if (impl->currentTexture) {
wgpuTextureRelease(impl->currentTexture);
impl->currentTexture = nullptr;
}
if (impl->depthStencilView) {
wgpuTextureViewRelease(impl->depthStencilView);
impl->depthStencilView = nullptr;
}
if (impl->depthStencilTexture) {
wgpuTextureDestroy(impl->depthStencilTexture);
wgpuTextureRelease(impl->depthStencilTexture);
impl->depthStencilTexture = nullptr;
}
if (impl->surface) {
if (impl->surfaceConfigured) {
wgpuSurfaceUnconfigure(impl->surface);
}
wgpuSurfaceRelease(impl->surface);
impl->surface = nullptr;
}
impl->queue = nullptr;
impl->device = nullptr;
#if MLN_WEBGPU_IMPL_DAWN
if (impl->adapter) {
wgpuAdapterRelease(impl->adapter);
impl->adapter = nullptr;
}
impl->instance.reset();
#elif MLN_WEBGPU_IMPL_WGPU
impl->adapter = nullptr;
impl->instance = nullptr;
#endif
}
void MLNMapViewWebGPUImpl::createView() {
if (impl->webGPUView) {
return;
}
const auto scaleFactor = MLNEffectiveScaleFactorForView(mapView);
impl->webGPUView = [[MLNWebGPUView alloc] initWithFrame:mapView.bounds];
impl->webGPUView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
impl->webGPUView.contentScaleFactor = scaleFactor;
impl->webGPUView.contentMode = UIViewContentModeCenter;
impl->webGPUView.opaque = mapView.opaque;
impl->webGPUView.layer.opaque = mapView.opaque;
impl->webGPUView.userInteractionEnabled = NO;
CAMetalLayer* metalLayer = (CAMetalLayer*)impl->webGPUView.layer;
metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
metalLayer.framebufferOnly = NO;
CGSize drawableSize =
CGSizeMake(mapView.bounds.size.width * scaleFactor, mapView.bounds.size.height * scaleFactor);
metalLayer.drawableSize = drawableSize;
#if MLN_WEBGPU_IMPL_DAWN
// Dawn needs the Metal device set on the layer before creating a surface.
id<MTLDevice> mtlDevice = dawn::native::metal::GetMTLDevice(impl->device.Get());
metalLayer.device = mtlDevice;
#endif
if (@available(iOS 13.0, *)) {
metalLayer.presentsWithTransaction = impl->presentsWithTransaction;
}
[mapView insertSubview:impl->webGPUView atIndex:0];
createSurface();
uint32_t w = static_cast<uint32_t>(drawableSize.width);
uint32_t h = static_cast<uint32_t>(drawableSize.height);
if (w > 0 && h > 0) {
impl->framebufferSize = {w, h};
size = impl->framebufferSize;
configureSurface(w, h);
}
}
void MLNMapViewWebGPUImpl::createSurface() {
if (!impl->webGPUView) return;
CAMetalLayer* metalLayer = (CAMetalLayer*)impl->webGPUView.layer;
#if MLN_WEBGPU_IMPL_DAWN
if (!impl->instance) return;
WGPUSurfaceSourceMetalLayer metalDesc = WGPU_SURFACE_SOURCE_METAL_LAYER_INIT;
metalDesc.layer = (__bridge void*)metalLayer;
WGPUSurfaceDescriptor surfaceDesc = WGPU_SURFACE_DESCRIPTOR_INIT;
surfaceDesc.nextInChain = &metalDesc.chain;
WGPUInstance rawInstance = impl->instance->Get();
impl->surface = wgpuInstanceCreateSurface(rawInstance, &surfaceDesc);
#elif MLN_WEBGPU_IMPL_WGPU
if (!impl->instance) return;
WGPUSurfaceSourceMetalLayer metalDesc = {};
metalDesc.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
metalDesc.chain.next = nullptr;
metalDesc.layer = (__bridge void*)metalLayer;
WGPUSurfaceDescriptor surfaceDesc = {};
surfaceDesc.nextInChain = &metalDesc.chain;
impl->surface =
wgpuInstanceCreateSurface(static_cast<WGPUInstance>(impl->instance), &surfaceDesc);
#endif
if (!impl->surface) {
mbgl::Log::Error(mbgl::Event::Render, "WebGPU iOS: Failed to create surface");
}
}
void MLNMapViewWebGPUImpl::configureSurface(uint32_t width, uint32_t height) {
if (!impl->surface || !impl->device || width == 0 || height == 0) return;
WGPUSurfaceCapabilities capabilities = {};
#if MLN_WEBGPU_IMPL_DAWN
WGPUStatus capStatus = wgpuSurfaceGetCapabilities(impl->surface, impl->adapter, &capabilities);
#elif MLN_WEBGPU_IMPL_WGPU
WGPUStatus capStatus = wgpuSurfaceGetCapabilities(
impl->surface, static_cast<WGPUAdapter>(impl->adapter), &capabilities);
#endif
WGPUCompositeAlphaMode selectedAlphaMode = WGPUCompositeAlphaMode_Opaque;
WGPUPresentMode selectedPresentMode = WGPUPresentMode_Fifo;
if (capStatus == WGPUStatus_Success) {
const WGPUTextureFormat preferredFormats[] = {
WGPUTextureFormat_BGRA8Unorm,
WGPUTextureFormat_RGBA8Unorm,
};
for (auto preferred : preferredFormats) {
for (size_t i = 0; i < capabilities.formatCount; ++i) {
if (capabilities.formats[i] == preferred) {
impl->surfaceFormat = preferred;
goto formatFound;
}
}
}
if (capabilities.formatCount > 0) {
impl->surfaceFormat = capabilities.formats[0];
}
formatFound:
if (capabilities.alphaModeCount > 0) {
selectedAlphaMode = capabilities.alphaModes[0];
}
if (capabilities.presentModeCount > 0) {
selectedPresentMode = capabilities.presentModes[0];
}
wgpuSurfaceCapabilitiesFreeMembers(capabilities);
}
setColorFormat(static_cast<wgpu::TextureFormat>(impl->surfaceFormat));
#if MLN_WEBGPU_IMPL_DAWN
WGPUSurfaceConfiguration config = WGPU_SURFACE_CONFIGURATION_INIT;
config.device = impl->device.Get();
#elif MLN_WEBGPU_IMPL_WGPU
WGPUSurfaceConfiguration config = {};
config.device = static_cast<WGPUDevice>(impl->device);
#endif
config.format = impl->surfaceFormat;
config.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc;
config.alphaMode = selectedAlphaMode;
config.width = width;
config.height = height;
config.presentMode = selectedPresentMode;
wgpuSurfaceConfigure(impl->surface, &config);
impl->surfaceConfigured = true;
createDepthStencilTexture(width, height);
}
void MLNMapViewWebGPUImpl::createDepthStencilTexture(uint32_t width, uint32_t height) {
if (impl->depthStencilView) {
wgpuTextureViewRelease(impl->depthStencilView);
impl->depthStencilView = nullptr;
}
if (impl->depthStencilTexture) {
wgpuTextureDestroy(impl->depthStencilTexture);
wgpuTextureRelease(impl->depthStencilTexture);
impl->depthStencilTexture = nullptr;
}
if (!impl->device || width == 0 || height == 0) return;
WGPUTextureFormat dsFormat = static_cast<WGPUTextureFormat>(getDepthStencilFormat());
#if MLN_WEBGPU_IMPL_DAWN
WGPUTextureDescriptor depthDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
#elif MLN_WEBGPU_IMPL_WGPU
WGPUTextureDescriptor depthDesc = {};
#endif
depthDesc.usage = WGPUTextureUsage_RenderAttachment;
depthDesc.dimension = WGPUTextureDimension_2D;
depthDesc.size = {width, height, 1};
depthDesc.format = dsFormat;
depthDesc.mipLevelCount = 1;
depthDesc.sampleCount = 1;
#if MLN_WEBGPU_IMPL_DAWN
impl->depthStencilTexture = wgpuDeviceCreateTexture(impl->device.Get(), &depthDesc);
#elif MLN_WEBGPU_IMPL_WGPU
impl->depthStencilTexture =
wgpuDeviceCreateTexture(static_cast<WGPUDevice>(impl->device), &depthDesc);
#endif
if (!impl->depthStencilTexture) {
mbgl::Log::Warning(mbgl::Event::Render, "WebGPU iOS: Failed to create depth/stencil texture");
return;
}
#if MLN_WEBGPU_IMPL_DAWN
WGPUTextureViewDescriptor viewDesc = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT;
#elif MLN_WEBGPU_IMPL_WGPU
WGPUTextureViewDescriptor viewDesc = {};
#endif
viewDesc.format = dsFormat;
viewDesc.dimension = WGPUTextureViewDimension_2D;
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 1;
viewDesc.baseArrayLayer = 0;
viewDesc.arrayLayerCount = 1;
viewDesc.aspect = WGPUTextureAspect_All;
impl->depthStencilView = wgpuTextureCreateView(impl->depthStencilTexture, &viewDesc);
if (!impl->depthStencilView) {
mbgl::Log::Warning(mbgl::Event::Render, "WebGPU iOS: Failed to create depth/stencil view");
}
}
void* MLNMapViewWebGPUImpl::getCurrentTextureView() {
if (!impl->surface || !impl->surfaceConfigured) {
return nullptr;
}
presentSurface();
WGPUSurfaceTexture surfaceTexture = {};
wgpuSurfaceGetCurrentTexture(impl->surface, &surfaceTexture);
if (!surfaceTexture.texture) {
return nullptr;
}
auto status = static_cast<WGPUSurfaceGetCurrentTextureStatus>(surfaceTexture.status);
if (status == WGPUSurfaceGetCurrentTextureStatus_Outdated ||
status == WGPUSurfaceGetCurrentTextureStatus_Lost) {
wgpuTextureRelease(surfaceTexture.texture);
configureSurface(impl->framebufferSize.width, impl->framebufferSize.height);
return nullptr;
}
if (status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) {
wgpuTextureRelease(surfaceTexture.texture);
return nullptr;
}
impl->currentTexture = surfaceTexture.texture;
#if MLN_WEBGPU_IMPL_DAWN
WGPUTextureViewDescriptor viewDesc = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT;
#elif MLN_WEBGPU_IMPL_WGPU
WGPUTextureViewDescriptor viewDesc = {};
#endif
viewDesc.format = impl->surfaceFormat;
viewDesc.dimension = WGPUTextureViewDimension_2D;
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 1;
viewDesc.baseArrayLayer = 0;
viewDesc.arrayLayerCount = 1;
viewDesc.aspect = WGPUTextureAspect_All;
impl->currentTextureView = wgpuTextureCreateView(impl->currentTexture, &viewDesc);
return impl->currentTextureView;
}
void MLNMapViewWebGPUImpl::markNeedsPresent() { impl->needsPresent = true; }
void MLNMapViewWebGPUImpl::presentSurface() {
if (!impl->surface || !impl->surfaceConfigured || !impl->needsPresent) {
return;
}
impl->needsPresent = false;
if (impl->currentTextureView) {
wgpuSurfacePresent(impl->surface);
wgpuTextureViewRelease(impl->currentTextureView);
impl->currentTextureView = nullptr;
}
if (impl->currentTexture) {
wgpuTextureRelease(impl->currentTexture);
impl->currentTexture = nullptr;
}
if (impl->device) {
#if MLN_WEBGPU_IMPL_DAWN
wgpuDeviceTick(impl->device.Get());
#elif MLN_WEBGPU_IMPL_WGPU
wgpuDevicePoll(static_cast<WGPUDevice>(impl->device), false, nullptr);
#endif
}
}
void* MLNMapViewWebGPUImpl::getDepthStencilView() { return impl->depthStencilView; }
mbgl::Size MLNMapViewWebGPUImpl::getFramebufferSize() const { return impl->framebufferSize; }
UIView* MLNMapViewWebGPUImpl::getView() { return impl->webGPUView; }
void MLNMapViewWebGPUImpl::setOpaque(const bool opaque) {
impl->webGPUView.opaque = opaque;
impl->webGPUView.layer.opaque = opaque;
}
void MLNMapViewWebGPUImpl::setPresentsWithTransaction(const bool value) {
impl->presentsWithTransaction = value;
if (@available(iOS 13.0, *)) {
if (CAMetalLayer* metalLayer = MLN_OBJC_DYNAMIC_CAST(impl->webGPUView.layer, CAMetalLayer)) {
metalLayer.presentsWithTransaction = value;
}
}
}
void MLNMapViewWebGPUImpl::display() { render(); }
void MLNMapViewWebGPUImpl::deleteView() {
if (impl->currentTextureView) {
wgpuTextureViewRelease(impl->currentTextureView);
impl->currentTextureView = nullptr;
}
if (impl->currentTexture) {
wgpuTextureRelease(impl->currentTexture);
impl->currentTexture = nullptr;
}
if (impl->depthStencilView) {
wgpuTextureViewRelease(impl->depthStencilView);
impl->depthStencilView = nullptr;
}
if (impl->depthStencilTexture) {
wgpuTextureDestroy(impl->depthStencilTexture);
wgpuTextureRelease(impl->depthStencilTexture);
impl->depthStencilTexture = nullptr;
}
if (impl->surface) {
if (impl->surfaceConfigured) {
wgpuSurfaceUnconfigure(impl->surface);
impl->surfaceConfigured = false;
}
wgpuSurfaceRelease(impl->surface);
impl->surface = nullptr;
}
[impl->webGPUView removeFromSuperview];
impl->webGPUView = nil;
}
void MLNMapViewWebGPUImpl::layoutChanged() {
const auto scaleFactor = MLNEffectiveScaleFactorForView(mapView);
const auto screenSize = mapView.bounds.size;
impl->webGPUView.contentScaleFactor = scaleFactor;
CGSize drawableSize = CGSizeMake(screenSize.width * scaleFactor, screenSize.height * scaleFactor);
CAMetalLayer* metalLayer = (CAMetalLayer*)impl->webGPUView.layer;
metalLayer.drawableSize = drawableSize;
uint32_t w = static_cast<uint32_t>(drawableSize.width);
uint32_t h = static_cast<uint32_t>(drawableSize.height);
impl->framebufferSize = {w, h};
size = impl->framebufferSize;
if (impl->currentTextureView) {
wgpuTextureViewRelease(impl->currentTextureView);
impl->currentTextureView = nullptr;
}
if (impl->currentTexture) {
wgpuTextureRelease(impl->currentTexture);
impl->currentTexture = nullptr;
}
if (w > 0 && h > 0) {
configureSurface(w, h);
}
}
UIImage* MLNMapViewWebGPUImpl::snapshot() {
if (!impl->currentTexture || !impl->device || !impl->queue || impl->framebufferSize.width == 0 ||
impl->framebufferSize.height == 0) {
return nil;
}
const auto& fbSize = impl->framebufferSize;
constexpr uint32_t bytesPerPixel = 4;
constexpr uint32_t bytesPerRowAlignment = 256u;
const uint32_t rowStride = fbSize.width * bytesPerPixel;
const uint32_t alignedRowStride =
((rowStride + bytesPerRowAlignment - 1) / bytesPerRowAlignment) * bytesPerRowAlignment;
const uint64_t alignedDataSize = static_cast<uint64_t>(alignedRowStride) * fbSize.height;
#if MLN_WEBGPU_IMPL_DAWN
auto device = impl->device.Get();
auto queue = impl->queue.Get();
#elif MLN_WEBGPU_IMPL_WGPU
auto device = static_cast<WGPUDevice>(impl->device);
auto queue = static_cast<WGPUQueue>(impl->queue);
#endif
#if MLN_WEBGPU_IMPL_DAWN
wgpuDeviceTick(device);
#endif
WGPUBufferDescriptor bufferDesc = {};
#if MLN_WEBGPU_IMPL_DAWN
WGPUStringView bufferLabel = {"Snapshot Buffer", strlen("Snapshot Buffer")};
bufferDesc.label = bufferLabel;
#endif
bufferDesc.size = alignedDataSize;
bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead;
WGPUBuffer stagingBuffer = wgpuDeviceCreateBuffer(device, &bufferDesc);
if (!stagingBuffer) {
return nil;
}
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
if (encoder) {
WGPUTexelCopyTextureInfo src = {};
src.texture = impl->currentTexture;
src.mipLevel = 0;
src.origin = {0, 0, 0};
src.aspect = WGPUTextureAspect_All;
WGPUTexelCopyBufferInfo dst = {};
dst.buffer = stagingBuffer;
dst.layout.offset = 0;
dst.layout.bytesPerRow = alignedRowStride;
dst.layout.rowsPerImage = fbSize.height;
WGPUExtent3D copySize = {fbSize.width, fbSize.height, 1};
wgpuCommandEncoderCopyTextureToBuffer(encoder, &src, &dst, &copySize);
WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr);
wgpuQueueSubmit(queue, 1, &commands);
wgpuCommandBufferRelease(commands);
wgpuCommandEncoderRelease(encoder);
}
struct MapContext {
WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error;
bool completed = false;
};
MapContext mapContext;
WGPUBufferMapCallbackInfo callbackInfo = {};
callbackInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView, void* userdata1, void*) {
auto* ctx = static_cast<MapContext*>(userdata1);
ctx->status = status;
ctx->completed = true;
};
callbackInfo.userdata1 = &mapContext;
#if MLN_WEBGPU_IMPL_DAWN
callbackInfo.mode = WGPUCallbackMode_WaitAnyOnly;
WGPUFuture future =
wgpuBufferMapAsync(stagingBuffer, WGPUMapMode_Read, 0, alignedDataSize, callbackInfo);
WGPUInstance instance = impl->instance->Get();
if (instance) {
WGPUFutureWaitInfo waitInfo = {};
waitInfo.future = future;
waitInfo.completed = false;
wgpuInstanceWaitAny(instance, 1, &waitInfo, UINT64_MAX);
}
#elif MLN_WEBGPU_IMPL_WGPU
callbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
wgpuBufferMapAsync(stagingBuffer, WGPUMapMode_Read, 0, alignedDataSize, callbackInfo);
for (int i = 0; i < 1000 && !mapContext.completed; ++i) {
wgpuDevicePoll(device, true, nullptr);
if (!mapContext.completed) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
#endif
UIImage* result = nil;
if (mapContext.status == WGPUMapAsyncStatus_Success) {
const auto* mappedData = static_cast<const uint8_t*>(
wgpuBufferGetConstMappedRange(stagingBuffer, 0, alignedDataSize));
if (mappedData) {
auto rgbaData = std::make_unique<uint8_t[]>(static_cast<size_t>(rowStride) * fbSize.height);
const bool swizzle = (impl->surfaceFormat == WGPUTextureFormat_BGRA8Unorm);
for (uint32_t row = 0; row < fbSize.height; ++row) {
const auto* src = mappedData + static_cast<size_t>(row) * alignedRowStride;
auto* dst = rgbaData.get() + static_cast<size_t>(row) * rowStride;
if (swizzle) {
for (uint32_t x = 0; x < fbSize.width; ++x) {
dst[x * 4 + 0] = src[x * 4 + 2];
dst[x * 4 + 1] = src[x * 4 + 1];
dst[x * 4 + 2] = src[x * 4 + 0];
dst[x * 4 + 3] = src[x * 4 + 3];
}
} else {
std::memcpy(dst, src, rowStride);
}
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef ctx =
CGBitmapContextCreate(rgbaData.get(), fbSize.width, fbSize.height, 8, rowStride,
colorSpace, kCGImageAlphaPremultipliedLast);
if (ctx) {
CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
if (cgImage) {
const CGFloat scale = impl->webGPUView.contentScaleFactor;
result = [UIImage imageWithCGImage:cgImage scale:scale orientation:UIImageOrientationUp];
CGImageRelease(cgImage);
}
CGContextRelease(ctx);
}
CGColorSpaceRelease(colorSpace);
}
}
wgpuBufferUnmap(stagingBuffer);
wgpuBufferRelease(stagingBuffer);
presentSurface();
return result;
}
MLNBackendResource* MLNMapViewWebGPUImpl::getObject() { return [[MLNBackendResource alloc] init]; }
void MLNMapViewWebGPUImpl::activate() {
if (impl->activationCount++) {
return;
}
}
void MLNMapViewWebGPUImpl::deactivate() {
if (--impl->activationCount) {
return;
}
}

View File

@@ -8,8 +8,10 @@
#include <mbgl/map/map_options.hpp>
#include <mbgl/map/mode.hpp>
#include <mbgl/math/wrap.hpp>
#if MLN_RENDER_BACKEND_METAL
#include <mbgl/mtl/mtl_fwd.hpp>
#include <mbgl/mtl/render_pass.hpp>
#endif
#include <mbgl/plugin/plugin_layer.hpp>
#include <mbgl/plugin/plugin_layer_factory.hpp>
#include <mbgl/plugin/plugin_layer_impl.hpp>
@@ -7602,10 +7604,13 @@ static void *windowScreenContext = &windowScreenContext;
// Set the render function
auto renderFunction = [weakPlugInLayer, weakMapView](mbgl::PaintParameters &paintParameters) {
#if MLN_RENDER_BACKEND_METAL
const mbgl::mtl::RenderPass &renderPass =
static_cast<mbgl::mtl::RenderPass &>(*paintParameters.renderPass);
id<MTLRenderCommandEncoder> encoder =
(__bridge id<MTLRenderCommandEncoder>)renderPass.getMetalEncoder().get();
id encoder = (__bridge id)renderPass.getMetalEncoder().get();
#else
id encoder = nil;
#endif
MLNMapView *strongMapView = weakMapView;

34
vendor/dawn.cmake vendored
View File

@@ -57,18 +57,48 @@ set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "Disable Dawn samples" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "Disable Dawn tests" FORCE)
set(DAWN_BUILD_BENCHMARKS OFF CACHE BOOL "Disable Dawn benchmarks" FORCE)
set(DAWN_BUILD_NODE_BINDINGS OFF CACHE BOOL "Disable Dawn Node bindings" FORCE)
set(TINT_BUILD_CMD_TOOLS OFF CACHE BOOL "Disable Tint command line tools" FORCE)
set(DAWN_WERROR OFF CACHE BOOL "Disable -Werror in Dawn" FORCE)
set(DAWN_BUILD_PROTOBUF OFF CACHE BOOL "Disable protobuf for cross-compilation" FORCE)
if(NOT TARGET dawn::webgpu_dawn)
# GLFW is not available on iOS (Dawn auto-detects it via UNIX AND NOT ANDROID)
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
include("${CMAKE_CURRENT_LIST_DIR}/dawn.ios.cmake")
endif()
# The Xcode generator does not properly handle $<TARGET_OBJECTS:...> in add_library(),
# which breaks Dawn's bundle_libraries() macro. Disable the monolithic library for Xcode
# and link individual Dawn component targets instead.
if(CMAKE_GENERATOR STREQUAL "Xcode")
set(DAWN_BUILD_MONOLITHIC_LIBRARY "OFF" CACHE STRING "" FORCE)
endif()
if(NOT TARGET dawn_native)
if(APPLE)
enable_language(OBJC OBJCXX)
endif()
add_subdirectory(${DAWN_DIR} ${DAWN_BUILD_DIR})
endif()
# Tint's MSL validator includes Metal headers which use GNU anonymous structs.
# Tint compiles with -pedantic-errors, turning these into build errors.
if(TARGET tint_lang_msl_validate)
target_compile_options(tint_lang_msl_validate PRIVATE
-Wno-gnu-anonymous-struct -Wno-nested-anon-types)
endif()
add_library(mbgl-vendor-dawn INTERFACE)
target_link_libraries(mbgl-vendor-dawn INTERFACE dawn::webgpu_dawn)
if(TARGET dawn::webgpu_dawn)
target_link_libraries(mbgl-vendor-dawn INTERFACE dawn::webgpu_dawn)
else()
# Xcode path: link individual Dawn component targets.
# Use BUILD_INTERFACE to avoid export-set requirements.
target_link_libraries(mbgl-vendor-dawn INTERFACE
$<BUILD_INTERFACE:dawn_native>
$<BUILD_INTERFACE:dawn_proc>
)
endif()
target_include_directories(mbgl-vendor-dawn
SYSTEM INTERFACE

2
vendor/dawn.ios.cmake generated vendored Normal file
View File

@@ -0,0 +1,2 @@
# GLFW is not available on iOS (Dawn auto-detects it via UNIX AND NOT ANDROID)
set(DAWN_USE_GLFW OFF CACHE BOOL "Disable GLFW on iOS" FORCE)

View File

@@ -9,14 +9,7 @@ elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86")
endif()
set(_wgpu_lib_name "libwgpu_native.a")
macro(mln_wgpu_android_find_library)
foreach(_search_path ${_wgpu_lib_search_paths})
if(EXISTS "${_search_path}/${_wgpu_lib_name}")
set(WGPU_LIBRARY "${_search_path}/${_wgpu_lib_name}" CACHE FILEPATH "wgpu-native library")
break()
endif()
endforeach()
endmacro()
macro(mln_wgpu_android_link_library target)
find_package(Threads REQUIRED)

51
vendor/wgpu.cmake vendored
View File

@@ -25,11 +25,15 @@ endif()
# Platform-specific library names and paths
if(APPLE)
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
include("${CMAKE_CURRENT_LIST_DIR}/wgpu.ios.cmake")
else()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
set(_wgpu_lib_arch "aarch64-apple-darwin")
else()
set(_wgpu_lib_arch "x86_64-apple-darwin")
endif()
endif()
set(_wgpu_lib_name "libwgpu_native.a")
elseif(ANDROID)
include("${CMAKE_CURRENT_LIST_DIR}/wgpu.android.cmake")
@@ -53,24 +57,37 @@ endif()
# Search platform-specific path first to prefer cross-compiled libraries
set(_wgpu_lib_search_paths
"${_mln_wgpu_source_dir}/target/${_wgpu_lib_arch}/release"
)
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
mln_wgpu_ios_append_search_paths(_wgpu_lib_search_paths)
endif()
list(APPEND _wgpu_lib_search_paths
"${_mln_wgpu_source_dir}/target/release"
"${_mln_wgpu_source_dir}/lib"
"${_mln_wgpu_source_dir}/build/release"
)
# On Android, prefer the static library to avoid needing to ship a separate .so
if(ANDROID)
mln_wgpu_android_find_library()
else()
# On Android/iOS, use manual path search since find_library may not work with cross-compilation toolchains
macro(mln_wgpu_find_library)
if(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
foreach(_search_path ${_wgpu_lib_search_paths})
if(EXISTS "${_search_path}/${_wgpu_lib_name}")
set(WGPU_LIBRARY "${_search_path}/${_wgpu_lib_name}" CACHE FILEPATH "wgpu-native library")
break()
endif()
endforeach()
else()
find_library(WGPU_LIBRARY
NAMES wgpu_native libwgpu_native
PATHS ${_wgpu_lib_search_paths}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
endif()
endif()
endmacro()
mln_wgpu_find_library()
# If not found, try to build it using cargo
if(NOT WGPU_LIBRARY)
message(STATUS "Pre-built wgpu-native library not found, attempting to build from source...")
@@ -89,8 +106,22 @@ if(NOT WGPU_LIBRARY)
# Build wgpu-native using cargo
message(STATUS "Building wgpu-native with cargo (this may take a few minutes)...")
# For cross-compilation targets (iOS, Android), pass --target and platform-specific features
set(_cargo_extra_args "")
if(_wgpu_lib_arch AND NOT _wgpu_lib_arch MATCHES "apple-darwin$")
list(APPEND _cargo_extra_args --target ${_wgpu_lib_arch})
endif()
if(_wgpu_cargo_features)
list(APPEND _cargo_extra_args ${_wgpu_cargo_features})
endif()
set(_cargo_env_prefix "")
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
mln_wgpu_ios_setup_cargo_env(_cargo_env_prefix)
endif()
execute_process(
COMMAND ${CARGO_EXECUTABLE} build --release
COMMAND ${_cargo_env_prefix} ${CARGO_EXECUTABLE} build --release ${_cargo_extra_args}
WORKING_DIRECTORY ${_mln_wgpu_source_dir}
RESULT_VARIABLE _cargo_result
OUTPUT_VARIABLE _cargo_output
@@ -105,11 +136,7 @@ if(NOT WGPU_LIBRARY)
endif()
# Try to find the library again
find_library(WGPU_LIBRARY
NAMES wgpu_native libwgpu_native
PATHS ${_wgpu_lib_search_paths}
NO_DEFAULT_PATH
)
mln_wgpu_find_library()
if(NOT WGPU_LIBRARY)
message(FATAL_ERROR "Failed to locate wgpu-native library after building")

30
vendor/wgpu.ios.cmake generated vendored Normal file
View File

@@ -0,0 +1,30 @@
if(CMAKE_OSX_SYSROOT MATCHES "[Ss]imulator")
set(_wgpu_lib_arch "aarch64-apple-ios-sim")
else()
set(_wgpu_lib_arch "aarch64-apple-ios")
endif()
set(_wgpu_cargo_features "--no-default-features;--features;metal,wgsl,spirv,glsl")
macro(mln_wgpu_ios_append_search_paths paths)
# With the Xcode generator, CMAKE_OSX_SYSROOT is "iphoneos" at configure time even when
# the user builds for simulator. Search both iOS architectures so the right one is found.
if(_wgpu_lib_arch STREQUAL "aarch64-apple-ios")
list(APPEND ${paths} "${_mln_wgpu_source_dir}/target/aarch64-apple-ios-sim/release")
else()
list(APPEND ${paths} "${_mln_wgpu_source_dir}/target/aarch64-apple-ios/release")
endif()
endmacro()
macro(mln_wgpu_ios_setup_cargo_env env_prefix_var)
# On iOS, bindgen's clang_macro_fallback() uses a separate clang process that
# doesn't inherit the .clang_arg() settings from build.rs. We must set
# BINDGEN_EXTRA_CLANG_ARGS so it can find system headers (needed for UINT32_MAX etc.)
execute_process(
COMMAND xcrun --sdk iphoneos --show-sdk-path
OUTPUT_VARIABLE _ios_sdk_path
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(${env_prefix_var} ${CMAKE_COMMAND} -E env "BINDGEN_EXTRA_CLANG_ARGS=--target=arm64-apple-ios -isysroot ${_ios_sdk_path}")
endmacro()