diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index eec390f04..9bef2da3d 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -46,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - renderer: [opengl, vulkan] + renderer: [opengl, vulkan, multiBackend] defaults: run: working-directory: platform/android @@ -169,6 +169,7 @@ jobs: case "${{ matrix.renderer }}" in opengl) echo "renderer=OpenGL" >> "$GITHUB_ENV" ;; vulkan) echo "renderer=Vulkan" >> "$GITHUB_ENV" ;; + multiBackend) echo "renderer=MultiBackend" >> "$GITHUB_ENV" ;; *) echo "::error ::Unknown renderer '${{ matrix.renderer }}'"; exit 1 ;; esac diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index ee184e0f4..686a26d74 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -74,6 +74,7 @@ jobs: RENDERER: - vulkan - opengl + - multiBackend BUILDTYPE: - Release - Debug @@ -133,8 +134,12 @@ jobs: if [ "$renderer" = "opengl" ]; then ./gradlew :MapLibreAndroid:publishOpengl${{ env.buildtype }}PublicationToMavenCentralRepository ./gradlew :MapLibreAndroid:publishDefault${{ env.buildtype }}PublicationToMavenCentralRepository - else + elif [ "$renderer" = "vulkan" ]; then ./gradlew :MapLibreAndroid:publishVulkan${{ env.buildtype }}PublicationToMavenCentralRepository + elif [ "$renderer" = "multiBackend" ]; then + ./gradlew :MapLibreAndroid:publishMultibackend${{ env.buildtype }}PublicationToMavenCentralRepository + else + echo "::error ::Unknown renderer '$renderer'"; exit 1 fi env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} diff --git a/.github/workflows/android-testapp-release.yml b/.github/workflows/android-testapp-release.yml index d5efa767a..c3b9f4703 100644 --- a/.github/workflows/android-testapp-release.yml +++ b/.github/workflows/android-testapp-release.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - renderer: [opengl, vulkan] + renderer: [opengl, vulkan, multibackend] defaults: run: working-directory: platform/android diff --git a/docs/mdbook/src/platforms/android/android-rendering-backends.md b/docs/mdbook/src/platforms/android/android-rendering-backends.md index 11588fdff..193169b0a 100644 --- a/docs/mdbook/src/platforms/android/android-rendering-backends.md +++ b/docs/mdbook/src/platforms/android/android-rendering-backends.md @@ -4,12 +4,15 @@ 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) | +| Flavor | Backend | Status | Maven Central artifact | Notes | +|---|---|---|---|---| +| `opengl` | OpenGL ES | Stable | `org.maplibre.gl:android-sdk-opengl` | Default, widest device compatibility | +| `vulkan` | Vulkan | Stable | `org.maplibre.gl:android-sdk-vulkan` | Better performance on modern devices | +| `webgpuDawn` | WebGPU (Dawn) | Experimental | not published | Uses Vulkan internally, see [WebGPU](./android-webgpu.md) | +| `webgpuWgpu` | WebGPU (wgpu-native) | Experimental | not published | Uses Vulkan internally, arm64-v8a only, see [WebGPU](./android-webgpu.md) | +| `multiBackend` | OpenGL ES + Vulkan | Experimental | `org.maplibre.gl:android-sdk-vulkan-opengl` | Both backends in one AAR, chosen at runtime | + +`org.maplibre.gl:android-sdk` is also published and currently maps to the `vulkan` flavor's release build. `-debug` suffixed artifact (e.g. `android-sdk-vulkan-debug`) is published for each flavor as well. ## Building with a specific backend @@ -29,6 +32,9 @@ cd platform/android # WebGPU (wgpu-native) ./gradlew :MapLibreAndroidTestApp:installWebgpuWgpuDebug + +# Both OpenGL ES and Vulkan, backend chosen at runtime +./gradlew :MapLibreAndroidTestApp:installMultiBackendDebug ``` The same pattern applies to the library module (`MapLibreAndroid`): @@ -49,5 +55,8 @@ Each flavor sets different CMake arguments: - `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` +- `multiBackend`: `-DMLN_ANDROID_MULTI_BACKEND=ON` -Only one backend is active per build. Renderer-specific Java code lives in `src/opengl/`, `src/vulkan/`, or `src/webgpu/` source sets. +For `opengl`, `vulkan`, and the WebGPU flavors, only one backend is compiled in and it's fixed for the lifetime of the app. `multiBackend` is the exception - it ships both the OpenGL ES and Vulkan native libraries and picks one at runtime. + +Renderer-specific Java code is split between flavor-specific source sets (`src/opengl/`, `src/vulkan/`, `src/multiBackend/`) and shared per-backend code in `src/sharedRenderer/opengl/` and `src/sharedRenderer/vulkan/`. diff --git a/docs/mdbook/src/platforms/android/android-webgpu.md b/docs/mdbook/src/platforms/android/android-webgpu.md index c61f5c52e..2d1fc5eb0 100644 --- a/docs/mdbook/src/platforms/android/android-webgpu.md +++ b/docs/mdbook/src/platforms/android/android-webgpu.md @@ -1,6 +1,6 @@ # 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. +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/vulkan/` and `src/sharedRenderer/vulkan/` Java source sets. ## Building with Dawn diff --git a/platform/android/.gitignore b/platform/android/.gitignore index 17dbdfca6..6f79a27d8 100644 --- a/platform/android/.gitignore +++ b/platform/android/.gitignore @@ -1,2 +1,3 @@ -MapLibreAndroid/src/vulkanDebug +MapLibreAndroid/src/vulkanDebug/jniLibs +MapLibreAndroid/src/multiBackendDebug/jniLibs prefab-headers/ diff --git a/platform/android/Makefile b/platform/android/Makefile index 6eebedc40..894d7b37b 100644 --- a/platform/android/Makefile +++ b/platform/android/Makefile @@ -16,8 +16,9 @@ endif ifeq ($(RENDERER), opengl) else ifeq ($(RENDERER), vulkan) +else ifeq ($(RENDERER), multiBackend) else - $(error RENDERER must be 'opengl' or 'vulkan') + $(error RENDERER must be 'opengl' or 'vulkan' or 'multiBackend') endif buildtype := $(shell echo "$(BUILDTYPE)" | tr "[A-Z]" "[a-z]") diff --git a/platform/android/MapLibreAndroid/build.gradle.kts b/platform/android/MapLibreAndroid/build.gradle.kts index 56708debf..5a645ac39 100644 --- a/platform/android/MapLibreAndroid/build.gradle.kts +++ b/platform/android/MapLibreAndroid/build.gradle.kts @@ -109,18 +109,38 @@ android { } } } + create("multiBackend") { + dimension = "renderer" + externalNativeBuild { + cmake { + arguments("-DMLN_ANDROID_MULTI_BACKEND=ON") + targets("maplibre-opengl", "maplibre-vulkan") + } + } + } } sourceSets { getByName("opengl") { - java.srcDirs("src/opengl/java/") + java.srcDirs("src/opengl/java/", "src/sharedRenderer/opengl/java/") + } + getByName("vulkan") { + java.srcDirs("src/vulkan/java/", "src/sharedRenderer/vulkan/java/") } listOf("webgpuDawn", "webgpuWgpu").forEach { getByName(it) { - java.srcDirs("src/vulkan/java") + java.srcDirs("src/vulkan/java/", "src/sharedRenderer/vulkan/java/") manifest.srcFile("src/vulkan/AndroidManifest.xml") } } + getByName("multiBackend") { + java.srcDirs( + "src/multiBackend/java/", + "src/sharedRenderer/opengl/java/", + "src/sharedRenderer/vulkan/java/" + ) + manifest.srcFile("src/multiBackend/AndroidManifest.xml") + } } // Build native libraries @@ -227,7 +247,5 @@ configurations { } } -// apply() - // intentionally disabled // apply(plugin = "maplibre.jacoco-report") diff --git a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt index 61a178fbc..a1c8a73e0 100644 --- a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt +++ b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt @@ -2,8 +2,18 @@ cmake_minimum_required(VERSION 3.10) project(MapLibreAndroid) -add_subdirectory(../../../../../ - ../../../../../${ANDROID_ABI}) +# (a) Multi-flavor build is self-contained in MultiBackend.cmake and only entered for the +# Gradle `multiBackend` flavor (which passes -DMLN_ANDROID_MULTI_BACKEND=ON). It never touches +# the shared top-level CMakeLists; the single-backend path below is unaffected. +if(MLN_ANDROID_MULTI_BACKEND) + include(${CMAKE_CURRENT_SOURCE_DIR}/MultiBackend.cmake) + return() +endif() + +# (b) Build core into THIS configure's own build dir (was: ../../../../../${ANDROID_ABI}, an +# out-of-tree path both multi sub-builds collided on). Each configure now has its own dir, so +# the multi sub-builds isolate automatically -- no MLN_ANDROID_TOP_BINARY_DIR guard needed. +add_subdirectory(../../../../../ ${CMAKE_CURRENT_BINARY_DIR}/maplibre-core) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/platform/android/MapLibreAndroid/src/cpp/MultiBackend.cmake b/platform/android/MapLibreAndroid/src/cpp/MultiBackend.cmake new file mode 100644 index 000000000..a4a7dd4e8 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/MultiBackend.cmake @@ -0,0 +1,85 @@ +# Android multi-backend build (Gradle `multiBackend` flavor only). +# +# Builds libmaplibre.so (Vulkan) and libmaplibre-opengl.so (OpenGL) by invoking this directory's +# stock single-backend build twice via ExternalProject, and surfaces both to AGP as SHARED +# targets so both .so land in the multiBackend AAR. +# +# Reached via include() from src/cpp/CMakeLists.txt while project(MapLibreAndroid) is in effect, +# so the add_library targets below land in the AGP-invoked MapLibreAndroid project (AGP's CMake +# file-API query only sees targets declared there) and CMAKE_CURRENT_SOURCE_DIR stays src/cpp. +include(ExternalProject) + +set(_src ${CMAKE_CURRENT_SOURCE_DIR}) # src/cpp -- the single-backend entry + +# Forward the exact Android toolchain AGP handed this configure to each sub-build. Each sub-build +# takes the single-backend path (MLN_ANDROID_MULTI_BACKEND=OFF) and builds core into its own +# BINARY_DIR (see src/cpp/CMakeLists.txt edit (b)), so the two sub-builds isolate automatically. +set(_fwd + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DANDROID_ABI=${ANDROID_ABI} + -DANDROID_PLATFORM=${ANDROID_PLATFORM} + -DANDROID_NDK=${ANDROID_NDK} + -DANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN} + -DANDROID_STL=${ANDROID_STL} + -DANDROID_CPP_FEATURES=${ANDROID_CPP_FEATURES} + -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=${ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DMLN_ANDROID_MULTI_BACKEND=OFF) + +# Distinct BINARY_DIR per backend => distinct core build dir => no shared static-lib / LTO dir. +set(_gl ${CMAKE_BINARY_DIR}/multi-opengl) +set(_vk ${CMAKE_BINARY_DIR}/multi-vulkan) + +ExternalProject_Add(maplibre-opengl-build + SOURCE_DIR ${_src} + BINARY_DIR ${_gl} + CMAKE_ARGS + ${_fwd} + -DMLN_WITH_OPENGL=ON + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${_gl}/lib + BUILD_COMMAND ${CMAKE_COMMAND} --build --target maplibre + INSTALL_COMMAND "" + BUILD_ALWAYS 1) + +ExternalProject_Add(maplibre-vulkan-build + SOURCE_DIR ${_src} + BINARY_DIR ${_vk} + CMAKE_ARGS + ${_fwd} + -DMLN_WITH_VULKAN=ON + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${_vk}/lib + BUILD_COMMAND ${CMAKE_COMMAND} --build --target maplibre + INSTALL_COMMAND "" + BUILD_ALWAYS 1) + +# Surface both externally-built .so to AGP via stub SHARED targets whose output file is +# overwritten with the real backend .so. AGP packages targets it builds; ExternalProject outputs +# are opaque to it and IMPORTED targets aren't packaged -- hence this bridge. +set(_stub ${CMAKE_BINARY_DIR}/maplibre_multi_stub.cpp) +file(WRITE ${_stub} "extern \"C\" void maplibre_multi_backend_stub() {}\n") + +add_library(maplibre-opengl SHARED ${_stub}) # -> libmaplibre-opengl.so +add_dependencies(maplibre-opengl maplibre-opengl-build) +add_custom_command(TARGET maplibre-opengl POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${_gl}/lib/libmaplibre.so + $ + VERBATIM) + +add_library(maplibre-vulkan SHARED ${_stub}) +# Produce libmaplibre.so (not libmaplibre-vulkan.so): Vulkan is the multiBackend AAR's +# default/primary library, matching the SDK-wide convention where Vulkan is the default +# backend and OpenGL ships as the explicitly-named "-opengl" variant. +set_target_properties(maplibre-vulkan PROPERTIES OUTPUT_NAME maplibre) +add_dependencies(maplibre-vulkan maplibre-vulkan-build) +add_custom_command(TARGET maplibre-vulkan POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${_vk}/lib/libmaplibre.so + $ + VERBATIM) + +# AGP's defaultConfig nativeTargets always lists "maplibre"; provide a custom target so +# `cmake --build . --target maplibre` triggers both .so builds. +add_custom_target(maplibre DEPENDS maplibre-opengl maplibre-vulkan) + +install(TARGETS maplibre-opengl maplibre-vulkan LIBRARY DESTINATION lib) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java index 62c19e426..b5ae77792 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java @@ -53,23 +53,7 @@ public final class MapLibre { @UiThread @NonNull public static synchronized MapLibre getInstance(@NonNull Context context) { - ThreadUtils.init(context); - ThreadUtils.checkThread(TAG); - if (INSTANCE == null) { - Context appContext = context.getApplicationContext(); - FileSource.initializeFileDirsPaths(appContext); - INSTANCE = new MapLibre(appContext, null); - ConnectivityReceiver.instance(appContext); - } - - TileServerOptions tileServerOptions = TileServerOptions.get(WellKnownTileServer.MapLibre); - INSTANCE.tileServerOptions = tileServerOptions; - INSTANCE.apiKey = null; - FileSource fileSource = FileSource.getInstance(context); - fileSource.setTileServerOptions(tileServerOptions); - fileSource.setApiKey(null); - - return INSTANCE; + return getInstance(context, null, WellKnownTileServer.MapLibre); } /** @@ -89,12 +73,42 @@ public final class MapLibre { @UiThread @NonNull public static synchronized MapLibre getInstance(@NonNull Context context, @Nullable String apiKey, - WellKnownTileServer tileServer) { + @NonNull WellKnownTileServer tileServer) { + return getInstance(context, apiKey, tileServer, null); + } + + /** + * Get an instance of MapLibre, explicitly picking the rendering engine to use. + *

