osgx::debug
August 20, 2026 · View on GitHub
osgx/Debug.hpp provides three independent systems that can be used together or separately. It is
not included by osgx.hpp; opt in explicitly with #include "osgx/Debug.hpp".
For a Dear ImGui front-end, see osgx::imgui — it is a sibling of osgx::debug, not
nested under it, since most of it (sliders, Panel, texture/stats browsers) has nothing to do with
GL_KHR_debug specifically; only its ProfilerSection reaches into osgx::debug.
System 1 — GL Driver Message Callback
glDebugMessageCallback is the GL driver speaking to you. When the driver detects
an error, a performance warning, or a shader issue, it calls your registered function.
This is a completely separate pipe from the other two systems — no queries, no groups,
just the driver reporting what it observed.
osgx::debug::initialize(gc)— resolves allGL_KHR_debugextension pointers for a context.osgx::debug::installDefaultCallback()— registers a callback that writes driver messages toosg::notify.osgx::debug::setCallback(fn)/clearCallback()— install or remove a custom callback.osgx::debug::enableDebugOutput()/disableDebugOutput()— toggleGL_DEBUG_OUTPUT[_SYNCHRONOUS].osgx::debug::GraphicsOperation— asetRealizeOperation()helper that callsinitialize()once the context exists.
System 2 — KHR Debug Group Annotations
glPushDebugGroup / glPopDebugGroup are you speaking to the GL driver — and by
extension to tools like apitrace and RenderDoc that intercept those calls. Annotations
appear as named, nested regions in a trace, making it immediately clear which draw calls
belong to which logical pass.
Critical rule: all calls require a current GL context. Never call pushGroup or
messageInsert from application-side C++ code (e.g. from main() or a viewer loop);
always use the camera callback objects, which fire while the context is guaranteed current.
Annotations are camera-scoped. A PRE_DRAW / FINAL_DRAW callback pair brackets
everything that camera renders — not individual nodes or groups within the scene graph.
Sub-groups of a single camera's scene cannot be individually annotated this way; for
per-group bracketing, each group needs its own RTT camera. For per-drawable bracketing,
use ProfilerCallback instead.
osgx::debug::Scoped— use this by default. RAII push/pop; construct on the stack inside anydrawImplementationor draw callback. Begin and end are in the same scope.osgx::debug::AnnotationGroup— use only when begin and end must live in separate callback objects. Its sole purpose is to share push/pop state acrossAnnotationBeginCallback(PRE_DRAW) andAnnotationEndCallback(FINAL_DRAW) so the group remains matched across the camera's frame boundary. Not a general-purpose annotation type.osgx::debug::AnnotationBeginCallback/AnnotationEndCallback— install anAnnotationGroupon a camera'sPRE_DRAWorFINAL_DRAWslot so it fires at the correct time.osgx::debug::appendCameraDrawCallback()/prependCameraDrawCallback()— safely add callbacks to an existing slot without clobbering others.
Groups nest naturally. FrameByFrameViewer installs a single Frame group on its
camera automatically. Any other object with its own RTT camera (e.g. an osgSlug::Atlas)
can install its own AnnotationBeginCallback / AnnotationEndCallback and it will
appear nested inside the viewer's group in the trace:
Frame
Atlas: glyph-cache
[draw calls]
Atlas: labels
[draw calls]
/Frame
System 3 — GPU Timestamp Profiler
glQueryCounter wraps each drawable in a pair of GPU timestamp queries so you can
measure actual GPU cost per draw call, averaged over a ring buffer of recent frames.
This is a two-phase design to avoid stalling the GPU pipeline mid-submission.
Phase 1 — submit (ProfilerCallback, per drawable):
glQueryCounter(begin) → draw → glQueryCounter(end) → push to FrameAccumulator
Never blocks. Install with ProfilerVisitor or attach ProfilerCallback directly.
Phase 2 — drain (ProfilerFinalCallback, per camera):
after all draws: read GL_QUERY_RESULT → update ring buffers → print/report
Blocks only on the last outstanding query. Install on the camera's FINAL_DRAW slot.
osgx::debug::ProfilerCallback<N>— drawable wrapper; issues timestamp queries and measures CPU time.osgx::debug::ProfilerVisitor<N>— walks the scene graph and installsProfilerCallbackon every drawable.osgx::debug::ProfilerFinalCallback<N>— camera callback that drainsFrameAccumulatorresults.QueryMode::SYNC(default) — blocks per-query; suitable for step-by-step viewers.QueryMode::ASYNC— drains the previous frame's results with zero meaningful stall; use for continuous renderers.
Data flow summary
GL driver ──glDebugMessageCallback──► your callback (errors, warnings)
you ──AnnotationBegin/EndCallback──► GL driver ──► apitrace / RenderDoc
ProfilerCallback ──► FrameAccumulator ──► ProfilerFinalCallback ──► osg::notify (or osgx::imgui::Widget)
FrameByFrameViewer::requestRender() ◄── 'n' key (or an osgx::imgui button)
The three flows are independent. Annotations never need to know the profiler exists, and the driver callback has nothing to do with either.
FrameByFrameViewer
A single, consolidated osgViewer::Viewer subclass for apitrace-friendly debugging.
Press n to advance one frame; the viewer idles between keystrokes so a capture
session does not flood with unintended frames. On first render it automatically installs
Frame / Render annotation groups on the camera (System 2). Combine with
ProfilerVisitor + ProfilerFinalCallback (System 3) and installDefaultCallback()
(System 1) for a fully instrumented scene.
Extensions
- GL_KHR_debug
- GL_ARB_debug_output
- GL_EXT_debug_marker
- GL_EXT_debug_label
- GL_AMD_debug_output