Skip to content

Chapter 10: Debugging

Overview

Debugging GPU code is challenging. This chapter covers:

  • Validation layers for error detection
  • Debug naming and labels
  • Profiling and performance analysis
  • Common debugging techniques

What you'll learn:

  • Setting up validation layers
  • Using GPU debugging tools
  • Identifying performance bottlenecks

Validation Layers

Vulkan has minimal runtime error checking for performance. Validation layers add comprehensive checking during development.

Enabling Validation

const char* layers[] = {
    "VK_LAYER_KHRONOS_validation"
};

VkInstanceCreateInfo create_info = {
    .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
    .enabledLayerCount = 1,
    .ppEnabledLayerNames = layers,
    .enabledExtensionCount = 1,
    .ppEnabledExtensionNames = (const char*[]){
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME
    }
};

Debug Callback

Receive validation messages:

static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(
    VkDebugUtilsMessageSeverityFlagBitsEXT severity,
    VkDebugUtilsMessageTypeFlagsEXT type,
    const VkDebugUtilsMessengerCallbackDataEXT* data,
    void* user_data) 
{
    const char* severity_str = "";
    if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
        severity_str = "ERROR";
    else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
        severity_str = "WARNING";
    else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT)
        severity_str = "INFO";

    fprintf(stderr, "[Vulkan %s] %s\n", severity_str, data->pMessage);

    return VK_FALSE;  // Don't abort
}

// Create messenger
VkDebugUtilsMessengerCreateInfoEXT messenger_info = {
    .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
    .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
                       VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
    .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
                   VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
                   VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
    .pfnUserCallback = debug_callback
};

VkDebugUtilsMessengerEXT messenger;
// Need to load function pointer
PFN_vkCreateDebugUtilsMessengerEXT func = 
    (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
        instance, "vkCreateDebugUtilsMessengerEXT");
func(instance, &messenger_info, NULL, &messenger);

Debug Object Names

Name your objects for clearer error messages:

VkDebugUtilsObjectNameInfoEXT name_info = {
    .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
    .objectType = VK_OBJECT_TYPE_BUFFER,
    .objectHandle = (uint64_t)input_buffer,
    .pObjectName = "Input Data Buffer"
};

PFN_vkSetDebugUtilsObjectNameEXT setName = 
    (PFN_vkSetDebugUtilsObjectNameEXT)vkGetDeviceProcAddr(
        device, "vkSetDebugUtilsObjectNameEXT");
setName(device, &name_info);

Now errors reference "Input Data Buffer" instead of "VkBuffer 0x7f3a2b1c":

[Vulkan ERROR] Buffer "Input Data Buffer" is being used without proper synchronization

Debug Labels

Mark regions in command buffers:

// Helper function
void cmd_begin_label(VkCommandBuffer cmd, const char* name, 
                     float r, float g, float b) {
    VkDebugUtilsLabelEXT label = {
        .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT,
        .pLabelName = name,
        .color = {r, g, b, 1.0f}
    };

    PFN_vkCmdBeginDebugUtilsLabelEXT func = ...;
    func(cmd, &label);
}

void cmd_end_label(VkCommandBuffer cmd) {
    PFN_vkCmdEndDebugUtilsLabelEXT func = ...;
    func(cmd);
}

// Usage
cmd_begin_label(cmd, "Data Processing", 0.0f, 1.0f, 0.0f);
vkCmdBindPipeline(cmd, ...);
vkCmdDispatch(cmd, 256, 1, 1);
cmd_end_label(cmd);

cmd_begin_label(cmd, "Post-Processing", 1.0f, 0.0f, 0.0f);
// ...
cmd_end_label(cmd);

These labels appear in debugging tools like RenderDoc.

GPU Timestamps

Measure GPU execution time:

// Create query pool
VkQueryPoolCreateInfo query_info = {
    .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
    .queryType = VK_QUERY_TYPE_TIMESTAMP,
    .queryCount = 2
};

VkQueryPool query_pool;
vkCreateQueryPool(device, &query_info, NULL, &query_pool);

// Record timestamps
vkCmdResetQueryPool(cmd, query_pool, 0, 2);
vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 
                    query_pool, 0);

vkCmdDispatch(cmd, 256, 1, 1);

vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 
                    query_pool, 1);

// After execution, read results
uint64_t timestamps[2];
vkGetQueryPoolResults(device, query_pool, 0, 2, 
                      sizeof(timestamps), timestamps, 
                      sizeof(uint64_t),
                      VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);

float nano_per_tick = props.limits.timestampPeriod;
float ms = (timestamps[1] - timestamps[0]) * nano_per_tick / 1e6;
printf("Dispatch took %.3f ms\n", ms);

Pipeline Statistics

Query execution statistics:

VkQueryPoolCreateInfo query_info = {
    .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
    .queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS,
    .queryCount = 1,
    .pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT
};

// Wrap dispatch in query
vkCmdBeginQuery(cmd, query_pool, 0, 0);
vkCmdDispatch(cmd, 256, 1, 1);
vkCmdEndQuery(cmd, query_pool, 0);

