Add multiBackend Android flavor for runtime OpenGL/Vulkan switching (#4288)

This commit is contained in:
Veniamin Vynohradov
2026-08-06 16:54:58 +03:00
committed by GitHub
parent ccb9af119e
commit b0388d186d
47 changed files with 896 additions and 163 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -30,7 +30,7 @@ jobs:
strategy:
fail-fast: false
matrix:
renderer: [opengl, vulkan]
renderer: [opengl, vulkan, multibackend]
defaults:
run:
working-directory: platform/android

View File

@@ -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/`.

View File

@@ -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

View File

@@ -1,2 +1,3 @@
MapLibreAndroid/src/vulkanDebug
MapLibreAndroid/src/vulkanDebug/jniLibs
MapLibreAndroid/src/multiBackendDebug/jniLibs
prefab-headers/

View File

@@ -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]")

View File

@@ -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")

View File

@@ -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)

View 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)

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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);
}
};
}
}

View File

@@ -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);
}
};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
};
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -115,6 +115,15 @@ android {
}
}
}
create("multiBackend") {
dimension = "renderer"
externalNativeBuild {
cmake {
arguments("-DMLN_ANDROID_MULTI_BACKEND=ON")
targets("maplibre-opengl", "maplibre-vulkan")
}
}
}
}
buildFeatures {

View File

@@ -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"

View File

@@ -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()) {

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}
}
}

View File

@@ -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))
}
}
}
}

View File

@@ -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.

View 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)