From 57a14cf110170fa8ec8abfa48a33022aecfcee04 Mon Sep 17 00:00:00 2001 From: Birk Skyum <74932975+birkskyum@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:24:14 +0100 Subject: [PATCH] Support for WebGPU backend in Android SDK (#4097) --- .github/workflows/android-ci.yml | 122 ++- CMakePresets.json | 30 + docs/mdbook/src/SUMMARY.md | 2 + .../android/android-rendering-backends.md | 53 ++ .../src/platforms/android/android-webgpu.md | 73 ++ .../android/MapLibreAndroid/build.gradle.kts | 25 + .../MapLibreAndroid/src/cpp/CMakeLists.txt | 16 + .../cpp/android_webgpu_renderer_backend.cpp | 776 ++++++++++++++++++ .../cpp/android_webgpu_renderer_backend.hpp | 53 ++ .../MapLibreAndroidTestApp/build.gradle.kts | 19 + .../activity/style/HeatmapLayerActivity.kt | 2 +- platform/android/android.cmake | 8 + render-test/android/app/build.gradle.kts | 19 + vendor/wgpu.android.cmake | 29 + vendor/wgpu.cmake | 26 +- 15 files changed, 1242 insertions(+), 11 deletions(-) create mode 100644 docs/mdbook/src/platforms/android/android-rendering-backends.md create mode 100644 docs/mdbook/src/platforms/android/android-webgpu.md create mode 100644 platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.cpp create mode 100644 platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.hpp create mode 100644 vendor/wgpu.android.cmake diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index f623a250a..14b55dcf2 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -311,6 +311,124 @@ jobs: with: flavor: ${{ matrix.flavor }} + android-build-webgpu-dawn: + runs-on: ubuntu-24.04 + needs: + - pre_job + if: needs.pre_job.outputs.should_skip != 'true' + defaults: + run: + working-directory: platform/android + env: + IS_LOCAL_DEVELOPMENT: false + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + with: + submodules: recursive + persist-credentials: false + + - uses: ./.github/actions/setup-android-ci + + - name: Configure AWS Credentials + if: vars.OIDC_AWS_ROLE_TO_ASSUME + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v4 + with: + aws-region: us-west-2 + role-to-assume: ${{ vars.OIDC_AWS_ROLE_TO_ASSUME }} + role-session-name: ${{ github.run_id }} + + - name: Configure sccache + run: | + echo SCCACHE_BUCKET=maplibre-native-sccache >> "$GITHUB_ENV" + echo SCCACHE_REGION=eu-central-1 >> "$GITHUB_ENV" + + if [ -z "${AWS_SECRET_ACCESS_KEY}" ]; then + echo "AWS_SECRET_ACCESS_KEY not set; not uploading sccache cache to S3" + echo SCCACHE_S3_NO_CREDENTIALS=1 >> "$GITHUB_ENV" + fi + + - name: Build WebGPU Dawn library for arm64-v8a + run: ./gradlew :MapLibreAndroid:assembleWebgpuDawnDebug -Pmaplibre.abis=arm64-v8a + + - name: Show sccache stats + run: sccache --show-stats + + android-build-webgpu-wgpu: + runs-on: ubuntu-24.04 + needs: + - pre_job + if: needs.pre_job.outputs.should_skip != 'true' + defaults: + run: + working-directory: platform/android + env: + IS_LOCAL_DEVELOPMENT: false + ANDROID_ARCH: arm64-v8a + RUST_TARGET: aarch64-linux-android + C_COMPILER: aarch64-linux-android26-clang + BINDGEN_TARGET: aarch64-linux-android26 + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + with: + submodules: recursive + persist-credentials: false + + - uses: ./.github/actions/setup-android-ci + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + toolchain: stable + + - name: Add Android Rust target + run: rustup target add ${{ env.RUST_TARGET }} + + - name: Configure cargo for Android NDK linker + run: | + LATEST_NDK=$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1) + NDK_BIN="$LATEST_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" + mkdir -p ../../vendor/wgpu-native/.cargo + cat > ../../vendor/wgpu-native/.cargo/config.toml << EOF + [target.${{ env.RUST_TARGET }}] + linker = "$NDK_BIN/${{ env.C_COMPILER }}" + EOF + + - name: Pre-build wgpu-native for Android + working-directory: vendor/wgpu-native + run: | + LATEST_NDK=$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1) + NDK_PREBUILT="$LATEST_NDK/toolchains/llvm/prebuilt/linux-x86_64" + RESOURCE_DIR=$("$NDK_PREBUILT/bin/clang" --print-resource-dir) + BINDGEN_EXTRA_CLANG_ARGS=$(echo "${{ env.RUST_TARGET }}" | tr '-' '_') + export BINDGEN_EXTRA_CLANG_ARGS + ENV_VAR="BINDGEN_EXTRA_CLANG_ARGS_$BINDGEN_EXTRA_CLANG_ARGS" + export "${ENV_VAR}=--sysroot=$NDK_PREBUILT/sysroot --target=${{ env.BINDGEN_TARGET }} -isystem $RESOURCE_DIR/include" + cargo build --release --target ${{ env.RUST_TARGET }} --no-default-features --features vulkan,wgsl,spirv,glsl + + - name: Configure AWS Credentials + if: vars.OIDC_AWS_ROLE_TO_ASSUME + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v4 + with: + aws-region: us-west-2 + role-to-assume: ${{ vars.OIDC_AWS_ROLE_TO_ASSUME }} + role-session-name: ${{ github.run_id }} + + - name: Configure sccache + run: | + echo SCCACHE_BUCKET=maplibre-native-sccache >> "$GITHUB_ENV" + echo SCCACHE_REGION=eu-central-1 >> "$GITHUB_ENV" + + if [ -z "${AWS_SECRET_ACCESS_KEY}" ]; then + echo "AWS_SECRET_ACCESS_KEY not set; not uploading sccache cache to S3" + echo SCCACHE_S3_NO_CREDENTIALS=1 >> "$GITHUB_ENV" + fi + + - name: Build WebGPU wgpu-native library for Android + run: ./gradlew :MapLibreAndroid:assembleWebgpuWgpuDebug -Pmaplibre.abis=${{ env.ANDROID_ARCH }} + + - name: Show sccache stats + run: sccache --show-stats + android-ci-result: runs-on: ubuntu-latest permissions: @@ -322,9 +440,11 @@ jobs: - android-build - android-build-cpp-test - android-build-render-test + - android-build-webgpu-dawn + - android-build-webgpu-wgpu steps: - name: Set success=true - if: needs.android-build.result == 'success' && needs.android-build-cpp-test.result == 'success' && needs.android-build-render-test.result == 'success' + if: needs.android-build.result == 'success' && needs.android-build-cpp-test.result == 'success' && needs.android-build-render-test.result == 'success' && needs.android-build-webgpu-dawn.result == 'success' && needs.android-build-webgpu-wgpu.result == 'success' run: echo success=true > "$GITHUB_ENV" - name: Mark result as failed diff --git a/CMakePresets.json b/CMakePresets.json index 87c6f394f..5d6841441 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -185,6 +185,36 @@ "MLN_WITH_X11": "ON" } }, + { + "name": "android-webgpu-dawn", + "displayName": "Android WebGPU (Dawn)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-android-webgpu-dawn", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_NDK}/build/cmake/android.toolchain.cmake", + "CMAKE_ANDROID_NDK": "$env{ANDROID_NDK}", + "ANDROID_ABI": "arm64-v8a", + "ANDROID_PLATFORM": "android-26", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL_DAWN": "ON" + } + }, + { + "name": "android-webgpu-wgpu", + "displayName": "Android WebGPU (wgpu-native)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-android-webgpu-wgpu", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_NDK}/build/cmake/android.toolchain.cmake", + "CMAKE_ANDROID_NDK": "$env{ANDROID_NDK}", + "ANDROID_ABI": "arm64-v8a", + "ANDROID_PLATFORM": "android-26", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL_WGPU": "ON" + } + }, { "name": "linux-opengl-node", "inherits": "linux", diff --git a/docs/mdbook/src/SUMMARY.md b/docs/mdbook/src/SUMMARY.md index 4aeb0e96a..f0e07352e 100644 --- a/docs/mdbook/src/SUMMARY.md +++ b/docs/mdbook/src/SUMMARY.md @@ -8,6 +8,8 @@ - [Documentation](./platforms/android/android-documentation.md) - [Benchmark](./platforms/android/benchmark.md) - [Release](./platforms/android/release.md) + - [Rendering Backends](./platforms/android/android-rendering-backends.md) + - [WebGPU](./platforms/android/android-webgpu.md) - [iOS](./platforms/ios/README.md) - [Tests](./platforms/ios/ios-tests.md) - [Documentation](./platforms/ios/ios-documentation.md) diff --git a/docs/mdbook/src/platforms/android/android-rendering-backends.md b/docs/mdbook/src/platforms/android/android-rendering-backends.md new file mode 100644 index 000000000..11588fdff --- /dev/null +++ b/docs/mdbook/src/platforms/android/android-rendering-backends.md @@ -0,0 +1,53 @@ +# Rendering Backends + +The rendering backend is selected at build time through Gradle product flavors. + +## Available backends + +| Flavor | Backend | Status | Notes | +|---|---|---|---| +| `opengl` | OpenGL ES | Stable | Default, widest device compatibility | +| `vulkan` | Vulkan | Stable | Better performance on modern devices | +| `webgpuDawn` | WebGPU (Dawn) | Experimental | Uses Vulkan internally, see [WebGPU](./android-webgpu.md) | +| `webgpuWgpu` | WebGPU (wgpu-native) | Experimental | Uses Vulkan internally, arm64-v8a only, see [WebGPU](./android-webgpu.md) | + +## Building with a specific backend + +The flavor name is part of the Gradle task. For the test app: + +```bash +cd platform/android + +# OpenGL ES +./gradlew :MapLibreAndroidTestApp:installOpenglDebug + +# Vulkan +./gradlew :MapLibreAndroidTestApp:installVulkanDebug + +# WebGPU (Dawn) +./gradlew :MapLibreAndroidTestApp:installWebgpuDawnDebug + +# WebGPU (wgpu-native) +./gradlew :MapLibreAndroidTestApp:installWebgpuWgpuDebug +``` + +The same pattern applies to the library module (`MapLibreAndroid`): + +```bash +./gradlew :MapLibreAndroid:assembleVulkanRelease +``` + +## Selecting in Android Studio + +In Android Studio, use the **Build Variants** panel (View > Tool Windows > Build Variants) to pick the renderer flavor. Each module will show a dropdown combining the flavor and build type, e.g. `vulkanDebug` or `openglRelease`. + +## How it works + +Each flavor sets different CMake arguments: + +- `opengl`: `-DMLN_WITH_OPENGL=ON` +- `vulkan`: `-DMLN_WITH_VULKAN=ON` +- `webgpuDawn`: `-DMLN_WITH_WEBGPU=ON`, `-DMLN_WEBGPU_IMPL_DAWN=ON` +- `webgpuWgpu`: `-DMLN_WITH_WEBGPU=ON`, `-DMLN_WEBGPU_IMPL_WGPU=ON` + +Only one backend is active per build. Renderer-specific Java code lives in `src/opengl/`, `src/vulkan/`, or `src/webgpu/` source sets. diff --git a/docs/mdbook/src/platforms/android/android-webgpu.md b/docs/mdbook/src/platforms/android/android-webgpu.md new file mode 100644 index 000000000..c61f5c52e --- /dev/null +++ b/docs/mdbook/src/platforms/android/android-webgpu.md @@ -0,0 +1,73 @@ +# WebGPU on Android + +Two WebGPU implementations are supported: **Dawn** (Google, C++) and **wgpu-native** (Rust). Both use Vulkan internally on Android. The Gradle product flavors are `webgpuDawn` and `webgpuWgpu`, sharing the `src/webgpu/` Java source set. + +## Building with Dawn + +Dawn is fetched and built from source via CMake `FetchContent`, so no extra setup is needed. First build is slow (~20 min) because of Dawn's dependencies (abseil, SPIRV-Tools, Tint, etc.). + +**Android Studio:** Open the **Build Variants** panel (View > Tool Windows > Build Variants) and select `webgpuDawnDebug` for both `:MapLibreAndroid` and `:MapLibreAndroidTestApp`, then run normally. + +**Terminal:** + +```bash +cd platform/android +./gradlew :MapLibreAndroidTestApp:installWebgpuDawnDebug +``` + +## Building with wgpu-native + +wgpu-native requires a pre-built static library for the target ABI. The `webgpuWgpu` flavor is currently limited to `arm64-v8a`. + +### 1. Cross-compile wgpu-native for Android + +Install the Rust Android target: + +```bash +rustup target add aarch64-linux-android +``` + +Point cargo at the NDK linker. Create or edit `~/.cargo/config.toml`: + +```toml +[target.aarch64-linux-android] +linker = "/toolchains/llvm/prebuilt//bin/aarch64-linux-android26-clang" +``` + +Replace `` with the path to your Android NDK and `` with your platform (e.g. `darwin-x86_64` on macOS). + +Then build: + +```bash +cd vendor/wgpu-native +cargo build --release --target aarch64-linux-android +``` + +This produces `vendor/wgpu-native/target/aarch64-linux-android/release/libwgpu_native.a`, which the CMake build picks up automatically. + +### 2. Build and install + +**Android Studio:** Select `webgpuWgpuDebug` in the Build Variants panel and run. + +**Terminal:** + +```bash +cd platform/android +./gradlew :MapLibreAndroidTestApp:installWebgpuWgpuDebug +``` + +## Debugging wgpu-native + +wgpu-native logs are routed to Android logcat under the `wgpu-native` tag (warnings and errors by default). Rust panic messages appear under `wgpu-native-stderr`. Filter with: + +```bash +adb logcat -s wgpu-native:W wgpu-native-stderr:E +``` + +To increase verbosity, change `WGPULogLevel_Warn` to `WGPULogLevel_Info` (or `Debug` / `Trace`) in `android_webgpu_renderer_backend.cpp`. + +## Notes + +The native backend is in `MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.cpp`. It uses `#if MLN_WEBGPU_IMPL_DAWN` / `#elif MLN_WEBGPU_IMPL_WGPU` for the API differences, same pattern as the desktop backends. + +wgpu-native's adapter request forces `backendType = Vulkan` — without this it may pick GL on some emulators, which can't present to an ANativeWindow surface. diff --git a/platform/android/MapLibreAndroid/build.gradle.kts b/platform/android/MapLibreAndroid/build.gradle.kts index ae93e794f..555194d1f 100644 --- a/platform/android/MapLibreAndroid/build.gradle.kts +++ b/platform/android/MapLibreAndroid/build.gradle.kts @@ -90,12 +90,37 @@ android { } } } + create("webgpuDawn") { + dimension = "renderer" + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_DAWN=ON") + } + } + } + create("webgpuWgpu") { + dimension = "renderer" + ndk { + abiFilters += "arm64-v8a" + } + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_WGPU=ON") + } + } + } } sourceSets { getByName("opengl") { java.srcDirs("src/opengl/java/") } + listOf("webgpuDawn", "webgpuWgpu").forEach { + getByName(it) { + java.srcDirs("src/vulkan/java") + manifest.srcFile("src/vulkan/AndroidManifest.xml") + } + } } // Build native libraries diff --git a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt index bc31ea6ea..b78316ead 100644 --- a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt +++ b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt @@ -217,6 +217,22 @@ if(MLN_WITH_VULKAN) ) endif() +if(MLN_WITH_WEBGPU) + target_sources( + maplibre PRIVATE + ${PROJECT_SOURCE_DIR}/android_webgpu_renderer_backend.cpp + ${PROJECT_SOURCE_DIR}/android_webgpu_renderer_backend.hpp + ) + target_compile_definitions(maplibre PRIVATE MLN_RENDER_BACKEND_WEBGPU=1) + if(MLN_WEBGPU_IMPL_DAWN AND TARGET mbgl-vendor-dawn) + target_compile_definitions(maplibre PRIVATE MLN_WEBGPU_IMPL_DAWN=1) + target_link_libraries(maplibre PRIVATE mbgl-vendor-dawn) + elseif(MLN_WEBGPU_IMPL_WGPU AND TARGET mbgl-vendor-wgpu) + target_compile_definitions(maplibre PRIVATE MLN_WEBGPU_IMPL_WGPU=1) + target_link_libraries(maplibre PRIVATE mbgl-vendor-wgpu) + endif() +endif() + target_include_directories( maplibre PRIVATE ${PROJECT_SOURCE_DIR} diff --git a/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.cpp b/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.cpp new file mode 100644 index 000000000..40fa22816 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.cpp @@ -0,0 +1,776 @@ +#include "android_webgpu_renderer_backend.hpp" + +#include +#include +#include +#include + +#include + +#if MLN_WEBGPU_IMPL_DAWN +#include +#elif MLN_WEBGPU_IMPL_WGPU +#include +#endif + +#include +#include + +#if MLN_WEBGPU_IMPL_WGPU +#include +#include +#include +#include +#include + +static void wgpuLogCallback(WGPULogLevel level, WGPUStringView message, void* /*userdata*/) { + const int androidLevel = [&]() { + switch (level) { + case WGPULogLevel_Error: + return ANDROID_LOG_ERROR; + case WGPULogLevel_Warn: + return ANDROID_LOG_WARN; + case WGPULogLevel_Info: + return ANDROID_LOG_INFO; + case WGPULogLevel_Debug: + return ANDROID_LOG_DEBUG; + case WGPULogLevel_Trace: + return ANDROID_LOG_VERBOSE; + default: + return ANDROID_LOG_INFO; + } + }(); + if (message.data && message.length > 0) { + __android_log_print(androidLevel, "wgpu-native", "%.*s", static_cast(message.length), message.data); + } +} + +// Redirect stderr to logcat so Rust panic messages are visible +static void startStderrToLogcat() { + static std::once_flag flag; + std::call_once(flag, [] { + int pipefd[2]; + if (pipe(pipefd) == -1) return; + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + + std::thread([fd = pipefd[0]]() { + char buf[1024]; + ssize_t n; + while ((n = read(fd, buf, sizeof(buf) - 1)) > 0) { + buf[n] = '\0'; + __android_log_print(ANDROID_LOG_ERROR, "wgpu-native-stderr", "%s", buf); + } + close(fd); + }).detach(); + }); +} +#endif + +namespace mbgl { +namespace android { + +#if MLN_WEBGPU_IMPL_DAWN +static std::string errorTypeToString(wgpu::ErrorType type) { + switch (type) { + case wgpu::ErrorType::NoError: + return "NoError"; + case wgpu::ErrorType::Validation: + return "Validation"; + case wgpu::ErrorType::OutOfMemory: + return "OutOfMemory"; + case wgpu::ErrorType::Internal: + return "Internal"; + case wgpu::ErrorType::Unknown: + return "Unknown"; + default: + return "Unknown"; + } +} + +static std::string deviceLostReasonToString(wgpu::DeviceLostReason reason) { + switch (reason) { + case wgpu::DeviceLostReason::Unknown: + return "Unknown"; + case wgpu::DeviceLostReason::Destroyed: + return "Destroyed"; + case wgpu::DeviceLostReason::FailedCreation: + return "FailedCreation"; + case wgpu::DeviceLostReason::CallbackCancelled: + return "CallbackCancelled"; + default: + return "Unknown"; + } +} +#endif + +class AndroidWebGPURenderableResource final : public webgpu::RenderableResource { +public: + AndroidWebGPURenderableResource(AndroidWebGPURendererBackend& 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: + AndroidWebGPURendererBackend& backend; +}; + +class AndroidWebGPURendererBackend::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; + + Size framebufferSize; +}; + +AndroidWebGPURendererBackend::AndroidWebGPURendererBackend(ANativeWindow* window_) + : webgpu::RendererBackend(gfx::ContextMode::Unique), + gfx::Renderable({64, 64}, std::make_unique(*this)), + impl(std::make_unique()), + window(window_) { +#if MLN_WEBGPU_IMPL_WGPU + startStderrToLogcat(); + wgpuSetLogLevel(WGPULogLevel_Warn); + wgpuSetLogCallback(wgpuLogCallback, nullptr); +#endif + +#if MLN_WEBGPU_IMPL_DAWN + 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); + + std::vector adapters = impl->instance->EnumerateAdapters(); + if (adapters.empty()) { + Log::Error(Event::Render, "WebGPU: no adapters found"); + return; + } + + auto findVulkanAdapter = [&]() -> dawn::native::Adapter* { + for (auto& a : adapters) { + WGPUAdapterInfo info = WGPU_ADAPTER_INFO_INIT; + if (wgpuAdapterGetInfo(a.Get(), &info) == WGPUStatus_Success) { + const auto backendType = info.backendType; + wgpuAdapterInfoFreeMembers(info); + if (backendType == WGPUBackendType_Vulkan) { + return &a; + } + } + } + return &adapters.front(); + }; + + dawn::native::Adapter& selectedAdapter = *findVulkanAdapter(); + + WGPUSupportedFeatures features = {}; + wgpuAdapterGetFeatures(selectedAdapter.Get(), &features); +#elif MLN_WEBGPU_IMPL_WGPU + wgpu::InstanceDescriptor instanceDesc = {}; + impl->instance = wgpu::createInstance(instanceDesc); + + if (!impl->instance) { + Log::Error(Event::Render, "WebGPU: failed to create instance"); + return; + } + + // Force Vulkan — the GL backend can't present to an ANativeWindow surface. + wgpu::RequestAdapterOptions adapterOpts = {}; + adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance; + adapterOpts.backendType = wgpu::BackendType::Vulkan; + + impl->adapter = impl->instance.requestAdapter(adapterOpts); + if (!impl->adapter) { + Log::Error(Event::Render, "WebGPU: no adapter found"); + return; + } + + WGPUSupportedFeatures features = {}; + wgpuAdapterGetFeatures(static_cast(impl->adapter), &features); +#endif + + std::vector requiredFeatures; + wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Depth24PlusStencil8; + if (features.featureCount > 0 && features.features) { + for (size_t i = 0; i < features.featureCount; ++i) { + if (static_cast(features.features[i]) == wgpu::FeatureName::Depth32FloatStencil8) { + requiredFeatures.push_back(wgpu::FeatureName::Depth32FloatStencil8); + depthStencilFormat = wgpu::TextureFormat::Depth32FloatStencil8; + break; + } + } + wgpuSupportedFeaturesFreeMembers(features); + } + + wgpu::DeviceDescriptor deviceDesc = {}; +#if MLN_WEBGPU_IMPL_DAWN + deviceDesc.label = "MapLibre Android WebGPU Device"; + deviceDesc.SetUncapturedErrorCallback([](const wgpu::Device&, wgpu::ErrorType type, wgpu::StringView message) { + Log::Error(Event::Render, + std::string("Dawn error [") + errorTypeToString(type) + "] " + + (message.data ? std::string(message.data, message.length) : std::string())); + }); + deviceDesc.SetDeviceLostCallback( + wgpu::CallbackMode::AllowSpontaneous, + [](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) { + Log::Error(Event::Render, + std::string("Dawn device lost [") + deviceLostReasonToString(reason) + "] " + + (message.data ? std::string(message.data, message.length) : std::string())); + }); + 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) { + Log::Error(Event::Render, "WebGPU: 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) { + Log::Error(Event::Render, "WebGPU: 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); + + createSurface(); + + const int w = ANativeWindow_getWidth(window); + const int h = ANativeWindow_getHeight(window); + if (w > 0 && h > 0) { + impl->framebufferSize = {static_cast(w), static_cast(h)}; + size = impl->framebufferSize; + configureSurface(impl->framebufferSize.width, impl->framebufferSize.height); + } +} + +AndroidWebGPURendererBackend::~AndroidWebGPURendererBackend() { + if (impl) { + 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 AndroidWebGPURendererBackend::createSurface() { +#if MLN_WEBGPU_IMPL_DAWN + if (!impl->instance || !window) return; + + WGPUSurfaceSourceAndroidNativeWindow androidDesc = WGPU_SURFACE_SOURCE_ANDROID_NATIVE_WINDOW_INIT; + androidDesc.window = window; + + WGPUSurfaceDescriptor surfaceDesc = WGPU_SURFACE_DESCRIPTOR_INIT; + surfaceDesc.nextInChain = &androidDesc.chain; + + WGPUInstance rawInstance = impl->instance->Get(); + impl->surface = wgpuInstanceCreateSurface(rawInstance, &surfaceDesc); +#elif MLN_WEBGPU_IMPL_WGPU + if (!impl->instance || !window) return; + + WGPUSurfaceSourceAndroidNativeWindow androidDesc = {}; + androidDesc.chain.sType = WGPUSType_SurfaceSourceAndroidNativeWindow; + androidDesc.chain.next = nullptr; + androidDesc.window = window; + + WGPUSurfaceDescriptor surfaceDesc = {}; + surfaceDesc.nextInChain = &androidDesc.chain; + + impl->surface = wgpuInstanceCreateSurface(static_cast(impl->instance), &surfaceDesc); +#endif + + if (!impl->surface) { + Log::Error(Event::Render, "WebGPU: failed to create surface"); + } +} + +void AndroidWebGPURendererBackend::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 AndroidWebGPURendererBackend::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) { + Log::Warning(Event::Render, "WebGPU: 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) { + Log::Warning(Event::Render, "WebGPU: failed to create depth/stencil view"); + } +} + +void AndroidWebGPURendererBackend::resizeFramebuffer(int width, int height) { + if (width <= 0 || height <= 0) return; + + impl->framebufferSize = {static_cast(width), static_cast(height)}; + size = impl->framebufferSize; + + if (impl->currentTextureView) { + wgpuTextureViewRelease(impl->currentTextureView); + impl->currentTextureView = nullptr; + } + if (impl->currentTexture) { + wgpuTextureRelease(impl->currentTexture); + impl->currentTexture = nullptr; + } + + configureSurface(impl->framebufferSize.width, impl->framebufferSize.height); +} + +PremultipliedImage AndroidWebGPURendererBackend::readFramebuffer() { + if (!impl->currentTexture || !impl->device || !impl->queue || impl->framebufferSize.width == 0 || + impl->framebufferSize.height == 0) { + return PremultipliedImage(impl->framebufferSize.isEmpty() ? Size(2, 2) : impl->framebufferSize); + } + + 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; + + auto data = std::make_unique(static_cast(rowStride) * 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 = {"Readback Buffer", strlen("Readback Buffer")}; + bufferDesc.label = bufferLabel; +#endif + bufferDesc.size = alignedDataSize; + bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; + + WGPUBuffer stagingBuffer = wgpuDeviceCreateBuffer(device, &bufferDesc); + if (!stagingBuffer) { + Log::Error(Event::Render, "WebGPU readFramebuffer: failed to create staging buffer"); + return {fbSize, std::move(data)}; + } + + mbgl::Scoped scopedCleanup([stagingBuffer]() { + wgpuBufferUnmap(stagingBuffer); + wgpuBufferRelease(stagingBuffer); + }); + + 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; + + WGPUWaitStatus waitStatus = wgpuInstanceWaitAny(instance, 1, &waitInfo, UINT64_MAX); + if (waitStatus != WGPUWaitStatus_Success || !waitInfo.completed) { + Log::Error(Event::Render, "WebGPU readFramebuffer: WaitAny failed"); + } + } +#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)); + } + } + if (!mapContext.completed) { + Log::Error(Event::Render, "WebGPU readFramebuffer: buffer mapping timeout after polling"); + } +#endif + + if (mapContext.status == WGPUMapAsyncStatus_Success) { + const auto* mappedData = static_cast( + wgpuBufferGetConstMappedRange(stagingBuffer, 0, alignedDataSize)); + if (mappedData) { + 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 = data.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); + } + } + } + } else { + Log::Error(Event::Render, "WebGPU readFramebuffer: buffer mapping failed"); + } + + presentSurface(); + + return {fbSize, std::move(data)}; +} + +void* AndroidWebGPURendererBackend::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; + + // Actual texture size may differ from configured size (Vulkan currentExtent clamping) + const uint32_t texW = wgpuTextureGetWidth(surfaceTexture.texture); + const uint32_t texH = wgpuTextureGetHeight(surfaceTexture.texture); + if (texW != size.width || texH != size.height) { + if (texW > 0 && texH > 0) { + size = {texW, texH}; + impl->framebufferSize = size; + createDepthStencilTexture(texW, texH); + } + } + +#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 AndroidWebGPURendererBackend::markNeedsPresent() { + impl->needsPresent = true; +} + +void AndroidWebGPURendererBackend::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* AndroidWebGPURendererBackend::getDepthStencilView() { + return impl->depthStencilView; +} + +mbgl::Size AndroidWebGPURendererBackend::getFramebufferSize() const { + return impl->framebufferSize; +} + +} // namespace android +} // namespace mbgl + +namespace mbgl { +namespace gfx { + +template <> +std::unique_ptr Backend::Create( + ANativeWindow* window) { + return std::make_unique(window); +} + +} // namespace gfx +} // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.hpp b/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.hpp new file mode 100644 index 000000000..68ab28ccf --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/android_webgpu_renderer_backend.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include "android_renderer_backend.hpp" +#include + +#if MLN_WEBGPU_IMPL_WGPU +#include +#endif + +namespace mbgl { +namespace android { + +class AndroidWebGPURendererBackend : public AndroidRendererBackend, + public webgpu::RendererBackend, + public gfx::Renderable { +public: + AndroidWebGPURendererBackend(ANativeWindow*); + ~AndroidWebGPURendererBackend() override; + + ANativeWindow* getWindow() { return window; } + mbgl::gfx::RendererBackend& getImpl() override { return *this; } + + void resizeFramebuffer(int width, int height) override; + PremultipliedImage readFramebuffer() override; + +public: + mbgl::gfx::Renderable& getDefaultRenderable() override { return *this; } + + void* getCurrentTextureView() override; + void* getDepthStencilView() override; + mbgl::Size getFramebufferSize() const override; + + void presentSurface(); + void markNeedsPresent(); + +protected: + void activate() override {} + void deactivate() override {} + +private: + void createSurface(); + void configureSurface(uint32_t width, uint32_t height); + void createDepthStencilTexture(uint32_t width, uint32_t height); + + class Impl; + std::unique_ptr impl; + ANativeWindow* window; +}; + +} // namespace android +} // namespace mbgl diff --git a/platform/android/MapLibreAndroidTestApp/build.gradle.kts b/platform/android/MapLibreAndroidTestApp/build.gradle.kts index 8eaf99ec9..6b2cf1e45 100644 --- a/platform/android/MapLibreAndroidTestApp/build.gradle.kts +++ b/platform/android/MapLibreAndroidTestApp/build.gradle.kts @@ -93,6 +93,25 @@ android { } } } + create("webgpuDawn") { + dimension = "renderer" + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_DAWN=ON") + } + } + } + create("webgpuWgpu") { + dimension = "renderer" + ndk { + abiFilters += "arm64-v8a" + } + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_WGPU=ON") + } + } + } } buildFeatures { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt index cabaaabcd..0e4337303 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt @@ -36,7 +36,7 @@ class HeatmapLayerActivity : AppCompatActivity() { Style.Builder() .fromUri(TestStyles.getPredefinedStyleWithFallback("Pastel")) .withSource(createEarthquakeSource()) - .withLayerAbove(createHeatmapLayer(), "country_label") + .withLayer(createHeatmapLayer()) .withLayerBelow(createCircleLayer(), HEATMAP_LAYER_ID) ) } catch (exception: URISyntaxException) { diff --git a/platform/android/android.cmake b/platform/android/android.cmake index cb9e28e5a..106764ac7 100644 --- a/platform/android/android.cmake +++ b/platform/android/android.cmake @@ -91,6 +91,14 @@ if(MLN_WITH_VULKAN) ) endif() +if(MLN_WITH_WEBGPU) + target_sources( + mbgl-core + PRIVATE + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/headless_backend.cpp + ) +endif() + target_include_directories( mbgl-core PRIVATE ${PROJECT_SOURCE_DIR}/platform/default/include diff --git a/render-test/android/app/build.gradle.kts b/render-test/android/app/build.gradle.kts index 00bd54a0a..aca56f883 100644 --- a/render-test/android/app/build.gradle.kts +++ b/render-test/android/app/build.gradle.kts @@ -74,6 +74,25 @@ android { } } } + create("webgpuDawn") { + dimension = "renderer" + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_DAWN=ON") + } + } + } + create("webgpuWgpu") { + dimension = "renderer" + ndk { + abiFilters += "arm64-v8a" + } + externalNativeBuild { + cmake { + arguments("-DMLN_WITH_WEBGPU=ON", "-DMLN_WEBGPU_IMPL_WGPU=ON") + } + } + } } namespace = "android.app" diff --git a/vendor/wgpu.android.cmake b/vendor/wgpu.android.cmake new file mode 100644 index 000000000..ad8e57b27 --- /dev/null +++ b/vendor/wgpu.android.cmake @@ -0,0 +1,29 @@ +if(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(_wgpu_lib_arch "aarch64-linux-android") +elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a") + set(_wgpu_lib_arch "armv7-linux-androideabi") +elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") + set(_wgpu_lib_arch "x86_64-linux-android") +elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") + set(_wgpu_lib_arch "i686-linux-android") +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) + target_link_libraries(${target} INTERFACE + Threads::Threads + ${CMAKE_DL_LIBS} + log + android + ) +endmacro() diff --git a/vendor/wgpu.cmake b/vendor/wgpu.cmake index 58c7f65d6..feec65974 100644 --- a/vendor/wgpu.cmake +++ b/vendor/wgpu.cmake @@ -31,7 +31,8 @@ if(APPLE) set(_wgpu_lib_arch "x86_64-apple-darwin") endif() set(_wgpu_lib_name "libwgpu_native.a") - set(_wgpu_lib_suffix ".dylib") +elseif(ANDROID) + include("${CMAKE_CURRENT_LIST_DIR}/wgpu.android.cmake") elseif(WIN32) if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(_wgpu_lib_arch "x86_64-pc-windows-msvc") @@ -39,7 +40,6 @@ elseif(WIN32) set(_wgpu_lib_arch "i686-pc-windows-msvc") endif() set(_wgpu_lib_name "wgpu_native.lib") - set(_wgpu_lib_suffix ".dll") else() # Linux if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") set(_wgpu_lib_arch "aarch64-unknown-linux-gnu") @@ -47,22 +47,28 @@ else() # Linux set(_wgpu_lib_arch "x86_64-unknown-linux-gnu") endif() set(_wgpu_lib_name "libwgpu_native.a") - set(_wgpu_lib_suffix ".so") endif() # Look for prebuilt wgpu-native library +# Search platform-specific path first to prefer cross-compiled libraries set(_wgpu_lib_search_paths - "${_mln_wgpu_source_dir}/target/release" "${_mln_wgpu_source_dir}/target/${_wgpu_lib_arch}/release" + "${_mln_wgpu_source_dir}/target/release" "${_mln_wgpu_source_dir}/lib" "${_mln_wgpu_source_dir}/build/release" ) -find_library(WGPU_LIBRARY - NAMES wgpu_native libwgpu_native - PATHS ${_wgpu_lib_search_paths} - NO_DEFAULT_PATH -) +# 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() # If not found, try to build it using cargo if(NOT WGPU_LIBRARY) @@ -235,6 +241,8 @@ elseif(WIN32) bcrypt ntdll ) +elseif(ANDROID) + mln_wgpu_android_link_library(mbgl-vendor-wgpu) else() # Linux # wgpu-native on Linux might need these depending on the build configuration find_package(Threads REQUIRED)