Immediate-mode debug-draw for C++23 + Vulkan, using dynamic rendering. Think Dear ImGui, but for 3D debug geometry: queue shapes each frame, upload, draw.
- Vulkan C headers, Vulkan 1.3 dynamic rendering (no render passes/framebuffers)
- vkm for math (
vec3,mat4, …) - Shaders authored in Slang, compiled to SPIR-V and embedded into the library
- Built with CMake; consumable via
FetchContent - You own the
VkInstance/VkDevice/queue and theVkCommandBuffer; vkdd creates everything else it needs (pipelines, shader module, vertex buffers).
include(FetchContent)
FetchContent_Declare(vkdd
GIT_REPOSITORY https://github.com/qbart/vkdd
GIT_TAG master)
FetchContent_MakeAvailable(vkdd)
target_link_libraries(my_app PRIVATE vkdd::vkdd)vkdd pulls in vkm automatically. For Vulkan it uses your installed SDK if
find_package(Vulkan) succeeds (and links the loader for you); otherwise it
fetches Vulkan-Headers so it still compiles. Shaders are pre-compiled and
committed under src/generated/; if slangc is on PATH they are recompiled at
build time.
#include <vkdd/vkdd.h>
vkdd::DebugDraw dd;
// startup
vkdd::InitInfo info;
info.physicalDevice = phys;
info.device = device;
info.colorFormat = swapchainFormat; // your dynamic-rendering color format
info.depthFormat = depthFormat; // or VK_FORMAT_UNDEFINED for no depth
info.framesInFlight = 2; // match your in-flight count
dd.init(info);
// per frame
dd.reset();
dd.grid();
dd.filled();
dd.box({0, 0.5f, 0}, {1, 1, 1}, vkdd::CYAN);
dd.sphere({2, 1, 0}, 0.75f, vkdd::GREEN);
dd.wire();
dd.sphere({2, 1, 0}, 0.85f);
dd.arrow({0, 0, 0}, {0, 2, 2}, vkdd::RED);
dd.update(cmd); // upload — call OUTSIDE vkCmdBeginRendering
// ... vkCmdBeginRendering(...) ...
dd.draw(viewProj, cmd); // record draws — call INSIDE rendering
// ... vkCmdEndRendering(...) ...
// shutdown (also happens in the destructor)
dd.shutdown();See examples/usage.cpp for a fuller walkthrough.
point, line, arrow, triangle, circle, sphere, box (axis-aligned and
oriented), aabb, plane (with optional normal), cone, cylinder, capsule,
axisTriad, frustum / frustumInv, grid / gridSimple, cross, arc,
disc, ring, polyline, bezier, normals.
Volumetric primitives obey the current style (filled() / wire()).
dd.filled() / dd.wire(); // style for volumetric shapes
dd.duration(2.0f); // persist shapes for 2s (see reset(dt) below); 0 = this frame
dd.noDepth() / dd.depthTest(b); // x-ray overlay vs depth-tested
dd.alpha(0.4f); // translucent fills
dd.lineWidth(3.0f); // thick screen-space lines (needs viewport in draw())
dd.channel("physics"); // tag shapes; toggle groups with setChannelEnabled()
dd.pushTransform(m); dd.popTransform(); // queue shapes in local spacePass your frame delta to reset(dt) so timed shapes age out.
dd.text2d(10, 10, "fps: 60", vkdd::WHITE, 2.0f); // screen pixels
dd.text3d(worldPos, "enemy", vkdd::RED); // billboarded world labelBuilt-in 8×8 bitmap font rendered as screen-space quads — no texture, sampler, or descriptor sets, so no queue is needed for setup. Text is always an overlay.
for (auto& body : bodies) dd.boxInstanced(body.transform, vkdd::LIME);
dd.sphereInstanced(center, radius, vkdd::GREEN);Uses built-in unit meshes; honors depth/alpha/channel/transform (not style/duration).
- Viewport/scissor are dynamic. vkdd does not set them; bind them with
vkCmdSetViewport/vkCmdSetScissorbeforedraw()(you almost certainly already do in your frame). This lets vkdd work at any resolution without being told the extent. - Buffers are host-visible and ring-buffered by
framesInFlight. Data queued for frame N stays valid until frame N's GPU work finishes, which your own per-frame fence already guarantees — so keepframesInFlightaccurate. update()must be called outsidevkCmdBeginRendering/EndRendering(it's the upload point) anddraw()inside it.- Attachment formats and sample count in
InitInfomust match the attachments you render into. If they don't, pipeline creation is valid but rendering is not. - Frustum assumes Vulkan NDC (
z ∈ [0,1]). Pass the sameviewProjyou give todraw(). - Thick lines and text need the viewport extent — pass it as the third arg to
draw(viewProj, cmd, extent). Plain (1px) lines and everything else don't. - Solids use a built-in two-sided shade so they read as 3D; lines/points are flat. Culling is disabled (debug geometry is viewed from any angle).
- Set
InitInfo.debugNames = trueto name vkdd's objects and wrap its draws in a label (needsVK_EXT_debug_utils) — handy in RenderDoc / validation. - Set
InitInfo.errorCallbackto be told about init failures and capacity overflow.
InitInfo exposes maxLineVertices, maxTriangleVertices, maxPointVertices,
maxThickLineVertices, maxInstances, and maxTextVertices (a line = 2 verts,
triangle = 3, point = 1, thick-line segment = 6, each lit font pixel = 6). Overflow
is dropped, flagged in stats().overflowed, and reported via errorCallback; use
stats() to right-size them. Depth-tested and x-ray geometry use separate buffers.