+ * This class manages the API key, application context, and connectivity state. + *

+ * + * @param context Android context which holds or is an application context + * @param apiKey api key + * @param tileServer the tile server whose predefined configuration will be used to + * bootstrap the SDK. The predefined configuration includes + * rules for converting resource URLs between normal and canonical forms + * and set of predefined styles available on the server. + * @param type the rendering engine to use; ignored if MapLibre has already been + * initialized in this process. If null, the default rendering engine will be used. + * @return the single instance of MapLibre + * @throws UnsupportedOperationException if {@code type} isn't supported by this + * build of the SDK (single-backend flavors only support their compiled-in backend). + */ + @UiThread + @NonNull + public static synchronized MapLibre getInstance(@NonNull Context context, @Nullable String apiKey, + @NonNull WellKnownTileServer tileServer, + @Nullable RenderingEngine.Type type) { ThreadUtils.init(context); ThreadUtils.checkThread(TAG); if (INSTANCE == null) { Timber.plant(); Context appContext = context.getApplicationContext(); + if (type == null) { + type = RenderingEngine.getDefaultRenderingEngine(appContext); + } + RenderingEngine.setCurrentType(type); FileSource.initializeFileDirsPaths(appContext); INSTANCE = new MapLibre(appContext, apiKey); ConnectivityReceiver.instance(appContext); diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/MapRendererFactory.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/MapRendererFactory.java similarity index 50% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/MapRendererFactory.java rename to platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/MapRendererFactory.java index 64320ecd3..fecbeddb8 100644 --- a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/MapRendererFactory.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/MapRendererFactory.java @@ -7,18 +7,25 @@ import android.view.TextureView; import androidx.annotation.Keep; import androidx.annotation.NonNull; -import org.maplibre.android.maps.renderer.surfaceview.GLSurfaceViewMapRenderer; -import org.maplibre.android.maps.renderer.surfaceview.MapLibreGLSurfaceView; import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; -import org.maplibre.android.maps.renderer.textureview.GLTextureViewRenderThread; import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; +/** + * Shared factory used by MapRenderer.create(). The shape of renderer construction + * (anonymous subclass wiring of initCallback, render-thread attachment) lives here; + * the backend-specific concrete-type instantiation is delegated to the + * flavor-provided {@link RendererStrategy}. + */ @Keep -public class MapRendererFactory { - public static TextureViewMapRenderer newTextureViewMapRenderer(@NonNull Context context, TextureView textureView, - String localFontFamily, boolean translucentSurface, - Runnable initCallback) { +public final class MapRendererFactory { + private MapRendererFactory() {} + + public static TextureViewMapRenderer newTextureViewMapRenderer(@NonNull Context context, + TextureView textureView, + String localFontFamily, + boolean translucentSurface, + Runnable initCallback) { TextureViewMapRenderer mapRenderer = new TextureViewMapRenderer(context, textureView, localFontFamily, translucentSurface) { @Override @@ -27,23 +34,15 @@ public class MapRendererFactory { super.onSurfaceCreated(surface); } }; - - mapRenderer.setRenderThread(new GLTextureViewRenderThread(textureView, mapRenderer)); + RendererStrategy.attachTextureRenderThread(textureView, mapRenderer); return mapRenderer; } - public static SurfaceViewMapRenderer newSurfaceViewMapRenderer(@NonNull Context context, String localFontFamily, - boolean renderSurfaceOnTop, Runnable initCallback) { - - MapLibreGLSurfaceView surfaceView = new MapLibreGLSurfaceView(context); - surfaceView.setZOrderMediaOverlay(renderSurfaceOnTop); - - return new GLSurfaceViewMapRenderer(context, surfaceView, localFontFamily) { - @Override - public void onSurfaceCreated(Surface surface) { - initCallback.run(); - super.onSurfaceCreated(surface); - } - }; + public static SurfaceViewMapRenderer newSurfaceViewMapRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + return RendererStrategy.createSurfaceViewRenderer( + context, localFontFamily, renderSurfaceOnTop, initCallback); } } diff --git a/platform/android/MapLibreAndroid/src/multiBackend/AndroidManifest.xml b/platform/android/MapLibreAndroid/src/multiBackend/AndroidManifest.xml new file mode 100644 index 000000000..5322b8f37 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/multiBackend/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/RenderingEngine.java b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/RenderingEngine.java new file mode 100644 index 000000000..fb1bf0fd1 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/RenderingEngine.java @@ -0,0 +1,70 @@ +package org.maplibre.android; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +/** + * multiBackend flavor: the rendering engine is selectable at runtime. + * + *

