RT Graphics Programming
Lesson 15 of 28

Part 2 · Vulkan implementation

15. Vulkan Makes Ownership Explicit

Create the API, device and queue relationships that will own every later resource and command.

Graphics glossary 30 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Vulkan is often introduced by the number of objects required before a pixel appears. That makes the API look like a ceremony whose purpose is to be completed as quickly as possible. Copy an initialisation function, accept the first device returned by enumeration and celebrate when a window changes colour. The colour is evidence that some work reached some device. It is not yet evidence that we understand which object owns that work or why the selected device can execute the renderer we intend to build.

Part II therefore begins with ownership and selection. The manual renderer let JavaScript and the browser supply an execution environment. Vulkan asks the application to state that environment: which API version it expects, which physical device satisfies its requirements, which queue family can execute and present the work, and which logical device enables the features used by later lessons. This explicitness is valuable only if each choice has a criterion.

What you should be able to account for

  • Distinguish a Vulkan instance, physical device, logical device, queue family and queue.
  • Request a Vulkan 1.3 teaching baseline without confusing that request with the loader's supported version.
  • Select a device using reported capabilities and retain a written reason for every rejection.

Choose a baseline before choosing a device

The current Vulkan specification is newer than the minimum required by this course. Our compute renderer uses Vulkan 1.3 as its core baseline because synchronisation2 is core there and the resulting responsibilities are available without teaching an older and newer barrier vocabulary at the same time. A Vulkan 1.4 implementation can still create an application that requests 1.3. Hardware ray tracing remains feature and extension gated in Lessons 27 and 28; a high core version does not silently promise those facilities.

First ask what the loader supports. Rejecting a loader below the course baseline is better than creating an instance and discovering later that a required core feature cannot be enabled.

uint32_t loaderVersion = VK_API_VERSION_1_0;
VkResult versionResult = vkEnumerateInstanceVersion(&loaderVersion);
if (versionResult != VK_SUCCESS ||
    loaderVersion < VK_API_VERSION_1_3) {
    throw std::runtime_error("Vulkan 1.3 loader required");
}

VkApplicationInfo applicationInfo{
    .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
    .pApplicationName = "Inspectable Vulkan Ray Tracer",
    .applicationVersion = VK_MAKE_API_VERSION(0, 1, 0, 0),
    .pEngineName = "Course renderer",
    .engineVersion = VK_MAKE_API_VERSION(0, 1, 0, 0),
    .apiVersion = VK_API_VERSION_1_3
};

The application version describes our program. The API version describes the Vulkan interface it requests. Neither value selects a GPU. That happens only after an instance has connected the application to the loader and the enabled instance extensions.

Create the instance deliberately

A window-system library may supply the platform-specific instance extensions needed to create a presentation surface. That library is allowed to create a window and report extension names. It is not allowed to choose the renderer's physical device, construct its rays or decide its synchronisation. Keep the boundary visible.

std::vector<const char*> instanceExtensions =
    window.requiredVulkanInstanceExtensions();

#ifndef NDEBUG
instanceExtensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
const char* validationLayer = "VK_LAYER_KHRONOS_validation";
#endif

VkInstanceCreateInfo instanceInfo{
    .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
    .pApplicationInfo = &applicationInfo,
    .enabledExtensionCount =
        static_cast<uint32_t>(instanceExtensions.size()),
    .ppEnabledExtensionNames = instanceExtensions.data()
};

#ifndef NDEBUG
instanceInfo.enabledLayerCount = 1;
instanceInfo.ppEnabledLayerNames = &validationLayer;
#endif

VkInstance instance = VK_NULL_HANDLE;
check(vkCreateInstance(&instanceInfo, nullptr, &instance));

Before enabling the validation layer, enumerate layers and establish that it is present. Validation is a diagnostic witness. It can report many invalid API relationships, but a quiet validation log does not prove that the camera ray, intersection interval or final colour is correct. Part I remains our behavioural reference.

Enumerate physical devices as candidates

A physical device is a reported implementation, not an object we create. For each candidate, record its properties, Vulkan 1.3 features, memory properties, device extensions and queue families. The course baseline requires a queue family that can execute compute work and support presentation to the selected surface. We also require the swapchain device extension. We do not enable the ray-tracing extensions merely because one candidate reports them.

