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

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

@@ -1,174 +0,0 @@
package org.maplibre.android.maps.renderer.surfaceview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import java.lang.ref.WeakReference;
public class MapLibreVulkanSurfaceView extends MapLibreSurfaceView {
protected final WeakReference<MapLibreVulkanSurfaceView> viewWeakReference = new WeakReference<>(this);
public MapLibreVulkanSurfaceView(Context context) {
super(context);
}
public MapLibreVulkanSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void createRenderThread() {
renderThread = new VulkanThread(viewWeakReference);
}
static class VulkanThread extends MapLibreSurfaceView.RenderThread {
VulkanThread(WeakReference<MapLibreVulkanSurfaceView> surfaceViewWeakRef) {
super(surfaceViewWeakRef.get().renderThreadManager);
mSurfaceViewWeakRef = surfaceViewWeakRef;
}
@Override
protected void guardedRun() throws InterruptedException {
wantRenderNotification = false;
boolean sizeChanged = false;
boolean initSurface = false;
boolean wantRenderNotification = false;
boolean doRenderNotification = false;
int w = 0;
int h = 0;
Runnable event = null;
Runnable finishDrawingRunnable = null;
while (true) {
synchronized (renderThreadManager) {
while (true) {
if (shouldExit) {
return;
}
if (!eventQueue.isEmpty()) {
event = eventQueue.remove(0);
break;
}
// Update the pause state.
if (paused != requestPaused) {
paused = requestPaused;
renderThreadManager.notifyAll();
}
// lost surface
if (!hasSurface && !waitingForSurface) {
MapLibreVulkanSurfaceView view = mSurfaceViewWeakRef.get();
if (view != null && graphicsSurfaceCreated) {
view.renderer.onSurfaceDestroyed();
}
graphicsSurfaceCreated = false;
waitingForSurface = true;
renderThreadManager.notifyAll();
}
// acquired surface
if (hasSurface && waitingForSurface) {
MapLibreVulkanSurfaceView view = mSurfaceViewWeakRef.get();
if (view != null) {
initSurface = true;
graphicsSurfaceCreated = true;
}
waitingForSurface = false;
renderThreadManager.notifyAll();
}
if (doRenderNotification) {
this.wantRenderNotification = false;
doRenderNotification = false;
renderComplete = true;
renderThreadManager.notifyAll();
}
if (this.finishDrawingRunnable != null) {
finishDrawingRunnable = this.finishDrawingRunnable;
this.finishDrawingRunnable = null;
}
// Ready to draw?
if (readyToDraw() && graphicsSurfaceCreated) {
if (this.sizeChanged) {
sizeChanged = true;
w = width;
h = height;
this.wantRenderNotification = true;
this.sizeChanged = false;
}
requestRender = false;
renderThreadManager.notifyAll();
if (this.wantRenderNotification) {
wantRenderNotification = true;
}
break;
} else {
if (finishDrawingRunnable != null) {
Log.w(TAG, "Warning, !readyToDraw() but waiting for "
+ "draw finished! Early reporting draw finished.");
finishDrawingRunnable.run();
finishDrawingRunnable = null;
}
}
// By design, this is the only place in a RenderThread thread where we wait().
renderThreadManager.wait();
}
} // end of synchronized(sRenderThreadManager)
if (event != null) {
event.run();
event = null;
continue;
}
if (initSurface) {
MapLibreVulkanSurfaceView view = mSurfaceViewWeakRef.get();
if (view != null) {
view.renderer.onSurfaceCreated(view.getHolder().getSurface());
initSurface = false;
}
}
if (sizeChanged) {
MapLibreVulkanSurfaceView view = mSurfaceViewWeakRef.get();
if (view != null) {
view.renderer.onSurfaceChanged(w, h);
sizeChanged = false;
}
}
{
MapLibreVulkanSurfaceView view = mSurfaceViewWeakRef.get();
if (view != null) {
view.renderer.onDrawFrame();
if (finishDrawingRunnable != null) {
finishDrawingRunnable.run();
finishDrawingRunnable = null;
}
}
}
if (wantRenderNotification) {
doRenderNotification = true;
wantRenderNotification = false;
}
}
}
@Override
public boolean ableToDraw() {
return graphicsSurfaceCreated && readyToDraw();
}
private boolean graphicsSurfaceCreated;
private final WeakReference<MapLibreVulkanSurfaceView> mSurfaceViewWeakRef;
}
}

View File

@@ -1,16 +0,0 @@
package org.maplibre.android.maps.renderer.surfaceview;
import android.content.Context;
import androidx.annotation.NonNull;
public class VulkanSurfaceViewMapRenderer extends SurfaceViewMapRenderer {
public VulkanSurfaceViewMapRenderer(Context context,
@NonNull MapLibreVulkanSurfaceView surfaceView,
String localIdeographFontFamily) {
super(context, surfaceView, localIdeographFontFamily);
this.surfaceView.setRenderer(this);
}
}

View File

@@ -1,139 +0,0 @@
package org.maplibre.android.maps.renderer.textureview;
import android.view.Surface;
import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
public class VulkanTextureViewRenderThread extends TextureViewRenderThread {
private Surface surface;
@UiThread
public VulkanTextureViewRenderThread(@NonNull TextureView textureView, @NonNull TextureViewMapRenderer mapRenderer) {
super(textureView, mapRenderer);
}
void cleanup() {
if (surface != null) {
mapRenderer.onSurfaceDestroyed();
surface.release();
surface = null;
}
hasNativeSurface = false;
}
// Thread implementation
@Override
public void run() {
try {
while (true) {
Runnable event = null;
boolean createSurface = false;
boolean sizeChanged = false;
int w = -1;
int h = -1;
// Guarded block
synchronized (lock) {
while (true) {
if (shouldExit) {
return;
}
// If any events are scheduled, pop one for processing
if (!eventQueue.isEmpty()) {
event = eventQueue.remove(0);
break;
}
if (this.destroySurface) {
if (surface != null) {
mapRenderer.onSurfaceDestroyed();
surface.release();
surface = null;
}
this.hasNativeSurface = false;
this.destroySurface = false;
lock.notifyAll();
break;
}
if (surfaceTexture != null && !paused && requestRender) {
w = width;
h = height;
if (surface == null) {
surface = new Surface(surfaceTexture);
this.hasNativeSurface = true;
createSurface = true;
}
if (this.sizeChanged) {
sizeChanged = true;
this.sizeChanged = false;
}
// Reset the request render flag now, so we can catch new requests
// while rendering
requestRender = false;
// Break the guarded loop and continue to process
break;
}
// Wait until needed
lock.wait();
} // end guarded while loop
} // end guarded block
// Run event, if any
if (event != null) {
event.run();
continue;
}
if (createSurface) {
mapRenderer.onSurfaceCreated(surface);
mapRenderer.onSurfaceChanged(w, h);
createSurface = false;
continue;
}
// If the surface size has changed inform the map renderer.
if (sizeChanged) {
mapRenderer.onSurfaceChanged(w, h);
sizeChanged = false;
continue;
}
if (surface == null) {
continue;
}
// Time to render a frame
mapRenderer.onDrawFrame();
}
} catch (InterruptedException err) {
// To be expected
} finally {
// Signal we're done
synchronized (lock) {
cleanup();
this.exited = true;
lock.notifyAll();
}
}
}
}

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