Skip to content

Troubleshooting

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

Troubleshooting

This page collects the problems an integrator is most likely to hit when embedding vgl, with their causes and fixes: a globe that does not draw, depth artefacts, tiles that never load, memory budgets, versioned-structure errors, missing DLLs on Windows, web builds that refuse to start and Ogre render system issues. It starts with the two tools that diagnose nearly everything: the log and the statistics. Each entry names the result codes and log messages that vgl actually produces, so you can search this page for the text you see.

First: turn on logging and read the statistics

Logging

vgl is silent unless you give the context a log callback: it never writes to standard output, standard error or a debugger itself. The default minimum level is VGL_LOG_LEVEL_WARNING. While integrating, set VGL_LOG_LEVEL_INFO or VGL_LOG_LEVEL_DEBUG. The backends log through the same context (vgl_context_log), so one callback receives everything.

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

vgl_context_create_info info = {.struct_size = sizeof(info)};
info.io = &io;
info.tasks = &tasks;
info.log = on_log;
info.log_level = VGL_LOG_LEVEL_DEBUG;
auto info = vgl::init<vgl_context_create_info>();
info.io = &io;
info.tasks = &tasks;
info.log_level = VGL_LOG_LEVEL_INFO;
info.log = [](void*, vgl_log_level, const char* message) {
    std::fprintf(stderr, "vgl: %s\n", message);
};

Messages start with the API function involved, say what happened and, where the fix is known, how to fix it, for example vgl_pack_create: the context has no I/O interface; pass one in vgl_context_create_info::io. Rules are in docs/c-api.md, section 13.

Every function returns a vgl_result. Zero is success, positive values are non-error statuses (VGL_RESULT_INCOMPLETE, VGL_RESULT_NOT_READY), negative values are errors. Test with VGL_SUCCEEDED / VGL_FAILED, not == VGL_RESULT_SUCCESS, where a positive status is acceptable, and print names with vgl_result_name_get (C++: vgl::result_name_get).

Statistics

vgl_context_statistics_get (C++: vgl::context::statistics_get) tells you what the context is doing. It is an output structure: set struct_size (at least 8) and the library fills what fits.

vgl_context_statistics s = {.struct_size = sizeof(s)};
if (vgl_context_statistics_get(context, &s) == VGL_RESULT_SUCCESS)
{
    printf("packs loading %u, failed %u; reads %u; selected %u, drawn %u; "
           "CPU %llu / %llu, GPU %llu / %llu\n",
           s.packs_loading, s.packs_failed, s.reads_outstanding, s.tiles_selected,
           s.tiles_drawn, (unsigned long long)s.cpu_used, (unsigned long long)s.cpu_budget,
           (unsigned long long)s.gpu_used, (unsigned long long)s.gpu_budget);
}

How to read the fields:

Symptom in the statistics Meaning See
packs_failed above 0 A pack could not be used A pack fails
packs_loading stays above 0 A manifest read never completes Tiles never load
tiles_selected is 0 No view is set, the view sees no globe, or no elevation pack is ready Nothing draws
tiles_selected above 0, tiles_drawn 0 Selection works, but the consumer does not draw (frame not taken, or resources not created) Nothing draws
reads_outstanding stays high Reads are not completing: a slow or stuck I/O interface Tiles never load
meshes_waiting or textures_waiting stay high Resources are offered but not created; the upload budget or GPU budget may be too small Memory and budgets
cpu_used near cpu_budget Decoded tiles are being evicted Memory and budgets

When loading has settled, nothing is loading, building or uploading, and every selected tile is drawn. The samples test exactly this (sample::idle in samples/common/sample_check.cpp):

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;

The interactive samples show all of these in their panel, which is the quickest way to see whether a problem is in your packs or in your integration: point a sample at your packs with --pack first.

Nothing draws

Work through these in order.

1. The context has no I/O interface. Without vgl_context_create_info::io, vgl_pack_create fails with VGL_RESULT_INVALID_OPERATION and logs "the context has no I/O interface". Pass the defaults module's file reader (vgl_file_reader_io_interface_get) or your own (Host interfaces, Defaults module).

