Chapter 01: Hello Vulkan Compute¶
Overview¶
In this chapter, you'll create your first Vulkan program. We won't run any compute shaders yet — the goal is to understand the initialization process:
- Create a Vulkan instance
- Enumerate and select a GPU
- Create a logical device with a compute queue
The Code¶
Let's walk through the complete program:
Includes and Setup¶
We include the Vulkan header directly. No helper libraries yet — we want to see every Vulkan call.
Step 1: Create a Vulkan Instance¶
The instance is your connection to the Vulkan library:
VkApplicationInfo app_info = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "CH01 Hello Compute",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_2
};
VkInstanceCreateInfo instance_info = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app_info
};
VkInstance instance;
VkResult result = vkCreateInstance(&instance_info, NULL, &instance);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed to create Vulkan instance!\n");
return 1;
}
printf("Vulkan instance created\n");
sType Pattern
Every Vulkan structure has an sType field that identifies its structure type. This enables the driver to validate and extend structures.
macOS Note
On macOS with MoltenVK, you need the portability extension. The full code handles this with #ifdef __APPLE__.
Step 2: Find a Physical Device (GPU)¶
Enumerate available GPUs:
uint32_t device_count = 0;
vkEnumeratePhysicalDevices(instance, &device_count, NULL);
if (device_count == 0) {
fprintf(stderr, "No GPUs with Vulkan support found!\n");
return 1;
}
VkPhysicalDevice* devices = malloc(device_count * sizeof(VkPhysicalDevice));
vkEnumeratePhysicalDevices(instance, &device_count, devices);
The two-call pattern is common in Vulkan:
- First call with
NULLto get the count - Allocate memory
- Second call to fill the array
Step 3: Query Device Properties¶
For each GPU, get its properties and find compute queues:
for (uint32_t i = 0; i < device_count; i++) {
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(devices[i], &props);
printf("GPU %u: %s\n", i, props.deviceName);
printf(" Type: %s\n",
props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
? "Discrete GPU" : "Other");
printf(" API Version: %u.%u.%u\n",
VK_VERSION_MAJOR(props.apiVersion),
VK_VERSION_MINOR(props.apiVersion),
VK_VERSION_PATCH(props.apiVersion));
}
Step 4: Find a Compute Queue Family¶
GPUs organize their queues into families with different capabilities:
uint32_t queue_family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical_device,
&queue_family_count, NULL);
VkQueueFamilyProperties* queue_families =
malloc(queue_family_count * sizeof(VkQueueFamilyProperties));
vkGetPhysicalDeviceQueueFamilyProperties(physical_device,
&queue_family_count,
queue_families);
uint32_t compute_queue_family = UINT32_MAX;
for (uint32_t j = 0; j < queue_family_count; j++) {
if (queue_families[j].queueFlags & VK_QUEUE_COMPUTE_BIT) {
printf(" Queue family %u: supports COMPUTE (%u queues)\n",
j, queue_families[j].queueCount);
if (compute_queue_family == UINT32_MAX) {
compute_queue_family = j;
}
}
}
We look for any queue family that supports VK_QUEUE_COMPUTE_BIT. The loop
visits every family so it can print the full list, and keeps the first match
rather than breaking early.
Step 5: Create a Logical Device¶
The logical device is your interface to the GPU:
float queue_priority = 1.0f;
VkDeviceQueueCreateInfo queue_info = {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = compute_queue_family,
.queueCount = 1,
.pQueuePriorities = &queue_priority
};
VkDeviceCreateInfo device_info = {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queue_info
};
VkDevice device;
result = vkCreateDevice(physical_device, &device_info, NULL, &device);
Step 6: Get the Queue Handle¶
VkQueue compute_queue;
vkGetDeviceQueue(device, compute_queue_family, 0, &compute_queue);
printf("Compute queue retrieved\n");
Now we have everything needed to submit compute work!
Cleanup¶
Always destroy objects in reverse order of creation:
Running the Example¶
Output from an Apple M3 Pro — your GPU name, API version and queue family list will differ:
==============================================
VKCompute - Chapter 01: Hello Vulkan Compute
==============================================
Vulkan instance created
Found 1 GPU(s)
GPU 0: Apple M3 Pro
Type: Integrated GPU
API Version: 1.2.323
Queue family 0: supports COMPUTE (1 queues)
Queue family 1: supports COMPUTE (1 queues)
Queue family 2: supports COMPUTE (1 queues)
Queue family 3: supports COMPUTE (1 queues)
Selected GPU with compute queue family 0
Logical device created
Compute queue retrieved
=== SUCCESS ===
Vulkan is initialized and ready for compute work!
Cleanup complete
Key Concepts¶
Object Hierarchy¶
VkInstance
└── VkPhysicalDevice (GPU hardware)
└── VkDevice (logical interface)
└── VkQueue (command submission)
Physical vs Logical Device¶
| Aspect | Physical Device | Logical Device |
|---|---|---|
| Represents | Actual GPU hardware | Your session with GPU |
| Count | One per GPU | Multiple possible |
| Created by | Vulkan driver | Your code |
| Contains | Properties, limits | Queues, resources |
Queue Families¶
GPUs have different queue types:
- Compute: General computation (
VK_QUEUE_COMPUTE_BIT) - Graphics: Rendering (
VK_QUEUE_GRAPHICS_BIT) - Transfer: Memory copies (
VK_QUEUE_TRANSFER_BIT) - Sparse: Sparse memory (
VK_QUEUE_SPARSE_BINDING_BIT)
For compute, we only need VK_QUEUE_COMPUTE_BIT.
Exercises¶
-
Multiple GPUs: If you have multiple GPUs, modify the code to list all of them and let the user choose.
-
Queue Exploration: Print all queue families and their capabilities, not just compute.
-
Device Features: Query and print
VkPhysicalDeviceFeaturesto see what your GPU supports.
Common Errors¶
VK_ERROR_INCOMPATIBLE_DRIVER (-9)¶
On macOS, you need the portability extension:
const char* extensions[] = {VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME};
instance_info.enabledExtensionCount = 1;
instance_info.ppEnabledExtensionNames = extensions;
instance_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
VK_ERROR_INITIALIZATION_FAILED (-3)¶
- Check that Vulkan drivers are installed
- Update GPU drivers
- Verify Vulkan SDK installation with
vulkaninfo
What's Next?¶
We have a Vulkan device and queue, but we haven't done any actual computation yet. In Chapter 02, we'll create buffers to send data to the GPU.