Call {@link #setCurrentType(Type)} before the first call to + * {@link MapLibre#getInstance(android.content.Context)} (or any other path that + * triggers native library loading). {@link #setCurrentType(Type)} throws once + * {@link MapLibre#getInstance(android.content.Context)} has been called, since the + * native library is loaded by then and the selection can no longer take effect for + * the current process.

+ */ +@Keep +public final class RenderingEngine { + + /** + * Matches the android.hardware.vulkan.version `uses-feature` declared for this flavor. + */ + private static final int REQUIRED_VULKAN_VERSION = 0x400003; + + public enum Type { + OPENGL, + VULKAN + } + + private static volatile Type currentType = Type.VULKAN; + + private RenderingEngine() {} + + @NonNull + public static Type getCurrentType() { + return currentType; + } + + /** + * @throws IllegalStateException if {@link MapLibre#getInstance(android.content.Context)} + * has already been called in this process. + */ + static void setCurrentType(@NonNull Type type) { + if (MapLibre.hasInstance()) { + throw new IllegalStateException( + "RenderingEngine.setCurrentType() must be called before MapLibre.getInstance()."); + } + currentType = type; + } + + /** + * Determines which rendering engine to use when none was explicitly requested: + * Vulkan if this device's hardware supports it, OpenGL otherwise. + */ + @NonNull + static Type getDefaultRenderingEngine(@NonNull Context context) { + return isVulkanSupported(context) ? Type.VULKAN : Type.OPENGL; + } + + private static boolean isVulkanSupported(@NonNull Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return false; + } + return context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, REQUIRED_VULKAN_VERSION); + } +} diff --git a/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/maps/renderer/RendererStrategy.java b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/maps/renderer/RendererStrategy.java new file mode 100644 index 000000000..884dde3fa --- /dev/null +++ b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/maps/renderer/RendererStrategy.java @@ -0,0 +1,41 @@ +package org.maplibre.android.maps.renderer; + +import android.content.Context; +import android.view.TextureView; + +import androidx.annotation.NonNull; + +import org.maplibre.android.RenderingEngine; +import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; + +/** + * multiBackend flavor glue. Dispatches to {@link OpenGLRendererStrategy} or + * {@link VulkanRendererStrategy} based on {@link RenderingEngine#getCurrentType()} + * at the moment the renderer is constructed. + */ +final class RendererStrategy { + + private RendererStrategy() {} + + static void attachTextureRenderThread(@NonNull TextureView textureView, + @NonNull TextureViewMapRenderer renderer) { + if (RenderingEngine.getCurrentType() == RenderingEngine.Type.VULKAN) { + VulkanRendererStrategy.attachTextureRenderThread(textureView, renderer); + } else { + OpenGLRendererStrategy.attachTextureRenderThread(textureView, renderer); + } + } + + static SurfaceViewMapRenderer createSurfaceViewRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + if (RenderingEngine.getCurrentType() == RenderingEngine.Type.VULKAN) { + return VulkanRendererStrategy.createSurfaceViewRenderer( + context, localFontFamily, renderSurfaceOnTop, initCallback); + } + return OpenGLRendererStrategy.createSurfaceViewRenderer( + context, localFontFamily, renderSurfaceOnTop, initCallback); + } +} diff --git a/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java new file mode 100644 index 000000000..d38ea8fd0 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/multiBackend/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java @@ -0,0 +1,31 @@ +package org.maplibre.android.module.loader; + +import org.maplibre.android.LibraryLoader; +import org.maplibre.android.LibraryLoaderProvider; +import org.maplibre.android.RenderingEngine; + +/** + * multiBackend flavor: when asked to load "maplibre", routes to either + * libmaplibre.so (Vulkan, the default name) or libmaplibre-opengl.so based on + * {@link RenderingEngine#getCurrentType()}. Any other library name is loaded + * verbatim. + */ +public class LibraryLoaderProviderImpl implements LibraryLoaderProvider { + + @Override + public LibraryLoader getDefaultLibraryLoader() { + return new MultiBackendLibraryLoader(); + } + + private static class MultiBackendLibraryLoader extends LibraryLoader { + @Override + public void load(String name) { + if ("maplibre".equals(name) + && RenderingEngine.getCurrentType() == RenderingEngine.Type.OPENGL) { + System.loadLibrary("maplibre-opengl"); + } else { + System.loadLibrary(name); + } + } + } +} diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/RenderingEngine.java b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/RenderingEngine.java new file mode 100644 index 000000000..52622a3b3 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/RenderingEngine.java @@ -0,0 +1,70 @@ +package org.maplibre.android; + +import android.content.Context; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +/** + * The rendering engine baked into this build of the SDK. + * + *

In single-backend flavors (opengl, vulkan, ...) the engine is fixed at build + * time and {@link #setCurrentType(Type)} only accepts the matching value. In the + * multiBackend flavor this class is mutable and {@code setCurrentType(...)} must + * be called before {@link MapLibre#getInstance(android.content.Context)} to pick + * a backend for the process lifetime.

+ */ +@Keep +public final class RenderingEngine { + + /** Supported rendering backends. WebGPU flavors report the closest match. */ + public enum Type { + OPENGL, + VULKAN + } + + private static volatile Type currentType = Type.OPENGL; + + private RenderingEngine() {} + + /** + * @return the rendering backend used by the currently loaded library. + */ + @NonNull + public static Type getCurrentType() { + return currentType; + } + + /** + * This flavor's engine is fixed at compile time, so there's nothing to select — + * always {@link Type#OPENGL}. + */ + @NonNull + static Type getDefaultRenderingEngine(@NonNull Context context) { + return Type.OPENGL; + } + + /** + * Single-backend flavor: this is a no-op when {@code type} matches the + * compiled-in backend, and throws otherwise. Use the multiBackend flavor of + * the SDK if you need to switch backends at runtime. + * + * @param type the desired backend + * @throws IllegalStateException if {@link MapLibre#getInstance(android.content.Context)} + * has already been called in this process. + * @throws UnsupportedOperationException if {@code type} differs from the + * compiled-in backend + */ + static void setCurrentType(@NonNull Type type) { + if (MapLibre.hasInstance()) { + throw new IllegalStateException( + "RenderingEngine.setCurrentType() must be called before MapLibre.getInstance()."); + } + if (type != Type.OPENGL) { + throw new UnsupportedOperationException( + "This MapLibre Android build supports only " + Type.OPENGL + + ". Use the multiBackend flavor to switch backends at runtime."); + } + currentType = type; + } +} diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/RendererStrategy.java b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/RendererStrategy.java new file mode 100644 index 000000000..ddd02dd10 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/RendererStrategy.java @@ -0,0 +1,32 @@ +package org.maplibre.android.maps.renderer; + +import android.content.Context; +import android.view.TextureView; + +import androidx.annotation.NonNull; + +import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; + +/** + * OpenGL flavor glue. Delegates straight to {@link OpenGLRendererStrategy}. + * Exists only so the shared {@link MapRendererFactory} in main can call + * {@code RendererStrategy.X(...)} without a per-flavor import. + */ +final class RendererStrategy { + + private RendererStrategy() {} + + static void attachTextureRenderThread(@NonNull TextureView textureView, + @NonNull TextureViewMapRenderer renderer) { + OpenGLRendererStrategy.attachTextureRenderThread(textureView, renderer); + } + + static SurfaceViewMapRenderer createSurfaceViewRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + return OpenGLRendererStrategy.createSurfaceViewRenderer( + context, localFontFamily, renderSurfaceOnTop, initCallback); + } +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java similarity index 64% rename from platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java rename to platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java index 02ea2b843..d6a340c95 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java +++ b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java @@ -4,25 +4,15 @@ import org.maplibre.android.LibraryLoader; import org.maplibre.android.LibraryLoaderProvider; /** - * Concrete implementation of a native library loader. - *

- *

+ * OpenGL flavor: loads the single-backend libmaplibre.so via System.loadLibrary. */ public class LibraryLoaderProviderImpl implements LibraryLoaderProvider { - /** - * Creates and returns a the default Library Loader. - * - * @return the default library loader - */ @Override public LibraryLoader getDefaultLibraryLoader() { return new SystemLibraryLoader(); } - /** - * Concrete implementation of a LibraryLoader using System.loadLibrary. - */ private static class SystemLibraryLoader extends LibraryLoader { @Override public void load(String name) { diff --git a/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/OpenGLRendererStrategy.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/OpenGLRendererStrategy.java new file mode 100644 index 000000000..1b9b167b5 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/OpenGLRendererStrategy.java @@ -0,0 +1,42 @@ +package org.maplibre.android.maps.renderer; + +import android.content.Context; +import android.view.Surface; +import android.view.TextureView; + +import androidx.annotation.NonNull; + +import org.maplibre.android.maps.renderer.surfaceview.GLSurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.surfaceview.MapLibreGLSurfaceView; +import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.GLTextureViewRenderThread; +import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; + +/** + * OpenGL concrete impl behind {@link MapRendererFactory}. Lives alongside the + * other GL renderer helpers in src/sharedRenderer/opengl/. + */ +final class OpenGLRendererStrategy { + + private OpenGLRendererStrategy() {} + + static void attachTextureRenderThread(@NonNull TextureView textureView, + @NonNull TextureViewMapRenderer renderer) { + renderer.setRenderThread(new GLTextureViewRenderThread(textureView, renderer)); + } + + static SurfaceViewMapRenderer createSurfaceViewRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + MapLibreGLSurfaceView surfaceView = new MapLibreGLSurfaceView(context); + surfaceView.setZOrderMediaOverlay(renderSurfaceOnTop); + return new GLSurfaceViewMapRenderer(context, surfaceView, localFontFamily) { + @Override + public void onSurfaceCreated(Surface surface) { + initCallback.run(); + super.onSurfaceCreated(surface); + } + }; + } +} diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLConfigChooser.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLConfigChooser.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLConfigChooser.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLConfigChooser.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLContextFactory.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLContextFactory.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLContextFactory.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLContextFactory.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLLogWrapper.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLLogWrapper.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLLogWrapper.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLLogWrapper.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLWindowSurfaceFactory.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLWindowSurfaceFactory.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/EGLWindowSurfaceFactory.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/EGLWindowSurfaceFactory.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/package-info.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/package-info.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/egl/package-info.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/egl/package-info.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/GLSurfaceViewMapRenderer.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/surfaceview/GLSurfaceViewMapRenderer.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/GLSurfaceViewMapRenderer.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/surfaceview/GLSurfaceViewMapRenderer.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/textureview/GLTextureViewRenderThread.java b/platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/textureview/GLTextureViewRenderThread.java similarity index 100% rename from platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/textureview/GLTextureViewRenderThread.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/opengl/java/org/maplibre/android/maps/renderer/textureview/GLTextureViewRenderThread.java diff --git a/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/VulkanRendererStrategy.java b/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/VulkanRendererStrategy.java new file mode 100644 index 000000000..8d9c8c58f --- /dev/null +++ b/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/VulkanRendererStrategy.java @@ -0,0 +1,42 @@ +package org.maplibre.android.maps.renderer; + +import android.content.Context; +import android.view.Surface; +import android.view.TextureView; + +import androidx.annotation.NonNull; + +import org.maplibre.android.maps.renderer.surfaceview.MapLibreVulkanSurfaceView; +import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.surfaceview.VulkanSurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.VulkanTextureViewRenderThread; + +/** + * Vulkan concrete impl behind {@link MapRendererFactory}. Lives alongside the + * other Vulkan renderer helpers in src/sharedRenderer/vulkan/. + */ +final class VulkanRendererStrategy { + + private VulkanRendererStrategy() {} + + static void attachTextureRenderThread(@NonNull TextureView textureView, + @NonNull TextureViewMapRenderer renderer) { + renderer.setRenderThread(new VulkanTextureViewRenderThread(textureView, renderer)); + } + + static SurfaceViewMapRenderer createSurfaceViewRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + MapLibreVulkanSurfaceView surfaceView = new MapLibreVulkanSurfaceView(context); + surfaceView.setZOrderMediaOverlay(renderSurfaceOnTop); + return new VulkanSurfaceViewMapRenderer(context, surfaceView, localFontFamily) { + @Override + public void onSurfaceCreated(Surface surface) { + initCallback.run(); + super.onSurfaceCreated(surface); + } + }; + } +} diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreVulkanSurfaceView.java b/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreVulkanSurfaceView.java similarity index 100% rename from platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreVulkanSurfaceView.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreVulkanSurfaceView.java diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/VulkanSurfaceViewMapRenderer.java b/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/VulkanSurfaceViewMapRenderer.java similarity index 100% rename from platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/VulkanSurfaceViewMapRenderer.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/surfaceview/VulkanSurfaceViewMapRenderer.java diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/textureview/VulkanTextureViewRenderThread.java b/platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/textureview/VulkanTextureViewRenderThread.java similarity index 100% rename from platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/textureview/VulkanTextureViewRenderThread.java rename to platform/android/MapLibreAndroid/src/sharedRenderer/vulkan/java/org/maplibre/android/maps/renderer/textureview/VulkanTextureViewRenderThread.java diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/RenderingEngine.java b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/RenderingEngine.java new file mode 100644 index 000000000..e9c9c44a8 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/RenderingEngine.java @@ -0,0 +1,56 @@ +package org.maplibre.android; + +import android.content.Context; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +/** + * Vulkan (and WebGPU) flavor: the rendering engine is fixed at build time. + * See the opengl flavor's javadoc for the multiBackend contrast. + */ +@Keep +public final class RenderingEngine { + + public enum Type { + OPENGL, + VULKAN + } + + private static volatile Type currentType = Type.VULKAN; + + private RenderingEngine() {} + + @NonNull + public static Type getCurrentType() { + return currentType; + } + + /** + * This flavor's engine is fixed at compile time, so there's nothing to select — + * always {@link Type#VULKAN}. + */ + @NonNull + static Type getDefaultRenderingEngine(@NonNull Context context) { + return Type.VULKAN; + } + + /** + * @throws IllegalStateException if {@link MapLibre#getInstance(android.content.Context)} + * has already been called in this process. + * @throws UnsupportedOperationException if {@code type} differs from the + * compiled-in backend + */ + static void setCurrentType(@NonNull Type type) { + if (MapLibre.hasInstance()) { + throw new IllegalStateException( + "RenderingEngine.setCurrentType() must be called before MapLibre.getInstance()."); + } + if (type != Type.VULKAN) { + throw new UnsupportedOperationException( + "This MapLibre Android build supports only " + Type.VULKAN + + ". Use the multiBackend flavor to switch backends at runtime."); + } + currentType = type; + } +} diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/MapRendererFactory.java b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/MapRendererFactory.java deleted file mode 100644 index 2ac787831..000000000 --- a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/MapRendererFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.maplibre.android.maps.renderer; - -import android.content.Context; -import android.view.Surface; -import android.view.TextureView; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; - -import org.maplibre.android.maps.renderer.surfaceview.MapLibreVulkanSurfaceView; -import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; -import org.maplibre.android.maps.renderer.surfaceview.VulkanSurfaceViewMapRenderer; -import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; -import org.maplibre.android.maps.renderer.textureview.VulkanTextureViewRenderThread; - -@Keep -public class MapRendererFactory { - public static TextureViewMapRenderer newTextureViewMapRenderer(@NonNull Context context, TextureView textureView, - String localFontFamily, boolean translucentSurface, - Runnable initCallback) { - - TextureViewMapRenderer mapRenderer = new TextureViewMapRenderer(context, textureView, - localFontFamily, translucentSurface) { - @Override - protected void onSurfaceCreated(Surface surface) { - initCallback.run(); - super.onSurfaceCreated(surface); - } - }; - - mapRenderer.setRenderThread(new VulkanTextureViewRenderThread(textureView, mapRenderer)); - return mapRenderer; - } - - public static SurfaceViewMapRenderer newSurfaceViewMapRenderer(@NonNull Context context, String localFontFamily, - boolean renderSurfaceOnTop, Runnable initCallback) { - - MapLibreVulkanSurfaceView surfaceView = new MapLibreVulkanSurfaceView(context); - surfaceView.setZOrderMediaOverlay(renderSurfaceOnTop); - - return new VulkanSurfaceViewMapRenderer(context, surfaceView, localFontFamily) { - @Override - public void onSurfaceCreated(Surface surface) { - initCallback.run(); - super.onSurfaceCreated(surface); - } - }; - } -} diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/RendererStrategy.java b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/RendererStrategy.java new file mode 100644 index 000000000..4d8213f77 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/maps/renderer/RendererStrategy.java @@ -0,0 +1,32 @@ +package org.maplibre.android.maps.renderer; + +import android.content.Context; +import android.view.TextureView; + +import androidx.annotation.NonNull; + +import org.maplibre.android.maps.renderer.surfaceview.SurfaceViewMapRenderer; +import org.maplibre.android.maps.renderer.textureview.TextureViewMapRenderer; + +/** + * Vulkan flavor glue. Delegates straight to {@link VulkanRendererStrategy}. + * Exists only so the shared {@link MapRendererFactory} in main can call + * {@code RendererStrategy.X(...)} without a per-flavor import. + */ +final class RendererStrategy { + + private RendererStrategy() {} + + static void attachTextureRenderThread(@NonNull TextureView textureView, + @NonNull TextureViewMapRenderer renderer) { + VulkanRendererStrategy.attachTextureRenderThread(textureView, renderer); + } + + static SurfaceViewMapRenderer createSurfaceViewRenderer(@NonNull Context context, + String localFontFamily, + boolean renderSurfaceOnTop, + Runnable initCallback) { + return VulkanRendererStrategy.createSurfaceViewRenderer( + context, localFontFamily, renderSurfaceOnTop, initCallback); + } +} diff --git a/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java new file mode 100644 index 000000000..c6da8917d --- /dev/null +++ b/platform/android/MapLibreAndroid/src/vulkan/java/org/maplibre/android/module/loader/LibraryLoaderProviderImpl.java @@ -0,0 +1,23 @@ +package org.maplibre.android.module.loader; + +import org.maplibre.android.LibraryLoader; +import org.maplibre.android.LibraryLoaderProvider; + +/** + * Vulkan and WebGPU flavors: loads the single-backend libmaplibre.so via + * System.loadLibrary. + */ +public class LibraryLoaderProviderImpl implements LibraryLoaderProvider { + + @Override + public LibraryLoader getDefaultLibraryLoader() { + return new SystemLibraryLoader(); + } + + private static class SystemLibraryLoader extends LibraryLoader { + @Override + public void load(String name) { + System.loadLibrary(name); + } + } +} diff --git a/platform/android/MapLibreAndroidTestApp/build.gradle.kts b/platform/android/MapLibreAndroidTestApp/build.gradle.kts index f664ec276..402ed28e7 100644 --- a/platform/android/MapLibreAndroidTestApp/build.gradle.kts +++ b/platform/android/MapLibreAndroidTestApp/build.gradle.kts @@ -115,6 +115,15 @@ android { } } } + create("multiBackend") { + dimension = "renderer" + externalNativeBuild { + cmake { + arguments("-DMLN_ANDROID_MULTI_BACKEND=ON") + targets("maplibre-opengl", "maplibre-vulkan") + } + } + } } buildFeatures { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/MapLibreApplication.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/MapLibreApplication.kt index 4968be903..82097ba39 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/MapLibreApplication.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/MapLibreApplication.kt @@ -4,12 +4,14 @@ import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy import android.text.TextUtils +import android.widget.Toast import androidx.multidex.MultiDexApplication import org.maplibre.android.MapStrictMode import org.maplibre.android.MapLibre import org.maplibre.android.WellKnownTileServer import org.maplibre.android.log.Logger import org.maplibre.android.testapp.utils.ApiKeyUtils +import org.maplibre.android.testapp.utils.RenderingEnginePreference import org.maplibre.android.testapp.utils.TileLoadingMeasurementUtils import org.maplibre.android.testapp.utils.TimberLogger import timber.log.Timber @@ -61,11 +63,37 @@ open class MapLibreApplication : MultiDexApplication() { if (apiKey != null) { validateApiKey(apiKey) } - MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER) + initializeMapLibre(apiKey) TileLoadingMeasurementUtils.setUpTileLoadingMeasurement() MapStrictMode.setStrictModeEnabled(true) } + // The rendering engine is bound to the MapLibre instance at construction time, so a + // persisted choice has to be passed into getInstance() itself rather than set + // separately beforehand. A build installed over a different flavor (same + // applicationId across flavors) may have a persisted choice the compiled-in backend + // doesn't support, so an unsupported choice can't be allowed to crash startup — fall + // back to the default engine instead. + // + // If nothing is persisted yet (first launch, or preference cleared), MapLibre.getInstance() + // itself selects Vulkan automatically when this device's hardware supports it. + private fun initializeMapLibre(apiKey: String?) { + val persistedType = RenderingEnginePreference.load(this) + try { + MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER, persistedType) + } catch (error: UnsupportedOperationException) { + RenderingEnginePreference.clear(this) + + Timber.w(error, "Rendering engine %s is not supported by this build", persistedType) + Toast.makeText( + this, + getString(R.string.rendering_engine_unsupported, persistedType?.name), + Toast.LENGTH_LONG + ).show() + MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER) + } + } + companion object { val TILE_SERVER = WellKnownTileServer.MapLibre private const val DEFAULT_API_KEY = "YOUR_API_KEY_GOES_HERE" diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt index 1a008eaf7..5241b024c 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt @@ -7,8 +7,10 @@ import android.content.pm.PackageManager import android.content.res.Resources.NotFoundException import android.os.Bundle import android.view.Menu +import android.view.MenuItem import android.view.View import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.lifecycle.lifecycleScope @@ -18,11 +20,14 @@ import androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.maplibre.android.RenderingEngine import org.maplibre.android.testapp.R import org.maplibre.android.testapp.adapter.FeatureAdapter import org.maplibre.android.testapp.adapter.FeatureSectionAdapter import org.maplibre.android.testapp.model.activity.Feature +import org.maplibre.android.testapp.utils.AppRestarter import org.maplibre.android.testapp.utils.ItemClickSupport +import org.maplibre.android.testapp.utils.RenderingEnginePreference import timber.log.Timber import java.util.* @@ -184,6 +189,49 @@ class FeatureOverviewActivity : AppCompatActivity() { return true } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.action_rendering_engine) { + showRenderingEngineDialog() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun showRenderingEngineDialog() { + val engineTypes = RenderingEngine.Type.values() + val currentType = RenderingEngine.getCurrentType() + val labels = engineTypes.map { type -> + if (type == currentType) { + getString(R.string.rendering_engine_current_label, type.name) + } else { + type.name + } + }.toTypedArray() + val checkedIndex = engineTypes.indexOf(currentType) + AlertDialog.Builder(this) + .setTitle(R.string.rendering_engine_dialog_title) + .setSingleChoiceItems(labels, checkedIndex) { dialog, which -> + dialog.dismiss() + switchRenderingEngine(engineTypes[which]) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + // RenderingEngine.setCurrentType() throws once MapLibre.getInstance() has run + // (which already happened in Application#onCreate() by the time this activity is + // shown), so it can't be probed here. The choice is persisted and applied on the + // next launch instead, before MapLibre.getInstance() runs — see + // MapLibreApplication#applyPersistedRenderingEngine(), which also surfaces the + // unsupported-backend error if the persisted choice turns out not to be supported. + private fun switchRenderingEngine(type: RenderingEngine.Type) { + if (type == RenderingEngine.getCurrentType()) { + return + } + RenderingEnginePreference.save(this, type) + AppRestarter.restart(this) + } + // Filter the features based on the search query private fun filterFeatures(query: String?) { val filteredFeatures = if (query.isNullOrEmpty()) { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/AppRestarter.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/AppRestarter.kt new file mode 100644 index 000000000..8e65626b1 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/AppRestarter.kt @@ -0,0 +1,19 @@ +package org.maplibre.android.testapp.utils + +import android.content.Context +import android.content.Intent + +/** + * Restarts the app: relaunches its launcher activity in a fresh task, then kills this + * process. Used after changing process-lifetime state (like the rendering engine) that + * only takes effect on the next launch. + */ +object AppRestarter { + fun restart(context: Context) { + val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) + requireNotNull(intent) { "No launch intent for package ${context.packageName}" } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + context.startActivity(intent) + Runtime.getRuntime().exit(0) + } +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/RenderingEnginePreference.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/RenderingEnginePreference.kt new file mode 100644 index 000000000..134868b69 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/RenderingEnginePreference.kt @@ -0,0 +1,36 @@ +package org.maplibre.android.testapp.utils + +import android.content.Context +import org.maplibre.android.RenderingEngine + +/** + * Persists the user's chosen [RenderingEngine.Type] across process restarts. + * + * Single-backend flavors lock the compiled-in backend and throw from + * [RenderingEngine.setCurrentType] for any other value; the multiBackend + * flavor accepts either value but only applies it on the next native library + * load. Either way the choice needs to survive an app restart, which is what + * this class is for. + */ +object RenderingEnginePreference { + + private const val PREFS_NAME = "rendering_engine_prefs" + private const val KEY_ENGINE_TYPE = "engine_type" + + /** Returns the persisted choice, or null if the user never picked one. */ + fun load(context: Context): RenderingEngine.Type? { + val stored = prefs(context).getString(KEY_ENGINE_TYPE, null) ?: return null + return runCatching { RenderingEngine.Type.valueOf(stored) }.getOrNull() + } + + fun save(context: Context, type: RenderingEngine.Type) { + prefs(context).edit().putString(KEY_ENGINE_TYPE, type.name).commit() + } + + fun clear(context: Context) { + prefs(context).edit().clear().commit() + } + + private fun prefs(context: Context) = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/menu/menu_feature_overview.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/menu/menu_feature_overview.xml index 6d798f3ce..30bc0d118 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/res/menu/menu_feature_overview.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/menu/menu_feature_overview.xml @@ -7,4 +7,8 @@ android:title="@string/search" app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="always|collapseActionView" /> + diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml index 75bf61e83..685f24cf3 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml @@ -9,4 +9,9 @@ Writes out avg. fps and 1% fps after world tour with .flyTo() Search Data Sources + %1$s is not supported by this build. Use the multiBackend flavor to switch backends at runtime. + Rendering Engine + Select Rendering Engine + %1$s (current) + Cancel diff --git a/platform/android/buildSrc/src/main/kotlin/DownloadVulkanValidationPlugin.kt b/platform/android/buildSrc/src/main/kotlin/DownloadVulkanValidationPlugin.kt deleted file mode 100644 index 0190fe84b..000000000 --- a/platform/android/buildSrc/src/main/kotlin/DownloadVulkanValidationPlugin.kt +++ /dev/null @@ -1,32 +0,0 @@ -import com.android.build.api.dsl.ApplicationExtension -import com.android.build.api.dsl.LibraryExtension -import org.gradle.api.Plugin -import org.gradle.api.Project - - -open class DownloadVulkanValidationPlugin : Plugin { - override fun apply(project: Project) { - project.configureVulkanJniLibs() - } -} - -internal fun Project.configureVulkanJniLibs() { - val appExtension = extensions.findByType(ApplicationExtension::class.java) - if (appExtension != null) { - appExtension.sourceSets { - getByName("vulkan") { - jniLibs.srcDir(tasks.named("unzip").get().outputs.files.asFileTree) - } - } - return - } - - val libraryExtension = extensions.findByType(LibraryExtension::class.java) - if (libraryExtension != null) { - libraryExtension.sourceSets { - getByName("vulkan") { - jniLibs.srcDir(tasks.named("unzip").get().outputs.files.asFileTree) - } - } - } -} diff --git a/platform/android/buildSrc/src/main/kotlin/maplibre.download-vulkan-validation.gradle.kts b/platform/android/buildSrc/src/main/kotlin/maplibre.download-vulkan-validation.gradle.kts index 9372c7966..d23edb4fc 100644 --- a/platform/android/buildSrc/src/main/kotlin/maplibre.download-vulkan-validation.gradle.kts +++ b/platform/android/buildSrc/src/main/kotlin/maplibre.download-vulkan-validation.gradle.kts @@ -26,7 +26,10 @@ if (extra.has("vvl_version")) { // Declare variables for the Vulkan Validation Layers download and extraction paths. val vvlSite = "https://github.com/KhronosGroup/Vulkan-ValidationLayers" val vvlLibRoot = projectDir // Set project root or change to rootDir if needed -val vvlJniLibDir = "$vvlLibRoot/src/vulkanDebug/jniLibs" +val vvlJniLibDirs = arrayOf( + "$vvlLibRoot/src/vulkanDebug/jniLibs", + "$vvlLibRoot/src/multiBackendDebug/jniLibs" // multiBackend flavor also uses Vulkan rendering engine +) val vvlSoName = "libVkLayer_khronos_validation.so" abstract class Download : DefaultTask() { @@ -55,19 +58,23 @@ val download = tasks.register("download") { file = file("$vvlLibRoot/android-binaries-$vvlVersion.zip") } -val unzip = tasks.register("unzip") { +val unzip = tasks.register("unzip") { dependsOn(download) - from(zipTree(file("$vvlLibRoot/android-binaries-$vvlVersion.zip"))) { - eachFile { - path = path.substringAfter("/") - } - includeEmptyDirs = false // Optional: Exclude empty directories if not needed - } - into(file(vvlJniLibDir)) + doLast { + vvlJniLibDirs.forEach { vvlJniLibDir -> + mkdir(vvlJniLibDir) - doFirst { - mkdir(vvlJniLibDir) + copy { + from(zipTree(file("$vvlLibRoot/android-binaries-$vvlVersion.zip"))) { + eachFile { + path = path.substringAfter("/") + } + includeEmptyDirs = false // Optional: Exclude empty directories if not needed + } + into(file(vvlJniLibDir)) + } + } } } diff --git a/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts b/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts index 3a72a111e..dd9dbc1a8 100644 --- a/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts +++ b/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts @@ -22,6 +22,8 @@ androidLibrary.publishing { singleVariant("vulkanDebug") singleVariant("openglRelease") singleVariant("openglDebug") + singleVariant("multiBackendRelease") + singleVariant("multiBackendDebug") } afterEvaluate { @@ -143,6 +145,8 @@ afterEvaluate { // OpenGL ES with this artifact ID if that happens. configureMavenPublication("opengl", "openglrelease", "-opengl", " (OpenGL ES)") configureMavenPublication("opengl", "opengldebug", "-opengl-debug", " (OpenGL ES, Debug)", "Debug") + configureMavenPublication("multiBackend", "multibackendrelease", "-vulkan-opengl", " (Vulkan + OpenGL ES)") + configureMavenPublication("multiBackend", "multibackenddebug", "-vulkan-opengl-debug", " (Vulkan + OpenGL ES, Debug)", "Debug") } // Wire per-variant compile classpaths into the Javadoc task. diff --git a/platform/android/docs/data/rendering-engine.md b/platform/android/docs/data/rendering-engine.md new file mode 100644 index 000000000..4a9a09b3d --- /dev/null +++ b/platform/android/docs/data/rendering-engine.md @@ -0,0 +1,74 @@ +# Rendering Engine (OpenGL ES / Vulkan) + +MapLibre Android is published as several artifacts on Maven Central, each built with a different rendering backend: + +| Artifact | Backend | +|---|---| +| `org.maplibre.gl:android-sdk` | Vulkan (the current default) | +| `org.maplibre.gl:android-sdk-opengl` | OpenGL ES | +| `org.maplibre.gl:android-sdk-vulkan` | Vulkan | +| `org.maplibre.gl:android-sdk-vulkan-opengl` | Both, selected at runtime | + +Most apps should use the default configuration: `org.maplibre.gl:android-sdk` + +If you'd like to use Vulkan but still want to have the OpenGL fallback option, then use should use `org.maplibre.gl:android-sdk-vulkan-opengl` artifact. It packs both backends into a single AAR and picks between them at runtime rather than at build time. + +PS: Since `org.maplibre.gl:android-sdk-vulkan-opengl` artifact carries multiple native libraries, it's larger than a single-backend build by the size of the second library which is ~10 MB. + +## Adding the dependency + +```gradle +dependencies { + implementation 'org.maplibre.gl:android-sdk-vulkan-opengl:' +} +``` + +Replace `` with the [latest MapLibre Android version](https://github.com/maplibre/maplibre-native/releases?q=android&expanded=true). + +## Selecting a backend + +Pass a `RenderingEngine.Type` to `MapLibre.getInstance()`: + +```java +// Explicit selection +MapLibre.getInstance(context, apiKey, tileServer, RenderingEngine.Type.VULKAN); + +// Auto-detect +MapLibre.getInstance(context, apiKey, tileServer, null); + +// Auto-detect +MapLibre.getInstance(context, apiKey, tileServer); + +// Auto-detect +MapLibre.getInstance(context); +``` + +Auto-detection is enabled by default. Every `MapLibre.getInstance()` overload that does not take a `RenderingEngine.Type` automatically detects backend. + +Auto-detection selects Vulkan when the device runs Android 7.0 (API 24) or later and reports Vulkan 1.0 hardware support through `PackageManager.FEATURE_VULKAN_HARDWARE_VERSION` feature. Everything else gets OpenGL ES. + +PS: `RenderingEngine` ships in every artifact. If you try to set unsupported `RenderingEngine` backend in single-backend build, then `MapLibre.getInstance()` throws `UnsupportedOperationException` exception. + +## Get active backend + +```java +// Active backend +RenderingEngine.Type active = RenderingEngine.getCurrentType(); +``` + +`RenderingEngine.getCurrentType()` reports the active backend once `MapLibre.getInstance()` has resolved it. + +The first `MapLibre.getInstance()` call loads the native library for the selected backend, then the backend is fixed for the lifetime of the process. Any subsequent `MapLibre.getInstance()` calls with a different `RenderingEngine.Type` will be ignored. The process restart is required to apply a different rendering backend. + +## OpenGL ES vs. Vulkan + +| | OpenGL ES | Vulkan | +|---|---|---| +| Device compatibility | Any device | Vulkan 1.0 hardware, Android 7.0 (API 24)+ | +| Performance | Baseline | Better on modern devices | +| Maturity | Longest track record | Newer, some open issues remain | + +## See Also + +- [MapLibre Android API Documentation](https://maplibre.org/maplibre-native/android/api/) +- [Rendering Backends (developer docs)](https://maplibre.org/maplibre-native/docs/book/platforms/android/android-rendering-backends.html)