2. No packs, or the packs failed. Check vgl_pack_state_get (C++: vgl::pack::state_get) for each pack, or packs_failed in the statistics. See A pack fails.

3. No view, or no update. Each frame, call vgl_context_view_set and then vgl_context_update before drawing. Without a view, nothing is selected and the backends draw nothing. vgl_context_view_set fails with VGL_RESULT_INVALID_ARGUMENT and logs "the view is invalid; check that the matrices are finite, that the view matrix is an invertible affine transform, and that the viewport is not empty".

4. The camera is not where you think. The view matrix goes from ECEF (Earth-centred, Earth-fixed, in metres) to camera space. A camera at the origin is inside the Earth, and one at a few hundred units sees nothing of a globe 6,378 km in radius. Build the eye from vgl_ellipsoid_geodetic_to_ecef (see Geodesy and tiling), or copy sample::camera_view_matrix from samples/common/sample_camera.hpp. Matrices are column-major multiplying column vectors (vgl_dmat4); row-major matrices that multiply row vectors, common with Direct3D, can be passed unchanged.

5. The backend failed to create. Check the result of vgl_gl_backend_create, vgl_d3d11_backend_create or vgl::ogre::globe::create, and read the log:

Result Cause
VGL_RESULT_INVALID_ARGUMENT The OpenGL loader (get_proc_address) is missing, or a required pointer is null
VGL_RESULT_NOT_SUPPORTED OpenGL older than 3.3 or OpenGL ES older than 3.0, a missing function, or reversed-Z without clip control. The log names which
VGL_RESULT_BACKEND_ERROR A shader failed to compile or link; the log has the driver's message

With OpenGL, the loader must also return OpenGL 1.x functions. glfwGetProcAddress, SDL_GL_GetProcAddress and EGL 1.5's eglGetProcAddress do; wglGetProcAddress alone does not (vgl/gl.h). The OpenGL context must be current on the calling thread.

6. The depth buffer does not match the depth convention. This is the most common cause of a black or empty view once everything loads. The backend's convention, the host's projection and the host's depth buffer must agree (Render interface, docs/render-interface.md, section 8):

Convention Projection Depth buffer Clear Compare
VGL_DEPTH_CONVENTION_LOGARITHMIC (default) Conventional; near and far do not matter Any 1 LESS
VGL_DEPTH_CONVENTION_REVERSED_Z Reversed (near to 1, far to 0), ideally infinite far 32-bit float 0 GREATER
VGL_DEPTH_CONVENTION_CONVENTIONAL Conventional Any 1 LESS

Typical mistakes: clearing depth to 1 with reversed-Z (nothing passes GREATER), a 24-bit integer depth buffer with reversed-Z (z-fighting), or forgetting that reversed-Z on OpenGL needs OpenGL 4.5, GL_ARB_clip_control or GL_EXT_clip_control. The OpenGL backend sets the clip control depth mode itself while it draws (and restores it), but the host's own geometry and clears must use the same. With Direct3D 11, projections map depth to [0, 1].

7. Faces culled the wrong way. If you render into a texture with a projection that flips y, set vgl_gl_draw_info::mirrored to VGL_TRUE so the backend culls the other side.

8. Host state. The backends save and restore the state they change (listed at the top of vgl/gl.h and vgl/d3d11.h), but draw with the host's framebuffer or render targets, viewport and scissor as they are. Bind your framebuffer and set the viewport before drawing. With OpenGL, the host's stencil test and colour mask also apply to the globe, so a colour mask or stencil test left enabled from earlier passes can hide it. With Direct3D 11, the backend draws with the stencil test and blending off. Conversely, if your own rendering breaks after vgl draws, check whether it relies on state the backend does not list (it restores only what it changes).

9. A frame is open. vgl_context_update fails with VGL_RESULT_INVALID_OPERATION ("a frame is open; call vgl_frame_end before updating") and so does a second vgl_context_frame_begin ("a frame is already open; call vgl_frame_end first"). With a custom consumer of the render interface, end every frame you begin, also on error paths. The backends begin and end their own frames inside their draw calls; do not open one around them.

