Repository navigation
Mobile Platforms
vgl's runtime is platform-independent, and Android and iOS are among its targets: Android 7.0 (API 24, 64-bit only) and iOS 15. This page describes the cross-build presets for both, how they are checked in CI, what an integrator must supply on a phone or tablet (an OpenGL ES 3.0 context, a function loader, I/O and tasks), and the current status of each backend on mobile.
Status. In vgl 1.0, the Android and iOS builds of the library, the defaults module, the OpenGL / OpenGL ES backend, the tests and the headless sample are compiled in CI, but nothing is run on a device, emulator or simulator yet, and the repository has no mobile sample app. The OpenGL ES path itself is tested on desktop (Mesa's OpenGL ES, and WebGL 2 in Chrome). Treat mobile as supported by design and by build, not yet verified on hardware.
| Component | Android | iOS | Notes |
|---|---|---|---|
Core (vgl::vgl) |
Builds | Builds | Uses only the C and C++ standard libraries |
Defaults module (vgl::defaults) |
Builds | Builds | Thread pool, file reader, disk cache. The presets leave the HTTP reader off |
OpenGL backend (vgl::gl) |
Builds | Builds | The OpenGL ES 3.0 path, the same code that runs on WebGL 2 and Mesa |
| Direct3D 11 backend | No | No | Windows only |
Ogre backend (vgl::ogre) |
Planned | Planned | Ogre on OpenGL ES 3 (Android) and Metal (iOS) is milestone M3b, after version 1 |
| Tests, headless sample | Built, not run | Built, not run | Running them on an emulator or simulator is planned (design document, section 12.2) |
| Interactive samples | No | No | They use GLFW, which is for desktops |
| Baker | No | No | Windows and Linux only |
Graphics on mobile go through OpenGL ES 3.0 for now (design document, section 4). Apple has deprecated OpenGL ES on iOS since iOS 12, but it is still available; the planned long-term Apple path is Ogre's Metal render system, and a raw Metal backend is not planned. OpenGL ES 2.0 is out of scope.
The android-arm64 preset cross-builds with the NDK's CMake toolchain. Set ANDROID_NDK to an
NDK (r27c is tested; CI installs ndk;27.2.12479018) and have Ninja on the path:
export ANDROID_NDK=$ANDROID_HOME/ndk/27.2.12479018
cmake --workflow --preset android-arm64-releaseor step by step:
cmake --preset android-arm64
cmake --build --preset android-arm64-release| Setting | Value |
|---|---|
| Toolchain | $ANDROID_NDK/build/cmake/android.toolchain.cmake |
ANDROID_ABI |
arm64-v8a |
ANDROID_PLATFORM |
android-24 (Android 7.0) |
ANDROID_STL |
c++_static |
BUILD_SHARED_LIBS |
OFF |
VGL_ZSTD_PROVIDER |
BUNDLED (Zstandard compiled into vgl) |
The workflow configures and builds only; the tests are built for Android but not run, since
that needs an emulator. 32-bit ARM is not supported (Google Play requires 64-bit code).
x86_64, for the emulator, is a target in the design but has no preset; configure with
-DANDROID_ABI=x86_64 to try it (not checked in CI).
On Android, a static vgl adds only m to its link interface: the NDK's toolchain links its
C++ runtime (libc++) itself.
The usual way is to build vgl with your app's native code: add it with add_subdirectory in
the CMake script your module's externalNativeBuild points to. Gradle passes the ABI, platform
and STL, and vgl's tests, samples and install rules are off when it is not the top-level
project.
# app/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(globe LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(BUILD_SHARED_LIBS OFF)
set(VGL_ZSTD_PROVIDER BUNDLED)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../../../third_party/vgl vgl)
add_library(globe SHARED globe.cpp)
target_link_libraries(globe PRIVATE vgl::vgl vgl::defaults vgl::gl android EGL GLESv3 log)Pitfalls:
- vgl needs CMake 3.24 or later. The CMake that Android Studio uses by default may be older;
install a newer one through the SDK manager and set
externalNativeBuild { cmake { version = "..." } }in the module's Gradle file. - Build with the NDK's
c++_staticorc++_sharedSTL, as for any C++20 code; vgl needs no other runtime. - Packs inside the APK are compressed by default, which makes random access slow. Keep them
uncompressed, for example with
androidResources { noCompress += listOf("vglm", "vgls") }in the module's Gradle file.
The ios-arm64 preset builds on a Mac with Xcode installed and Ninja on the path:
cmake --workflow --preset ios-arm64-release| Setting | Value |
|---|---|
CMAKE_SYSTEM_NAME |
iOS |
CMAKE_OSX_ARCHITECTURES |
arm64 |
CMAKE_OSX_DEPLOYMENT_TARGET |
15.0 |
BUILD_SHARED_LIBS |
OFF |
VGL_ZSTD_PROVIDER |
BUNDLED |
As for Android, the tests are built but not run (that needs a simulator build). There is no
simulator preset; a simulator build would set CMAKE_OSX_SYSROOT to iphonesimulator, which CI
does not check.
Install the static libraries and headers into a prefix and add them to your app target:
cmake --preset ios-arm64
cmake --build --preset ios-arm64-release
cmake --install build/ios-arm64 --config Release --prefix ios-prefixLink libvgl.a, libvgl-defaults.a and libvgl-gl.a from ios-prefix/lib, add
ios-prefix/include to the header search paths, and link the OpenGLES framework. Apple's
toolchain links libc++ itself. If your app is built with CMake, find_package(vgl) with
CMAKE_PREFIX_PATH pointing at the prefix gives you the same targets as on desktop
(Installing vgl).
vgl never creates windows, OpenGL contexts, threads or network connections, and never opens files itself (design document, sections 4 and 7.5). On mobile, as elsewhere, the host provides these through the interfaces described in Host interfaces.
Create an OpenGL ES 3.0 (or later) context and make it current on your render thread before you create the backend:
-
Android: EGL, with
EGL_CONTEXT_CLIENT_VERSION3 (orEGL_CONTEXT_MAJOR_VERSION3), on the surface of aSurfaceVieworGLSurfaceView. -
iOS: an
EAGLContextwithkEAGLRenderingAPIOpenGLES3, drawing into a framebuffer object you create, with a color and a depth attachment. iOS has no default framebuffer: bind yours beforevgl_gl_backend_draw, which draws into whatever framebuffer is bound.
The backend draws into the host's framebuffer with the host's viewport, and restores the OpenGL state it changes (OpenGL backend).
vgl_gl_backend_create_info.get_proc_address is required. The backend links no OpenGL library;
it loads every function it uses through your loader, which must also return core OpenGL ES
functions (not only extensions). A loader that looks in the GLES library first and falls back
to EGL covers both cases on Android:
#include <EGL/egl.h>
#include <dlfcn.h>
#include <vgl/gl.h>
static vgl_gl_proc load_gl(void* user_data, const char* name)
{
void* gles = user_data; /* dlopen("libGLESv3.so", RTLD_NOW), done once by the host */
void* f = dlsym(gles, name);
if (f == NULL)
{
f = (void*)eglGetProcAddress(name);
}
return (vgl_gl_proc)f;
}On iOS, the OpenGL ES functions are exported by the OpenGLES framework, so
dlsym(RTLD_DEFAULT, name) finds them once the app links it.
Then create the backend on the current context:
vgl_gl_backend_create_info backend_info = {.struct_size = sizeof(backend_info)};
backend_info.get_proc_address = load_gl;
backend_info.get_proc_address_user_data = gles; /* the dlopen handle */
/* depth_convention stays 0: VGL_DEPTH_CONVENTION_LOGARITHMIC. */
vgl_gl_backend* backend = NULL;
vgl_result result = vgl_gl_backend_create(context, &backend_info, &backend);
if (result != VGL_RESULT_SUCCESS)
{
/* The context's log says why: version, a missing function, or the depth convention. */
}Mobile GPUs rarely offer clip control (GL_EXT_clip_control), which reversed-Z needs, so use
the default logarithmic convention, VGL_DEPTH_CONVENTION_LOGARITHMIC. It works on every
OpenGL ES 3.0 device and gives a usable depth range from orbit to the ground. Clear depth to 1
and compare with LESS, as for any conventional depth buffer
(Render interface).
Android destroys the EGL context when an app goes to the background on many devices, and iOS apps may tear down their context too. When that happens:
- Do not call
vgl_gl_backend_destroyon a dead context. Keep the backend. - Create the new context and make it current.
- Call
vgl_gl_backend_device_reset(backend). It forgets the old OpenGL objects without deleting them, loads the functions again, rebuilds its shaders, and makes the context offer its render resources again. They are uploaded over the next frames, within the upload budget, while coarser tiles are drawn.
If you set stars or clouds with vgl_gl_backend_stars_set or vgl_gl_backend_clouds_set, the
backend keeps its copies and draws them again after the reset. A host with its own renderer
calls vgl_context_resources_reset instead (Render interface).
Decoding runs on the host's threads through a vgl_task_interface. You have three choices:
-
The defaults module's thread pool (
vgl_thread_pool_create, thenvgl_thread_pool_task_interface_get). It builds on Android and iOS. Its defaultthread_count(0) is one less than the hardware threads, which on a phone with big and little cores may be more than you want; setthread_countexplicitly, for example 2 to 4. -
Your engine's job system, behind a
vgl_task_interfaceof your own:submitmust run the function exactly once, on any thread, and never block on other tasks. -
No task interface.
vgl_context_updatethen decodes on the calling thread, within itstime_budget(2 ms by default), which keeps frame times predictable on slow devices at the cost of slower loading.
The context reads packs by URI and byte range through a vgl_io_interface
(Host interfaces). It asks for <pack uri>/pack.vglm (the manifest) and
ranges of the pack's .vgls shards.
iOS. An app bundle and the app's container are ordinary files, so the defaults module's
file reader works: pass absolute paths (UTF-8) to vgl_pack_create, for example the bundle's
resource path plus /packs/gebco.
Android. Files in the app's internal or external storage work with the file reader too.
Packs inside the APK are not files, so either copy them to storage on first start, or read them
with an I/O interface on AAssetManager. A minimal one, which completes each read on the
calling thread:
#include <android/asset_manager.h>
#include <stdio.h>
#include <stdlib.h>
#include <vgl/vgl.h>
/* Reads "<uri>" from the APK's assets; the context calls this inside its functions. */
static void asset_read(void* user_data, vgl_io_request* request, const vgl_io_read* read)
{
AAssetManager* assets = (AAssetManager*)user_data;
AAsset* asset = AAssetManager_open(assets, read->uri, AASSET_MODE_RANDOM);
if (asset == NULL)
{
vgl_io_request_complete(request, VGL_RESULT_NOT_FOUND, NULL, 0);
return;
}
const uint64_t length = (uint64_t)AAsset_getLength64(asset);
uint64_t size = 0;
if (read->offset < length)
{
size = length - read->offset;
if (read->size != VGL_IO_SIZE_TO_END && read->size < size)
{
size = read->size;
}
}
if (size == 0)
{
/* A range past the end delivers no bytes, as the file reader does. */
AAsset_close(asset);
vgl_io_request_complete(request, VGL_RESULT_SUCCESS, NULL, 0);
return;
}
unsigned char* data = malloc((size_t)size);
uint64_t done = 0;
if (data != NULL && AAsset_seek64(asset, (off64_t)read->offset, SEEK_SET) >= 0)
{
while (done < size)
{
const int n = AAsset_read(asset, data + done, (size_t)(size - done));
if (n <= 0)
{
break;
}
done += (uint64_t)n;
}
}
AAsset_close(asset);
if (done == size)
{
vgl_io_request_complete(request, VGL_RESULT_SUCCESS, data, (size_t)size);
}
else
{
vgl_io_request_complete(request, VGL_RESULT_IO_ERROR, NULL, 0);
}
free(data); /* vgl copies the data before vgl_io_request_complete returns. */
}
/* At start-up, with the AAssetManager from the activity: */
vgl_io_interface io = {0};
io.version = VGL_IO_INTERFACE_VERSION;
io.user_data = asset_manager;
io.read = asset_read;
vgl_context_create_info context_info = {.struct_size = sizeof(context_info)};
context_info.io = &io; /* the table is copied; asset_manager must outlive the context */
context_info.tasks = &tasks; /* from the thread pool, or your own */Then add packs by their path inside assets/, for example
pack_info.uri = "packs/gebco";. This reader blocks the thread that calls into the context
while it reads. For large views, hand reads to a worker instead (vgl_io_request_complete may
be called from any thread, later), as the defaults module's file reader does with its thread
pool.
From a web server. The presets leave VGL_DEFAULTS_HTTP off on mobile, and no mobile build
of the libcurl reader is checked in CI. The design's answer for hosts with a network layer of
their own (OkHttp or HttpURLConnection on Android, NSURLSession on iOS) is to implement the
I/O interface on it: range requests for byte ranges, VGL_RESULT_NOT_FOUND for 404 and 410, an
empty success for 416. The defaults module's disk cache (vgl_disk_cache) can sit in front of
such an interface to keep what was read for offline use, since it wraps any
vgl_io_interface (it caches http:// and https:// URIs and passes others straight
through). See Hosting packs for what servers must support.
Phones have less memory than desktops. Both budgets are set when the context is created:
context_info.cpu_budget = 128ull * 1024 * 1024; /* decoded tiles and pack metadata (0: 256 MiB) */
context_info.gpu_budget = 256ull * 1024 * 1024; /* render resources (0: 512 MiB) */and the upload budget per frame is the upload_budget of vgl_gl_draw_info, which spreads
uploads over frames after a large view change. vgl_context_statistics_get reports what is in
use, to tune them (Context API).
- Host interfaces: the I/O, task and allocator interfaces in detail
- OpenGL backend: state, depth conventions, device reset
- Defaults module: thread pool, file reader, disk cache
- Building vgl and Installing vgl
- Ogre backend: the planned mobile render systems
- Design document, section 4
Getting vgl
Using the library
Rendering
Data
Reference
Specifications