// Read results
uint64_t invocations;
vkGetQueryPoolResults(device, query_pool, 0, 1, 
                      sizeof(invocations), &invocations,
                      sizeof(uint64_t), VK_QUERY_RESULT_64_BIT);
printf("Shader invocations: %lu\n", invocations);

Debugging Tools

RenderDoc

Free, open-source GPU debugger:

  1. Launch your app through RenderDoc
  2. Capture a frame
  3. Inspect:
  4. Command buffer contents
  5. Buffer data at each stage
  6. Shader debugging (step through execution)

NVIDIA Nsight

For NVIDIA GPUs: - Nsight Graphics: Frame debugging - Nsight Compute: Kernel profiling - Nsight Systems: System-wide profiling

AMD RGP/RGD

For AMD GPUs: - Radeon GPU Profiler (RGP): Performance analysis - Radeon GPU Detective (RGD): Crash debugging

Intel GPA

For Intel GPUs: - Graphics Performance Analyzers - Metrics and trace analysis

Common Validation Errors

Synchronization Hazard

SYNC-HAZARD-WRITE-AFTER-READ: vkCmdDispatch(): Hazard WRITE_AFTER_READ
for Buffer "Output Buffer". Prior access by vkCmdDispatch() with usage READ.

Fix: Add a barrier between dispatches.

Missing Descriptor

VUID-vkCmdDispatch-None-02699: Descriptor set 0 binding 2 is not bound.

Fix: Update all required descriptor bindings.

Invalid Layout

VUID-VkWriteDescriptorSet-descriptorType-00327: descriptorType is 
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER but pBufferInfo[0].buffer was 
created with VK_BUFFER_USAGE_STORAGE_BUFFER_BIT.

Fix: Match buffer usage to descriptor type.

Printf Debugging

Some drivers support printf from shaders:

#extension GL_EXT_debug_printf : enable

void main() {
    debugPrintfEXT("Invocation %u: value = %f\n", 
                   gl_GlobalInvocationID.x, my_value);
}

Enable with:

export VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT

Performance

Printf severely impacts performance. Use only for debugging specific issues.

Running the Example

./build/bin/ch10_debugging

Output from an Apple M3 Pro, abridged:

Validation layers: ENABLED
Debug utilities: AVAILABLE

=== Creating Named Resources ===
Named: CommandPool, Buffers, Memory

=== Setting Up GPU Timestamps ===
Timestamp period: 1.00 ns

=== Executing with Timestamps ===

=== Timing Results ===
CPU wall time: 4.245 ms
GPU total time: 0.416 ms
  First dispatch: 0.264 ms
  Barrier: 0.006 ms
  Second dispatch: 0.145 ms
Throughput: 18.79 GB/s

=== Verification ===
Expected: input * 4 (doubled twice)
  [0] 0.0 -> 0.0 (expected 0.0) [ok]
  [1] 1.0 -> 4.0 (expected 4.0) [ok]
  ...
All 1048576 values verified correct

=== Common Debugging Scenarios ===
1. Validation layer messages appear above if there are issues
2. Object names help identify resources in error messages
3. Debug labels help track command buffer execution
4. Timestamps pinpoint performance bottlenecks

Chapter 10 completed!

A clean run prints no validation output

The sample does not deliberately trigger an error. If the validation layer has nothing to report you will see no [Vulkan ERROR] lines at all — that is the expected result. Note also the gap between CPU wall time (4.2 ms) and GPU time (0.4 ms): most of the wall clock is submission and fence wait, which is exactly the kind of thing timestamps expose.

Best Practices

During Development

  • ✅ Always enable validation layers
  • ✅ Name all objects
  • ✅ Use debug labels for command sections
  • ✅ Check return values

For Release

  • ❌ Disable validation layers (performance)
  • ✅ Keep error handling
  • ✅ Add telemetry for crash reporting

Performance Debugging Checklist

  1. Are dispatches too small? Increase work per dispatch
  2. Too many barriers? Batch independent work
  3. Memory bandwidth bound? Use shared memory
  4. Occupancy limited? Reduce register usage
  5. Pipeline switching overhead? Batch by pipeline

Exercises

  1. Custom Callback: Log validation messages to a file with timestamps.

  2. Benchmark Suite: Create automated performance tests with timestamp queries.

  3. Error Injection: Intentionally trigger different validation errors to learn the messages.

Congratulations!

You've completed the VKCompute tutorial series! You now understand:

  • Vulkan initialization and device setup
  • Buffer management and memory types
  • Descriptor sets and resource binding
  • Pipeline creation and management
  • GPU synchronization
  • Push constants and specialization
  • Subgroup operations
  • Debugging and profiling

Where to Go Next

  • Vulkan Specification: The authoritative reference
  • Vulkan Guide: Practical patterns and best practices
  • GPU Gems: Advanced compute techniques
  • Vendor Documentation: NVIDIA, AMD, Intel optimization guides

Happy computing! 🚀