Add multiBackend Android flavor for runtime OpenGL/Vulkan switching (#4288)
This commit is contained in:
committed by
GitHub
parent
ccb9af119e
commit
b0388d186d
3
.github/workflows/android-ci.yml
vendored
3
.github/workflows/android-ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
7
.github/workflows/android-release.yml
vendored
7
.github/workflows/android-release.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
renderer: [opengl, vulkan]
|
||||
renderer: [opengl, vulkan, multibackend]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: platform/android
|
||||
|
||||
@@ -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/`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
3
platform/android/.gitignore
vendored
3
platform/android/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
MapLibreAndroid/src/vulkanDebug
|
||||
MapLibreAndroid/src/vulkanDebug/jniLibs
|
||||
MapLibreAndroid/src/multiBackendDebug/jniLibs
|
||||
prefab-headers/
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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<DownloadVulkanValidationPlugin>()
|
||||
|
||||
// intentionally disabled
|
||||
// apply(plugin = "maplibre.jacoco-report")
|
||||
|
||||
@@ -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)
|
||||
|
||||
85
platform/android/MapLibreAndroid/src/cpp/MultiBackend.cmake
Normal file
85
platform/android/MapLibreAndroid/src/cpp/MultiBackend.cmake
Normal file
@@ -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 <BINARY_DIR> --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 <BINARY_DIR> --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
|
||||
$<TARGET_FILE:maplibre-opengl>
|
||||
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
|
||||
$<TARGET_FILE:maplibre-vulkan>
|
||||
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)
|
||||
@@ -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.
|
||||
* <p>
|
||||
* This class manages the API key, application context, and connectivity state.
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Vulkan support is optional in the multiBackend flavor; the app falls back
|
||||
to OpenGL when Vulkan is unavailable. -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.vulkan.version"
|
||||
android:version="0x400003"
|
||||
android:required="false" />
|
||||
</manifest>
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,15 @@ import org.maplibre.android.LibraryLoader;
|
||||
import org.maplibre.android.LibraryLoaderProvider;
|
||||
|
||||
/**
|
||||
* Concrete implementation of a native library loader.
|
||||
* <p>
|
||||
* </p>
|
||||
* 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) {
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,15 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
create("multiBackend") {
|
||||
dimension = "renderer"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments("-DMLN_ANDROID_MULTI_BACKEND=ON")
|
||||
targets("maplibre-opengl", "maplibre-vulkan")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -7,4 +7,8 @@
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView" />
|
||||
<item
|
||||
android:id="@+id/action_rendering_engine"
|
||||
android:title="@string/action_rendering_engine"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
<string name="description_world_tour_benchmark">Writes out avg. fps and 1% fps after world tour with .flyTo()</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="category_data">Data Sources</string>
|
||||
<string name="rendering_engine_unsupported">%1$s is not supported by this build. Use the multiBackend flavor to switch backends at runtime.</string>
|
||||
<string name="action_rendering_engine">Rendering Engine</string>
|
||||
<string name="rendering_engine_dialog_title">Select Rendering Engine</string>
|
||||
<string name="rendering_engine_current_label">%1$s (current)</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
</resources>
|
||||
|
||||
@@ -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<Project> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>("download") {
|
||||
file = file("$vvlLibRoot/android-binaries-$vvlVersion.zip")
|
||||
}
|
||||
|
||||
val unzip = tasks.register<Copy>("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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
74
platform/android/docs/data/rendering-engine.md
Normal file
74
platform/android/docs/data/rendering-engine.md
Normal file
@@ -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:<version>'
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<version>` 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)
|
||||
Reference in New Issue
Block a user