10. An imagery layer is hidden. An imagery pack at opacity 0 is neither loaded nor drawn (vgl_pack_opacity_set). Without imagery, terrain is coloured by height.

11. Only coarse, placeholder-like tiles. Right after start or a jump, regions draw coarse placeholders until their tiles arrive; the upload budget (upload_budget, default VGL_UPLOAD_BUDGET_DEFAULT, 16 MiB per frame) spreads uploads over several frames. If the detail never arrives, see Tiles never load.

Z-fighting, jitter and depth artefacts

Z-fighting at a distance or flickering terrain. You are probably using VGL_DEPTH_CONVENTION_CONVENTIONAL, which is meant for small scenes only. Use the logarithmic convention (works everywhere, including OpenGL ES, WebGL 2 and macOS) or reversed-Z with a 32-bit floating-point depth buffer. If you must use the conventional convention, raise the near plane with the altitude, as the Ogre sample does (near = max(1, altitude * 0.05)).

Host geometry does not depth-test correctly against the globe with the logarithmic convention. Your own shaders must write the same logarithmic depth. Pass the same far distance (logarithmic_far, 0 selects VGL_LOGARITHMIC_DEPTH_FAR_DEFAULT, 100,000 km) and use the snippet in docs/render-interface.md, section 8.

Reversed-Z with Ogre shows z-fighting. Ogre's render windows keep a 24-bit integer depth buffer even with the render system's "Reversed Z-Buffer" option. Draw the globe into a render texture (which then gets a 32-bit float depth buffer) and show that, as the Ogre sample does, or use the logarithmic convention. See Ogre render system issues.

Jitter close to the ground. vgl's vertices are single precision relative to each tile's centre, and the core composes each tile's model-view matrix in double precision, so the globe itself does not jitter. Jitter comes from the host's camera: pass the view matrix to vgl_context_view_set in double precision (vgl_dmat4) built from double-precision ECEF positions, not a matrix that went through float. With Ogre, the globe replaces Ogre's single-precision view with its own model-view; keep the scene node it is attached to at the origin, unrotated and unscaled. An Ogre build with OGRE_DOUBLE_PRECISION is not needed.

A pack fails

A failed pack has VGL_PACK_STATE_FAILED, and vgl_pack_state_get gives the reason as its failure result. The log has an error of the form vgl_context_update: pack <uri>: <what> (<result>).

Log message Result Cause and fix
reading pack.vglm failed; check the pack's URI VGL_RESULT_NOT_FOUND or VGL_RESULT_IO_ERROR The URI must be the pack's directory: vgl appends /pack.vglm. Do not include pack.vglm in the URI, and check relative paths against the working directory
pack.vglm is invalid VGL_RESULT_CORRUPT_DATA or VGL_RESULT_VERSION_MISMATCH The manifest is damaged, truncated (an upload that was compressed or cut short) or of a format version this vgl does not read. Check it with vgl-bake inspect <pack> and vgl-bake validate <pack> (Baker)
the pack uses a different ellipsoid than the context VGL_RESULT_NOT_SUPPORTED The pack was baked on another ellipsoid than vgl_context_create_info::ellipsoid (WGS84 by default)
the context already has a geoid pack VGL_RESULT_INVALID_OPERATION A context takes at most one geoid pack
its attribution could not be copied VGL_RESULT_OUT_OF_MEMORY Allocation failed

Errors in individual shards and tiles are logged as vgl_context_update: pack <uri>: shard <path>: ... and vgl_context_update: pack <uri>: tile <level>/<x>/<y>: ..., and imagery that does not decode as imagery tile <level>/<x>/<y> does not decode. A shard that failed for lack of memory is logged as a warning and read again when next needed.

vgl_pack_create itself fails only for invalid arguments (for example "info->uri is null; set it to the pack's directory"), a missing I/O interface, newer-version fields or out of memory: the pack loads asynchronously, so URI problems appear later, as above.