struct Candidate {
    VkPhysicalDevice physical = VK_NULL_HANDLE;
    uint32_t queueFamily = UINT32_MAX;
    VkPhysicalDeviceProperties properties{};
    VkPhysicalDeviceVulkan13Features features13{
        .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES
    };
};

bool suitable(const Candidate& candidate) {
    return candidate.queueFamily != UINT32_MAX
        && candidate.features13.synchronization2 == VK_TRUE
        && supportsDeviceExtension(
               candidate.physical,
               VK_KHR_SWAPCHAIN_EXTENSION_NAME);
}

The complete selector also checks that the surface has at least one usable format and presentation mode. A discrete device is not automatically superior. An integrated implementation satisfying every requirement may be the appropriate candidate, especially when unified memory avoids transfers that another device would require. We can rank suitable devices later. Suitability is the gate.

Create one logical device and retrieve its queue

The logical device enables a chosen subset of the physical device's reported features and creates queues from named families. It owns most Vulkan objects constructed in subsequent lessons. This ownership is why a buffer created from one logical device cannot be submitted through a queue belonging to another.

float priority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{
    .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
    .queueFamilyIndex = selected.queueFamily,
    .queueCount = 1,
    .pQueuePriorities = &priority
};

VkPhysicalDeviceVulkan13Features enabled13{
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
    .synchronization2 = VK_TRUE
};

const char* deviceExtensions[] = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};

VkDeviceCreateInfo deviceInfo{
    .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
    .pNext = &enabled13,
    .queueCreateInfoCount = 1,
    .pQueueCreateInfos = &queueInfo,
    .enabledExtensionCount = 1,
    .ppEnabledExtensionNames = deviceExtensions
};

VkDevice device = VK_NULL_HANDLE;
check(vkCreateDevice(selected.physical, &deviceInfo, nullptr, &device));

VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueue(device, selected.queueFamily, 0, &queue);

VkQueue is retrieved, not independently created. The logical device creation request asked for one queue from the selected family; index zero retrieves that queue. Later commands will be recorded elsewhere and submitted here. Recording is not execution, and a queue handle is not a background thread promised to our application.

Reject a tempting device

Candidate A reports Vulkan 1.4 and hardware ray tracing but no presentation support for the chosen surface on its compute family. Candidate B reports Vulkan 1.3, synchronisation2, the swapchain extension and one family supporting compute and presentation. For the renderer built in Lessons 15 to 26, B satisfies the complete baseline and A does not. The ray-tracing features of A do not repair the missing presentation route. Lesson 27 may revisit device requirements, but it must not rewrite this decision after resources have already been created.

Name the owner

Which object owns an image created in Lesson 16: the instance, physical device, logical device, queue family or queue?

The answer

The image is created from and owned by the logical device. The physical device supplied the properties and memory capabilities used to choose that logical device. A command buffer may later use the image and a queue may execute those commands, but neither becomes the image's owner.

Official reference: consult the Khronos Vulkan initialisation chapter and extension-enabling guide when checking the current object and feature rules.

Make the selection explain itself

Extend the selector so every physical device produces a short report containing API version, device type, queue-family flags, presentation support, swapchain support and synchronisation2. Do not print only the winning device. A rejection without a recorded failed criterion is difficult to distinguish from an enumeration bug. Run the program with validation enabled, then deliberately request a device extension that no candidate supports and confirm that selection fails before vkCreateDevice.

Carry the ownership chain forward

Retain the instance, selected physical device, logical device, queue-family index and queue as separate fields in the renderer. The physical device will answer capability and memory questions. The logical device will create resources. The family index will create a compatible command pool. The queue will execute submissions. Collapsing those fields into an unnamed initialisation block may shorten a listing, but it removes the account needed when a later object is incompatible.

We have not rendered anything. That is the correct result for this lesson. We have established who may create the storage image, who reports its memory requirements and where recorded work will eventually be submitted. Lesson 16 now constructs the first device resource without pretending that an image handle is also its memory.