Repository navigation
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.
- Function overview
- Creating and destroying a context
- Packs
- Imagery layers: opacity and priority
- The geoid
- The view and tile selection
- The per-frame update
- Height queries
- Ray queries and picking
- Projecting positions to the screen
- Attribution
- Statistics
- Texture formats and resources
- Allocator and log access for libraries
- Budgets
- Error handling
| 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.
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.
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_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_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:
- Backends (on the render thread, with their graphics context current).
- Packs.
- The context.
- 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.
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.
| 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_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);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_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_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.
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_result vgl_pack_opacity_set(vgl_pack* pack, float opacity);
vgl_result vgl_pack_opacity_get(const vgl_pack* pack, float* out_opacity);-
opacityruns from 0 (hidden) to 1 (opaque); new packs have 1. Values outside [0, 1] fail withVGL_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_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.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_LEVELin 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_READYwith the height above the ellipsoid, and draw items havehas_geoidset toVGL_FALSE.
| 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_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_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>.
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.
| 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_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 */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.
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_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.
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.
}The interactive samples share a Google Earth style navigation
(samples/common/navigation.hpp
and navigation.cpp) that keeps the grabbed point under the cursor:
- 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. - 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.
- 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.
- 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_queryat the camera's position.
The OpenGL sample's check verifies these to within a pixel.
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_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_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);
}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.
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).
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_resetfunctions 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.
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_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_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");| 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.
- Every function returns
vgl_result. UseVGL_SUCCEEDEDandVGL_FAILED, since the queries returnVGL_RESULT_NOT_READY(a success) while data loads, and the attribution may returnVGL_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_getorpacks_failed. - No C++ exceptions cross the API. The C++ wrapper returns
vgl::expected<T>orvgl::result, all[[nodiscard]]: discard deliberately withstatic_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.
- Getting Started: a complete first program.
- Core Concepts: the lifecycle, threading and coordinate frames behind these calls.
-
Host Interfaces and Defaults Module: the
io,tasks,allocatorandlogof the create info. - Render Interface: frames, resources and the draw list.
-
Geodesy and Tiling:
vgl_ellipsoid_*and the tile scheme. -
Cpp Wrapper:
vgl::context,vgl::packandvgl::expected. - The header itself: include/vgl/context.h.
Getting vgl
Using the library
Rendering
Data
Reference
Specifications