Tiles never load

Height queries return VGL_RESULT_NOT_READY forever. Queries and loads only progress when you call vgl_context_update, which processes completed reads. Call it every frame, also in tools without rendering.

Reads never complete. Your I/O interface must eventually complete every request with vgl_io_request_complete, from any thread, including failures and cancellations. reads_outstanding that never drops points to a request that was never completed. Without a task interface, decoding runs inside vgl_context_update within its time_budget (2 ms by default), which is slower; give the context the defaults module's thread pool or your own task system (Host interfaces).

HTTP packs fail immediately. The HTTP reader needs vgl built with VGL_DEFAULTS_HTTP; otherwise vgl_http_reader_create returns VGL_RESULT_NOT_SUPPORTED. The samples print "vgl was built without VGL_DEFAULTS_HTTP, so it cannot read packs from a web server". URIs that do not start with http:// or https:// go to the reader's fallback interface, and without one complete with VGL_RESULT_NOT_FOUND (in a browser they are fetched relative to the page).

HTTP status codes. The HTTP reader completes a 404 or 410 with VGL_RESULT_NOT_FOUND, a 416 (range past the end) with no bytes, and other failures, timeouts among them (30 seconds by default, vgl_http_reader_create_info::timeout), with VGL_RESULT_IO_ERROR.

Loading is very slow over HTTP: the server ignores range requests. vgl reads byte ranges of shards. A server that ignores the Range header answers 200 with the whole file; the reader still works, keeping only the range, but every read downloads a whole shard (10 to 25 MB by default). Check the host with vgl-bake check <url>, which reports byte ranges that do not get 206 as an error (Hosting packs). Also make sure the host or CDN does not compress pack files: send them as application/octet-stream, uncompressed.

Packs on another site do not load in a browser: CORS. A page reads packs from another origin only if the server allows it with CORS, including a preflight for the Range header, and, for a cross-origin isolated page, Cross-Origin-Resource-Policy: cross-origin or CORS. A CORS failure gives the fetch no answer, which the reader retries a few times and then completes with VGL_RESULT_IO_ERROR; the browser console shows the CORS error. The reference server sets these headers; for object stores see the CORS rules in Hosting packs. vgl-bake check <url> warns about missing CORS headers.

Packs stop updating after a rebake. Manifests change in place while shards get new names. If a cache or CDN keeps manifests for long, readers see the old manifest. Serve manifests with a short cache lifetime (the reference server uses a minute) and shards as immutable.

Offline with the disk cache. With a vgl_disk_cache in front of the HTTP reader, what was read before is served from disk, and manifests are served offline (offline_hits in vgl_disk_cache_statistics). Parts never read before cannot load offline.

Memory and budgets

vgl has two budgets, both in vgl_context_create_info:

Budget Default What it holds What happens at the limit
cpu_budget 256 MiB Decoded tiles and pack metadata Decoded tiles beyond the budget are evicted in vgl_context_update
gpu_budget 512 MiB Resources offered to or created by the consumer Meshes and textures not drawn in the current frame are destroyed, least recently drawn first

VGL_RESULT_OUT_OF_MEMORY. Means an allocation failed or a budget does not allow it. With a host allocator (Host interfaces), check that it does not refuse allocations. A height or ray query whose read could not start for lack of memory logs "a read of finer data could not start for lack of memory; the query reports VGL_RESULT_NOT_READY until it can".

Detail stops improving or tiles reload constantly. Compare cpu_used with cpu_budget and gpu_used with gpu_budget. A view near the budget keeps evicting and reloading. Raise the budget, raise vgl_view::max_screen_space_error (default 2 pixels) to select fewer, coarser tiles, or render at a lower resolution. The meshes of selected tiles and the textures they want are exempt from GPU eviction, so a budget that is too small for the view cannot be enforced; gpu_used above gpu_budget is the sign.

Resources that could not be created. A consumer that cannot create a resource reports it with vgl_frame_resource_failed; the core logs "resource could not be created (); it is offered again in a later frame". Persistent messages mean the GPU is out of memory: lower gpu_budget.

