Skip to content

Cpp Wrapper

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

C++ 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.

Headers

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.

Conventions

Names

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

Errors without exceptions

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 is std::expected<T, vgl::result> when available. Test it with if (x) or x.has_value(), read the value with *x or x->, and the error with x.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) and vgl::failed(r) mirror VGL_SUCCEEDED and VGL_FAILED: positive results such as vgl::result::not_ready and vgl::result::incomplete are 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 failed expected, as std::unexpected does.
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.

RAII handles

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, last

A 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.

Versioned structures and vgl::init<T>()

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).

Spans

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 classes

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, "...")).

Version

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.

Reference by header

vgl/context.hpp

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/render.hpp

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).

vgl/foundation.hpp: implementing host interfaces in C++

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's destroy entry. 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) returns expected<io_request*>, for chaining.
  • vgl::io_size_to_end is VGL_IO_SIZE_TO_END; vgl::io_request, vgl::io_read, vgl::io_complete_fn and vgl::task_fn alias 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/geodesy.hpp

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/defaults.hpp

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.

Backends

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.

Common tasks in C++

Create a context with the defaults and load packs

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.

Wait for packs to load

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.

Set the view and run frames

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 here

With 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.

Query heights

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

Pick the terrain under the cursor

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
}

Change layers at run time

static_cast<void>(sentinel.opacity_set(0.5f)); // blend an imagery layer
static_cast<void>(sentinel.priority_set(10));  // draw it above the others

Show attribution and statistics

for (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);

Project a position to the screen

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
}

See also

Clone this wiki locally