Skip to content

qbart/vkdd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vkdd

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 the VkCommandBuffer; vkdd creates everything else it needs (pipelines, shader module, vertex buffers).

Integrating

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.

Usage

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

Primitives

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

Draw state (immediate-mode; applies to subsequent shapes, reset each frame)

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 space

Pass your frame delta to reset(dt) so timed shapes age out.

Text

dd.text2d(10, 10, "fps: 60", vkdd::WHITE, 2.0f);     // screen pixels
dd.text3d(worldPos, "enemy", vkdd::RED);             // billboarded world label

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

Instancing (many copies, one draw call)

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

Things to know (Vulkan correctness)

  • Viewport/scissor are dynamic. vkdd does not set them; bind them with vkCmdSetViewport/vkCmdSetScissor before draw() (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 keep framesInFlight accurate.
  • update() must be called outside vkCmdBeginRendering/EndRendering (it's the upload point) and draw() inside it.
  • Attachment formats and sample count in InitInfo must 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 same viewProj you give to draw().
  • 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 = true to name vkdd's objects and wrap its draws in a label (needs VK_EXT_debug_utils) — handy in RenderDoc / validation.
  • Set InitInfo.errorCallback to be told about init failures and capacity overflow.

Capacities

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.

About

Immediate-mode debug-draw library for Vulkan.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Contributors