Device loss. After a lost Direct3D device or OpenGL context, call the backend's vgl_gl_backend_device_reset / vgl_d3d11_backend_device_reset (Ogre: vgl::ogre::globe::device_reset), or for your own consumer vgl_context_resources_reset. Until a failed reset succeeds, vgl_gl_backend_draw fails with VGL_RESULT_INVALID_OPERATION.

struct_size and version errors

Every structure that may grow starts with struct_size (see Core concepts and docs/c-api.md, section 8).

Result Cause Fix
VGL_RESULT_INVALID_ARGUMENT struct_size is 0 or smaller than the first published version of the structure, or an output structure's struct_size is below 8 Set struct_size = sizeof(the structure)
VGL_RESULT_NOT_SUPPORTED with "fields from a newer version set" Your headers are newer than the vgl binary, and you set a field the binary does not know Use headers and binaries of the same version, or leave the newer fields zero
VGL_RESULT_VERSION_MISMATCH An interface table (vgl_io_interface, vgl_task_interface, vgl_allocator_interface) has version 0 or one this vgl does not know Set version to VGL_IO_INTERFACE_VERSION, VGL_TASK_INTERFACE_VERSION or VGL_ALLOCATOR_INTERFACE_VERSION

Always zero-initialize and then set struct_size. In C, vgl_context_create_info info = {.struct_size = sizeof(info)}; zeroes the rest. In C++, use vgl::init<T>(), which value-initializes and sets struct_size. Do not fill structures with positional aggregate initializers ({sizeof(info), 0, 1024, ...}) beyond struct_size: on 32-bit targets and wasm32, VGL_POINTER_PADDING adds a padding field after each pointer, so the positions differ between targets. Assign fields by name instead. vgl_version_get returns the version of the binary, which may differ from the VGL_VERSION_* macros you compiled against.

DLL not found on Windows

Error 0xc0000135, or "The code execution cannot proceed because vgl.dll was not found". With shared builds (BUILD_SHARED_LIBS=ON, the windows-msvc-shared preset, or a shared release archive), your executable needs vgl.dll and, depending on what it links, vgl-defaults.dll, vgl-gl.dll and vgl-d3d11.dll next to it or on PATH. vgl's own build puts all executables and DLLs together in build/<preset>/bin/<configuration>/ for this reason. In your project, copy them in a post-build step, for example:

add_custom_command(TARGET my_app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        $<TARGET_RUNTIME_DLLS:my_app> $<TARGET_FILE_DIR:my_app>
    COMMAND_EXPAND_LISTS)

or use the static libraries (windows-msvc-static, the static archive). Note that a static MSVC library from the Release archives links only into Release builds; Debug builds use the shared archive, vcpkg or Conan (Installing vgl).

A dependency DLL is missing (a conda environment leaked into the build). CMake uses an installed Zstandard when find_package(zstd) finds one, and otherwise compiles a downloaded one into vgl. If you configure in a shell that has a conda environment on PATH, such as the one the baker needs for GDAL on Windows, find_package can pick up the environment's packages, for example a zstd_DIR inside it. vgl then links that environment's shared zstd DLL, and executables fail with 0xc0000135 when run from a normal shell. This also happens to an existing build tree when CMake re-runs in such a shell (for example after a CMakeLists.txt changed). Fixes:

  • Keep baker builds and other builds in separate shells; activate the conda environment only for the baker.
  • In a clean shell, clear the cached paths and reconfigure: cmake -U zstd_DIR --preset windows-msvc-shared (and any other *_DIR that points into the environment).
  • Or force the bundled Zstandard: -DVGL_ZSTD_PROVIDER=BUNDLED.

The configure output says which one is used: "vgl: using installed Zstandard ..." or "vgl: using downloaded Zstandard ..., compiled into vgl".

The baker cannot find GDAL's DLLs. vgl-bake.exe needs its conda environment active, so that GDAL's DLLs and the PROJ database are found (tools/baker/README.md).

Web builds

