Skip to content

Context API

Jeremiah van Oosten edited this page Oct 18, 2026 · 1 revision

Context API

This page is a task-oriented guide to <vgl/context.h>, the heart of the C API: creating and destroying a context and every field of its create info, adding and removing packs (local or over any I/O interface), imagery layers with their opacity and priority, the geoid, the view and tile selection, the per-frame update, height queries, ray queries for picking (and how the samples' grab navigation uses them), attribution, statistics, texture formats, budgets and error handling. Every public function of context.h is covered, grouped by task, with C examples and the matching calls of the header-only C++ wrapper (<vgl/context.hpp>). Read Core Concepts first for the architecture; the conventions all of this follows are in docs/c-api.md.

Contents

Function overview

Task C function C++ wrapper Thread safety
Create a context vgl_context_create vgl::context::create Free-threaded
Destroy a context vgl_context_destroy vgl::context destructor, reset Context-synchronized
Add a pack vgl_pack_create vgl::pack::create Context-synchronized
Remove a pack vgl_pack_destroy vgl::pack destructor, reset Context-synchronized
Pack state vgl_pack_state_get vgl::pack::state_get Context-synchronized
Pack priority vgl_pack_priority_set, vgl_pack_priority_get priority_set, priority_get Context-synchronized
Layer opacity vgl_pack_opacity_set, vgl_pack_opacity_get opacity_set, opacity_get Context-synchronized
Set the view vgl_context_view_set vgl::context::view_set Context-synchronized
Get the view vgl_context_view_get vgl::context::view_get Context-synchronized
Advance loading vgl_context_update vgl::context::update Context-synchronized
Height at a position vgl_context_height_query vgl::context::height_query Context-synchronized
Ray against the terrain vgl_context_ray_query vgl::context::ray_query Context-synchronized
Position to screen vgl_view_project, vgl_view_geodetic_project vgl::view_project, vgl::view_geodetic_project Free-threaded
Credits in view vgl_context_attribution_get vgl::context::attribution_get Context-synchronized
Statistics vgl_context_statistics_get vgl::context::statistics_get Context-synchronized
The context's allocator vgl_context_allocator_get vgl::context::allocator_get Free-threaded
Log through the context vgl_context_log vgl::context::log Free-threaded

Two functions that act on a context live in <vgl/render.h> and are covered briefly here: vgl_context_texture_formats_set and vgl_context_resources_reset. The frame functions (vgl_context_frame_begin and the rest) are on Render Interface.

"Context-synchronized" means calls on one context, or on its packs, must not overlap; they may come from any thread, one at a time. See Core Concepts.

Creating and destroying a context

A context holds all of the library's state: packs, the tile cache, selection and the render resources. There is no global state, so several contexts are fully independent.

vgl_context_create_info

Zero-initialize, set struct_size, then fill in what you need. Every zero field selects its default.

Field Type Default (zero) Meaning
struct_size uint32_t Required sizeof(vgl_context_create_info)
log_level vgl_log_level Warnings Minimum level of messages passed to log: VGL_LOG_LEVEL_ERROR, _WARNING, _INFO or _DEBUG. VGL_LOG_LEVEL_DEFAULT selects warnings.
cpu_budget uint64_t 256 MiB Memory for decoded tiles, cached imagery, pack metadata and meshes waiting to upload, in bytes
allocator const vgl_allocator_interface* C heap Every allocation of the context goes through it. Must be thread-safe if tasks is set or I/O completes on other threads. It must outlive every request and task of the context.
io const vgl_io_interface* None How the context reads packs. Without it, vgl_pack_create fails with VGL_RESULT_INVALID_OPERATION.
tasks const vgl_task_interface* None Where decoding and mesh building run. Without it, vgl_context_update does the work itself, within its time budget.
ellipsoid const vgl_ellipsoid* WGS84 The reference ellipsoid. Packs must use the same one.
log vgl_log_fn Silent Receives log messages. Must be thread-safe if tasks is set.
log_user_data void* Null Passed to log
gpu_budget uint64_t 512 MiB GPU memory for the resources of the render interface, in bytes

The *_padding fields after each pointer exist only on 32-bit targets (such as WebAssembly); zero-initializing the structure takes care of them.

The interface tables (allocator, io, tasks) are copied, so they may live on the stack, but the objects behind their user_data pointers must stay valid until the context is destroyed. The same goes for log_user_data.

vgl_context_create

vgl_result vgl_context_create(const vgl_context_create_info* info, vgl_context** out_context);

info may be null, which selects every default, including no I/O: such a context accepts a view and updates, but cannot load packs, so it has no terrain to draw or query. Results:

Result When
VGL_RESULT_SUCCESS Created
VGL_RESULT_INVALID_ARGUMENT out_context is null, or info or one of its interfaces is invalid (a required callback missing, an invalid ellipsoid)
VGL_RESULT_NOT_SUPPORTED info sets fields of a newer vgl version
VGL_RESULT_VERSION_MISMATCH An interface has a version this vgl does not know
VGL_RESULT_OUT_OF_MEMORY Allocation failed

A complete setup with the Defaults Module:

#include <vgl/defaults.h>
#include <vgl/vgl.h>

#include <stdio.h>

static void log_message(void* user_data, vgl_log_level level, const char* message)
{
    (void)user_data;
    fprintf(stderr, "vgl [%d]: %s\n", (int)level, message);
}

/* ... */
vgl_thread_pool* pool = NULL;
vgl_file_reader* reader = NULL;
vgl_file_reader_create_info reader_info = {.struct_size = sizeof(reader_info)};
vgl_task_interface tasks;
vgl_io_interface io;
vgl_context_create_info info = {.struct_size = sizeof(info)};
vgl_context* context = NULL;
vgl_result r;

if (vgl_thread_pool_create(NULL, &pool) != VGL_RESULT_SUCCESS)
{
    /* handle the error */
}
reader_info.pool = pool; /* Read files on the pool's threads. */
if (vgl_file_reader_create(&reader_info, &reader) != VGL_RESULT_SUCCESS)
{
    /* handle the error */
}
vgl_thread_pool_task_interface_get(pool, &tasks);
vgl_file_reader_io_interface_get(reader, &io);

info.log_level = VGL_LOG_LEVEL_INFO;
info.cpu_budget = 128ull * 1024 * 1024; /* 128 MiB, for a smaller device. */
info.tasks = &tasks;
info.io = &io;
info.log = log_message;
r = vgl_context_create(&info, &context);
if (VGL_FAILED(r))
{
    const char* name = NULL;
    vgl_result_name_get(r, &name);
    fprintf(stderr, "vgl_context_create: %s\n", name);
}

In C++:

vgl::expected<vgl::thread_pool> pool = vgl::thread_pool::create();
vgl::expected<vgl::file_reader> reader = vgl::file_reader::create(*pool);
const vgl_task_interface tasks = pool->task_interface_get();
const vgl_io_interface io = reader->io_interface_get();

auto info = vgl::init<vgl_context_create_info>();
info.log_level = VGL_LOG_LEVEL_INFO;
info.tasks = &tasks;
info.io = &io;
info.log = [](void*, vgl_log_level, const char* message) {
    std::fprintf(stderr, "vgl: %s\n", message);
};
vgl::expected<vgl::context> ctx = vgl::context::create(&info);
if (!ctx)
{
    std::fprintf(stderr, "%s\n", vgl::result_name_get(ctx.error()));
}

To supply your own I/O (for example, reading from an Android APK or a game's archive), scheduling or allocator instead, see Host Interfaces.

vgl_context_destroy

vgl_result vgl_context_destroy(vgl_context* context);

Destroys the context and every pack still in it, logging a warning for each pack (destroy your packs first). Reads and tasks still outstanding are cancelled; when the host completes them later, the completions are discarded and freed. Tasks that are running are waited for. Always returns VGL_RESULT_SUCCESS; null does nothing.

Order of destruction:

  1. Backends (on the render thread, with their graphics context current).
  2. Packs.
  3. The context.
  4. The objects behind the interfaces: the defaults module's readers and cache, then the pool.

The context does not destroy any GPU objects the consumer created; backends free their own.

Packs

A pack is a directory holding a manifest (pack.vglm) and shards. Its kind (elevation, imagery or geoid) is recorded in the manifest, so the same call adds any of them. See Core Concepts for how packs stack.

vgl_pack_create_info

Field Type Default Meaning
struct_size uint32_t Required sizeof(vgl_pack_create_info)
priority int32_t 0 Packs with a higher priority win where they have data. Among equal priorities, the pack created later wins. Ignored for a geoid pack.
uri const char* Required The pack's directory, as the I/O interface understands it. The context appends / and a file name (pack.vglm, base-<digest>.vgls, shards/<y>/<x>-<digest>.vgls). Copied.

vgl_pack_create

vgl_result vgl_pack_create(vgl_context* context, const vgl_pack_create_info* info,
                           vgl_pack** out_pack);

Adds the pack and starts reading its manifest. It returns at once; the pack is VGL_PACK_STATE_LOADING until its manifest has been read and checked, at an update after the read completes. Results: VGL_RESULT_SUCCESS; VGL_RESULT_INVALID_ARGUMENT (a null pointer or uri); VGL_RESULT_NOT_SUPPORTED (fields of a newer version); VGL_RESULT_INVALID_OPERATION (the context has no I/O interface); VGL_RESULT_OUT_OF_MEMORY.

Problems with the pack itself (missing, corrupt, a newer format) do not fail this call: they show up later as VGL_PACK_STATE_FAILED.

vgl_pack_create_info pack_info = {.struct_size = sizeof(pack_info)};
vgl_pack* gebco = NULL;
vgl_pack* norway = NULL;

pack_info.uri = "/data/packs/gebco";
vgl_pack_create(context, &pack_info, &gebco);

/* A regional pack that wins over the global base wherever it has data. */
pack_info.uri = "/data/packs/glo30-norway";
pack_info.priority = 1;
vgl_pack_create(context, &pack_info, &norway);
vgl::expected<vgl::pack> gebco = vgl::pack::create(*ctx, "/data/packs/gebco");
vgl::expected<vgl::pack> norway = vgl::pack::create(*ctx, "/data/packs/glo30-norway", 1);

Local packs and packs over HTTP

The URI means whatever the context's I/O interface makes of it. The context itself never opens files or connections:

I/O interface URIs it reads
vgl_file_reader UTF-8 paths, absolute or relative to the working directory, with an optional file:// prefix (not percent-decoded)
vgl_http_reader (with VGL_DEFAULTS_HTTP) http:// and https:// URIs, with range requests; others go to its fallback interface, such as a file reader
vgl_disk_cache Caches the http:// and https:// reads of the interface it wraps
Your own Anything: an archive, an APK's assets, an app bundle, an encrypted store

So packs on a web server only need a different I/O interface; the calls above stay the same. A sketch with the HTTP reader in front of the file reader (see Defaults Module for the details and the disk cache):

vgl_http_reader_create_info http_info = {.struct_size = sizeof(http_info)};
vgl_http_reader* http = NULL;
vgl_io_interface files;
vgl_io_interface io;

vgl_file_reader_io_interface_get(reader, &files);
http_info.fallback = &files; /* Paths that are not http:// or https:// are read from disk. */
if (vgl_http_reader_create(&http_info, &http) == VGL_RESULT_NOT_SUPPORTED)
{
    /* vgl was built without VGL_DEFAULTS_HTTP. */
}
vgl_http_reader_io_interface_get(http, &io);
/* context_info.io = &io; then: */
pack_info.uri = "https://packs.example.org/glo30-norway";

The project hosts no packs: the URI points at packs you baked and host yourself (Hosting Packs, Reference Server).

vgl_pack_state_get

vgl_result vgl_pack_state_get(const vgl_pack* pack, vgl_pack_state* out_state,
                              vgl_result* out_failure);
State Meaning
VGL_PACK_STATE_LOADING The manifest is being read
VGL_PACK_STATE_READY The pack is in use
VGL_PACK_STATE_FAILED The pack cannot be used; out_failure says why

out_failure is optional and receives VGL_RESULT_SUCCESS unless the pack failed. Typical failures: VGL_RESULT_NOT_FOUND (no pack.vglm at the URI), VGL_RESULT_IO_ERROR, VGL_RESULT_CORRUPT_DATA, VGL_RESULT_VERSION_MISMATCH (a pack format this vgl does not read), and VGL_RESULT_INVALID_OPERATION for a second geoid pack. The log has the details. A failed pack takes no part in anything; destroy it when convenient.

Waiting until all packs are ready, for a tool or a test:

static int packs_settled(vgl_pack* const* packs, int count)
{
    int i;
    for (i = 0; i < count; ++i)
    {
        vgl_pack_state state;
        vgl_result failure;
        vgl_pack_state_get(packs[i], &state, &failure);
        if (state == VGL_PACK_STATE_LOADING)
        {
            return 0;
        }
        if (state == VGL_PACK_STATE_FAILED)
        {
            const char* name = NULL;
            vgl_result_name_get(failure, &name);
            fprintf(stderr, "pack %d failed: %s\n", i, name);
        }
    }
    return 1;
}

/* Packs advance only in updates. */
while (!packs_settled(packs, pack_count))
{
    vgl_context_update(context, NULL);
    /* In an interactive host, this is simply the frame loop. */
}

In C++, pack.state_get() returns vgl::expected<vgl::pack_status> with state (vgl::pack_state::loading, ready, failed) and failure.

The statistics also count packs still loading and failed (packs_loading, packs_failed).

vgl_pack_destroy

vgl_result vgl_pack_destroy(vgl_pack* pack);

Removes a pack from its context at any time: its outstanding reads are cancelled and its decoded tiles freed. The terrain and imagery it supplied are replaced by what the other packs have over the next frames. Always succeeds; null does nothing. In C++, vgl::pack destroys its pack when it goes out of scope; destroy packs before their context.

Imagery layers: opacity and priority

Each imagery pack is a layer. Layers draw from the lowest priority at the bottom to the highest at the top, each blended over those below by its image's alpha times its opacity. The OpenGL backend blends the top four layers of each tile. Both settings can change at any time.

vgl_pack_opacity_set and vgl_pack_opacity_get

vgl_result vgl_pack_opacity_set(vgl_pack* pack, float opacity);
vgl_result vgl_pack_opacity_get(const vgl_pack* pack, float* out_opacity);
  • opacity runs from 0 (hidden) to 1 (opaque); new packs have 1. Values outside [0, 1] fail with VGL_RESULT_INVALID_ARGUMENT.
  • At 0 the layer is hidden: its imagery is neither loaded nor drawn, and its attribution is not reported. Use this to switch layers off rather than destroying and recreating packs.
  • Ignored for elevation and geoid packs.

vgl_pack_priority_set and vgl_pack_priority_get

vgl_result vgl_pack_priority_set(vgl_pack* pack, int32_t priority);
vgl_result vgl_pack_priority_get(const vgl_pack* pack, int32_t* out_priority);
  • Changes the priority given at creation. Among equal priorities, the pack created later still wins.
  • Imagery layers are drawn in the new order from the next frame.
  • For elevation packs, the terrain is rebuilt in the new order, and drawn in the old order until the new meshes are ready, without holes.
  • Ignored for a geoid pack.

The samples' layer panel works this way: a check box sets opacity 0 or 1, a slider sets the opacity, and reordering the list sets priorities. A cross-fade between two imagery packs:

/* t from 0 to 1: Sentinel-2 detail fades in over Blue Marble. */
vgl_pack_opacity_set(sentinel2, (float)t);
static_cast<void>(sentinel2.opacity_set(0.5f));
static_cast<void>(bluemarble.priority_set(-1)); // Below every layer at priority 0.

The geoid

Elevation packs store heights above the ellipsoid. A geoid pack (EGM2008, baked by the Baker; the sample set has a coarse one in samples/data/packs/egm2008) adds heights above mean sea level:

  • Add it like any other pack, with vgl_pack_create; its manifest says it is a geoid. Its priority is ignored.
  • A context takes at most one. A second geoid pack fails with VGL_RESULT_INVALID_OPERATION.
  • It enables VGL_HEIGHT_REFERENCE_MEAN_SEA_LEVEL in height queries, the backends' sea surface (VGL_DRAW_FEATURE_SEA_SURFACE), and contours and height colours above mean sea level. Each draw item carries the geoid height at its tile's corners (vgl_draw_item::geoid_heights, has_geoid) for renderers that need it.
  • Until it has loaded, mean-sea-level queries return VGL_RESULT_NOT_READY with the height above the ellipsoid, and draw items have has_geoid set to VGL_FALSE.

The view and tile selection

vgl_view

Field Type Default Meaning
struct_size uint32_t Required sizeof(vgl_view)
reserved uint32_t 0 Must be 0
view_matrix vgl_dmat4 Required From ECEF to camera space. Must be invertible.
projection_matrix vgl_dmat4 Required From camera space to clip space, perspective or orthographic
viewport_width, viewport_height uint32_t Required The render target's size in pixels, both at least 1
max_screen_space_error double 2 The largest screen-space error of a selected tile, in pixels. Larger values load less and look coarser.

Matrices are column-major and multiply column vectors. Any handedness and any depth mapping work: OpenGL or Direct3D clip space, reversed or infinite projections. Selection culls with the frustum's four side planes and the plane through the eye, and ignores the near and far planes, so a projection's depth mapping never hides terrain. The eye position comes from the inverse of the view matrix. Hosts with row-major, row-vector matrices (common with Direct3D) pass theirs unchanged.

vgl_context_view_set

vgl_result vgl_context_view_set(vgl_context* context, const vgl_view* view);

Sets the view that the next updates select tiles for; the view is copied. Pass null to stop selecting (frames then draw nothing). VGL_RESULT_INVALID_ARGUMENT means a matrix element is not finite, the view matrix is not invertible, the viewport is empty, or the error is negative; VGL_RESULT_NOT_SUPPORTED means fields of a newer version are set.

A camera at a geodetic position looking at the Earth's centre, with a right-handed OpenGL projection (as in samples/c/main.c, whose look_at_earth and perspective helpers are also on Getting Started):

vgl_ellipsoid wgs84;
vgl_geodetic where = {VGL_DEGREES_TO_RADIANS(61.1), VGL_DEGREES_TO_RADIANS(6.2), 2.0e6};
vgl_dvec3 eye;
vgl_view view = {.struct_size = sizeof(view)};

vgl_ellipsoid_wgs84_get(&wgs84);
vgl_ellipsoid_geodetic_to_ecef(&wgs84, &where, &eye);

view.view_matrix = look_at_earth(eye);
view.projection_matrix = perspective(VGL_DEGREES_TO_RADIANS(60.0), (double)width / height);
view.viewport_width = (uint32_t)width;
view.viewport_height = (uint32_t)height;
if (vgl_context_view_set(context, &view) != VGL_RESULT_SUCCESS)
{
    /* The log says which part of the view is invalid. */
}

For a camera near the ground, build the view from the local East-North-Up frame (vgl_ellipsoid_enu_frame_get), as the samples' camera does; see Geodesy and Tiling.

Set the view every frame, before vgl_context_update, even when the camera holds still: the viewport size may change, and the update uses the latest view.

vgl_context_view_get

vgl_result vgl_context_view_get(const vgl_context* context, vgl_view* out_view);

Returns the view last set, for example for a backend that takes the host's projection. out_view is an output structure: set its struct_size (at least 8). Returns VGL_RESULT_NOT_FOUND, leaving out_view unchanged, when no view is set. In C++, ctx.view_get() returns vgl::expected<vgl_view>.

Drawing from another camera

The tiles are selected for the view of the last update, but a frame can compose its matrices for another camera: vgl_frame_begin_info::view_matrix, or vgl_gl_draw_info::view_matrix and projection_matrix for the OpenGL backend. A host that updates at 20 Hz and draws at 60 Hz uses this. The OpenGL sample's frozen selection (F5) does the same to inspect selection from outside.

The per-frame update

vgl_update_info

Field Type Default Meaning
struct_size uint32_t Required sizeof(vgl_update_info)
reserved uint32_t 0 Must be 0
time_budget double 2 ms Seconds the update may spend decoding, when there is no task interface. Completions are always processed in full, and at least one decode runs if any is waiting.

vgl_context_update

vgl_result vgl_context_update(vgl_context* context, const vgl_update_info* info);

Call it once per frame, after vgl_context_view_set and before drawing. It processes completed reads and tasks, decodes within the time budget when there is no task interface, selects tiles for the view and requests the missing ones, and evicts decoded tiles beyond the CPU budget. info may be null for the defaults.

Results: VGL_RESULT_SUCCESS; VGL_RESULT_INVALID_ARGUMENT; VGL_RESULT_NOT_SUPPORTED (fields of a newer version); VGL_RESULT_INVALID_OPERATION if a frame is open. The update and the frame functions must not overlap: a host that updates on one thread and draws on another serializes them.

A single-threaded host without a task interface trades frame time for loading speed with the time budget:

vgl_update_info update = {.struct_size = sizeof(update)};
update.time_budget = 0.004; /* 4 ms of decoding per frame. */
vgl_context_update(context, &update);
auto update = vgl::init<vgl_update_info>();
update.time_budget = 0.004;
static_cast<void>(ctx->update(&update));

The frame loop of a host is then:

vgl_context_view_set(context, &view);   /* 1. the camera */
vgl_context_update(context, NULL);      /* 2. loading and selection */
/* host clears its target, draws its scene */
vgl_gl_backend_draw(backend, &draw);    /* 3. the globe, into the host's pass */
/* host draws transparent geometry, the UI, presents */

Height queries

vgl_context_height_query

vgl_result vgl_context_height_query(vgl_context* context, double latitude, double longitude,
                                    vgl_height_reference reference, double* out_height);

Returns the terrain height at a geodetic position (radians), from the highest-priority elevation pack with data there, at the deepest level loaded so far, interpolated as the pack format specifies. reference is VGL_HEIGHT_REFERENCE_ELLIPSOID or VGL_HEIGHT_REFERENCE_MEAN_SEA_LEVEL (the latter needs a geoid pack).

Result out_height Meaning
VGL_RESULT_SUCCESS The final height From the finest data the packs have at the position
VGL_RESULT_NOT_READY The best current value: NaN while no data has loaded at all, above the ellipsoid while the geoid has not Finer data, a manifest or the geoid is still loading, or a read could not start for lack of memory (logged)
VGL_RESULT_NOT_FOUND Unchanged No ready elevation pack has data there
VGL_RESULT_INVALID_OPERATION Unchanged Mean sea level was asked for, and the context has no geoid pack
VGL_RESULT_INVALID_ARGUMENT Unchanged A null pointer, an angle not finite or out of range, an unknown reference

A query starts reading the finer data it needs, even where nothing is drawn. Call vgl_context_update to receive it, then query again. So a query is cheap to repeat each frame, and converges:

double height = 0.0;
const double lat = VGL_DEGREES_TO_RADIANS(61.2);
const double lon = VGL_DEGREES_TO_RADIANS(7.1);
vgl_result r = vgl_context_height_query(context, lat, lon,
                                        VGL_HEIGHT_REFERENCE_MEAN_SEA_LEVEL, &height);
if (r == VGL_RESULT_SUCCESS)
{
    /* Final: place the vehicle. */
}
else if (r == VGL_RESULT_NOT_READY && height == height) /* not NaN */
{
    /* Provisional: use it, and query again next frame. */
}

In C++, height_query returns vgl::expected<vgl::height_sample>, whose status is vgl::result::success or vgl::result::not_ready and whose height is NaN until data has loaded:

const vgl::expected<vgl::height_sample> h = ctx->height_query(
    vgl::degrees_to_radians(61.2), vgl::degrees_to_radians(7.1),
    vgl::height_reference::mean_sea_level);
if (h && h->status == vgl::result::success)
{
    place_vehicle(h->height);
}

Notes:

  • The height comes from decoded tiles, not from the drawn meshes. Far from the camera, drawn tiles are coarser, so the drawn surface can differ from the queried height by about the screen-space error.
  • Queries for many positions start many reads; the update caps outstanding reads, and the rest follow.

Ray queries and picking

vgl_ray_hit

An output structure: set struct_size (at least 8); the library writes the fields that fit.

Field Type Meaning
struct_size uint32_t Set by the caller; the library sets it to the bytes it wrote
reserved uint32_t 0
distance double Along the ray from its origin, in metres, whatever the direction's length
position vgl_dvec3 The hit, ECEF, in metres
geodetic vgl_geodetic The same position, with its height above the ellipsoid

vgl_context_ray_query

vgl_result vgl_context_ray_query(vgl_context* context, const vgl_dvec3* origin,
                                 const vgl_dvec3* direction, vgl_ray_hit* out_hit);

Intersects a ray with the terrain that height queries see: the deepest data loaded so far of the highest-priority elevation pack with data, and the ellipsoid where none has. The origin may lie anywhere, even under the terrain, which then counts as a hit at distance 0. The direction may have any length but 0.

Result out_hit Meaning
VGL_RESULT_SUCCESS The nearest hit From the finest data the packs have there
VGL_RESULT_NOT_READY The best current hit Finer data at the hit, or a manifest, is still loading
VGL_RESULT_NOT_FOUND Unchanged The ray misses the globe
VGL_RESULT_INVALID_ARGUMENT Unchanged A null pointer, struct_size below 8, a value not finite, or a zero direction

Like a height query, it starts the reads of finer data at the hit; update and query again to refine. Ridges narrower than the finest loaded sample spacing may be missed, and under the cursor the hit may lie off the drawn surface by about the screen-space error.

A ray through the cursor

For a view matrix that is a rigid transform into an OpenGL-style camera space (x right, y up, looking down -z), as look_at_earth produces, the camera's axes and position are in the matrix itself:

#include <math.h>

/* The ray through window pixel (x, y), y downwards from the top, for a rigid, right-handed
 * view matrix and a symmetric perspective with vertical field of view fovy (radians). */
static void cursor_ray(const vgl_dmat4* v, double fovy, double width, double height,
                       double x, double y, vgl_dvec3* origin, vgl_dvec3* direction)
{
    const double* m = v->m;
    const double t = tan(fovy / 2.0);
    const double sx = (2.0 * x / width - 1.0) * t * (width / height);
    const double sy = (1.0 - 2.0 * y / height) * t;
    /* Rows of the rotation are the camera's right, up and backward axes in ECEF. */
    const vgl_dvec3 right = {m[0], m[4], m[8]};
    const vgl_dvec3 up = {m[1], m[5], m[9]};
    const vgl_dvec3 back = {m[2], m[6], m[10]};
    /* The eye is -R^T t. */
    origin->x = -(m[0] * m[12] + m[1] * m[13] + m[2] * m[14]);
    origin->y = -(m[4] * m[12] + m[5] * m[13] + m[6] * m[14]);
    origin->z = -(m[8] * m[12] + m[9] * m[13] + m[10] * m[14]);
    direction->x = right.x * sx + up.x * sy - back.x;
    direction->y = right.y * sx + up.y * sy - back.y;
    direction->z = right.z * sx + up.z * sy - back.z;
}

/* On a click: */
vgl_dvec3 origin;
vgl_dvec3 direction;
vgl_ray_hit hit = {.struct_size = sizeof(hit)};
cursor_ray(&view.view_matrix, fovy, width, height, cursor_x, cursor_y, &origin, &direction);
if (VGL_SUCCEEDED(vgl_context_ray_query(context, &origin, &direction, &hit)))
{
    printf("picked %.5f, %.5f at %.1f m\n",
           VGL_RADIANS_TO_DEGREES(hit.geodetic.latitude),
           VGL_RADIANS_TO_DEGREES(hit.geodetic.longitude),
           hit.geodetic.height);
}

VGL_SUCCEEDED accepts VGL_RESULT_NOT_READY as well, which is right for interactive picking: the hit is the best one so far.

In C++, ray_query returns vgl::expected<vgl::ray_query_result>, with hit and status; a miss is the error vgl::result::not_found:

if (const auto q = ctx->ray_query(origin, direction))
{
    const vgl_geodetic& g = q->hit.geodetic;
    // q->status is vgl::result::success, or vgl::result::not_ready while finer data loads.
}

How the samples' grab navigation uses it

The interactive samples share a Google Earth style navigation (samples/common/navigation.hpp and navigation.cpp) that keeps the grabbed point under the cursor:

  1. When a drag, scroll or double-click starts, it casts a ray from the eye through the cursor with vgl_context_ray_query. On a hit (VGL_SUCCEEDED, so a provisional hit counts, with a distance above 0), it takes the hit's ECEF position as the grabbed point and holds the hit's height above the ellipsoid (never above the camera) until the movement ends.
  2. While the drag goes on, it finds the point under the cursor by intersecting the ray with the ellipsoid raised to that held height (semi-axes a + h and b + h), not with the terrain, so the result moves smoothly even as finer tiles arrive.
  3. It turns the globe about its centre so that this point moves onto the grabbed one, which therefore stays under the cursor. Zooms move towards the picked point; orbits turn and tilt about it.
  4. Without terrain picking (no hit), and for key movements, it falls back to the ellipsoid raised to the terrain's height under the camera, which the sample gets from vgl_context_height_query at the camera's position.

The OpenGL sample's check verifies these to within a pixel.

Projecting positions to the screen

Two free-threaded functions take a view rather than a context, so they work on any thread and with any view, for placing labels or markers over the globe.

vgl_view_project

vgl_result vgl_view_project(const vgl_view* view, const vgl_dvec3* position,
                            vgl_dvec3* out_window);

Projects an ECEF position, composed in double precision. out_window receives x in pixels from the viewport's left edge, y in pixels from its top edge downwards (pixel (i, j) has its centre at (i + 0.5, j + 0.5)), and z, the clip-space w, which for a perspective projection is the distance along the view axis in metres. OpenGL's window coordinates count y upwards: they are (x, viewport_height - y).

Returns VGL_RESULT_SUCCESS when the position is in front of the camera and VGL_RESULT_NOT_FOUND when it is behind it (or on the plane through it), leaving out_window unchanged. It does not say whether the position is visible: it may be outside the viewport, beyond the far plane, or behind the terrain or the globe. To test occlusion, cast a ray from the camera towards it with vgl_context_ray_query and compare distances.

vgl_view_geodetic_project

vgl_result vgl_view_geodetic_project(const vgl_view* view, const vgl_ellipsoid* ellipsoid,
                                     const vgl_geodetic* geodetic, vgl_dvec3* out_window);

The same for a geodetic position: vgl_ellipsoid_geodetic_to_ecef, then vgl_view_project. ellipsoid may be null for WGS84. The height is above the ellipsoid; for a height above mean sea level, add the geoid height first (the difference of the two height queries).

vgl_geodetic bergen = {VGL_DEGREES_TO_RADIANS(60.39), VGL_DEGREES_TO_RADIANS(5.32), 50.0};
vgl_dvec3 window;
if (vgl_view_geodetic_project(&view, NULL, &bergen, &window) == VGL_RESULT_SUCCESS &&
    window.x >= 0.0 && window.x < view.viewport_width &&
    window.y >= 0.0 && window.y < view.viewport_height)
{
    draw_label("Bergen", window.x, window.y);
}
if (const auto w = vgl::view_geodetic_project(view, bergen))
{
    draw_label("Bergen", w->x, w->y);
}

Attribution

vgl_context_attribution_get

vgl_result vgl_context_attribution_get(const vgl_context* context, uint32_t* inout_count,
                                       const char** out_strings);

Returns the credits of the data in view: the attribution lines of every pack whose tiles the view selects or the last frame drew, in pack priority order, each distinct line once. The lines come from the packs' metadata, written by the baker. Display them while the data is visible, as the data's licences require (see Attribution and Licensing). Hidden layers (opacity 0) are not reported.

It uses the two-call pattern: with out_strings null, *inout_count receives the number of lines; otherwise *inout_count is the capacity, and receives the number written, with VGL_RESULT_INCOMPLETE if there were more. The strings are UTF-8, owned by the library, and valid until the pack they come from is destroyed.

uint32_t count = 0;
const char* lines[16];

vgl_context_attribution_get(context, &count, NULL);
if (count > 16)
{
    count = 16;
}
if (VGL_SUCCEEDED(vgl_context_attribution_get(context, &count, lines)))
{
    uint32_t i;
    for (i = 0; i < count; ++i)
    {
        draw_credit(lines[i]);
    }
}

In C++, attribution_get() returns a std::vector<const char*>, empty without a context:

for (const char* line : ctx->attribution_get())
{
    draw_credit(line);
}

Calling it every frame is fine; the set changes as the view moves.

Statistics

vgl_context_statistics_get

vgl_result vgl_context_statistics_get(const vgl_context* context,
                                      vgl_context_statistics* out_statistics);

An output structure: set struct_size (at least 8); the library writes the fields that fit. The OpenGL sample shows these in its panel (F1), and the headless sample prints them.

Field Type Meaning
cpu_used uint64_t Bytes in use against the CPU budget
cpu_budget uint64_t The CPU budget in effect
gpu_used uint64_t Bytes of resources offered to or created by the consumer
gpu_budget uint64_t The GPU budget in effect
packs_loading uint32_t Packs whose manifest is still being read
packs_failed uint32_t Packs in VGL_PACK_STATE_FAILED
reads_outstanding uint32_t Reads issued and not yet completed
tiles_decoding uint32_t Tiles read and waiting for, or in, decoding
tiles_decoded uint32_t Decoded tiles in the CPU cache
meshes_building uint32_t Meshes being built
meshes_waiting uint32_t Meshes built for selected tiles and waiting to be created by the consumer
textures_decoding uint32_t Imagery tiles being decoded into textures
textures_waiting uint32_t Textures that selected tiles want and the consumer has not created yet
resources_resident uint32_t Resources the consumer holds
tiles_selected uint32_t Tiles the view selects
tiles_drawn uint32_t Tiles the last frame drew
resources_created uint32_t Resources the last frame created
resources_destroyed uint32_t Resources the last frame destroyed
bytes_created uint64_t Bytes the last frame created
vertices_drawn uint64_t Vertices the last frame drew, skirts included
triangles_drawn uint64_t Triangles the last frame drew, skirts included
reads_total uint64_t Reads since the context was created
bytes_read_total uint64_t Bytes read since the context was created
tiles_loaded_total uint64_t Tiles loaded since the context was created
tile_load_time_mean double Mean seconds from the start of a tile's read to its decoding
update_time double Seconds the last vgl_context_update took
frame_time double Seconds the last vgl_context_frame_begin took

The view has finished loading when nothing is in flight and every selected tile is drawn. The pure-C sample's check waits for exactly this:

vgl_context_statistics s = {.struct_size = sizeof(s)};
vgl_context_statistics_get(context, &s);
const int settled = s.packs_loading == 0 && s.reads_outstanding == 0 &&
                    s.tiles_decoding == 0 && s.meshes_building == 0 &&
                    s.meshes_waiting == 0 && s.textures_waiting == 0 &&
                    s.resources_created == 0 && s.tiles_drawn == s.tiles_selected;

In C++, ctx->statistics_get() returns the structure (the context must be valid).

Texture formats and resources

Two functions of <vgl/render.h> act on the context and matter to anyone writing a renderer:

  • vgl_context_texture_formats_set(context, formats, format_count) states the texture formats the consumer can sample, most preferred first (VGL_TEXTURE_FORMAT_RGBA8, VGL_TEXTURE_FORMAT_RGBA8_SRGB, VGL_TEXTURE_FORMAT_BC1, VGL_TEXTURE_FORMAT_BC7, VGL_TEXTURE_FORMAT_ETC2_RGB8, VGL_TEXTURE_FORMAT_ASTC_4X4). RGBA8 is always assumed and is the format until the call. The built-in backends call it when they are created, so a host using one never does. A later call applies to textures created after it. Today the core decodes JPEG and PNG imagery to RGBA8 or RGBA8_SRGB (the same bytes, sRGB-encoded; the second asks the consumer to sample them as sRGB); the compressed formats are reserved for later work that needs no interface change. In C++: ctx.texture_formats_set(std::span<const vgl::texture_format>).
  • vgl_context_resources_reset(context) forgets every render resource after the consumer lost them (a lost device or OpenGL context, an Android app sent to the background); they are rebuilt and offered again over the next frames. The backends' device_reset functions and their destroy functions call it for you. Render thread, and not while a frame is open.

The rest of the render interface is on Render Interface.

Allocator and log access for libraries

These two exist for libraries that work on behalf of a context from outside the core, such as the backends or your own renderer, so that their memory and messages follow the host's choices.

vgl_context_allocator_get

vgl_result vgl_context_allocator_get(const vgl_context* context,
                                     vgl_allocator_interface* out_allocator);

Writes the context's allocator: the host's, or one that uses the C heap. The table stays valid while the context exists. Allocate with out_allocator->allocate(out_allocator->user_data, size, alignment) and free with the same size and alignment.

vgl_context_log

vgl_result vgl_context_log(const vgl_context* context, vgl_log_level level,
                           const char* message);

Passes a message to the context's log callback, if one is set and level is enabled. level is VGL_LOG_LEVEL_ERROR to VGL_LOG_LEVEL_DEBUG (VGL_LOG_LEVEL_DEFAULT is not a message level and fails with VGL_RESULT_INVALID_ARGUMENT). Follow the library's style: UTF-8 without a trailing newline, starting with the name of the function involved. The callback runs on the calling thread.

vgl_context_log(context, VGL_LOG_LEVEL_WARNING,
                "my_renderer_draw: the depth buffer has 16 bits; use 24 or more");

Budgets

Budget Where Default Tune it when
CPU vgl_context_create_info::cpu_budget 256 MiB Mobile devices (lower), or wide views with many packs (higher)
GPU vgl_context_create_info::gpu_budget 512 MiB To match the device's memory
Update time vgl_update_info::time_budget 2 ms No task interface: more loads faster, less keeps frames steady
Upload The backend's draw info upload_budget, or vgl_frame_begin_info::upload_budget 16 MiB Hitches on big view changes (lower), slow fill-in (higher)
Detail vgl_view::max_screen_space_error 2 pixels Higher values select coarser tiles: less memory, reads and triangles

Budgets are set at creation and fixed for the context's life. Eviction never removes what the current view needs, so a budget too small for the view shows up as cpu_used or gpu_used staying at the budget while tiles wait; raise the budget or the screen-space error. The statistics' cpu_budget and gpu_budget show the values in effect.

Error handling

  • Every function returns vgl_result. Use VGL_SUCCEEDED and VGL_FAILED, since the queries return VGL_RESULT_NOT_READY (a success) while data loads, and the attribution may return VGL_RESULT_INCOMPLETE.
  • Every failure of a function that takes a context logs one error message saying what was wrong and how to fix it, if a log callback is set. Set one during development.
  • Free-threaded functions without a context (vgl_view_project, vgl_view_geodetic_project) report through the result only.
  • A failure leaves the objects unchanged; the context stays usable, even after VGL_RESULT_OUT_OF_MEMORY.
  • Pack problems are not call failures: watch vgl_pack_state_get or packs_failed.
  • No C++ exceptions cross the API. The C++ wrapper returns vgl::expected<T> or vgl::result, all [[nodiscard]]: discard deliberately with static_cast<void>(...).

Common results and their usual causes:

Function Result Usual cause
vgl_context_create VGL_RESULT_VERSION_MISMATCH An interface's version is not the VGL_*_INTERFACE_VERSION of these headers
vgl_pack_create VGL_RESULT_INVALID_OPERATION No io in the create info
vgl_pack_state_get VGL_PACK_STATE_FAILED with VGL_RESULT_NOT_FOUND Wrong URI, or the I/O interface cannot read that kind of URI (for example https:// without the HTTP reader)
vgl_context_view_set VGL_RESULT_INVALID_ARGUMENT A zero viewport (minimized window) or a singular view matrix
vgl_context_update VGL_RESULT_INVALID_OPERATION Called while a frame is open
vgl_context_height_query VGL_RESULT_INVALID_OPERATION Mean sea level without a geoid pack
vgl_context_ray_query VGL_RESULT_INVALID_ARGUMENT out_hit->struct_size not set

More on Troubleshooting.

See also

Clone this wiki locally