Repository navigation
Cpp Wrapper
vgl's only ABI is its C API. On top of it sits a header-only C++20 wrapper in namespace vgl that adds RAII handles, vgl::expected results, std::span lists, enum classes and base classes for implementing the host interfaces, without exceptions and without a second ABI: C and C++ hosts link the same binary. This page explains the wrapper's conventions, lists what each header provides, and shows the common tasks in C++. The rules behind it are in section 16 of the C API conventions.
| Header | Wraps | Link |
|---|---|---|
<vgl/vgl.hpp> |
Everything below except the defaults and backends | vgl::vgl |
<vgl/base.hpp> |
vgl/base.h: vgl::result, vgl::expected, vgl::init<T>(), vgl::version_get()
|
vgl::vgl |
<vgl/foundation.hpp> |
vgl/foundation.h: vgl::log_level, vgl::allocator, vgl::io_interface, vgl::task_interface, vgl::io_request_complete, vgl::io_request_create
|
vgl::vgl |
<vgl/geodesy.hpp> |
vgl/geodesy.h: vgl::pi, angle helpers, ellipsoid conversions, ENU frames |
vgl::vgl |
<vgl/context.hpp> |
vgl/context.h: vgl::context, vgl::pack, queries, vgl::texture_format, vgl::view_project
|
vgl::vgl |
<vgl/render.hpp> |
vgl/render.h: vgl::frame, vgl::depth_convention
|
vgl::vgl |
<vgl/defaults.hpp> |
vgl/defaults.h: vgl::thread_pool, vgl::file_reader, vgl::http_reader, vgl::disk_cache
|
vgl::defaults |
<vgl/gl.hpp> |
vgl/gl.h: vgl::gl_backend
|
vgl::gl |
<vgl/d3d11.hpp> |
vgl/d3d11.h: vgl::d3d11_backend
|
vgl::d3d11 |
<vgl/ogre.hpp> |
The Ogre backend, which is C++ only: vgl::ogre::globe
|
vgl::ogre |
The wrapper headers include their C headers, so every C type and function is available alongside. The wrapper never hides the C API: get() gives you the C handle for any function the wrapper does not cover.
The wrapper needs C++20 (for std::span). It uses std::expected when the standard library provides it (C++23) and a small equivalent otherwise.
Names map mechanically from C: drop the vgl_ prefix and keep snake case. Object functions become member functions.
| C | C++ |
|---|---|
vgl_context |
vgl::context |
vgl_context_statistics_get(context, &s) |
ctx.statistics_get() |
vgl_pack_opacity_set(pack, 0.5f) |
pack.opacity_set(0.5f) |
VGL_RESULT_OUT_OF_MEMORY |
vgl::result::out_of_memory |
VGL_PACK_STATE_READY |
vgl::pack_state::ready |
VGL_DEPTH_CONVENTION_REVERSED_Z |
vgl::depth_convention::reversed_z |
Nothing in the wrapper throws, with one exception noted below, and it works with exceptions and RTTI disabled. Every function is [[nodiscard]].
- Functions that produce a value return
vgl::expected<T>, which isstd::expected<T, vgl::result>when available. Test it withif (x)orx.has_value(), read the value with*xorx->, and the error withx.error(). - Functions that only act return
vgl::result. - Functions whose C counterpart can fail only on arguments that C++ rules out (null pointers) return the value directly, for example
vgl::ellipsoid_wgs84_get(),ctx.statistics_get(),vgl::version_get(). -
vgl::succeeded(r)andvgl::failed(r)mirrorVGL_SUCCEEDEDandVGL_FAILED: positive results such asvgl::result::not_readyandvgl::result::incompleteare successes. -
vgl::result_name_get(r)returns the code's name, such as"VGL_RESULT_NOT_FOUND", or"VGL_RESULT_UNKNOWN". -
vgl::unexpected(r)constructs a failedexpected, asstd::unexpecteddoes.
vgl::expected<vgl::context> ctx = vgl::context::create(&info);
if (!ctx)
{
std::fprintf(stderr, "vgl: %s\n", vgl::result_name_get(ctx.error()));
return 1;
}
if (const vgl::result r = ctx->update(); vgl::failed(r))
{
std::fprintf(stderr, "update: %s\n", vgl::result_name_get(r));
}When the standard library has no <expected>, vgl::expected<T> is a minimal subset: has_value(), operator bool, value(), operator*, operator-> and error(). It requires a default-constructible T, and calling value() on a failed one is undefined behaviour, as with operator* on std::expected. Stick to that subset if your code must build with both.
The one function that can throw is vgl::context::attribution_get(), which returns a std::vector<const char*> and can throw std::bad_alloc if the vector cannot be allocated. In a build without exceptions, call vgl_context_attribution_get directly with your own storage if that matters to you.
vgl::context, vgl::pack, vgl::frame, vgl::thread_pool, vgl::file_reader, vgl::http_reader, vgl::disk_cache, vgl::gl_backend and vgl::d3d11_backend are move-only owners of one C handle.
| Member | Meaning |
|---|---|
static create(...) |
Creates the object; returns vgl::expected<T>. |
get() |
The C handle, for C functions. |
explicit operator bool |
Whether it holds a handle. |
release() |
Gives up ownership and returns the handle. |
reset(handle = nullptr) |
Destroys the current handle (if any) and takes the new one. |
| Destructor | Destroys the handle. |
Destruction order matters, because some objects must outlive others: packs before their context, backends before their context (on the render thread), the context before its I/O and task objects. Locals are destroyed in reverse order of declaration, so declare them in creation order, or call reset() explicitly in the right order, as the samples do:
packs.clear(); // vgl::pack objects, before their context
ctx->reset(); // the context
reader->reset(); // the I/O object it read through
pool->reset(); // the thread pool, lastA vgl::pack that outlives its vgl::context is a use-after-free: the context destroys its packs, and the vgl::pack destructor would then destroy one again.
Create-info and output structures are the C structures themselves. vgl::init<T>() returns a value-initialized T with struct_size set to sizeof(T). Use it for every structure that has struct_size, then assign fields:
auto info = vgl::init<vgl_context_create_info>();
info.log_level = VGL_LOG_LEVEL_INFO;
info.cpu_budget = 512ull << 20;
auto view = vgl::init<vgl_view>();
auto stats = vgl::init<vgl_context_statistics>();Prefer this over C++20 designated initializers: on 32-bit and wasm32 targets, many structures have extra padding fields (VGL_POINTER_PADDING), and designated initializers must name fields in declaration order, which makes portable code awkward. Value initialization plus assignment always works.
Interface tables (vgl_allocator_interface, vgl_io_interface, vgl_task_interface) have version instead of struct_size; the wrapper's base classes fill those in for you (see below).
Pointer-and-count pairs become std::span:
const std::array formats = {vgl::texture_format::bc7, vgl::texture_format::bc1};
static_cast<void>(ctx->texture_formats_set(formats));
for (const vgl_draw_item& item : frame->draw_list_get())
{
// ...
}Two-call functions (count, then fill) are wrapped in functions that return a container, as attribution_get() does.
| Enum class | C type | Values |
|---|---|---|
vgl::result |
vgl_result |
success, incomplete, not_ready, invalid_argument, out_of_memory, not_supported, invalid_operation, not_found, io_error, corrupt_data, version_mismatch, cancelled, backend_error, internal_error
|
vgl::log_level |
vgl_log_level |
default_level, error, warning, info, debug
|
vgl::pack_state |
vgl_pack_state |
loading, ready, failed
|
vgl::height_reference |
vgl_height_reference |
ellipsoid, mean_sea_level
|
vgl::texture_format |
vgl_texture_format |
rgba8, rgba8_srgb, bc1, bc7, etc2_rgb8, astc_4x4
|
vgl::depth_convention |
vgl_depth_convention |
logarithmic, reversed_z, conventional
|
The values are those of the C enumerations, so static_cast converts both ways. Fields of C structures keep their C types: info.log_level = VGL_LOG_LEVEL_INFO, create_info.depth_convention = VGL_DEPTH_CONVENTION_REVERSED_Z. Where the wrapper takes a value, it takes the enum class (ctx.log(vgl::log_level::info, "...")).
const vgl::version v = vgl::version_get(); // of the binary you run against
std::printf("vgl %u.%u.%u (headers %s)\n",
v.major_version, v.minor_version, v.patch_version, VGL_VERSION_STRING);vgl::version has major_version, minor_version and patch_version. The VGL_VERSION_MAJOR, VGL_VERSION_MINOR, VGL_VERSION_PATCH and VGL_VERSION_STRING macros give the version of the headers you compiled against, which may differ from a shared library's.
vgl::context
| Member | Returns | C function |
|---|---|---|
static create(const vgl_context_create_info* info = nullptr) |
expected<context> |
vgl_context_create |
update(const vgl_update_info* info = nullptr) |
result |
vgl_context_update |
view_set(const vgl_view* view) |
result |
vgl_context_view_set (null stops selection) |
view_get() |
expected<vgl_view> |
vgl_context_view_get (result::not_found without a view) |
height_query(lat, lon, height_reference = ellipsoid) |
expected<height_sample> |
vgl_context_height_query |
ray_query(origin, direction) |
expected<ray_query_result> |
vgl_context_ray_query (a miss is result::not_found) |
attribution_get() |
std::vector<const char*> |
vgl_context_attribution_get |
statistics_get() |
vgl_context_statistics |
vgl_context_statistics_get |
texture_formats_set(std::span<const texture_format>) |
result |
vgl_context_texture_formats_set |
resources_reset() |
result |
vgl_context_resources_reset |
allocator_get() |
vgl_allocator_interface |
vgl_context_allocator_get |
log(log_level, const char*) |
result |
vgl_context_log |
statistics_get() and allocator_get() assert that the context is valid.
Query results keep the status next to the value, because a query that succeeds may still be provisional:
struct height_sample
{
double height = NaN; // NaN until any data has loaded
vgl::result status = result::not_ready; // success when final, not_ready while loading
};
struct ray_query_result
{
vgl_ray_hit hit = vgl::init<vgl_ray_hit>();
vgl::result status = result::not_ready;
};An error (not_found, invalid_operation, invalid_argument) comes back as a failed expected instead.
vgl::pack
| Member | Returns | C function |
|---|---|---|
static create(context&, const vgl_pack_create_info&) |
expected<pack> |
vgl_pack_create |
static create(context&, const char* uri, std::int32_t priority = 0) |
expected<pack> |
vgl_pack_create |
state_get() |
expected<pack_status> |
vgl_pack_state_get; pack_status has state and failure
|
priority_set(std::int32_t), priority_get()
|
result, expected<std::int32_t>
|
vgl_pack_priority_set, _get
|
opacity_set(float), opacity_get()
|
result, expected<float>
|
vgl_pack_opacity_set, _get
|
Free functions: vgl::view_project(const vgl_view&, const vgl_dvec3&) and vgl::view_geodetic_project(const vgl_view&, const vgl_geodetic&, const vgl_ellipsoid* = nullptr) return expected<vgl_dvec3> with the window position (result::not_found behind the camera).
vgl::frame is an open frame of the render interface. It ends when it is destroyed, so an early return cannot leave a frame open, and its lists are valid until it ends.
| Member | Returns | C function |
|---|---|---|
static begin(context&, const vgl_frame_begin_info* info = nullptr) |
expected<frame> |
vgl_context_frame_begin |
destroys_get() |
std::span<const std::uint64_t> |
vgl_frame_destroys_get |
index_lists_get() |
std::span<const vgl_index_list_data> |
vgl_frame_index_lists_get |
meshes_get() |
std::span<const vgl_mesh_data> |
vgl_frame_meshes_get |
textures_get() |
std::span<const vgl_texture_data> |
vgl_frame_textures_get |
draw_list_get() |
std::span<const vgl_draw_item> |
vgl_frame_draw_list_get |
draw_layers_get() |
std::span<const vgl_draw_layer> |
vgl_frame_draw_layers_get |
resource_handle_set(resource, handle) |
result |
vgl_frame_resource_handle_set |
resource_failed(resource, result) |
result |
vgl_frame_resource_failed |
end() |
vgl_frame_end; the destructor then does nothing |
The spans are of this header's structures, so frame::begin accepts only strides (draw_item_size and the others in vgl_frame_begin_info) of 0 or this header's sizes, and returns result::invalid_argument for any other. A consumer built against a different header version that needs other strides reads the lists through the C functions. The getters return empty spans for a frame that is not open.
vgl::depth_convention is here too (see the enum table above).
The host interfaces are C tables of function pointers. The wrapper turns each into an abstract base class: you derive, override the virtual functions, and pass c_interface() wherever the C API takes the table. c_interface() fills in version, sets user_data to this, and points the entries at private static thunks (allocate_thunk, read_thunk, cancel_thunk, submit_thunk, destroy_thunk and so on) that cast user_data back to your object and call the virtual function. No RTTI is involved.
| Base class | Override | Optional overrides | Table |
|---|---|---|---|
vgl::allocator |
allocate(size, alignment), deallocate(memory, size, alignment)
|
vgl_allocator_interface |
|
vgl::io_interface |
read(io_request*, const io_read&) |
cancel(io_request*), on_destroy()
|
vgl_io_interface |
vgl::task_interface |
submit(task_fn, void* task_data) returning vgl::result
|
on_destroy() |
vgl_task_interface |
- All overrides are
noexcept: nothing may escape into the library. - The classes are neither copyable nor movable, because the C table points at the object. The object must outlive every context (and request, and task) that uses its table.
-
on_destroy()corresponds to the table'sdestroyentry. It is called once per context that uses the object, while that context is destroyed, on the destroying thread. Reads and tasks may still be outstanding then and must still be completed or run. The default does nothing. -
vgl::io_request_complete(request, vgl::result, std::span<const std::byte> data = {})completes a request; the bytes are copied. -
vgl::io_request_create(complete, user_data, allocator = nullptr)returnsexpected<io_request*>, for chaining. -
vgl::io_size_to_endisVGL_IO_SIZE_TO_END;vgl::io_request,vgl::io_read,vgl::io_complete_fnandvgl::task_fnalias the C types.
// I/O that serves packs from memory, completing every read inside read().
class memory_io final : public vgl::io_interface
{
public:
void add(std::string name, std::vector<std::byte> bytes) { files_[std::move(name)] = std::move(bytes); }
void read(vgl::io_request* request, const vgl::io_read& r) noexcept override
{
const auto it = files_.find(r.uri);
if (it == files_.end())
{
static_cast<void>(vgl::io_request_complete(request, vgl::result::not_found));
return;
}
const std::span<const std::byte> file = it->second;
const std::uint64_t offset = std::min<std::uint64_t>(r.offset, file.size());
const std::uint64_t size = std::min<std::uint64_t>(r.size, file.size() - offset);
static_cast<void>(vgl::io_request_complete(
request, vgl::result::success, file.subspan(static_cast<std::size_t>(offset), static_cast<std::size_t>(size))));
}
private:
std::map<std::string, std::vector<std::byte>, std::less<>> files_;
};
// Declines every task, so that vgl_context_update decodes (as tests/api_cpp.cpp does).
class inline_tasks final : public vgl::task_interface
{
public:
vgl::result submit(vgl::task_fn, void*) noexcept override { return vgl::result::not_supported; }
};
memory_io io; // outlive the context
inline_tasks tasks;
const vgl_io_interface io_table = io.c_interface();
const vgl_task_interface task_table = tasks.c_interface();
auto info = vgl::init<vgl_context_create_info>();
info.io = &io_table;
info.tasks = &task_table;Here io.add("mem:gebco/pack.vglm", ...) and a pack URI of mem:gebco would match, since reads are the pack URI plus / and the file's relative path. Complete examples, including an asynchronous reader with cancellation and an adapter for a job system, are on Host interfaces.
vgl::pi, vgl::degrees_to_radians, vgl::radians_to_degrees (both constexpr), vgl::ellipsoid_wgs84_get(), vgl::ellipsoid_geodetic_to_ecef(ellipsoid, geodetic) and vgl::ellipsoid_ecef_to_geodetic(ellipsoid, ecef) returning expected, vgl::ellipsoid_enu_frame_get(ellipsoid, origin) returning expected<vgl::enu_frame> (with enu_to_ecef and ecef_to_enu), and vgl::earth_rotation_angle_get(julian_date) returning expected<double>. See Geodesy and tiling.
vgl::thread_pool, vgl::file_reader, vgl::http_reader and vgl::disk_cache, each with create overloads and io_interface_get() or task_interface_get(). See Defaults module.
vgl::gl_backend (create(context&, const vgl_gl_backend_create_info&), draw, stars_set, clouds_set, device_reset) and vgl::d3d11_backend follow the same pattern; destroy them on the render thread before their context. The Ogre backend is a C++ API of its own, vgl::ogre::globe, an Ogre::MovableObject. See OpenGL backend, Direct3D 11 backend and Ogre backend.
Based on samples/headless/main.cpp:
#include <vgl/defaults.hpp>
#include <vgl/vgl.hpp>
#include <cstdio>
#include <vector>
int main()
{
vgl::expected<vgl::thread_pool> pool = vgl::thread_pool::create();
if (!pool)
{
return 1;
}
vgl::expected<vgl::file_reader> files = vgl::file_reader::create(*pool);
if (!files)
{
return 1;
}
const vgl_task_interface tasks = pool->task_interface_get();
const vgl_io_interface io = files->io_interface_get();
auto info = vgl::init<vgl_context_create_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, "context: %s\n", vgl::result_name_get(ctx.error()));
return 1;
}
std::vector<vgl::pack> packs;
for (const auto& [uri, priority] : {std::pair{"samples/data/packs/gebco", 0},
std::pair{"samples/data/packs/egm2008", 0},
std::pair{"samples/data/packs/bluemarble", 0}})
{
vgl::expected<vgl::pack> p = vgl::pack::create(*ctx, uri, priority);
if (!p)
{
std::fprintf(stderr, "%s: %s\n", uri, vgl::result_name_get(p.error()));
return 1;
}
packs.push_back(std::move(*p));
}
// ... frames, below ...
packs.clear();
ctx->reset();
files->reset();
pool->reset();
return 0;
}A captureless lambda converts to the C function pointer vgl_log_fn, as above.
for (bool loading = true; loading;)
{
static_cast<void>(ctx->update());
loading = false;
for (const vgl::pack& p : packs)
{
const vgl::expected<vgl::pack_status> s = p.state_get();
if (s && s->state == vgl::pack_state::loading)
{
loading = true;
}
else if (s && s->state == vgl::pack_state::failed)
{
std::fprintf(stderr, "pack failed: %s\n", vgl::result_name_get(s->failure));
}
}
}In an interactive host you would not spin like this: just call update() once per frame and check the states when you need them.
auto view = vgl::init<vgl_view>();
view.view_matrix = camera_view; // ECEF to camera, vgl_dmat4
view.projection_matrix = projection; // camera to clip
view.viewport_width = 1920;
view.viewport_height = 1080;
if (ctx->view_set(&view) != vgl::result::success)
{
// a matrix is not finite, the view is not invertible, or the viewport is empty
}
static_cast<void>(ctx->update());
{
vgl::expected<vgl::frame> f = vgl::frame::begin(*ctx);
if (f)
{
for (const std::uint64_t id : f->destroys_get()) { /* free the GPU resource */ }
for (const vgl_mesh_data& m : f->meshes_get()) { /* create a vertex buffer */ }
for (const vgl_draw_item& item : f->draw_list_get()) { /* draw */ }
}
} // the frame ends hereWith a backend, the backend runs the frame for you: call backend.draw(&draw_info) after update() instead (see OpenGL backend). See Geodesy and tiling for building camera_view.
const double lat = vgl::degrees_to_radians(61.6363);
const double lon = vgl::degrees_to_radians(8.3125);
vgl::expected<vgl::height_sample> h =
ctx->height_query(lat, lon, vgl::height_reference::mean_sea_level);
if (!h)
{
// result::not_found: no elevation pack covers it;
// result::invalid_operation: mean sea level without a geoid pack
}
else if (h->status == vgl::result::not_ready)
{
// a provisional value (NaN before any data): call update() and ask again
}
else
{
std::printf("%.1f m above mean sea level\n", h->height);
}vgl::expected<vgl::ray_query_result> q = ctx->ray_query(camera_position, ray_direction);
if (q)
{
const vgl_geodetic& g = q->hit.geodetic; // radians, height above the ellipsoid
std::printf("%.5f, %.5f, %.1f m%s\n",
vgl::radians_to_degrees(g.latitude),
vgl::radians_to_degrees(g.longitude),
g.height,
q->status == vgl::result::not_ready ? " (refining)" : "");
}
else if (q.error() == vgl::result::not_found)
{
// the ray misses the globe
}static_cast<void>(sentinel.opacity_set(0.5f)); // blend an imagery layer
static_cast<void>(sentinel.priority_set(10)); // draw it above the othersfor (const char* line : ctx->attribution_get())
{
overlay_text(line); // display while the data is visible
}
const vgl_context_statistics s = ctx->statistics_get();
std::printf("%u tiles drawn, %.1f MiB CPU\n", s.tiles_drawn, s.cpu_used / 1048576.0);const vgl_geodetic oslo{vgl::degrees_to_radians(59.91), vgl::degrees_to_radians(10.75), 100.0};
if (vgl::expected<vgl_dvec3> w = vgl::view_geodetic_project(view, oslo))
{
draw_label(w->x, w->y, "Oslo"); // pixels from the top left; w->z is the distance
}- Context API for what each function does in detail.
-
Host interfaces for the contracts behind
vgl::allocator,vgl::io_interfaceandvgl::task_interface. -
tests/api_cpp.cpp, which compiles against every part of the wrapper, and the Samples.
Getting vgl
Using the library
Rendering
Data
Reference
Specifications