"This page needs to be served with the headers Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp". The web build uses threads (the thread pool on Web Workers), which need shared memory, which browsers give only to cross-origin isolated pages. Opening the HTML file directly, or serving it without both headers, shows this message. Serve it with both headers, as samples/gl/web/Caddyfile does. In such a page, every resource from another origin, packs included, must allow it with CORS or Cross-Origin-Resource-Policy; see CORS above.

"This browser has no WebGL 2." The OpenGL backend needs WebGL 2 (OpenGL ES 3.0). Supported: Chrome and Edge 92, Firefox 79, Safari 15.2, or later.

Reversed-Z fails in the browser. WebGL 2 has no clip control, so the backend refuses VGL_DEPTH_CONVENTION_REVERSED_Z with VGL_RESULT_NOT_SUPPORTED. Use the logarithmic convention.

Memory. Growing memory with threads is slow, so the sample page starts with -sINITIAL_MEMORY=768MB, sized for the CPU budget, the meshes and the clouds. Size your own page's initial memory for your budgets.

See Web builds.

Ogre render system issues

"Ogre has no ... render system here". The Ogre sample selects "OpenGL 3+ Rendering Subsystem" (--rs gl) or "Direct3D11 Rendering Subsystem" (--rs d3d11). A downloaded Ogre is built with only the OpenGL 3+ render system and, on Windows, Direct3D 11. Other render systems (Vulkan, OpenGL ES 2, Metal) are not supported by the Ogre backend yet: Vulkan is planned before version 1, OpenGL ES 3, Metal and the mobile platforms later (M3b).

vgl::ogre::globe::create fails with not_supported. Either the render system lacks a shading language the globe has, or you asked for reversed-Z without turning on the render system's "Reversed Z-Buffer" option before Ogre::Root::initialise. The context's log says which. Set it as the sample does:

rs->setConfigOption("Reversed Z-Buffer", "Yes");

Shaders do not compile: OgreUnifiedShader.h not found. The globe needs Ogre's Media/Main among the resource locations of its resource group, as every Ogre application with the shader system has. The sample also adds Media/RTShaderLib for Ogre's shader system.

The globe is in the wrong place or jitters. Attach the globe to a scene node at the origin and keep it unrotated and unscaled. Keep Ogre's camera's field of view, aspect and clip distances matching the projection you give the context: Ogre multiplies only its projection.

vgl::ogre is missing after install. It is installed only when it links an installed Ogre. A downloaded Ogre is not installed, so neither is vgl::ogre. The Conan recipe has no Ogre backend, since ConanCenter has no Ogre 14; the vcpkg port has an ogre feature (Installing vgl).

Ogre's own log. The sample writes Ogre's log to vgl-ogre.log in the working directory. vgl's messages still go to the context's log callback. See Ogre backend.

Build configuration problems

Message at configure time Fix
vgl: GLFW not found and VGL_FETCH_DEPENDENCIES is off; skipping the OpenGL sample Install GLFW 3.3 or later, or turn VGL_FETCH_DEPENDENCIES on
vgl: Dear ImGui is not available (VGL_FETCH_DEPENDENCIES is off); skipping the ... sample Turn VGL_FETCH_DEPENDENCIES on: Dear ImGui is only downloaded
vgl: Zstandard was not found and VGL_FETCH_DEPENDENCIES is off. Install Zstandard, or turn VGL_FETCH_DEPENDENCIES on
vgl: the baker needs GDAL 3.7 or later. Install GDAL (Ubuntu: libgdal-dev; Windows: conda-forge, see Baker)
vgl: VGL_TEST_BROWSER needs Chrome and Caddy 2.11; name them with VGL_CHROME and VGL_CADDY. Put Chrome and Caddy on the path, or pass their paths

See Building vgl.

Reporting a problem

VGL_RESULT_INTERNAL_ERROR means a bug in vgl. Please open an issue at github.com/3dgep/vgl with the log at VGL_LOG_LEVEL_DEBUG, the statistics, your platform and backend, and, if you can, a reproduction with one of the samples.

Clone this wiki locally