diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 32fe5f879..046a57fd2 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -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' diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ea257888..9a680480d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/CMakePresets.json b/CMakePresets.json index 5d6841441..d55efb6e2 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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, diff --git a/docs/mdbook/src/SUMMARY.md b/docs/mdbook/src/SUMMARY.md index f0e07352e..741199521 100644 --- a/docs/mdbook/src/SUMMARY.md +++ b/docs/mdbook/src/SUMMARY.md @@ -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) diff --git a/docs/mdbook/src/platforms/README.md b/docs/mdbook/src/platforms/README.md index 278d17f08..bdae4623b 100644 --- a/docs/mdbook/src/platforms/README.md +++ b/docs/mdbook/src/platforms/README.md @@ -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] | ✅ | diff --git a/docs/mdbook/src/platforms/ios/ios-rendering-backends.md b/docs/mdbook/src/platforms/ios/ios-rendering-backends.md new file mode 100644 index 000000000..f18de8d63 --- /dev/null +++ b/docs/mdbook/src/platforms/ios/ios-rendering-backends.md @@ -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`. diff --git a/docs/mdbook/src/platforms/ios/ios-webgpu.md b/docs/mdbook/src/platforms/ios/ios-webgpu.md new file mode 100644 index 000000000..2fa890d6b --- /dev/null +++ b/docs/mdbook/src/platforms/ios/ios-webgpu.md @@ -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//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`. diff --git a/platform/darwin/darwin.cmake b/platform/darwin/darwin.cmake index d9bc2c707..5eb7538a3 100644 --- a/platform/darwin/darwin.cmake +++ b/platform/darwin/darwin.cmake @@ -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 - "${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" -) +# 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" + ) + if(MLN_WITH_METAL) + list(APPEND _custom_layer_sources "${CMAKE_CURRENT_LIST_DIR}/app/PluginLayerExampleMetalRendering.mm") + endif() -target_link_libraries( - custom-layer-examples - PUBLIC ios-sdk-static - PRIVATE mbgl-compiler-options mbgl-core -) + add_library( + custom-layer-examples + EXCLUDE_FROM_ALL + ${_custom_layer_sources} + ) -target_include_directories( - custom-layer-examples - PUBLIC - "${CMAKE_CURRENT_LIST_DIR}/app" - PRIVATE - "${PROJECT_SOURCE_DIR}/src" # FIXME: should not use private headers -) + target_link_libraries( + custom-layer-examples + PUBLIC ios-sdk-static + PRIVATE mbgl-compiler-options mbgl-core + ) + + target_include_directories( + custom-layer-examples + PUBLIC + "${CMAKE_CURRENT_LIST_DIR}/app" + PRIVATE + "${PROJECT_SOURCE_DIR}/src" # FIXME: should not use private headers + ) +endif() diff --git a/platform/ios/app/CMakeLists.txt b/platform/ios/app/CMakeLists.txt index 38bb9f98e..5a78a33cf 100644 --- a/platform/ios/app/CMakeLists.txt +++ b/platform/ios/app/CMakeLists.txt @@ -48,10 +48,17 @@ add_custom_command(TARGET app POST_BUILD COMMAND ls "$" ) -target_link_libraries( - app - custom-layer-examples # ios-sdk-static transitive dependency -) +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( diff --git a/platform/ios/app/MBXSnapshotTestViewController.h b/platform/ios/app/MBXSnapshotTestViewController.h new file mode 100644 index 000000000..bcf660bd1 --- /dev/null +++ b/platform/ios/app/MBXSnapshotTestViewController.h @@ -0,0 +1,9 @@ +#import + +@class MLNMapView; + +@interface MBXSnapshotTestViewController : UIViewController + +@property (nonatomic, weak) MLNMapView *sourceMapView; + +@end diff --git a/platform/ios/app/MBXSnapshotTestViewController.mm b/platform/ios/app/MBXSnapshotTestViewController.mm new file mode 100644 index 000000000..085b5df76 --- /dev/null +++ b/platform/ios/app/MBXSnapshotTestViewController.mm @@ -0,0 +1,96 @@ +#import "MBXSnapshotTestViewController.h" +#import "MLNMapView+Impl.h" +#import "MLNMapView_Private.h" +#import "Mapbox.h" + +@interface MBXSnapshotTestViewController () +@property (nonatomic) UIImageView *snapshotImageView; +@property (nonatomic, weak) id originalDelegate; +@property (nonatomic, weak) UIView *originalSuperview; +@property (nonatomic) NSArray *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 diff --git a/platform/ios/app/MBXSnapshotsViewController.m b/platform/ios/app/MBXSnapshotsViewController.m index e88c6dba2..c7eea94da 100644 --- a/platform/ios/app/MBXSnapshotsViewController.m +++ b/platform/ios/app/MBXSnapshotsViewController.m @@ -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 diff --git a/platform/ios/app/MBXViewController.mm b/platform/ios/app/MBXViewController.mm index c14d843ca..0ecdd35a6 100644 --- a/platform/ios/app/MBXViewController.mm +++ b/platform/ios/app/MBXViewController.mm @@ -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"]]; diff --git a/platform/ios/ios.cmake b/platform/ios/ios.cmake index 1f80ce81c..2eca63fcb 100644 --- a/platform/ios/ios.cmake +++ b/platform/ios/ios.cmake @@ -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) diff --git a/platform/ios/src/MLNMapView+Impl.mm b/platform/ios/src/MLNMapView+Impl.mm index 75d1382a2..2102bc007 100644 --- a/platform/ios/src/MLNMapView+Impl.mm +++ b/platform/ios/src/MLNMapView+Impl.mm @@ -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::Create(MLNMapView* nativeView) { #if MLN_RENDER_BACKEND_METAL return std::make_unique(nativeView); -#else // MLN_RENDER_BACKEND_OPENGL +#elif MLN_RENDER_BACKEND_WEBGPU + return std::make_unique(nativeView); +#elif MLN_RENDER_BACKEND_OPENGL return std::make_unique(nativeView); +#else +#error "No render backend defined" + return nullptr; #endif } diff --git a/platform/ios/src/MLNMapView+WebGPU.h b/platform/ios/src/MLNMapView+WebGPU.h new file mode 100644 index 000000000..e900df1b4 --- /dev/null +++ b/platform/ios/src/MLNMapView+WebGPU.h @@ -0,0 +1,50 @@ +#import "MLNMapView+Impl.h" +#import "MLNMapView_Private.h" + +#include +#include + +/// 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; +}; diff --git a/platform/ios/src/MLNMapView+WebGPU.mm b/platform/ios/src/MLNMapView+WebGPU.mm new file mode 100644 index 000000000..857728a0f --- /dev/null +++ b/platform/ios/src/MLNMapView+WebGPU.mm @@ -0,0 +1,802 @@ +#import "MLNDisplayUtils.h" +#import "MLNFoundation_Private.h" +#import "MLNLoggingConfiguration_Private.h" +#import "MLNMapView+WebGPU.h" + +#import +#import + +#import +#import + +#include + +#if MLN_WEBGPU_IMPL_DAWN +#include +#include +#include +#include +#elif MLN_WEBGPU_IMPL_WGPU +#include +#include +#endif + +#include +#include + +#if MLN_WEBGPU_IMPL_WGPU +#include +#include +#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(backend.getCurrentTextureView()); + } + + WGPUTextureView getDepthStencilTextureView() override { + return static_cast(backend.getDepthStencilView()); + } + +private: + MLNMapViewWebGPUImpl& backend; +}; + +class MLNMapViewWebGPUImpl::Impl { +public: +#if MLN_WEBGPU_IMPL_DAWN + std::unique_ptr 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(*this)), + impl(std::make_unique()) { +#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(&instanceDesc); + + wgpu::RequestAdapterOptions metalOpts = {}; + metalOpts.backendType = wgpu::BackendType::Metal; + std::vector 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(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(impl->adapter), &features); + if (features.featureCount > 0 && features.features) { + for (size_t i = 0; i < features.featureCount; ++i) { + if (static_cast(features.features[i]) == + wgpu::FeatureName::Depth32FloatStencil8) { + hasDepth32FloatStencil8 = true; + break; + } + } + wgpuSupportedFeaturesFreeMembers(features); + } +#endif + + std::vector 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(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(impl->device)); + setQueue(static_cast(impl->queue)); + setInstance(static_cast(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 = 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(drawableSize.width); + uint32_t h = static_cast(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(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(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(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(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(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(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(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(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(drawableSize.width); + uint32_t h = static_cast(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(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(impl->device); + auto queue = static_cast(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, ©Size); + + 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(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( + wgpuBufferGetConstMappedRange(stagingBuffer, 0, alignedDataSize)); + if (mappedData) { + auto rgbaData = std::make_unique(static_cast(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(row) * alignedRowStride; + auto* dst = rgbaData.get() + static_cast(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; + } +} diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index b47c9feb7..67f019eea 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -8,8 +8,10 @@ #include #include #include +#if MLN_RENDER_BACKEND_METAL #include #include +#endif #include #include #include @@ -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(*paintParameters.renderPass); - id encoder = - (__bridge id)renderPass.getMetalEncoder().get(); + id encoder = (__bridge id)renderPass.getMetalEncoder().get(); +#else + id encoder = nil; +#endif MLNMapView *strongMapView = weakMapView; diff --git a/vendor/dawn.cmake b/vendor/dawn.cmake index 8b739a2f7..48c05e122 100644 --- a/vendor/dawn.cmake +++ b/vendor/dawn.cmake @@ -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 $ 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 + $ + $ + ) +endif() target_include_directories(mbgl-vendor-dawn SYSTEM INTERFACE diff --git a/vendor/dawn.ios.cmake b/vendor/dawn.ios.cmake new file mode 100644 index 000000000..813da948c --- /dev/null +++ b/vendor/dawn.ios.cmake @@ -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) diff --git a/vendor/wgpu.android.cmake b/vendor/wgpu.android.cmake index ad8e57b27..a6ccc623f 100644 --- a/vendor/wgpu.android.cmake +++ b/vendor/wgpu.android.cmake @@ -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) diff --git a/vendor/wgpu.cmake b/vendor/wgpu.cmake index feec65974..8042c7b76 100644 --- a/vendor/wgpu.cmake +++ b/vendor/wgpu.cmake @@ -25,10 +25,14 @@ endif() # Platform-specific library names and paths if(APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") - set(_wgpu_lib_arch "aarch64-apple-darwin") + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + include("${CMAKE_CURRENT_LIST_DIR}/wgpu.ios.cmake") else() - set(_wgpu_lib_arch "x86_64-apple-darwin") + 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) @@ -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() - find_library(WGPU_LIBRARY - NAMES wgpu_native libwgpu_native - PATHS ${_wgpu_lib_search_paths} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - ) -endif() +# 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() +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") diff --git a/vendor/wgpu.ios.cmake b/vendor/wgpu.ios.cmake new file mode 100644 index 000000000..9d4ba04e2 --- /dev/null +++ b/vendor/wgpu.ios.cmake @@ -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()