Vulkan Tutorial in C - 004 - Initialization Part 2
Finishing up win32_init_vulkan
To recap, we're in the process of implementing the following function:
VulkanContext
win32_init_vulkan(HINSTANCE instance, s32 windowX, s32 windowY, u32 windowWidth,
u32 windowHeight, char *windowTitle)
{
VulkanContext vk = {0};
// First 5 steps done in the previous post
// Pick a physical device and the graphicsAndPresent queue family
// Create logical device
// Get graphicsAndPresentQueue from device
// Create swapchain
// Get swapchain images and create their views
return vk;
}
win32_init_vulkan(HINSTANCE instance, s32 windowX, s32 windowY, u32 windowWidth,
u32 windowHeight, char *windowTitle)
{
VulkanContext vk = {0};
// First 5 steps done in the previous post
// Pick a physical device and the graphicsAndPresent queue family
// Create logical device
// Get graphicsAndPresentQueue from device
// Create swapchain
// Get swapchain images and create their views
return vk;
}
Pick a physical device and the graphicsAndPresent queue family
u32 deviceCount = 0;
vkEnumeratePhysicalDevices(vk.instance, &deviceCount, NULL);
assert(deviceCount <= 8); // Ensure there are no more than 8 devices
VkPhysicalDevice devices[8] = {NULL};
vkEnumeratePhysicalDevices(vk.instance, &deviceCount, devices);
// Choose the first available device as a fallback
vk.physicalDevice = devices[0];
// Search for a dedicated GPU (discrete GPU)
for (u32 i = 0; i < deviceCount; i++)
{
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(devices[i], &props);
// If the device is a dedicated (discrete) GPU, prefer it
if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
{
// Choose it as the physical device and break the loop
vk.physicalDevice = devices[i];
break;
}
}
assert(vk.physicalDevice); // Ensure a physical device has been selected
// Query the queue family properties for the chosen physical device
u32 queueFamilyPropertyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(vk.physicalDevice,
&queueFamilyPropertyCount, NULL);
// Ensure there are no more than 3 queue families
assert(queueFamilyPropertyCount <= 3);
VkQueueFamilyProperties queueFamilyProperties[3] = {0};
vkGetPhysicalDeviceQueueFamilyProperties(vk.physicalDevice,
&queueFamilyPropertyCount,
queueFamilyProperties);
// Assume first queue family supports Graphics and Present capabilities
u32 queueFamilyIndex = 0;
// Ensure the queue family supports graphics
assert(queueFamilyProperties[queueFamilyIndex].queueFlags
& VK_QUEUE_GRAPHICS_BIT);
VkBool32 presentSupport = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(vk.physicalDevice, queueFamilyIndex,
vk.surface,
&presentSupport);
assert(presentSupport); // Ensure present support is available
// Store the queue family index that supports both graphics and present
vk.graphicsAndPresentQueueFamily = queueFamilyIndex;
Other Vulkan tutorials often go through complicated loops to search for physical devices with both graphics and presentation capabilities, sometimes even separating those into different queues (though in practice, they tend to overlap).vkEnumeratePhysicalDevices(vk.instance, &deviceCount, NULL);
assert(deviceCount <= 8); // Ensure there are no more than 8 devices
VkPhysicalDevice devices[8] = {NULL};
vkEnumeratePhysicalDevices(vk.instance, &deviceCount, devices);
// Choose the first available device as a fallback
vk.physicalDevice = devices[0];
// Search for a dedicated GPU (discrete GPU)
for (u32 i = 0; i < deviceCount; i++)
{
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(devices[i], &props);
// If the device is a dedicated (discrete) GPU, prefer it
if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
{
// Choose it as the physical device and break the loop
vk.physicalDevice = devices[i];
break;
}
}
assert(vk.physicalDevice); // Ensure a physical device has been selected
// Query the queue family properties for the chosen physical device
u32 queueFamilyPropertyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(vk.physicalDevice,
&queueFamilyPropertyCount, NULL);
// Ensure there are no more than 3 queue families
assert(queueFamilyPropertyCount <= 3);
VkQueueFamilyProperties queueFamilyProperties[3] = {0};
vkGetPhysicalDeviceQueueFamilyProperties(vk.physicalDevice,
&queueFamilyPropertyCount,
queueFamilyProperties);
// Assume first queue family supports Graphics and Present capabilities
u32 queueFamilyIndex = 0;
// Ensure the queue family supports graphics
assert(queueFamilyProperties[queueFamilyIndex].queueFlags
& VK_QUEUE_GRAPHICS_BIT);
VkBool32 presentSupport = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(vk.physicalDevice, queueFamilyIndex,
vk.surface,
&presentSupport);
assert(presentSupport); // Ensure present support is available
// Store the queue family index that supports both graphics and present
vk.graphicsAndPresentQueueFamily = queueFamilyIndex;
I don’t find this approach particularly compelling, so in this tutorial, I’ve opted to simplify things as much as possible. First, we just pick the first available device as a fallback. Then, I check for a dedicated GPU (since many systems have both an integrated and dedicated GPU), but I’ve avoided any unnecessary loops.
I won't be looping through queue families or assigning scores to GPUs based on their features. Instead, I assume the first queue family will support graphics capabilities and assert that in the code.
If this assertion fails on your system, you're on your own to implement a loop that picks a queue family with both graphics and presentation capabilities. I’m confident you can handle that! And if you run into this situation, feel free to drop a comment and let me know how you approached it.
Queue Priority
The Queue Priority concept in Vulkan is essentially a hint to the GPU scheduler, suggesting which queues from the same family should be prioritized. However, this is not particularly important for simple cases like ours right now. It's more useful in situations with heavy workloads, where some tasks are more critical than others.For example, if you have transfer queues for asset loading in advance and other queues dedicated to rendering the next frame, you may want to prioritize rendering. In such cases, you'd set a priority of 1 for the rendering queue, and a lower value (e.g., 0.5) for the asset loading queue.
Create logical device
f32 queuePriorities[] = { 1.0f };
VkDeviceQueueCreateInfo queueCreateInfo =
{
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
NULL,
0,
vk.graphicsAndPresentQueueFamily,
array_count(queuePriorities),
queuePriorities
};
VkDeviceQueueCreateInfo queueCreateInfos[] = {queueCreateInfo};
// Enable required device extensions (swapchain)
char *deviceExtensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
VkDeviceCreateInfo deviceCreateInfo =
{
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
NULL,
0,
array_count(queueCreateInfos),
queueCreateInfos,
0, // enabledLayerCount deprecated
NULL, // ppEnabledLayerNames deprecated
array_count(deviceExtensions),
deviceExtensions,
NULL // pEnabledFeatures
};
// Create the actual logical device finally
if (vkCreateDevice(vk.physicalDevice, &deviceCreateInfo, NULL,
&vk.device) != VK_SUCCESS)
{
assert(!"Failed to create logical device");
}
VkDeviceQueueCreateInfo queueCreateInfo =
{
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
NULL,
0,
vk.graphicsAndPresentQueueFamily,
array_count(queuePriorities),
queuePriorities
};
VkDeviceQueueCreateInfo queueCreateInfos[] = {queueCreateInfo};
// Enable required device extensions (swapchain)
char *deviceExtensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
VkDeviceCreateInfo deviceCreateInfo =
{
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
NULL,
0,
array_count(queueCreateInfos),
queueCreateInfos,
0, // enabledLayerCount deprecated
NULL, // ppEnabledLayerNames deprecated
array_count(deviceExtensions),
deviceExtensions,
NULL // pEnabledFeatures
};
// Create the actual logical device finally
if (vkCreateDevice(vk.physicalDevice, &deviceCreateInfo, NULL,
&vk.device) != VK_SUCCESS)
{
assert(!"Failed to create logical device");
}
Get graphicsAndPresentQueue from device
vkGetDeviceQueue(vk.device, vk.graphicsAndPresentQueueFamily, 0,
&vk.graphicsAndPresentQueue);
assert(vk.graphicsAndPresentQueue);
&vk.graphicsAndPresentQueue);
assert(vk.graphicsAndPresentQueue);
Create swapchain
// Query surface capabilities
VkSurfaceCapabilitiesKHR surfaceCapabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk.physicalDevice, vk.surface,
&surfaceCapabilities);
// Save the swapchain image format and extents
vk.swapchainImageFormat = VK_FORMAT_B8G8R8A8_SRGB;
vk.swapchainExtents = surfaceCapabilities.currentExtent;
VkSwapchainCreateInfoKHR swapchainCreateInfo =
{
VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
NULL,
0,
vk.surface,
array_count(vk.swapchainImages), // minImageCount (2)
vk.swapchainImageFormat,
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, // imageColorSpace
vk.swapchainExtents, // imageExtent
1, // imageArrayLayers
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, // imageUsage
VK_SHARING_MODE_EXCLUSIVE,
0, // queueFamilyIndexCount
NULL, // pQueueFamilyIndices
surfaceCapabilities.currentTransform, // preTransform
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
VK_PRESENT_MODE_FIFO_KHR,
VK_TRUE, // clipped
NULL // oldSwapchain
};
if (vkCreateSwapchainKHR(vk.device, &swapchainCreateInfo, NULL,
&vk.swapchain) != VK_SUCCESS)
{
assert(!"Failed to create the swapchain");
}
VkSurfaceCapabilitiesKHR surfaceCapabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk.physicalDevice, vk.surface,
&surfaceCapabilities);
// Save the swapchain image format and extents
vk.swapchainImageFormat = VK_FORMAT_B8G8R8A8_SRGB;
vk.swapchainExtents = surfaceCapabilities.currentExtent;
VkSwapchainCreateInfoKHR swapchainCreateInfo =
{
VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
NULL,
0,
vk.surface,
array_count(vk.swapchainImages), // minImageCount (2)
vk.swapchainImageFormat,
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, // imageColorSpace
vk.swapchainExtents, // imageExtent
1, // imageArrayLayers
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, // imageUsage
VK_SHARING_MODE_EXCLUSIVE,
0, // queueFamilyIndexCount
NULL, // pQueueFamilyIndices
surfaceCapabilities.currentTransform, // preTransform
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
VK_PRESENT_MODE_FIFO_KHR,
VK_TRUE, // clipped
NULL // oldSwapchain
};
if (vkCreateSwapchainKHR(vk.device, &swapchainCreateInfo, NULL,
&vk.swapchain) != VK_SUCCESS)
{
assert(!"Failed to create the swapchain");
}
Hang in there!
I bet you're thinking, "Phew! We can't possibly have much more to do, right?!"Wrong! You silly! Buckle up—we're just getting started.
Let's talk about images and image views. Even though we're not dealing with textures yet, the concept of image views already comes into play because of our swapchain images.
First off, images and image views are Vulkan abstractions, but they seem to map pretty closely to actual GPU hardware.
A VkImage refers to the raw pixel data itself.
A VkImageView is kind of a description of how we’ll interpret that data.
The image view defines things like:
- The format (e.g., RGBA, depth, etc.).
- The type (1D, 2D, etc.).
- The subresource range, which is a fancy way of saying we might only be interested in part of the image, not the whole thing.
For example, we could:
Render to an image at one point.
Later, read from that same image in a shader (think shadow maps, for instance).
This separation makes Vulkan more powerful, but also a bit more verbose. Welcome to Vulkan!
Swizzle: Turning Your Colors Around, Literally
Swizzle is a fancy term that, in this context, means rearranging the components of a color (R, G, B, A).Vulkan "allows" (read: forces) us to specify this when creating an image view. This means you could, for example, take image data in (A, R, G, B) format and remap it to (R, G, B, A) or something else entirely.
I won’t be doing any of that, though. So, I’ll just specify VK_COMPONENT_SWIZZLE_IDENTITY in all four places, which means the components will stay exactly as they are.
Get swapchain images and create their views
u32 imageCount = 0;
vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount, NULL);
assert(imageCount == array_count(vk.swapchainImages));
vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount,
vk.swapchainImages);
// For each swapchain image
for (u32 i = 0; i < imageCount; i++)
{
assert(vk.swapchainImages[i]);
VkComponentMapping swizzle =
{
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY
};
VkImageSubresourceRange subRange =
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // baseMipLevel
1, // levelCount
0, // baseArrayLayer
1 // layerCount
};
VkImageViewCreateInfo viewInfo =
{
VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
NULL,
0,
vk.swapchainImages[i],
VK_IMAGE_VIEW_TYPE_2D,
vk.swapchainImageFormat,
swizzle,
subRange
};
// Create the image view
vkCreateImageView(vk.device, &viewInfo, NULL,
&vk.swapchainImageViews[i]);
assert(vk.swapchainImageViews[i]);
}
vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount, NULL);
assert(imageCount == array_count(vk.swapchainImages));
vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount,
vk.swapchainImages);
// For each swapchain image
for (u32 i = 0; i < imageCount; i++)
{
assert(vk.swapchainImages[i]);
VkComponentMapping swizzle =
{
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY
};
VkImageSubresourceRange subRange =
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // baseMipLevel
1, // levelCount
0, // baseArrayLayer
1 // layerCount
};
VkImageViewCreateInfo viewInfo =
{
VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
NULL,
0,
vk.swapchainImages[i],
VK_IMAGE_VIEW_TYPE_2D,
vk.swapchainImageFormat,
swizzle,
subRange
};
// Create the image view
vkCreateImageView(vk.device, &viewInfo, NULL,
&vk.swapchainImageViews[i]);
assert(vk.swapchainImageViews[i]);
}
Initialization Done!
Phew! We’ve finally wrapped up the win32_init_vulkan function. Though, there’s still a ton of app-specific initialization to handle, which I decided to leave for later. As I mentioned earlier, that part is more tailored to the app itself, but I’m not complaining—it also makes this post a bit shorter.If you haven’t already (which would be quite surprising), now’s a good time to take a break—maybe even a few days—before continuing. Trust me, there’s still a long road ahead (I’d say we’re about halfway through).
In the next post, we’ll dive into the initialization of the app-specific Vulkan objects.
Next
Comments
Post a Comment