From fca909b54c0e605f0d92da44413f719bdafcd489 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 17 Oct 2025 14:08:24 -0700 Subject: [PATCH 1/2] Updating and rebasing. --- en/01_Overview.adoc | 23 ++- .../01_Presentation/00_Window_surface.adoc | 71 +++++++- .../01_Vertex_buffer_creation.adoc | 165 ++++++++++++++++-- en/07_Depth_buffering.adoc | 86 ++++++++- en/10_Multisampling.adoc | 46 ++++- en/14_Android.adoc | 37 ++-- 6 files changed, 387 insertions(+), 41 deletions(-) diff --git a/en/01_Overview.adoc b/en/01_Overview.adoc index bf0b8d347..8142fe026 100644 --- a/en/01_Overview.adoc +++ b/en/01_Overview.adoc @@ -275,10 +275,28 @@ program, you should refer back to this chapter. This chapter concludes with a short overview of how the Vulkan API is structured at a lower level. -For example, object creation generally follows this pattern: +For example, object creation generally follows this pattern in both the C API and the C++ RAII wrapper: -[,c++] +[source,multilang,c++,c] +.Object creation pattern ---- +// START c +VkXXXCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO; +createInfo.pNext = nullptr; +createInfo.foo = ...; +createInfo.bar = ...; + +VkXXX object; + + +if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) { + std::cerr << "failed to create object" << std::endl; + return false; +} +// END c + +// START c++ vk::XXXCreateInfo createInfo{}; createInfo.sType = vk::StructureType::eXXXCreateInfo; createInfo.pNext = nullptr; @@ -294,6 +312,7 @@ try { std::cerr << "Failed to create object: " << err.what() << std::endl; return false; } +// END c++ ---- Many structures in Vulkan require you to explicitly specify the type of diff --git a/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc b/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc index 9f8fea100..98faf5a5b 100644 --- a/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc +++ b/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc @@ -37,9 +37,16 @@ works in this tutorial. Start by adding a `surface` class member right below the debug callback. -[,c++] +[source,multilang,c++,c] +.Surface member ---- +// START c++ vk::raii::SurfaceKHR surface = nullptr; +// END c++ + +// START c +VkSurfaceKHR surface = VK_NULL_HANDLE; +// END c ---- Although the `VkSurfaceKHR` object and its usage is platform-agnostic, its @@ -72,10 +79,20 @@ Because a window surface is a Vulkan object, it comes with a important parameters: `hwnd` and `hinstance`. These are the handles to the window and the process. -[,c++] +[source,multilang,c++,c] +.Win32 surface create info ---- +// START c++ vk::Win32SurfaceCreateInfoKHR createInfo{.hinstance = GetModuleHandle(nullptr), .hwnd = glfwGetWin32Window(window)}; +// END c++ + +// START c +VkWin32SurfaceCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; +createInfo.hwnd = glfwGetWin32Window(window); +createInfo.hinstance = GetModuleHandle(nullptr); +// END c ---- The `glfwGetWin32Window` function is used to get the raw `HWND` from the GLFW @@ -88,9 +105,18 @@ Technically, this is a WSI extension function, but it is so commonly used that the standard Vulkan loader includes it, so unlike other extensions, you don't need to explicitly load it. -[,c++] +[source,multilang,c++,c] +.Create the Win32 surface ---- +// START c++ surface = instance.createWin32SurfaceKHR(createInfo); +// END c++ + +// START c +if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); +} +// END c ---- The process is similar for other platforms like Linux, where @@ -120,15 +146,26 @@ void createSurface() { The GLFW call takes simple parameters instead of a struct which makes the implementation of the function very straightforward: -[,c++] +[source,multilang,c++,c] +.GLFW window surface creation ---- +// START c++ void createSurface() { - VkSurfaceKHR _surface; + VkSurfaceKHR _surface; if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) { throw std::runtime_error("failed to create window surface!"); } surface = vk::raii::SurfaceKHR(instance, _surface); } +// END c++ + +// START c +void createSurface() { + if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); + } +} +// END c ---- However, as you see in the above, GLFW only deals with the Vulkan C API. @@ -159,8 +196,10 @@ graphics operations, and presenting to our window surface. The function to check support is `vk::raii::PhysicalDevice::getSurfaceSupportKHR`, which takes the queue family index and the surface as parameters: -[,c++] +[source,multilang,c++,c] +.Presentation support query ---- +// START c++ uint32_t queueIndex = ~0; for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) { @@ -176,6 +215,26 @@ if (queueIndex == ~0) { throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); } +// END c++ + +// START c +uint32_t queueIndex = ~0; +for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyCount; qfpIndex++) +{ + VkBool32 presentSupport = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, qfpIndex, surface, &presentSupport); + if ((queueFamilyProperties[qfpIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) + { + // found a queue family that supports both graphics and present + queueIndex = qfpIndex; + break; + } +} +if (queueIndex == ~0) +{ + throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); +} +// END c ---- == Creating the presentation queue diff --git a/en/04_Vertex_buffers/01_Vertex_buffer_creation.adoc b/en/04_Vertex_buffers/01_Vertex_buffer_creation.adoc index 7fc47f1bf..3cd40385d 100644 --- a/en/04_Vertex_buffers/01_Vertex_buffer_creation.adoc +++ b/en/04_Vertex_buffers/01_Vertex_buffer_creation.adoc @@ -40,11 +40,22 @@ void createVertexBuffer() Creating a buffer requires us to fill a `vk::BufferCreateInfo` structure. -[,c++] +[source,multilang,c++,c] +.Buffer create info ---- +// START c++ vk::BufferCreateInfo bufferInfo{.size = sizeof(vertices[0]) * vertices.size(), .usage = vk::BufferUsageFlagBits::eVertexBuffer, .sharingMode = vk::SharingMode::eExclusive}; +// END c++ + +// START c +VkBufferCreateInfo bufferInfo{}; +bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; +bufferInfo.size = sizeof(vertices[0]) * vertices.size(); +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +// END c ---- The `size` field specifies the size of the buffer in bytes. Calculating @@ -64,8 +75,10 @@ we'll leave it at the default value of `0`. We can now create the buffer with `vk::raii::Buffer` constructor. Define a class member to hold the buffer handle and call it `vertexBuffer`. -[,c++] +[source,multilang,c++,c] +.Vertex buffer handle and creation ---- +// START c++ vk::raii::Buffer vertexBuffer = nullptr; ... @@ -76,6 +89,27 @@ void createVertexBuffer() { .sharingMode = vk::SharingMode::eExclusive}; vertexBuffer = vk::raii::Buffer(device, bufferInfo); } +// END c++ + +// START c +VkBuffer vertexBuffer = VK_NULL_HANDLE; + +void createVertexBuffer() { + VkBufferCreateInfo bufferInfo = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = NULL, + .flags = 0, + .size = sizeof(vertices[0]) * vertices.size(), + .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = NULL + }; + if (vkCreateBuffer(device, &bufferInfo, NULL, &vertexBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); + } +} +// END c ---- The buffer should be available for use in rendering commands until the end of @@ -86,9 +120,17 @@ The buffer should be available for use in rendering commands until the end of The buffer has been created, but it doesn't have any memory assigned to it yet. The first step of allocating memory for the buffer is to query its memory requirements using the aptly named `vk::raii::Buffer::getMemoryRequirements` function. -[,c++] +[source,multilang,c++,c] +.Memory requirements query ---- +// START c++ vk::MemoryRequirements memRequirements = vertexBuffer.getMemoryRequirements(); +// END c++ + +// START c +VkMemoryRequirements memRequirements; +vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); +// END c ---- The `vk::MemoryRequirements` struct has three fields: @@ -102,18 +144,35 @@ Each type of memory varies in terms of allowed operations and performance charac We need to combine the requirements of the buffer and our own application requirements to find the right type of memory to use. Let's create a new function `findMemoryType` for this purpose. -[,c++] +[source,multilang,c++,c] +.findMemoryType signature ---- +// START c++ uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { } +// END c++ + +// START c +uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { + +} +// END c ---- First we need to query info about the available types of memory using `vk::raii::PhysicalDevice::getMemoryProperties`. -[,c++] +[source,multilang,c++,c] +.Device memory properties query ---- +// START c++ vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); +// END c++ + +// START c +VkPhysicalDeviceMemoryProperties memProperties; +vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +// END c ---- The `vk::PhysicalDeviceMemoryProperties` structure has two arrays `memoryTypes` and `memoryHeaps`. @@ -123,8 +182,22 @@ Right now we'll only concern ourselves with the type of memory and not the heap Let's first find a memory type that is suitable for the buffer itself: -[,c++] +[source,multilang,c++,c] +.Find any suitable memory type ---- +// START c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) +{ + if ((typeFilter & (1 << i))) + { + return i; + } +} + +throw std::runtime_error("failed to find suitable memory type!"); +// END c++ + +// START c for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if ((typeFilter & (1 << i))) @@ -134,6 +207,7 @@ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) } throw std::runtime_error("failed to find suitable memory type!"); +// END c ---- The `typeFilter` parameter will be used to specify the bit field of memory types that are suitable. @@ -149,8 +223,10 @@ We'll see why when we map the memory. We can now modify the loop to also check for the support of this property: -[,c++] +[source,multilang,c++,c] +.Memory type with required properties ---- +// START c++ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) @@ -158,6 +234,17 @@ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) return i; } } +// END c++ + +// START c +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) +{ + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) + { + return i; + } +} +// END c ---- We may have more than one desirable property, so we should check if the result of the bitwise AND is not just non-zero, but equal to the desired properties bit field. @@ -167,31 +254,63 @@ If there is a memory type suitable for the buffer that also has all the properti We now have a way to determine the right memory type, so we can actually allocate the memory by filling in the `vk::MemoryAllocateInfo` structure. -[,c++] +[source,multilang,c++,c] +.Memory allocation info ---- +// START c++ vk::MemoryAllocateInfo memoryAllocateInfo{ .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent)}; +// END c++ + +// START c +VkMemoryAllocateInfo memoryAllocateInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = NULL, + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) +}; +// END c ---- Memory allocation is now as simple as specifying the size and type, both of which are derived from the memory requirements of the vertex buffer and the desired property. Create a class member to store the handle to the memory and allocate it with the `vk::raii::DeviceMemory` constructor. -[,c++] +[source,multilang,c++,c] +.Allocate device memory for vertex buffer ---- +// START c++ vk::raii::Buffer vertexBuffer = nullptr; vk::raii::DeviceMemory vertexBufferMemory = nullptr; ... vertexBufferMemory = vk::raii::DeviceMemory(device, memoryAllocateInfo); +// END c++ + +// START c +VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE; + +... + +if (vkAllocateMemory(device, &memoryAllocateInfo, NULL, &vertexBufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate vertex buffer memory!"); +} +// END c ---- If memory allocation was successful, then we can now associate this memory with the buffer using `vk::raii::Buffer::bindBufferMemory`: -[,c++] +[source,multilang,c++,c] +.Bind buffer to memory ---- +// START c++ vertexBuffer.bindMemory( *vertexBufferMemory, 0 ); +// END c++ + +// START c +vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); +// END c ---- The first parameter is self-explanatory, and the second parameter is the offset within the region of memory. @@ -203,19 +322,41 @@ If the offset is non-zero, then it is required to be divisible by `memRequiremen It is now time to copy the vertex data to the buffer. This is done by https://en.wikipedia.org/wiki/Memory-mapped_I/O[mapping the buffer memory] into CPU accessible memory with `vk::raii::DeviceMemory::mapMemory`. -[,c++] +[source,multilang,c++,c] +.Map vertex buffer memory ---- +// START c++ void* data = vertexBufferMemory.mapMemory(0, bufferInfo.size); +// END c++ + +// START c +void* data = NULL; +if (vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data) != VK_SUCCESS) { + throw std::runtime_error("failed to map vertex buffer memory!"); +} +// END c ---- This function allows us to access a region of the specified memory resource defined by an offset and size. The offset and size here are `0` and `bufferInfo.size`, respectively. -[,c++] +[source,multilang,c++,c] +.Copy and unmap ---- +// START c++ void* data = vertexBufferMemory.mapMemory(0, bufferInfo.size); memcpy(data, vertices.data(), bufferInfo.size); vertexBufferMemory.unmapMemory(); +// END c++ + +// START c +void* data = NULL; +if (vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data) != VK_SUCCESS) { + throw std::runtime_error("failed to map vertex buffer memory!"); +} +memcpy(data, vertices.data(), bufferInfo.size); +vkUnmapMemory(device, vertexBufferMemory); +// END c ---- You can now simply `memcpy` the vertex data to the mapped memory and unmap it again using `vk::raii::DeviceMemory::unmapMemory`. diff --git a/en/07_Depth_buffering.adoc b/en/07_Depth_buffering.adoc index a512d7e83..dd55eb5c6 100644 --- a/en/07_Depth_buffering.adoc +++ b/en/07_Depth_buffering.adoc @@ -131,11 +131,20 @@ The difference is that the swap chain will not automatically create depth images We only need a single depth image, because only one draw operation is running at once. The depth image will again require the triplet of resources: image, memory and image view. -[,c++] +[source,multilang,c++,c] +.Depth image resources ---- +// START c++ vk::raii::Image depthImage = nullptr; vk::raii::DeviceMemory depthImageMemory = nullptr; vk::raii::ImageView depthImageView = nullptr; +// END c++ + +// START c +VkImage depthImage = VK_NULL_HANDLE; +VkDeviceMemory depthImageMemory = VK_NULL_HANDLE; +VkImageView depthImageView = VK_NULL_HANDLE; +// END c ---- Create a new function `createDepthResources` to set up these resources: @@ -177,21 +186,40 @@ We'll look at this in a future chapter. We could simply go for the `vk::Format::eD32Sfloat` format, because support for it is extremely common (see the hardware database), but it's nice to add some extra flexibility to our application where possible. We're going to write a function `findSupportedFormat` that takes a list of candidate formats in order from most desirable to least desirable, and checks which is the first one that is supported: -[,c++] +[source,multilang,c++,c] +.findSupportedFormat signature ---- +// START c++ vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { } +// END c++ + +// START c +VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { + +} +// END c ---- The support of a format depends on the tiling mode and usage, so we must also include these as parameters. The support of a format can be queried using the `physicalDevice.getFormatProperties` function: -[,c++] +[source,multilang,c++,c] +.Query format properties ---- +// START c++ for (const auto format : candidates) { vk::FormatProperties props = physicalDevice.getFormatProperties(format); } +// END c++ + +// START c +for (const auto format : candidates) { + VkFormatProperties props; + vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props); +} +// END c ---- The `vk::FormatProperties` struct contains three fields: @@ -202,19 +230,33 @@ The `vk::FormatProperties` struct contains three fields: Only the first two are relevant here, and the one we check depends on the `tiling` parameter of the function: -[,c++] +[source,multilang,c++,c] +.Check tiling features ---- +// START c++ if (((tiling == vk::ImageTiling::eLinear) && ((props.linearTilingFeatures & features) == features)) || ((tiling == vk::ImageTiling::eOptimal) && ((props.optimalTilingFeatures & features) == features))) { return format; } +// END c++ + +// START c +if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) { + return format; +} +if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) { + return format; +} +// END c ---- If none of the candidate formats support the desired usage, then we can either return a special value or simply throw an exception: -[,c++] +[source,multilang,c++,c] +.findSupportedFormat implementation ---- +// START c++ vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { for (const auto format : candidates) { vk::FormatProperties props = physicalDevice.getFormatProperties(format); @@ -228,18 +270,50 @@ vk::Format findSupportedFormat(const std::vector& candidates, vk::Im throw std::runtime_error("failed to find supported format!"); } +// END c++ + +// START c +VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { + for (const auto format : candidates) { + VkFormatProperties props; + vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props); + + if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) { + return format; + } + if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("failed to find supported format!"); +} +// END c ---- We'll use this function now to create a `findDepthFormat` helper function to select a format with a depth component that supports usage as depth attachment: -[,c++] +[source,multilang,c++,c] +.findDepthFormat helper ---- +// START c++ vk::Format findDepthFormat() { return findSupportedFormat({vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, vk::ImageTiling::eOptimal, vk::FormatFeatureFlagBits::eDepthStencilAttachment); } +// END c++ + +// START c +VkFormat findDepthFormat() { + return findSupportedFormat( + {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT}, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT + ); +} +// END c ---- Make sure to use the `vk::FormatFeatureFlagBits` instead of `vk::ImageUsageFlagBits` in this case. diff --git a/en/10_Multisampling.adoc b/en/10_Multisampling.adoc index 8e08c99d2..9387f73c6 100644 --- a/en/10_Multisampling.adoc +++ b/en/10_Multisampling.adoc @@ -33,11 +33,20 @@ Let's start off by determining how many samples our hardware can use. Most modern GPUs support at least eight samples, but this number is not guaranteed to be the same everywhere. We'll keep track of it by adding a new class member: -[,c++] +[source,multilang,c++,c] +.MSAA sample count member ---- +// START c++ ... vk::SampleCountFlagBits msaaSamples = vk::SampleCountFlagBits::e1; ... +// END c++ + +// START c +... +VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT; +... +// END c ---- By default, we'll be using only one sample per pixel which is equivalent to no multisampling, in which case the final image will remain unchanged. @@ -46,8 +55,10 @@ We're using a depth buffer as well, so we have to take into account the sample c The highest sample count that both support will be the maximum we can support. Add a function that will fetch this information for us: -[,c++] +[source,multilang,c++,c] +.getMaxUsableSampleCount helper ---- +// START c++ vk::SampleCountFlagBits getMaxUsableSampleCount() { vk::PhysicalDeviceProperties physicalDeviceProperties = physicalDevice->getProperties(); @@ -62,6 +73,24 @@ vk::SampleCountFlagBits getMaxUsableSampleCount() return vk::SampleCountFlagBits::e1; } +// END c++ + +// START c +VkSampleCountFlagBits getMaxUsableSampleCount() { + VkPhysicalDeviceProperties properties; + vkGetPhysicalDeviceProperties(physicalDevice, &properties); + + VkSampleCountFlags counts = properties.limits.framebufferColorSampleCounts & properties.limits.framebufferDepthSampleCounts; + if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; } + if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; } + if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; } + if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; } + if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; } + if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; } + + return VK_SAMPLE_COUNT_1_BIT; +} +// END c ---- We will now use this function to set the `msaaSamples` variable during the physical device selection process. @@ -87,13 +116,24 @@ This is why we have to create an additional render target and modify our current We only need one render target since only one drawing operation is active at a time, just like with the depth buffer. Add the following class members: -[,c++] +[source,multilang,c++,c] +.Color image resources ---- +// START c++ ... vk::raii::Image colorImage = nullptr; vk::raii::DeviceMemory colorImageMemory = nullptr; vk::raii::ImageView colorImageView = nullptr; ... +// END c++ + +// START c +... +VkImage colorImage = VK_NULL_HANDLE; +VkDeviceMemory colorImageMemory = VK_NULL_HANDLE; +VkImageView colorImageView = VK_NULL_HANDLE; +... +// END c ---- This new image will have to store the desired number of samples per pixel, so we need to pass this number to `vk::ImageCreateInfo` during the image creation process. diff --git a/en/14_Android.adoc b/en/14_Android.adoc index e5527d1e9..9b9477bc0 100644 --- a/en/14_Android.adoc +++ b/en/14_Android.adoc @@ -476,22 +476,34 @@ One of the key platform-specific differences in our Vulkan implementation is how Here's how we create a Vulkan surface on Android: -[source,cpp] +[source,multilang,c++,c] +.Creating the Vulkan Surface on Android ---- +// START c++ void createSurface() { - VkSurfaceKHR _surface; - VkResult result = VK_SUCCESS; + // RAII approach using Vulkan-Hpp + vk::AndroidSurfaceCreateInfoKHR createInfo{ + .window = androidApp->window + }; + surface = vk::raii::SurfaceKHR(instance, createInfo); +} +// END c++ + +// START c +void createSurface() { + VkSurfaceKHR _surface = VK_NULL_HANDLE; + + VkAndroidSurfaceCreateInfoKHR createInfo = { + .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, + .pNext = NULL, + .flags = 0, + .window = androidApp->window + }; - // Create Android surface - result = vkCreateAndroidSurfaceKHR( + VkResult result = vkCreateAndroidSurfaceKHR( *instance, - &(VkAndroidSurfaceCreateInfoKHR{ - .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, - .pNext = nullptr, - .flags = 0, - .window = androidApp->window - }), - nullptr, + &createInfo, + NULL, &_surface ); @@ -501,6 +513,7 @@ void createSurface() { surface = vk::raii::SurfaceKHR(instance, _surface); } +// END c ---- === Handling Android Events From d5c60883447926c829528c47ab18c67eb9b07dd4 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 19 Aug 2026 21:07:10 -0700 Subject: [PATCH 2/2] Get Multilang c/c++ working across entire base tutorial. depends upon https://github.com/KhronosGroup/Vulkan-Site/pull/219 --- .../00_Setup/00_Base_code.adoc | 42 ++- .../00_Setup/01_Instance.adoc | 129 ++++++- .../00_Setup/02_Validation_layers.adoc | 286 ++++++++++++++- .../04_Logical_device_and_queues.adoc | 138 ++++++- .../01_Presentation/00_Window_surface.adoc | 72 +++- .../01_Presentation/01_Swap_chain.adoc | 254 ++++++++++++- .../01_Presentation/02_Image_views.adoc | 72 +++- .../01_Shader_modules.adoc | 70 +++- .../02_Fixed_functions.adoc | 199 +++++++++- .../03_Dynamic_rendering.adoc | 40 +- .../04_Conclusion.adoc | 20 +- .../03_Drawing/00_Dynamic_rendering.adoc | 65 +++- .../03_Drawing/01_Command_buffers.adoc | 212 ++++++++++- .../02_Rendering_and_presentation.adoc | 134 ++++++- .../03_Drawing/03_Frames_in_flight.adoc | 111 +++++- .../04_Swap_chain_recreation.adoc | 113 +++++- .../00_Vertex_input_description.adoc | 59 ++- en/04_Vertex_buffers/02_Staging_buffer.adoc | 134 ++++++- en/04_Vertex_buffers/03_Index_buffer.adoc | 73 +++- .../00_Descriptor_set_layout_and_buffer.adoc | 109 +++++- .../01_Descriptor_pool_and_sets.adoc | 128 ++++++- en/06_Texture_mapping/00_Images.adoc | 231 +++++++++++- .../01_Image_view_and_sampler.adoc | 202 ++++++++++- .../02_Combined_image_sampler.adoc | 133 ++++++- en/08_Loading_models.adoc | 23 +- en/09_Generating_Mipmaps.adoc | 193 +++++++++- en/11_Compute_Shader.adoc | 342 +++++++++++++++++- 27 files changed, 3369 insertions(+), 215 deletions(-) diff --git a/en/03_Drawing_a_triangle/00_Setup/00_Base_code.adoc b/en/03_Drawing_a_triangle/00_Setup/00_Base_code.adoc index eeaf07029..81debcd8f 100644 --- a/en/03_Drawing_a_triangle/00_Setup/00_Base_code.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/00_Base_code.adoc @@ -128,11 +128,27 @@ allows you to specify callbacks for a custom memory allocator. We will ignore this parameter in the tutorial and always pass `nullptr` as argument. Using the Vulkan_hpp RAII module, we can rely upon the library to take care -of `vkCreateXXX` `vkAllocateXXX` `vkDestroyXXX` and `vkFreeXXX` so a block -of code that looks like this: +of `vkCreateXXX` `vkAllocateXXX` `vkDestroyXXX` and `vkFreeXXX`, replacing manual +creation and destruction of the instance with RAII-managed construction: -[,c++] +[source,multilang,c++,c] +.Manual instance creation versus RAII ---- +// START c++ +constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", + .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .apiVersion = vk::ApiVersion14}; + +vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo +}; + +instance = vk::raii::Instance(context, createInfo); +// END c++ + +// START c VkInstance instance; VkApplicationInfo appInfo{}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; @@ -155,24 +171,12 @@ if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { } vkDestroyInstance(instance, nullptr); +// END c ---- -can be directly replaced by this: - -[,c++] ----- -constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", - .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ), - .pEngineName = "No Engine", - .engineVersion = VK_MAKE_VERSION( 1, 0, 0 ), - .apiVersion = vk::ApiVersion14}; - -vk::InstanceCreateInfo createInfo{ - .pApplicationInfo = &appInfo -}; - -instance = vk::raii::Instance(context, createInfo); ----- +Notice that in the C version we have to remember to call `vkDestroyInstance` +ourselves once the instance is no longer needed, while the C++ version relies +on `vk::raii::Instance`'s destructor to do that for us automatically. == Integrating GLFW diff --git a/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc b/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc index 85710c7f0..8567f8f5b 100644 --- a/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc @@ -22,11 +22,18 @@ void initVulkan() { Additionally, add a data member to hold the handle to the instance and the raii context: -[,c++] +[source,multilang,c++,c] +.Instance-related class members ---- +// START c++ private: vk::raii::Context context; vk::raii::Instance instance = nullptr; +// END c++ + +// START c +VkInstance instance; +// END c ---- Now, to create an instance, we'll first have to fill in a struct with some @@ -35,8 +42,10 @@ provide some useful information to the driver to optimize our specific application, (e.g., because it uses a well-known graphics engine with certain special behavior). This struct is called `vk::ApplicationInfo`: -[,c++] +[source,multilang,c++,c] +.ApplicationInfo ---- +// START c++ void createInstance() { constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", @@ -45,6 +54,20 @@ void createInstance() .engineVersion = VK_MAKE_VERSION( 1, 0, 0 ), .apiVersion = vk::ApiVersion14}; } +// END c++ + +// START c +void createInstance() +{ + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_4; +} +// END c ---- While vk::ApiVersion10 or Vulkan 1.0 does exist, some functionality @@ -61,11 +84,20 @@ the Vulkan driver which global extensions and validation layers we want to use. Global here means that they apply to the entire program and not a specific device, which will become clear in the next few chapters. -[,c++] +[source,multilang,c++,c] +.InstanceCreateInfo ---- +// START c++ vk::InstanceCreateInfo createInfo{ .pApplicationInfo = &appInfo }; +// END c++ + +// START c +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +// END c ---- This structure has a member named flags, which we will handle later in this chapter. @@ -76,8 +108,10 @@ is a platform-agnostic API, which means that you need an extension to interface with the window system. GLFW has a handy built-in function that returns the extension(s) it needs to do that which we can pass to the struct: -[,c++] +[source,multilang,c++,c] +.Checking for GLFW extension support ---- +// START c++ // Get the required instance extensions from GLFW. uint32_t glfwExtensionCount = 0; auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); @@ -98,6 +132,42 @@ vk::InstanceCreateInfo createInfo{ .pApplicationInfo = &appInfo, .enabledExtensionCount = glfwExtensionCount, .ppEnabledExtensionNames = glfwExtensions}; +// END c++ + +// START c +// Get the required instance extensions from GLFW. +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +// Check if the required GLFW extensions are supported by the Vulkan implementation. +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +std::vector extensionProperties(extensionCount); +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensionProperties.data()); + +for (uint32_t i = 0; i < glfwExtensionCount; ++i) +{ + bool supported = false; + for (const auto& extensionProperty : extensionProperties) + { + if (strcmp(extensionProperty.extensionName, glfwExtensions[i]) == 0) + { + supported = true; + break; + } + } + if (!supported) + { + throw std::runtime_error("Required GLFW extension not supported: " + std::string(glfwExtensions[i])); + } +} + +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +// END c ---- The other missing piece is the Layers to enable. Here is where we'll talk @@ -108,9 +178,18 @@ in-depth in the next chapter, so leave this empty for now. We've now specified everything Vulkan needs to create an instance, and we can finally create the vk::raii::Instance: -[,c++] +[source,multilang,c++,c] +.Creating the instance ---- +// START c++ instance = vk::raii::Instance(context, createInfo); +// END c++ + +// START c +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +// END c ---- As you'll see, the general pattern that object creation function parameters in Vulkan follow is: @@ -189,8 +268,10 @@ to instance enabled extension list. Typically, the code could be like this: -[,c++] +[source,multilang,c++,c] +.Enabling the portability extension ---- +// START c++ constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ), .pEngineName = "No Engine", @@ -202,6 +283,30 @@ vk::InstanceCreateInfo createInfo{ .ppEnabledExtensionNames = { vk::KHRPortabilityEnumerationExtensionName } }; instance = vk::raii::Instance(m_context, createInfo); +// END c++ + +// START c +VkApplicationInfo appInfo{}; +appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; +appInfo.pApplicationName = "Hello Triangle"; +appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); +appInfo.pEngineName = "No Engine"; +appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); +appInfo.apiVersion = VK_API_VERSION_1_4; + +const char* portabilityExtension = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +createInfo.pApplicationInfo = &appInfo; +createInfo.enabledExtensionCount = 1; +createInfo.ppEnabledExtensionNames = &portabilityExtension; + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +// END c ---- == Checking for extension support @@ -217,9 +322,19 @@ the `vk::raii::Context::enumerateInstanceExtensionProperties` function. It retur a vector of the available extensions, which allows us to filter extensions by a specific validation layer, which we'll ignore for now. -[,c++] +[source,multilang,c++,c] +.Enumerating supported extensions ---- +// START c++ auto extensions = context.enumerateInstanceExtensionProperties(); +// END c++ + +// START c +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +std::vector extensions(extensionCount); +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +// END c ---- Each `vk::ExtensionProperties` struct contains the name and version of an diff --git a/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc b/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc index cd1e8dde8..28bc415ea 100644 --- a/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc @@ -105,8 +105,10 @@ through the requested layers and validate that all the required layers are supported by the Vulkan implementation. This check is performed directly in the `createInstance` function: -[,c++] +[source,multilang,c++,c] +.Checking for validation layer support ---- +// START c++ void createInstance() { ... @@ -132,6 +134,46 @@ void createInstance() ... } +// END c++ + +// START c +void createInstance() +{ + ... + + // Get the required layers + std::vector requiredLayers; + if (enableValidationLayers) + { + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation. + uint32_t layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + std::vector layerProperties(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, layerProperties.data()); + + for (const char* requiredLayer : requiredLayers) + { + bool layerFound = false; + for (const auto& layerProperty : layerProperties) + { + if (strcmp(layerProperty.layerName, requiredLayer) == 0) + { + layerFound = true; + break; + } + } + if (!layerFound) + { + throw std::runtime_error("Required layer not supported: " + std::string(requiredLayer)); + } + } + + ... +} +// END c ---- == Using extensions @@ -164,8 +206,10 @@ supported instance extensions by using the required extensions are listed in that list. This check is also performed directly in the `createInstance` function: -[,c++] +[source,multilang,c++,c] +.Checking for required instance extension support ---- +// START c++ void createInstance() { ... @@ -184,10 +228,46 @@ void createInstance() if (unsupportedPropertyIt != requiredExtensions.end()) { throw std::runtime_error("Required extension not supported: " + std::string(*unsupportedPropertyIt)); - } + } ... } +// END c++ + +// START c +void createInstance() +{ + ... + + // Get the required extensions. + auto requiredExtensions = getRequiredInstanceExtensions(); + + // Check if the required extensions are supported by the Vulkan implementation. + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + std::vector extensionProperties(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensionProperties.data()); + + for (const char* requiredExtension : requiredExtensions) + { + bool extensionFound = false; + for (const auto& extensionProperty : extensionProperties) + { + if (strcmp(extensionProperty.extensionName, requiredExtension) == 0) + { + extensionFound = true; + break; + } + } + if (!extensionFound) + { + throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension)); + } + } + + ... +} +// END c ---- Now run the program in debug mode and ensure that the error does not occur. If @@ -196,8 +276,10 @@ it does, then have a look at the FAQ. Finally, modify the `vk::InstanceCreateInfo` struct instantiation to include the validation layer names and the extension names: -[,c++] +[source,multilang,c++,c] +.Enabling the layers and extensions in InstanceCreateInfo ---- +// START c++ void createInstance() { ... @@ -209,6 +291,26 @@ void createInstance() .ppEnabledExtensionNames = requiredExtensions.data()}; instance = vk::raii::Instance(context, createInfo); } +// END c++ + +// START c +void createInstance() +{ + ... + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + createInfo.enabledLayerCount = static_cast(requiredLayers.size()); + createInfo.ppEnabledLayerNames = requiredLayers.data(); + createInfo.enabledExtensionCount = static_cast(requiredExtensions.size()); + createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +// END c ---- If the check was successful then the `vk::raii::Instance` constructor should not @@ -232,8 +334,10 @@ We'll first extent the `getRequiredInstanceExtensions` function based on whether validation layers are enabled or not: -[,c++] +[source,multilang,c++,c] +.Conditionally requesting the debug utils extension ---- +// START c++ std::vector getRequiredInstanceExtensions() { uint32_t glfwExtensionCount = 0; @@ -247,6 +351,23 @@ std::vector getRequiredInstanceExtensions() return extensions; } +// END c++ + +// START c +std::vector getRequiredInstanceExtensions() +{ + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + if (enableValidationLayers) + { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + return extensions; +} +// END c ---- The debug messenger extension is conditionally added. Note that we've used the @@ -258,8 +379,10 @@ function called `debugCallback` with the `PFN_vkDebugUtilsMessengerCallbackEXT` prototype. The `VKAPI_ATTR` and `VKAPI_CALL` ensure that the function has the right signature for Vulkan to call it. -[,c++] +[source,multilang,c++,c] +.The debug callback function ---- +// START c++ static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT * pCallbackData, @@ -269,6 +392,19 @@ static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSever return vk::False; } +// END c++ + +// START c +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT type, + const VkDebugUtilsMessengerCallbackDataEXT * pCallbackData, + void * pUserData) +{ + std::cerr << "validation layer: type " << type << " msg: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +// END c ---- The first parameter specifies the severity of the message, which is one of @@ -283,11 +419,20 @@ The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example: -[,c++] +[source,multilang,c++,c] +.Comparing severity levels ---- +// START c++ if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { // Message is important enough to show } +// END c++ + +// START c +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Message is important enough to show +} +// END c ---- The `messageType` parameter can have the following values: @@ -315,9 +460,16 @@ All that remains now is telling Vulkan about the callback function. Such a callback is part of a *debug messenger*, and you can have as many of them as you want. Add a class member for this handle right under `instance`: -[,c++] +[source,multilang,c++,c] +.Debug messenger class member ---- +// START c++ vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; +// END c++ + +// START c +VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; +// END c ---- Now add a function `setupDebugMessenger` to be called from `initVulkan` right @@ -340,8 +492,10 @@ void setupDebugMessenger() We'll need to fill in a structure with details about the messenger and its callback: -[,c++] +[source,multilang,c++,c] +.Filling in the debug messenger creation info ---- +// START c++ void setupDebugMessenger() { ... @@ -354,6 +508,34 @@ void setupDebugMessenger() .pfnUserCallback = &debugCallback}; debugMessenger = instance.createDebugUtilsMessengerEXT( debugUtilsMessengerCreateInfoEXT ); } +// END c++ + +// START c +void setupDebugMessenger() +{ + ... + VkDebugUtilsMessageSeverityFlagsEXT severityFlags = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + VkDebugUtilsMessageTypeFlagsEXT messageTypeFlags = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT; + + VkDebugUtilsMessengerCreateInfoEXT createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = severityFlags; + createInfo.messageType = messageTypeFlags; + createInfo.pfnUserCallback = &debugCallback; + + // vkCreateDebugUtilsMessengerEXT is an extension function, so it isn't + // part of the core loader and has to be looked up manually. + auto func = reinterpret_cast( + vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT")); + if (func == nullptr || func(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) + { + throw std::runtime_error("failed to set up debug messenger!"); + } +} +// END c ---- The `messageSeverity` field allows you to specify all the types of @@ -376,8 +558,10 @@ for more info about the possibilities. The complete `createInstance` function now looks like: -[,c++] +[source,multilang,c++,c] +.The complete createInstance function ---- +// START c++ void createInstance() { constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle", @@ -429,6 +613,88 @@ void createInstance() .ppEnabledExtensionNames = requiredExtensions.data() }; instance = vk::raii::Instance(context, createInfo); } +// END c++ + +// START c +void createInstance() +{ + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_4; + + // Get the required layers + std::vector requiredLayers; + if (enableValidationLayers) + { + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation. + uint32_t layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + std::vector layerProperties(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, layerProperties.data()); + + for (const char* requiredLayer : requiredLayers) + { + bool layerFound = false; + for (const auto& layerProperty : layerProperties) + { + if (strcmp(layerProperty.layerName, requiredLayer) == 0) + { + layerFound = true; + break; + } + } + if (!layerFound) + { + throw std::runtime_error("Required layer not supported: " + std::string(requiredLayer)); + } + } + + // Get the required extensions. + auto requiredExtensions = getRequiredInstanceExtensions(); + + // Check if the required extensions are supported by the Vulkan implementation. + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + std::vector extensionProperties(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensionProperties.data()); + + for (const char* requiredExtension : requiredExtensions) + { + bool extensionFound = false; + for (const auto& extensionProperty : extensionProperties) + { + if (strcmp(extensionProperty.extensionName, requiredExtension) == 0) + { + extensionFound = true; + break; + } + } + if (!extensionFound) + { + throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension)); + } + } + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + createInfo.enabledLayerCount = static_cast(requiredLayers.size()); + createInfo.ppEnabledLayerNames = requiredLayers.data(); + createInfo.enabledExtensionCount = static_cast(requiredExtensions.size()); + createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +// END c ---- == Configuration diff --git a/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc b/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc index bd32bbf54..00a5bc480 100644 --- a/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc @@ -13,9 +13,16 @@ same physical device if you have varying requirements. Start by adding a new class member to store the logical device handle in. -[,c++] +[source,multilang,c++,c] +.Logical device class member ---- +// START c++ vk::raii::Device device = nullptr; +// END c++ + +// START c +VkDevice device = VK_NULL_HANDLE; +// END c ---- Next, add a `createLogicalDevice` function that is called from `initVulkan`. @@ -41,12 +48,35 @@ structs again, of which the first one will be `vk::DeviceQueueCreateInfo`. This structure describes the number of queues we want for a single queue family. Right now we're only interested in a queue with graphics capabilities. -[,c++] +[source,multilang,c++,c] +.Finding the graphics queue family ---- +// START c++ std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); auto graphicsQueueFamilyProperty = std::ranges::find_if(queueFamilyProperties, [](auto const &qfp) { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); }); auto graphicsIndex = static_cast(std::distance(queueFamilyProperties.begin(), graphicsQueueFamilyProperty)); vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex }; +// END c++ + +// START c +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr); +std::vector queueFamilyProperties(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyProperties.data()); + +uint32_t graphicsIndex = 0; +for (; graphicsIndex < queueFamilyCount; ++graphicsIndex) +{ + if (queueFamilyProperties[graphicsIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) + { + break; + } +} + +VkDeviceQueueCreateInfo deviceQueueCreateInfo{}; +deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +deviceQueueCreateInfo.queueFamilyIndex = graphicsIndex; +// END c ---- The currently available drivers will only allow you to create a small number of @@ -58,10 +88,22 @@ Vulkan lets you assign priorities to queues to influence the scheduling of command buffer execution using floating point numbers between `0.0` and `1.0`. This is required even if there is only a single queue: -[,c++] +[source,multilang,c++,c] +.Assigning a queue priority ---- +// START c++ float queuePriority = 0.5f; vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex, .queueCount = 1, .pQueuePriorities = &queuePriority }; +// END c++ + +// START c +float queuePriority = 0.5f; +VkDeviceQueueCreateInfo deviceQueueCreateInfo{}; +deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +deviceQueueCreateInfo.queueFamilyIndex = graphicsIndex; +deviceQueueCreateInfo.queueCount = 1; +deviceQueueCreateInfo.pQueuePriorities = &queuePriority; +// END c ---- == Specifying used device features @@ -73,9 +115,16 @@ Right now we don't need anything special, so we can simply define it and leave everything to `vk::False`. We'll come back to this structure once we're about to start doing more interesting things with Vulkan. -[,c++] +[source,multilang,c++,c] +.Requested physical device features ---- +// START c++ vk::PhysicalDeviceFeatures deviceFeatures; +// END c++ + +// START c +VkPhysicalDeviceFeatures deviceFeatures{}; +// END c ---- == Enabling additional device features @@ -91,8 +140,10 @@ To enable multiple sets of features, Vulkan uses a concept called "structure cha The C++ Vulkan API provides a helper template called `vk::StructureChain` that makes this process easier. Let's see how to use it: -[,c++] +[source,multilang,c++,c] +.Chaining feature structures ---- +// START c++ // Create a chain of feature structures vk::StructureChain requiredDeviceExtension = { vk::KHRSwapchainExtensionName}; +// END c++ + +// START c +std::vector requiredDeviceExtension = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME}; +// END c ---- The `VK_KHR_swapchain` extension is required for presenting rendered images to the window. Other extensions provide additional functionality that we'll use in later parts of the tutorial. @@ -135,8 +217,10 @@ The `VK_KHR_swapchain` extension is required for presenting rendered images to t With all the necessary information prepared, we can now create the logical device. We need to fill in the `vk::DeviceCreateInfo` structure and connect our feature chain to it: -[,c++] +[source,multilang,c++,c] +.DeviceCreateInfo ---- +// START c++ vk::DeviceCreateInfo deviceCreateInfo{ .pNext = &featureChain.get(), .queueCreateInfoCount = 1, @@ -144,6 +228,17 @@ vk::DeviceCreateInfo deviceCreateInfo{ .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), .ppEnabledExtensionNames = requiredDeviceExtension.data() }; +// END c++ + +// START c +VkDeviceCreateInfo deviceCreateInfo{}; +deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +deviceCreateInfo.pNext = &featureChain; +deviceCreateInfo.queueCreateInfoCount = 1; +deviceCreateInfo.pQueueCreateInfos = &deviceQueueCreateInfo; +deviceCreateInfo.enabledExtensionCount = static_cast(requiredDeviceExtension.size()); +deviceCreateInfo.ppEnabledExtensionNames = requiredDeviceExtension.data(); +// END c ---- Reviewing how we connect our feature chain to the device creation process: @@ -172,9 +267,18 @@ That means that the `enabledLayerCount` and `ppEnabledLayerNames` fields of With the `vk::DeviceCreateInfo` filled in, we can now proceed with creating logical device. -[,c++] +[source,multilang,c++,c] +.Creating the logical device ---- +// START c++ device = vk::raii::Device(physicalDevice, deviceCreateInfo); +// END c++ + +// START c +if (vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); +} +// END c ---- Passed parameters are the physical device to interface with, and the create @@ -191,9 +295,16 @@ included as a parameter. The queues are automatically created along with the logical device, but we don't have a handle to interface with them yet. First, add a class member to store a handle to the graphics queue: -[,c++] +[source,multilang,c++,c] +.Graphics queue class member ---- +// START c++ vk::raii::Queue graphicsQueue = nullptr; +// END c++ + +// START c +VkQueue graphicsQueue = VK_NULL_HANDLE; +// END c ---- Device queues are implicitly cleaned up when the device is destroyed, so we @@ -204,9 +315,16 @@ queue family. The parameters are the logical device, queue family index, and que index. Because we're only creating a single queue from this family, we'll simply use queue index `0`. -[,c++] +[source,multilang,c++,c] +.Retrieving the graphics queue handle ---- +// START c++ graphicsQueue = vk::raii::Queue(device, graphicsIndex, 0); +// END c++ + +// START c +vkGetDeviceQueue(device, graphicsIndex, 0, &graphicsQueue); +// END c ---- With the logical device and queue handles, we can now actually start using the diff --git a/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc b/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc index 98faf5a5b..0337389f6 100644 --- a/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc +++ b/en/03_Drawing_a_triangle/01_Presentation/00_Window_surface.adoc @@ -244,8 +244,10 @@ create the queue and retrieve the `vk::raii::Queue` handle. We need to modify th filtering logic to find the best queue families to use as we detect them. Here's how we do it in one function at the device creation functions: -[,c++] +[source,multilang,c++,c] +.The complete createLogicalDevice function ---- +// START c++ void createLogicalDevice() { // find the index of the first queue family that supports graphics std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); @@ -291,6 +293,74 @@ void createLogicalDevice() { device = vk::raii::Device( physicalDevice, deviceCreateInfo ); queue = vk::raii::Queue(device, queueIndex, 0); } +// END c++ + +// START c +void createLogicalDevice() { + // find the index of the first queue family that supports graphics and present + uint32_t queueFamilyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr); + std::vector queueFamilyProperties(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyProperties.data()); + + uint32_t queueIndex = ~0; + for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyCount; qfpIndex++) + { + VkBool32 presentSupport = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, qfpIndex, surface, &presentSupport); + if ((queueFamilyProperties[qfpIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) + { + // found a queue family that supports both graphics and present + queueIndex = qfpIndex; + break; + } + } + if (queueIndex == ~0) + { + throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); + } + + // query for Vulkan 1.3 features, chaining pNext by hand + VkPhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures{}; + extendedDynamicStateFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT; + extendedDynamicStateFeatures.extendedDynamicState = VK_TRUE; + + VkPhysicalDeviceVulkan13Features vulkan13Features{}; + vulkan13Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + vulkan13Features.pNext = &extendedDynamicStateFeatures; + vulkan13Features.dynamicRendering = VK_TRUE; + + VkPhysicalDeviceVulkan11Features vulkan11Features{}; + vulkan11Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; + vulkan11Features.pNext = &vulkan13Features; + vulkan11Features.shaderDrawParameters = VK_TRUE; + + VkPhysicalDeviceFeatures2 featureChain{}; + featureChain.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + featureChain.pNext = &vulkan11Features; + + // create a Device + float queuePriority = 0.5f; + VkDeviceQueueCreateInfo deviceQueueCreateInfo{}; + deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + deviceQueueCreateInfo.queueFamilyIndex = queueIndex; + deviceQueueCreateInfo.queueCount = 1; + deviceQueueCreateInfo.pQueuePriorities = &queuePriority; + + VkDeviceCreateInfo deviceCreateInfo{}; + deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceCreateInfo.pNext = &featureChain; + deviceCreateInfo.queueCreateInfoCount = 1; + deviceCreateInfo.pQueueCreateInfos = &deviceQueueCreateInfo; + deviceCreateInfo.enabledExtensionCount = static_cast(requiredDeviceExtension.size()); + deviceCreateInfo.ppEnabledExtensionNames = requiredDeviceExtension.data(); + + if (vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); + } + vkGetDeviceQueue(device, queueIndex, 0, &queue); +} +// END c ---- In the xref:./01_Swap_chain.adoc[next chapter], we're going to look at swap chains and diff --git a/en/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.adoc b/en/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.adoc index 5047177f8..e762766c9 100644 --- a/en/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.adoc +++ b/en/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.adoc @@ -33,10 +33,18 @@ will catch misspellings. First declare a list of required device extensions, similar to the list of validation layers to enable. -[,c++] +[source,multilang,c++,c] +.Required device extensions ---- +// START c++ std::vector requiredDeviceExtension = { vk::KHRSwapchainExtensionName}; +// END c++ + +// START c +std::vector requiredDeviceExtension = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME}; +// END c ---- It should be noted that the availability of a presentation queue, @@ -57,8 +65,10 @@ deviceCreateInfo.ppEnabledExtensionNames = requiredDeviceExtension.data(); Alternatively, we can do this at the construction and keep this very succinct: -[,c++] +[source,multilang,c++,c] +.Enabling the extension at device creation ---- +// START c++ std::vector requiredDeviceExtension = { vk::KHRSwapchainExtensionName }; float queuePriority = 0.5f; @@ -68,6 +78,26 @@ vk::DeviceCreateInfo deviceCreateInfo{.pNext = &featureCh .pQueueCreateInfos = &deviceQueueCreateInfo, .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), .ppEnabledExtensionNames = requiredDeviceExtension.data()}; +// END c++ + +// START c +std::vector requiredDeviceExtension = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + +float queuePriority = 0.5f; +VkDeviceQueueCreateInfo deviceQueueCreateInfo{}; +deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +deviceQueueCreateInfo.queueFamilyIndex = queueIndex; +deviceQueueCreateInfo.queueCount = 1; +deviceQueueCreateInfo.pQueuePriorities = &queuePriority; + +VkDeviceCreateInfo deviceCreateInfo{}; +deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +deviceCreateInfo.pNext = &featureChain; +deviceCreateInfo.queueCreateInfoCount = 1; +deviceCreateInfo.pQueueCreateInfos = &deviceQueueCreateInfo; +deviceCreateInfo.enabledExtensionCount = static_cast(requiredDeviceExtension.size()); +deviceCreateInfo.ppEnabledExtensionNames = requiredDeviceExtension.data(); +// END c ---- == Querying details of swap chain support @@ -92,9 +122,17 @@ Let's start with the basic surface capabilities. These properties are straightforward to query and are returned into a single `vk::SurfaceCapabilitiesKHR` struct. -[,c++] +[source,multilang,c++,c] +.Querying surface capabilities ---- +// START c++ auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( *surface ); +// END c++ + +// START c +VkSurfaceCapabilitiesKHR surfaceCapabilities; +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities); +// END c ---- This function takes the specified `vk::SurfaceKHR` window surface into account @@ -103,17 +141,37 @@ have that as the first parameter because it is the core component of the swap ch The next step is about querying the supported surface formats. -[,c++] +[source,multilang,c++,c] +.Querying supported surface formats ---- +// START c++ std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR( *surface ); +// END c++ + +// START c +uint32_t formatCount = 0; +vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr); +std::vector availableFormats(formatCount); +vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, availableFormats.data()); +// END c ---- Finally, querying the supported presentation modes works exactly the same way with `vk::raii::PhysicalDevice::getSurfacePresentModesKHR`: -[,c++] +[source,multilang,c++,c] +.Querying supported presentation modes ---- +// START c++ std::vector availablePresentModes = physicalDevice.getSurfacePresentModesKHR( *surface ); +// END c++ + +// START c +uint32_t presentModeCount = 0; +vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, nullptr); +std::vector availablePresentModes(presentModeCount); +vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, availablePresentModes.data()); +// END c ---- All the details are available now. Swap chain support is enough for this @@ -174,14 +232,28 @@ If that fails, then we could start ranking the available formats based on how "good" they are, but in most cases it's okay to just settle with the first format that is specified. -[,c++] +[source,multilang,c++,c] +.chooseSwapSurfaceFormat ---- +// START c++ vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { const auto formatIt = std::ranges::find_if( availableFormats, [](const auto &format) { return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; }); return formatIt != availableFormats.end() ? *formatIt : availableFormats[0]; } +// END c++ + +// START c +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& format : availableFormats) { + if (format.format == VK_FORMAT_B8G8R8A8_SRGB && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return format; + } + } + return availableFormats[0]; +} +// END c ---- === Presentation mode @@ -229,8 +301,10 @@ where energy usage is more important, you will probably want to use `vk::PresentModeKHR::eFifo` instead. Now, let's look through the list to see if `vk::PresentModeKHR::eMailbox` is available: -[,c++] +[source,multilang,c++,c] +.chooseSwapPresentMode ---- +// START c++ vk::PresentModeKHR chooseSwapPresentMode(std::vector const &availablePresentModes) { assert(std::ranges::any_of(availablePresentModes, [](auto presentMode) { return presentMode == vk::PresentModeKHR::eFifo; })); @@ -239,6 +313,19 @@ vk::PresentModeKHR chooseSwapPresentMode(std::vector const & vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eFifo; } +// END c++ + +// START c +VkPresentModeKHR chooseSwapPresentMode(std::vector const &availablePresentModes) +{ + for (const auto& presentMode : availablePresentModes) { + if (presentMode == VK_PRESENT_MODE_MAILBOX_KHR) { + return presentMode; + } + } + return VK_PRESENT_MODE_FIFO_KHR; +} +// END c ---- === Swap extent @@ -275,8 +362,10 @@ for us, we can't just use the original `{WIDTH, HEIGHT}`. Instead, we must use `glfwGetFramebufferSize` to query the resolution of the window in pixel before matching it against the minimum and maximum image extent. -[,c++] +[source,multilang,c++,c] +.chooseSwapExtent ---- +// START c++ #include // Necessary for uint32_t #include // Necessary for std::numeric_limits #include // Necessary for std::clamp @@ -297,6 +386,34 @@ vk::Extent2D chooseSwapExtent(vk::SurfaceCapabilitiesKHR const &capabilities) std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) }; } +// END c++ + +// START c +#include // Necessary for uint32_t + +... + +static uint32_t clampU32(uint32_t value, uint32_t lo, uint32_t hi) { + if (value < lo) return lo; + if (value > hi) return hi; + return value; +} + +VkExtent2D chooseSwapExtent(VkSurfaceCapabilitiesKHR const &capabilities) +{ + if (capabilities.currentExtent.width != UINT32_MAX) + { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + VkExtent2D actualExtent; + actualExtent.width = clampU32((uint32_t)width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = clampU32((uint32_t)height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + return actualExtent; +} +// END c ---- The `clamp` function is used here to bound the values of `width` and @@ -312,8 +429,10 @@ create a working swap chain. Create a `createSwapChain` function that starts out with the results of these calls and make sure to call it from `initVulkan` after logical device creation. -[,c++] +[source,multilang,c++,c] +.Beginning of createSwapChain ---- +// START c++ void initVulkan() { createInstance(); setupDebugMessenger(); @@ -327,10 +446,35 @@ void createSwapChain() { vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( *surface ); swapChainExtent = chooseSwapExtent(surfaceCapabilities); uint32_t minImageCount = chooseSwapMinImageCount(surfaceCapabilities); - + std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR(*surface); swapChainSurfaceFormat = chooseSwapSurfaceFormat(availableFormats); } +// END c++ + +// START c +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); +} + +void createSwapChain() { + VkSurfaceCapabilitiesKHR surfaceCapabilities; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + uint32_t minImageCount = chooseSwapMinImageCount(surfaceCapabilities); + + uint32_t formatCount = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr); + std::vector availableFormats(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, availableFormats.data()); + swapChainSurfaceFormat = chooseSwapSurfaceFormat(availableFormats); +} +// END c ---- Aside from these properties, we also have to decide how many images we @@ -356,8 +500,10 @@ We should also make sure to not exceed the maximum number of images while doing this, where `0` is a special value that means that there is no maximum, resulting in this helper function -[,c++] +[source,multilang,c++,c] +.chooseSwapMinImageCount ---- +// START c++ uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) { auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); @@ -367,14 +513,29 @@ uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabi } return minImageCount; } +// END c++ + +// START c +uint32_t chooseSwapMinImageCount(VkSurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount > 3 ? surfaceCapabilities.minImageCount : 3; + if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount)) + { + minImageCount = surfaceCapabilities.maxImageCount; + } + return minImageCount; +} +// END c ---- As is tradition with Vulkan objects, creating the swap chain object requires filling in a large structure, to be fair, the swapchain is a fairly complex object so it is among the larger createInfo structures in Vulkan: -[,c++] +[source,multilang,c++,c] +.SwapchainCreateInfoKHR ---- +// START c++ vk::SwapchainCreateInfoKHR swapChainCreateInfo{.surface = *surface, .minImageCount = minImageCount, .imageFormat = swapChainSurfaceFormat.format, @@ -387,7 +548,24 @@ vk::SwapchainCreateInfoKHR swapChainCreateInfo{.surface = *surface, .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, .presentMode = chooseSwapPresentMode(availablePresentModes), .clipped = true}; -}; +// END c++ + +// START c +VkSwapchainCreateInfoKHR swapChainCreateInfo{}; +swapChainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; +swapChainCreateInfo.surface = surface; +swapChainCreateInfo.minImageCount = minImageCount; +swapChainCreateInfo.imageFormat = swapChainSurfaceFormat.format; +swapChainCreateInfo.imageColorSpace = swapChainSurfaceFormat.colorSpace; +swapChainCreateInfo.imageExtent = swapChainExtent; +swapChainCreateInfo.imageArrayLayers = 1; +swapChainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; +swapChainCreateInfo.preTransform = surfaceCapabilities.currentTransform; +swapChainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; +swapChainCreateInfo.presentMode = chooseSwapPresentMode(availablePresentModes); +swapChainCreateInfo.clipped = VK_TRUE; +// END c ---- [,c++] @@ -481,18 +659,40 @@ member to its default `nullptr`. Now add class members to store the `vk::SwapchainKHR` object and its images: -[,c++] +[source,multilang,c++,c] +.Swap chain class members ---- +// START c++ vk::raii::SwapchainKHR swapChain; std::vector swapChainImages; +// END c++ + +// START c +VkSwapchainKHR swapChain; +std::vector swapChainImages; +// END c ---- Creating the swap chain is now as simple as calling the constructor of `vk::raii::SwapchainKHR`: -[,c++] +[source,multilang,c++,c] +.Creating the swap chain ---- +// START c++ swapChain = vk::raii::SwapchainKHR( device, swapChainCreateInfo ); swapChainImages = swapChain.getImages(); +// END c++ + +// START c +if (vkCreateSwapchainKHR(device, &swapChainCreateInfo, nullptr, &swapChain) != VK_SUCCESS) { + throw std::runtime_error("failed to create swap chain!"); +} + +uint32_t imageCount = 0; +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +swapChainImages.resize(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +// END c ---- The parameters are the logical device and a swap chain creation info. @@ -514,20 +714,40 @@ The swap chain has been created now, so all that remains is retrieving the handles of the `vk::Image` objects it contains. We'll reference these during rendering operations in later chapters. -[,c++] +[source,multilang,c++,c] +.Retrieving the swap chain image handles ---- +// START c++ std::vector swapChainImages = swapChain->getImages(); +// END c++ + +// START c +uint32_t imageCount = 0; +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +std::vector swapChainImages(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +// END c ---- One last thing, store the format and extent we've chosen for the swap chain images in member variables. We'll need them in future chapters. -[,c++] +[source,multilang,c++,c] +.Final swap chain class members ---- +// START c++ vk::raii::SwapchainKHR swapChain = nullptr; std::vector swapChainImages; vk::SurfaceFormatKHR swapChainSurfaceFormat; vk::Extent2D swapChainExtent; +// END c++ + +// START c +VkSwapchainKHR swapChain = VK_NULL_HANDLE; +std::vector swapChainImages; +VkSurfaceFormatKHR swapChainSurfaceFormat; +VkExtent2D swapChainExtent; +// END c ---- We now have a set of images that can be drawn onto and can be presented to the diff --git a/en/03_Drawing_a_triangle/01_Presentation/02_Image_views.adoc b/en/03_Drawing_a_triangle/01_Presentation/02_Image_views.adoc index 6a9105320..47b08fc10 100644 --- a/en/03_Drawing_a_triangle/01_Presentation/02_Image_views.adoc +++ b/en/03_Drawing_a_triangle/01_Presentation/02_Image_views.adoc @@ -14,9 +14,16 @@ targets later on. First, add a class member to store the image views in: -[,c++] +[source,multilang,c++,c] +.Image view storage ---- +// START c++ std::vector swapChainImageViews; +// END c++ + +// START c +std::vector swapChainImageViews; +// END c ---- Create the `createImageViews` function and call it right after swap chain @@ -58,8 +65,10 @@ a bit though. The last member is the SubResource range, which is necessary, and we'll talk about shortly. -[,c++] +[source,multilang,c++,c] +.ImageViewCreateInfo ---- +// START c++ void createImageViews() { assert(swapChainImageViews.empty()); @@ -68,6 +77,24 @@ void createImageViews() .format = swapChainSurfaceFormat.format, .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } }; } +// END c++ + +// START c +void createImageViews() +{ + assert(swapChainImageViews.empty()); + + VkImageViewCreateInfo imageViewCreateInfo{}; + imageViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + imageViewCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + imageViewCreateInfo.format = swapChainSurfaceFormat.format; + imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageViewCreateInfo.subresourceRange.baseMipLevel = 0; + imageViewCreateInfo.subresourceRange.levelCount = 1; + imageViewCreateInfo.subresourceRange.baseArrayLayer = 0; + imageViewCreateInfo.subresourceRange.layerCount = 1; +} +// END c ---- The `components` field allows you to swizzle the color channels around. For @@ -76,10 +103,20 @@ texture. You can also map constant values of `0` and `1` to a channel. In our case, we'll stick to the default mapping by accepting the constructed defaults, but here's how to explicitly do it: -[,c++] +[source,multilang,c++,c] +.Explicit component mapping ---- +// START c++ imageViewCreateInfo.components = { vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity}; +// END c++ + +// START c +imageViewCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; +imageViewCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; +imageViewCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; +imageViewCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; +// END c ---- The `subresourceRange` field describes what the image's purpose is and which @@ -89,9 +126,18 @@ Note: you can either list all elements of the `subresourceRange`, as it's done in the code snippet above, or use designated initializers to just initialize those members that differ from the default values, like this: -[,c++] +[source,multilang,c++,c] +.Designated-initializer shortcut ---- +// START c++ imageViewCreateInfo.subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .levelCount = 1, .layerCount = 1}; +// END c++ + +// START c +imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +imageViewCreateInfo.subresourceRange.levelCount = 1; +imageViewCreateInfo.subresourceRange.layerCount = 1; +// END c ---- If you were working on a stereographic 3D application, then you would create a @@ -122,13 +168,29 @@ for (auto &image : swapChainImages) Creating the image view is now a matter of using the constructor of `vk::raii::CreateImageView`, here via `std::vector::emplace_back`: -[,c++] +[source,multilang,c++,c] +.Creating an image view for every swap chain image ---- +// START c++ for (auto &image : swapChainImages) { imageViewCreateInfo.image = image; swapChainImageViews.emplace_back( device, imageViewCreateInfo ); } +// END c++ + +// START c +for (auto &image : swapChainImages) +{ + imageViewCreateInfo.image = image; + + VkImageView imageView; + if (vkCreateImageView(device, &imageViewCreateInfo, nullptr, &imageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create image views!"); + } + swapChainImageViews.push_back(imageView); +} +// END c ---- An image view is sufficient to start using an image as a texture, but it's not quite ready to be used as a render target just yet. diff --git a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.adoc b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.adoc index 3c57df7cd..3505e6816 100644 --- a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.adoc +++ b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.adoc @@ -426,11 +426,20 @@ Before we can pass the code to the pipeline, we have to wrap it in a `vk::raii::ShaderModule` object. Let's create a helper function `createShaderModule` to do that. -[,c++] +[source,multilang,c++,c] +.createShaderModule signature ---- +// START c++ [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector& code) const { } +// END c++ + +// START c +VkShaderModule createShaderModule(const std::vector& code) +{ +} +// END c ---- The function will take a buffer with the bytecode as parameter and create a @@ -446,16 +455,36 @@ need to ensure that the data satisfies the alignment requirements of `uint32_t`. Lucky for us, the data is stored in an `std::vector` where the default allocator already ensures that the data satisfies the worst case alignment requirements. -[,c++] +[source,multilang,c++,c] +.ShaderModuleCreateInfo ---- +// START c++ vk::ShaderModuleCreateInfo createInfo{ .codeSize = code.size() * sizeof(char), .pCode = reinterpret_cast(code.data()) }; +// END c++ + +// START c +VkShaderModuleCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; +createInfo.codeSize = code.size() * sizeof(char); +createInfo.pCode = reinterpret_cast(code.data()); +// END c ---- The `vk::raii::ShaderModule` can then be created by its constructor: -[,c++] +[source,multilang,c++,c] +.Creating the shader module ---- +// START c++ vk::raii::ShaderModule shaderModule{ device, createInfo }; +// END c++ + +// START c +VkShaderModule shaderModule; +if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader module!"); +} +// END c ---- The parameters are the same as those in previous object creation functions: the @@ -487,9 +516,20 @@ of the actual pipeline creation process. We'll start by filling in the structure for the vertex shader, again in the `createGraphicsPipeline` function. -[,c++] +[source,multilang,c++,c] +.Vertex shader stage info ---- +// START c++ vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, .module = shaderModule, .pName = "vertMain" }; +// END c++ + +// START c +VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; +vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +vertShaderStageInfo.module = shaderModule; +vertShaderStageInfo.pName = "vertMain"; +// END c ---- The first parameter is the stage that we're operating in. The next two parameters @@ -512,16 +552,34 @@ If you don't have any constants like that, then you can set the member to Modifying the structure to suit the fragment shader is easy: -[,c++] +[source,multilang,c++,c] +.Fragment shader stage info ---- +// START c++ vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, .module = shaderModule, .pName = "fragMain" }; +// END c++ + +// START c +VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; +fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; +fragShaderStageInfo.module = shaderModule; +fragShaderStageInfo.pName = "fragMain"; +// END c ---- Finish by defining an array that contains these two structs, which we'll later use to reference them in the actual pipeline creation step. -[,c++] +[source,multilang,c++,c] +.Shader stage array ---- +// START c++ vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +// END c++ + +// START c +VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +// END c ---- That's all there is describing the programmable stages of the pipeline. diff --git a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.adoc b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.adoc index ed2a9abf3..e1a272604 100644 --- a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.adoc +++ b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.adoc @@ -15,11 +15,23 @@ pipeline at draw time. Examples are the size of the viewport, line width and blend constants. If you want to use dynamic state and keep these properties out, then you'll have to fill in a `vk::PipelineDynamicStateCreateInfo` structure like this: -[,c++] +[source,multilang,c++,c] +.PipelineDynamicStateCreateInfo ---- +// START c++ std::vector dynamicStates = {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; vk::PipelineDynamicStateCreateInfo dynamicState{.dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data()}; +// END c++ + +// START c +std::vector dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +// END c ---- This will cause the configuration of these values to be ignored, and you will be able (and required) to specify the data at drawing time. @@ -36,9 +48,17 @@ It describes this in roughly two ways: Because we're hard coding the vertex data directly in the vertex shader, we'll fill in this structure to specify that there is no vertex data to load for now. We'll get back to it in the vertex buffer chapter. -[,c++] +[source,multilang,c++,c] +.PipelineVertexInputStateCreateInfo ---- +// START c++ vk::PipelineVertexInputStateCreateInfo vertexInputInfo; +// END c++ + +// START c +VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; +vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; +// END c ---- The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members point to an array of structs that describe the aforementioned details for loading vertex data. @@ -61,9 +81,18 @@ If you set the `primitiveRestartEnable` member to `vk::True`, then it's possibl We intend to draw triangles throughout this tutorial, so we'll stick to the following data for the structure: -[,c++] +[source,multilang,c++,c] +.PipelineInputAssemblyStateCreateInfo ---- +// START c++ vk::PipelineInputAssemblyStateCreateInfo inputAssembly{.topology = vk::PrimitiveTopology::eTriangleList}; +// END c++ + +// START c +VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; +inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; +inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +// END c ---- == Viewports and scissors @@ -71,9 +100,22 @@ vk::PipelineInputAssemblyStateCreateInfo inputAssembly{.topology = vk::Primitive A viewport basically describes the region of the framebuffer that the output will be rendered to. This will almost always be `(0, 0)` to `(width, height)` and in this tutorial that will also be the case. -[,c++] +[source,multilang,c++,c] +.Viewport ---- +// START c++ vk::Viewport viewport{0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f}; +// END c++ + +// START c +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = static_cast(swapChainExtent.width); +viewport.height = static_cast(swapChainExtent.height); +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +// END c ---- Remember that the size of the swap chain and its images may differ from the `WIDTH` and `HEIGHT` of the window. @@ -93,9 +135,18 @@ image::/images/viewports_scissors.png[] So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: -[,c++] +[source,multilang,c++,c] +.Scissor rectangle ---- +// START c++ vk::Rect2D scissor{vk::Offset2D{ 0, 0 }, swapChainExtent}; +// END c++ + +// START c +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +// END c ---- Viewport(s) and scissor rectangle(s) can either be specified as a static part of the pipeline or as a dynamic state set in the command buffer. @@ -105,18 +156,40 @@ This is widespread and all implementations can handle this dynamic state without When opting for dynamic viewport(s) and scissor rectangle(s), you need to enable the respective dynamic states for the pipeline: -[,c++] +[source,multilang,c++,c] +.PipelineDynamicStateCreateInfo ---- +// START c++ std::vector dynamicStates = {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; vk::PipelineDynamicStateCreateInfo dynamicState{.dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data()}; +// END c++ + +// START c +std::vector dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +// END c ---- And then you only need to specify their count at pipeline creation time: -[,c++] +[source,multilang,c++,c] +.PipelineViewportStateCreateInfo (dynamic) ---- +// START c++ vk::PipelineViewportStateCreateInfo viewportState{.viewportCount = 1, .scissorCount = 1}; +// END c++ + +// START c +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.scissorCount = 1; +// END c ---- The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. @@ -127,9 +200,21 @@ Without dynamic state, the viewport and scissor rectangle need to be set in the This makes the viewport and scissor rectangle for this pipeline immutable. Any changes required to these values would require a new pipeline to be created with the new values. -[,c++] +[source,multilang,c++,c] +.PipelineViewportStateCreateInfo (static) ---- +// START c++ vk::PipelineViewportStateCreateInfo viewportState{.viewportCount = 1, .pViewports = &viewport, .scissorCount = 1, .pScissors = &scissor}; +// END c++ + +// START c +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.pViewports = &viewport; +viewportState.scissorCount = 1; +viewportState.pScissors = &scissor; +// END c ---- Independent of how you set them, it's possible to use multiple viewports and scissor rectangles on some graphics cards, so the structure members reference an array of them. @@ -141,8 +226,10 @@ The rasterizer takes the geometry shaped by the vertices from the vertex shader It also performs https://en.wikipedia.org/wiki/Z-buffering[depth testing], https://en.wikipedia.org/wiki/Back-face_culling[face culling] and the scissor test, and it can be configured to output fragments that fill entire polygons or just the edges (wireframe rendering). All this is configured using the `vk::PipelineRasterizationStateCreateInfo` structure. -[,c++] +[source,multilang,c++,c] +.PipelineRasterizationStateCreateInfo ---- +// START c++ vk::PipelineRasterizationStateCreateInfo rasterizer{.depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, .polygonMode = vk::PolygonMode::eFill, @@ -150,6 +237,19 @@ vk::PipelineRasterizationStateCreateInfo rasterizer{.depthClampEnable = v .frontFace = vk::FrontFace::eClockwise, .depthBiasEnable = vk::False, .lineWidth = 1.0f}; +// END c++ + +// START c +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +rasterizer.rasterizerDiscardEnable = VK_FALSE; +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.lineWidth = 1.0f; +// END c ---- If `depthClampEnable` is set to `vk::True`, then fragments that are beyond @@ -189,9 +289,19 @@ This mainly occurs along edges, which is also where the most noticeable aliasing Because it doesn't need to run the fragment shader multiple times if only one polygon maps to a pixel, it is significantly less expensive than simply rendering to a higher resolution and then downscaling. Enabling it requires enabling a GPU feature. -[,c++] +[source,multilang,c++,c] +.PipelineMultisampleStateCreateInfo ---- +// START c++ vk::PipelineMultisampleStateCreateInfo multisampling{.rasterizationSamples = vk::SampleCountFlagBits::e1, .sampleShadingEnable = vk::False}; +// END c++ + +// START c +VkPipelineMultisampleStateCreateInfo multisampling{}; +multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; +multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; +multisampling.sampleShadingEnable = VK_FALSE; +// END c ---- We'll revisit multisampling in later chapter, for now let's keep it disabled. @@ -214,11 +324,20 @@ There are two types of structs to configure color blending. The first struct, `vk::PipelineColorBlendAttachmentState` contains the configuration per attached framebuffer and the second struct, `vk::PipelineColorBlendStateCreateInfo` contains the _global_ color blending settings. In our case, we only have one framebuffer: -[,c++] +[source,multilang,c++,c] +.PipelineColorBlendAttachmentState (blending disabled) ---- +// START c++ vk::PipelineColorBlendAttachmentState colorBlendAttachment{ - .blendEnable = vk::False, + .blendEnable = vk::False, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA}; +// END c++ + +// START c +VkPipelineColorBlendAttachmentState colorBlendAttachment{}; +colorBlendAttachment.blendEnable = VK_FALSE; +colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; +// END c ---- This per-framebuffer struct allows you to configure the first way of color blending. @@ -251,8 +370,10 @@ finalColor.a = newAlpha.a; This can be achieved with the following parameters: -[,c++] +[source,multilang,c++,c] +.PipelineColorBlendAttachmentState (alpha blending) ---- +// START c++ vk::PipelineColorBlendAttachmentState colorBlendAttachment{ .blendEnable = vk::True, .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, @@ -262,16 +383,41 @@ vk::PipelineColorBlendAttachmentState colorBlendAttachment{ .dstAlphaBlendFactor = vk::BlendFactor::eZero, .alphaBlendOp = vk::BlendOp::eAdd, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA}; +// END c++ + +// START c +VkPipelineColorBlendAttachmentState colorBlendAttachment{}; +colorBlendAttachment.blendEnable = VK_TRUE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; +colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; +// END c ---- You can find all the possible operations in the `vk::BlendFactor` and `vk::BlendOp` enumerations in the specification. The second structure references the array of structures for all the framebuffers and allows you to set blend constants that you can use as blend factors in the aforementioned calculations. -[,c++] +[source,multilang,c++,c] +.PipelineColorBlendStateCreateInfo ---- +// START c++ vk::PipelineColorBlendStateCreateInfo colorBlending{ .logicOpEnable = vk::False, .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment}; +// END c++ + +// START c +VkPipelineColorBlendStateCreateInfo colorBlending{}; +colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; +colorBlending.logicOpEnable = VK_FALSE; +colorBlending.logicOp = VK_LOGIC_OP_COPY; +colorBlending.attachmentCount = 1; +colorBlending.pAttachments = &colorBlendAttachment; +// END c ---- If you want to use the second method of blending (a bitwise combination), then you should set `logicOpEnable` to `vk::True`. @@ -290,18 +436,39 @@ Even though we won't be using them until a future chapter, we are still required Create a class member to hold this object because we'll refer to it from other functions at a later point in time: -[,c++] +[source,multilang,c++,c] +.Pipeline layout class member ---- +// START c++ vk::raii::PipelineLayout pipelineLayout = nullptr; +// END c++ + +// START c +VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; +// END c ---- And then create the object in the `createGraphicsPipeline` function: -[,c++] +[source,multilang,c++,c] +.Creating the pipeline layout ---- +// START c++ vk::PipelineLayoutCreateInfo pipelineLayoutInfo{.setLayoutCount = 0, .pushConstantRangeCount = 0}; pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); +// END c++ + +// START c +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 0; +pipelineLayoutInfo.pushConstantRangeCount = 0; + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create pipeline layout!"); +} +// END c ---- The structure also specifies _push constants_, which are another way of passing dynamic values to shaders that we may get into in a future chapter. diff --git a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Dynamic_rendering.adoc b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Dynamic_rendering.adoc index e2bb05ea2..5679d9a0c 100644 --- a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Dynamic_rendering.adoc +++ b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Dynamic_rendering.adoc @@ -12,15 +12,27 @@ Dynamic rendering simplifies the rendering process by eliminating the need for r To use dynamic rendering, we need to specify the formats of the attachments that will be used during rendering. This is done through the `vk::PipelineRenderingCreateInfo` structure when creating the graphics pipeline: -[,c++] +[source,multilang,c++,c] +.PipelineRenderingCreateInfo ---- +// START c++ vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainSurfaceFormat.format }; +// END c++ + +// START c +VkPipelineRenderingCreateInfo pipelineRenderingCreateInfo{}; +pipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; +pipelineRenderingCreateInfo.colorAttachmentCount = 1; +pipelineRenderingCreateInfo.pColorAttachmentFormats = &swapChainSurfaceFormat.format; +// END c ---- This structure specifies that we'll be using one color attachment with the format of our swap chain images. We then include this structure in the `vk::StructureChain`, starting with the `vk::GraphicsPipelineCreateInfo` structure: -[,c++] +[source,multilang,c++,c] +.Chaining PipelineRenderingCreateInfo onto GraphicsPipelineCreateInfo ---- +// START c++ vk::StructureChain pipelineCreateInfoChain = { {.stageCount = 2, .pStages = shaderStages, @@ -34,6 +46,30 @@ vk::StructureChain()); +// END c++ + +// START c +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create graphics pipeline!"); +} +// END c ---- The second parameter, for which we've passed the `nullptr` argument, references an optional `vk::raii::PipelineCache` object. diff --git a/en/03_Drawing_a_triangle/03_Drawing/00_Dynamic_rendering.adoc b/en/03_Drawing_a_triangle/03_Drawing/00_Dynamic_rendering.adoc index ac9e06312..f34ee7140 100644 --- a/en/03_Drawing_a_triangle/03_Drawing/00_Dynamic_rendering.adoc +++ b/en/03_Drawing_a_triangle/03_Drawing/00_Dynamic_rendering.adoc @@ -19,8 +19,10 @@ Let's see how this works in practice. We'll be using the `vk::RenderingAttachmen In the next chapter, we'll create command buffers and record rendering commands. Here's a preview of how we'll use dynamic rendering: -[,c++] +[source,multilang,c++,c] +.Recording a frame with dynamic rendering ---- +// START c++ void recordCommandBuffer(uint32_t imageIndex) { commandBuffer.begin({}); @@ -73,6 +75,67 @@ void recordCommandBuffer(uint32_t imageIndex) commandBuffer.end(); } +// END c++ + +// START c +void recordCommandBuffer(uint32_t imageIndex) +{ + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + // Transition the image layout for rendering + transition_image_layout( + imageIndex, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + 0, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT + ); + + // Set up the color attachment + VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; + VkRenderingAttachmentInfo attachmentInfo{}; + attachmentInfo.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + attachmentInfo.imageView = swapChainImageViews[imageIndex]; + attachmentInfo.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachmentInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachmentInfo.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachmentInfo.clearValue = clearColor; + + // Set up the rendering info + VkRenderingInfo renderingInfo{}; + renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + renderingInfo.renderArea.offset = {0, 0}; + renderingInfo.renderArea.extent = swapChainExtent; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &attachmentInfo; + + // Begin rendering + vkCmdBeginRendering(commandBuffer, &renderingInfo); + + // Rendering commands will go here + + // End rendering + vkCmdEndRendering(commandBuffer); + + // Transition the image layout for presentation + transition_image_layout( + imageIndex, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + 0, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT + ); + + vkEndCommandBuffer(commandBuffer); +} +// END c ---- As you can see, we directly specify the image view to render to in the `vk::RenderingAttachmentInfo` structure. We also specify the load and store operations, similar to what we would do in a render pass. The `vk::RenderingInfo` structure then combines this with other rendering parameters. diff --git a/en/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc b/en/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc index 0ebb216a4..5cfe7e4a2 100644 --- a/en/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc +++ b/en/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc @@ -15,9 +15,16 @@ We have to create a command pool before we can create command buffers. Command pools manage the memory that is used to store the buffers and command buffers are allocated from them. Add a new class member to store a `vk::raii::CommandPool`: -[,c++] +[source,multilang,c++,c] +.Command pool class member ---- +// START c++ vk::raii::CommandPool commandPool = nullptr; +// END c++ + +// START c +VkCommandPool commandPool = VK_NULL_HANDLE; +// END c ---- Then create a new function `createCommandPool` and call it from `initVulkan` after the graphics pipeline was created. @@ -46,10 +53,20 @@ void createCommandPool() Command pool creation only takes two parameters: -[,c++] +[source,multilang,c++,c] +.CommandPoolCreateInfo ---- +// START c++ vk::CommandPoolCreateInfo poolInfo{.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueIndex}; +// END c++ + +// START c +VkCommandPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; +poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; +poolInfo.queueFamilyIndex = queueIndex; +// END c ---- There are two possible flags for command pools: @@ -66,9 +83,18 @@ Command buffers are executed by submitting them on one of the device queues, lik Each command pool can only allocate command buffers that are submitted on a single type of queue. We're going to record commands for drawing, which is why we've chosen the graphics queue family. -[,c++] +[source,multilang,c++,c] +.Creating the command pool ---- +// START c++ commandPool = vk::raii::CommandPool(device, poolInfo); +// END c++ + +// START c +if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create command pool!"); +} +// END c ---- Finish creating the command pool using the `vk::raii::CommandPool` constructor. @@ -82,9 +108,16 @@ We can now start allocating command buffers. Create a `vk::raii::CommandBuffer` object as a class member. Command buffers will be automatically freed when their command pool is destroyed, so we don't need explicit cleanup. -[,c++] +[source,multilang,c++,c] +.Command buffer class member ---- +// START c++ vk::raii::CommandBuffer commandBuffer = nullptr; +// END c++ + +// START c +VkCommandBuffer commandBuffer = VK_NULL_HANDLE; +// END c ---- We'll now start working on a `createCommandBuffer` function to allocate a single command buffer from the command pool. @@ -114,11 +147,26 @@ void createCommandBuffer() Command buffers are allocated with the `vk::raii::CommandBuffers` constructor, which takes a `vk::CommandBufferAllocateInfo` struct as parameter that specifies the command pool and number of buffers to allocate: -[,c++] +[source,multilang,c++,c] +.Allocating the command buffer ---- +// START c++ vk::CommandBufferAllocateInfo allocInfo{ .commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; commandBuffer = std::move(vk::raii::CommandBuffers(device, allocInfo).front()); +// END c++ + +// START c +VkCommandBufferAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +allocInfo.commandPool = commandPool; +allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +allocInfo.commandBufferCount = 1; + +if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); +} +// END c ---- The `level` parameter specifies if the allocated command buffers are primary or secondary command buffers. @@ -145,9 +193,20 @@ void recordCommandBuffer(uint32_t imageIndex) We always begin recording a command buffer by calling `vk::raii::CommandBuffer::begin` with a small `vk::CommandBufferBeginInfo` structure as argument that specifies some details about the usage of this specific command buffer. -[,c++] +[source,multilang,c++,c] +.Beginning command buffer recording ---- +// START c++ commandBuffer.begin({}); +// END c++ + +// START c +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} +// END c ---- The `flags` member of the `vk::CommandBufferBeginInfo` specifies how we're going to use the command buffer. @@ -171,8 +230,10 @@ Before we can start rendering to an image, we need to transition its layout to o We'll use a pipeline barrier to transition the image layout from `vk::ImageLayout::eUndefined` to `vk::ImageLayout::eColorAttachmentOptimal`: -[,c++] +[source,multilang,c++,c] +.transition_image_layout ---- +// START c++ void transition_image_layout( uint32_t imageIndex, vk::ImageLayout old_layout, @@ -204,6 +265,43 @@ void transition_image_layout( .pImageMemoryBarriers = &barrier}; commandBuffer.pipelineBarrier2(dependency_info); } +// END c++ + +// START c +void transition_image_layout( + uint32_t imageIndex, + VkImageLayout old_layout, + VkImageLayout new_layout, + VkAccessFlags2 src_access_mask, + VkAccessFlags2 dst_access_mask, + VkPipelineStageFlags2 src_stage_mask, + VkPipelineStageFlags2 dst_stage_mask) +{ + VkImageMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + barrier.srcStageMask = src_stage_mask; + barrier.srcAccessMask = src_access_mask; + barrier.dstStageMask = dst_stage_mask; + barrier.dstAccessMask = dst_access_mask; + barrier.oldLayout = old_layout; + barrier.newLayout = new_layout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = swapChainImages[imageIndex]; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + + VkDependencyInfo dependency_info{}; + dependency_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency_info.imageMemoryBarrierCount = 1; + dependency_info.pImageMemoryBarriers = &barrier; + + vkCmdPipelineBarrier2(commandBuffer, &dependency_info); +} +// END c ---- This function will be used to transition the image layout before and after rendering. @@ -228,8 +326,10 @@ transition_image_layout( First, we transition the image layout to `vk::ImageLayout::eColorAttachmentOptimal`. Then, we set up the color attachment: -[,c++] +[source,multilang,c++,c] +.Color attachment info ---- +// START c++ vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); vk::RenderingAttachmentInfo attachmentInfo = { .imageView = swapChainImageViews[imageIndex], @@ -237,28 +337,61 @@ vk::RenderingAttachmentInfo attachmentInfo = { .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = clearColor}; +// END c++ + +// START c +VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; + +VkRenderingAttachmentInfo attachmentInfo{}; +attachmentInfo.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; +attachmentInfo.imageView = swapChainImageViews[imageIndex]; +attachmentInfo.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +attachmentInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +attachmentInfo.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +attachmentInfo.clearValue = clearColor; +// END c ---- The `imageView` parameter specifies which image view to render to. The `imageLayout` parameter specifies the layout the image will be in during rendering. The `loadOp` parameter specifies what to do with the image before rendering, and the `storeOp` parameter specifies what to do with the image after rendering. We're using `vk::AttachmentLoadOp::eClear` to clear the image to black before rendering, and `vk::AttachmentStoreOp::eStore` to store the rendered image for later use. Next, we set up the rendering info: -[,c++] +[source,multilang,c++,c] +.RenderingInfo ---- +// START c++ vk::RenderingInfo renderingInfo = { .renderArea = {.offset = {0, 0}, .extent = swapChainExtent}, .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &attachmentInfo}; +// END c++ + +// START c +VkRenderingInfo renderingInfo{}; +renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; +renderingInfo.renderArea.offset = {0, 0}; +renderingInfo.renderArea.extent = swapChainExtent; +renderingInfo.layerCount = 1; +renderingInfo.colorAttachmentCount = 1; +renderingInfo.pColorAttachments = &attachmentInfo; +// END c ---- The `renderArea` parameter defines the size of the render area, similar to the render area in a render pass. The `layerCount` parameter specifies the number of layers to render to, which is 1 for a non-layered image. The `colorAttachmentCount` and `pColorAttachments` parameters specify the color attachments to render to. Now we can begin rendering: -[,c++] +[source,multilang,c++,c] +.Starting dynamic rendering ---- +// START c++ commandBuffer.beginRendering(renderingInfo); +// END c++ + +// START c +vkCmdBeginRendering(commandBuffer, &renderingInfo); +// END c ---- All the functions that record commands return `void`, so there will be no error handling until we've finished recording. @@ -269,9 +402,16 @@ The parameter for the `beginRendering` command is the rendering info we just set We can now bind the graphics pipeline: -[,c++] +[source,multilang,c++,c] +.Binding the graphics pipeline ---- +// START c++ commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); +// END c++ + +// START c +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +// END c ---- The first parameter specifies if the pipeline object is a graphics or compute pipeline. @@ -280,17 +420,43 @@ We've now told Vulkan which operations to execute in the graphics pipeline and w As noted in the link:../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state[fixed functions chapter], we did specify viewport and scissor state for this pipeline to be dynamic. So we need to set them in the command buffer before issuing our draw command: -[,c++] +[source,multilang,c++,c] +.Setting the dynamic viewport and scissor ---- +// START c++ commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); +// END c++ + +// START c +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = static_cast(swapChainExtent.width); +viewport.height = static_cast(swapChainExtent.height); +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +vkCmdSetScissor(commandBuffer, 0, 1, &scissor); +// END c ---- Now we are ready to issue the draw command for the triangle: -[,c++] +[source,multilang,c++,c] +.The draw call ---- +// START c++ commandBuffer.draw(3, 1, 0, 0); +// END c++ + +// START c +vkCmdDraw(commandBuffer, 3, 1, 0, 0); +// END c ---- The actual `vk::raii::CommandBuffer::draw` function is a bit anticlimactic, but it's so simple because of all the information we specified in advance. @@ -305,9 +471,16 @@ It has the following parameters: The rendering can now be ended: -[,c++] +[source,multilang,c++,c] +.Ending dynamic rendering ---- +// START c++ commandBuffer.endRendering(); +// END c++ + +// START c +vkCmdEndRendering(commandBuffer); +// END c ---- After rendering, we need to transition the image layout back to `vk::ImageLayout::ePresentSrcKHR` so it can be presented to the screen: @@ -328,9 +501,18 @@ transition_image_layout( And we've finished recording the command buffer: -[,c++] +[source,multilang,c++,c] +.Ending command buffer recording ---- +// START c++ commandBuffer.end(); +// END c++ + +// START c +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +// END c ---- In the xref:./02_Rendering_and_presentation.adoc[next chapter] we'll write the code for the main loop, which will acquire an image from the swap chain, record and execute a command buffer, then return the finished image to the swap chain. diff --git a/en/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc b/en/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc index bc13f7f8b..a90eed0cb 100644 --- a/en/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc +++ b/en/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc @@ -160,11 +160,20 @@ frame is rendered at a time. Create three class members to store these semaphore objects and fence object: -[,c++] +[source,multilang,c++,c] +.Synchronization object class members ---- +// START c++ vk::raii::Semaphore presentCompleteSemaphore = nullptr; vk::raii::Semaphore renderFinishedSemaphore = nullptr; vk::raii::Fence drawFence = nullptr; +// END c++ + +// START c +VkSemaphore presentCompleteSemaphore = VK_NULL_HANDLE; +VkSemaphore renderFinishedSemaphore = VK_NULL_HANDLE; +VkFence drawFence = VK_NULL_HANDLE; +// END c ---- To create the semaphores, we'll add the last `create` function for this part of the tutorial: `createSyncObjects`: @@ -195,14 +204,35 @@ void createSyncObjects() Creating semaphores requires filling in the `vk::SemaphoreCreateInfo`, but in the current version of the API it doesn't actually have any fields relevant to the tutorial: -[,c++] +[source,multilang,c++,c] +.createSyncObjects ---- +// START c++ void createSyncObjects() { presentCompleteSemaphore = vk::raii::Semaphore(device, vk::SemaphoreCreateInfo()); renderFinishedSemaphore = vk::raii::Semaphore(device, vk::SemaphoreCreateInfo()); drawFence = vk::raii::Fence(device, {.flags = vk::FenceCreateFlagBits::eSignaled}); } +// END c++ + +// START c +void createSyncObjects() +{ + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &presentCompleteSemaphore) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &drawFence) != VK_SUCCESS) { + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } +} +// END c ---- Future versions of the Vulkan API or extensions may add functionality for the `flags` and `pNext` members of `vk::SemaphoreCreateInfo` like it does for the other structures. @@ -214,8 +244,10 @@ Onto the main drawing function! At the start of the frame, we want to wait until the previous frame has finished, so that the command buffer and semaphores are available to use. To do that, we call `vk::raii::Device::waitForFences`: -[,c++] +[source,multilang,c++,c] +.Waiting for the previous frame ---- +// START c++ void drawFrame() { auto fenceResult = device.waitForFences(*drawFence, vk::True, UINT64_MAX); @@ -225,6 +257,18 @@ void drawFrame() } device.resetFences(*drawFence); } +// END c++ + +// START c +void drawFrame() +{ + if (vkWaitForFences(device, 1, &drawFence, VK_TRUE, UINT64_MAX) != VK_SUCCESS) + { + throw std::runtime_error("failed to wait for fence!"); + } + vkResetFences(device, 1, &drawFence); +} +// END c ---- The `vk::raii::Device::waitForFences` function takes an array of fences and waits on the host for either any or all of the fences to be signaled before returning. @@ -236,13 +280,25 @@ We need to make sure that the fence is reset if the previous frame has already h Next, let's grab an image from the framebuffer after the previous frame has finished: -[,c++] +[source,multilang,c++,c] +.Acquiring a swap chain image ---- +// START c++ void drawFrame() { ... auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphore, nullptr); } +// END c++ + +// START c +void drawFrame() +{ + ... + uint32_t imageIndex; + VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, presentCompleteSemaphore, VK_NULL_HANDLE, &imageIndex); +} +// END c ---- The first parameter specifies a timeout in nanoseconds for an image to become available. @@ -273,8 +329,10 @@ With a fully recorded command buffer, we can now submit it. Queue submission and synchronization is configured through parameters in the `vk::SubmitInfo` structure. -[,c++] +[source,multilang,c++,c] +.SubmitInfo ---- +// START c++ vk::PipelineStageFlags waitDestinationStageMask( vk::PipelineStageFlagBits::eColorAttachmentOutput ); const vk::SubmitInfo submitInfo{.waitSemaphoreCount = 1, .pWaitSemaphores = &*presentCompleteSemaphore, @@ -283,6 +341,21 @@ const vk::SubmitInfo submitInfo{.waitSemaphoreCount = 1, .pCommandBuffers = &*commandBuffer, .signalSemaphoreCount = 1, .pSignalSemaphores = &*renderFinishedSemaphore}; +// END c++ + +// START c +VkPipelineStageFlags waitDestinationStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +submitInfo.waitSemaphoreCount = 1; +submitInfo.pWaitSemaphores = &presentCompleteSemaphore; +submitInfo.pWaitDstStageMask = &waitDestinationStageMask; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = &renderFinishedSemaphore; +// END c ---- The first three parameters specify which semaphores to wait on before execution begins and in which stage(s) of the pipeline to wait. @@ -296,9 +369,18 @@ execution. We simply submit the single command buffer we have. The `pSignalSemaphores` parameter specifies which semaphores to signal once the command buffer(s) have finished execution. In our case we're using the `renderFinishedSemaphore` for that purpose. -[,c++] +[source,multilang,c++,c] +.Submitting the command buffer ---- +// START c++ queue.submit(submitInfo, *drawFence); +// END c++ + +// START c +if (vkQueueSubmit(queue, 1, &submitInfo, drawFence) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +// END c ---- We can now submit the command buffer to the graphics queue using `vk::raii::Queue::submit`. @@ -369,14 +451,27 @@ link:/attachments/15_hello_triangle.cpp[demo code.] The last step of drawing a frame is submitting the result back to the swap chain to have it eventually show up on the screen. Presentation is configured through a `vk::PresentInfoKHR` structure at the end of the `drawFrame` function. -[,c++] +[source,multilang,c++,c] +.PresentInfoKHR ---- +// START c++ const vk::PresentInfoKHR presentInfoKHR{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphore, .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex}; +// END c++ + +// START c +VkPresentInfoKHR presentInfoKHR{}; +presentInfoKHR.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; +presentInfoKHR.waitSemaphoreCount = 1; +presentInfoKHR.pWaitSemaphores = &renderFinishedSemaphore; +presentInfoKHR.swapchainCount = 1; +presentInfoKHR.pSwapchains = &swapChain; +presentInfoKHR.pImageIndices = &imageIndex; +// END c ---- The first two parameters specify which semaphores to wait on before presentation can happen, just like `vk::SubmitInfo`. @@ -394,9 +489,16 @@ There is one last optional parameter called `pResults`. It allows you to specify an array of `vk::Result` values to check for every swap chain if presentation was successful. It's not necessary if you're only using a single swap chain, because you can use the return value of the present function. -[,c++] +[source,multilang,c++,c] +.Presenting the image ---- +// START c++ result = queue.presentKHR(presentInfoKHR); +// END c++ + +// START c +result = vkQueuePresentKHR(queue, &presentInfoKHR); +// END c ---- The `vk::raii::Queue::presentKHR` function submits the request to present an image to the swap chain. @@ -423,8 +525,10 @@ Cleaning up resources while that is happening is a bad idea. To fix that problem, we should wait for the logical device to finish operations before exiting `mainLoop` and destroying the window: -[,c++] +[source,multilang,c++,c] +.Waiting for the device to become idle before exiting ---- +// START c++ void mainLoop() { while (!glfwWindowShouldClose(window)) { glfwPollEvents(); @@ -433,6 +537,18 @@ void mainLoop() { device.waitIdle(); } +// END c++ + +// START c +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + vkDeviceWaitIdle(device); +} +// END c ---- You can also wait for operations in a specific command queue to be finished with `vk::raii::Queue::waitIdle`. diff --git a/en/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc b/en/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc index 6d68ce01d..a508eb470 100644 --- a/en/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc +++ b/en/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc @@ -30,8 +30,10 @@ But giving the application control over the number of frames in flight is anothe Each frame should have its own command buffer, set of semaphores, and fence. Rename and then change them to be ``std::vector``s of the objects: -[,c++] +[source,multilang,c++,c] +.Per-frame synchronization object vectors ---- +// START c++ std::vector commandBuffers; ... @@ -39,25 +41,57 @@ std::vector commandBuffers; std::vector presentCompleteSemaphores; std::vector renderFinishedSemaphores; std::vector inFlightFences; +// END c++ + +// START c +std::vector commandBuffers; + +... + +std::vector presentCompleteSemaphores; +std::vector renderFinishedSemaphores; +std::vector inFlightFences; +// END c ---- Then we need to create multiple command buffers. Rename `createCommandBuffer` to `createCommandBuffers`. Next we need to resize the command buffers vector to the size of `MAX_FRAMES_IN_FLIGHT`, alter the `vk::CommandBufferAllocateInfo` to contain that many command buffers, and then change the destination to our vector of command buffers: -[,c++] +[source,multilang,c++,c] +.createCommandBuffers ---- +// START c++ void createCommandBuffers() { vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = MAX_FRAMES_IN_FLIGHT}; commandBuffers = vk::raii::CommandBuffers( device, allocInfo ); } +// END c++ + +// START c +void createCommandBuffers() +{ + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = MAX_FRAMES_IN_FLIGHT; + + commandBuffers.resize(MAX_FRAMES_IN_FLIGHT); + if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); + } +} +// END c ---- The `createSyncObjects` function should be changed to create all the objects: -[,c++] +[source,multilang,c++,c] +.createSyncObjects (per frame) ---- +// START c++ void createSyncObjects() { assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty()); @@ -73,6 +107,39 @@ void createSyncObjects() inFlightFences.emplace_back(device, vk::FenceCreateInfo{.flags = vk::FenceCreateFlagBits::eSignaled}); } } +// END c++ + +// START c +void createSyncObjects() +{ + assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty()); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + renderFinishedSemaphores.resize(swapChainImages.size()); + for (size_t i = 0; i < swapChainImages.size(); i++) + { + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } + } + + presentCompleteSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &presentCompleteSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } + } +} +// END c ---- To use the right objects every frame, we need to keep track of the current frame. @@ -85,14 +152,16 @@ uint32_t frameIndex = 0; The `drawFrame` function can now be modified to use the right objects: -[,c++] +[source,multilang,c++,c] +.drawFrame using per-frame indices ---- +// START c++ void drawFrame() { auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX); if (fenceResult != vk::Result::eSuccess) { throw std::runtime_error("failed to wait for fence!"); - } + } device.resetFences(*inFlightFences[frameIndex]); auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr); @@ -110,6 +179,38 @@ void drawFrame() { .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex]}; queue.submit(submitInfo, *inFlightFences[frameIndex]); } +// END c++ + +// START c +void drawFrame() { + if (vkWaitForFences(device, 1, &inFlightFences[frameIndex], VK_TRUE, UINT64_MAX) != VK_SUCCESS) + { + throw std::runtime_error("failed to wait for fence!"); + } + vkResetFences(device, 1, &inFlightFences[frameIndex]); + + uint32_t imageIndex; + VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, presentCompleteSemaphores[frameIndex], VK_NULL_HANDLE, &imageIndex); + + vkResetCommandBuffer(commandBuffers[frameIndex], 0); + recordCommandBuffer(imageIndex); + + VkPipelineStageFlags waitDestinationStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = &presentCompleteSemaphores[frameIndex]; + submitInfo.pWaitDstStageMask = &waitDestinationStageMask; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffers[frameIndex]; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &renderFinishedSemaphores[imageIndex]; + + if (vkQueueSubmit(queue, 1, &submitInfo, inFlightFences[frameIndex]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); + } +} +// END c ---- Of course, we shouldn't forget to advance to the next frame every time: diff --git a/en/03_Drawing_a_triangle/04_Swap_chain_recreation.adoc b/en/03_Drawing_a_triangle/04_Swap_chain_recreation.adoc index 3a942cdc4..1a315b481 100644 --- a/en/03_Drawing_a_triangle/04_Swap_chain_recreation.adoc +++ b/en/03_Drawing_a_triangle/04_Swap_chain_recreation.adoc @@ -55,8 +55,10 @@ This may require the application to recreate the renderpass to make sure the cha We'll move the cleanup code of all objects that are recreated as part of a swap chain refresh from `cleanup` to `cleanupSwapChain`: -[,c++] +[source,multilang,c++,c] +.cleanupSwapChain ---- +// START c++ void cleanupSwapChain() { swapChainImageViews.clear(); @@ -70,6 +72,26 @@ void cleanup() glfwDestroyWindow(window); glfwTerminate(); } +// END c++ + +// START c +void cleanupSwapChain() +{ + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + swapChainImageViews.clear(); + vkDestroySwapchainKHR(device, swapChain, nullptr); +} + +void cleanup() +{ + cleanupSwapChain(); + + glfwDestroyWindow(window); + glfwTerminate(); +} +// END c ---- Note that in `chooseSwapExtent` we already query the new window resolution to make sure that the swap chain images have the (new) right size, so there's no need to modify `chooseSwapExtent` (remember that we already had to use `glfwGetFramebufferSize` to get the resolution of the surface in pixels when creating the swap chain). @@ -129,8 +151,10 @@ else The `vk::raii::Queue::presentKHR` function returns the same values with the same meaning. In this case, we will also recreate the swap chain if it is suboptimal, because we want the best possible result. -[,c++] +[source,multilang,c++,c] +.Recreating the swap chain after presentation ---- +// START c++ const vk::PresentInfoKHR presentInfoKHR{.waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], .swapchainCount = 1, @@ -146,6 +170,26 @@ else // There are no other success codes than eSuccess; on any error code, presentKHR already threw an exception. assert(result == vk::Result::eSuccess); } +// END c++ + +// START c +VkPresentInfoKHR presentInfoKHR{}; +presentInfoKHR.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; +presentInfoKHR.waitSemaphoreCount = 1; +presentInfoKHR.pWaitSemaphores = &renderFinishedSemaphores[imageIndex]; +presentInfoKHR.swapchainCount = 1; +presentInfoKHR.pSwapchains = &swapChain; +presentInfoKHR.pImageIndices = &imageIndex; +result = vkQueuePresentKHR(queue, &presentInfoKHR); +if ((result == VK_SUBOPTIMAL_KHR) || (result == VK_ERROR_OUT_OF_DATE_KHR)) +{ + recreateSwapChain(); +} +else if (result != VK_SUCCESS) +{ + throw std::runtime_error("failed to present swap chain image!"); +} +// END c ---- == Fixing a deadlock @@ -162,8 +206,10 @@ Thus, if we return early, the fence is still signaled and `vk::raii::Device::wai The beginning of `drawFrame` should now look like this: -[,c++] +[source,multilang,c++,c] +.Deferring the fence reset until work is actually submitted ---- +// START c++ auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX); if (fenceResult != vk::Result::eSuccess) { @@ -185,6 +231,30 @@ if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) // Only reset the fence if we are submitting work device.resetFences(*inFlightFences[frameIndex]); +// END c++ + +// START c +if (vkWaitForFences(device, 1, &inFlightFences[frameIndex], VK_TRUE, UINT64_MAX) != VK_SUCCESS) +{ + throw std::runtime_error("failed to wait for fence!"); +} + +uint32_t imageIndex; +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, presentCompleteSemaphores[frameIndex], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) +{ + recreateSwapChain(); + return; +} +if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) +{ + throw std::runtime_error("failed to acquire swap chain image!"); +} + +// Only reset the fence if we are submitting work +vkResetFences(device, 1, &inFlightFences[frameIndex]); +// END c ---- == Handling resizes explicitly @@ -200,8 +270,10 @@ bool framebufferResized = false; The `drawFrame` function should then be modified to also check for this flag: -[,c++] +[source,multilang,c++,c] +.Also checking the resize flag ---- +// START c++ if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR || framebufferResized) { framebufferResized = false; @@ -211,6 +283,19 @@ else { ... } +// END c++ + +// START c +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) +{ + framebufferResized = false; + recreateSwapChain(); +} +else +{ + ... +} +// END c ---- It is important to do this after `vk::raii::Queue::presentKHR` to ensure that the semaphores are in a consistent state, otherwise a signaled semaphore may never be properly waited upon. @@ -265,8 +350,10 @@ There is another case where a swap chain may become out of date and that is a sp This case is special because it will result in a frame buffer size of `0`. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the `recreateSwapChain` function: -[,c++] +[source,multilang,c++,c] +.recreateSwapChain, handling minimization ---- +// START c++ void recreateSwapChain() { int width = 0, height = 0; glfwGetFramebufferSize(window, &width, &height); @@ -279,6 +366,22 @@ void recreateSwapChain() { ... } +// END c++ + +// START c +void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + ... +} +// END c ---- The initial call to `glfwGetFramebufferSize` handles the case where the size is already correct and `glfwWaitEvents` would have nothing to wait on. diff --git a/en/04_Vertex_buffers/00_Vertex_input_description.adoc b/en/04_Vertex_buffers/00_Vertex_input_description.adoc index 9f2989491..da8b26f16 100644 --- a/en/04_Vertex_buffers/00_Vertex_input_description.adoc +++ b/en/04_Vertex_buffers/00_Vertex_input_description.adoc @@ -87,8 +87,10 @@ There are two types of structures needed to convey this information. The first structure is `vk::VertexInputBindingDescription` and we'll add a member function to the `Vertex` struct to populate it with the right data. -[,c++] +[source,multilang,c++,c] +.getBindingDescription ---- +// START c++ struct Vertex { glm::vec2 pos; glm::vec3 color; @@ -98,6 +100,23 @@ struct Vertex { return {.binding = 0, .stride = sizeof(Vertex), .inputRate = vk::VertexInputRate::eVertex}; } }; +// END c++ + +// START c +struct Vertex { + float pos[2]; + float color[3]; +}; + +VkVertexInputBindingDescription getBindingDescription() +{ + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(struct Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + return bindingDescription; +} +// END c ---- A vertex binding describes at which rate to load data from memory throughout the vertices. @@ -117,8 +136,10 @@ We're not going to use instanced rendering, so we'll stick to per-vertex data. The second structure that describes how to handle vertex input is `vk::VertexInputAttributeDescription`. We're going to add another helper function to `Vertex` to fill in these structs. -[,c++] +[source,multilang,c++,c] +.getAttributeDescriptions ---- +// START c++ #include ... @@ -129,6 +150,22 @@ We're going to add another helper function to `Vertex` to fill in these structs. {.location = 1, .binding = 0, .format = vk::Format::eR32G32B32Sfloat, .offset = offsetof(Vertex, color)}}}; } } +// END c++ + +// START c +void getAttributeDescriptions(VkVertexInputAttributeDescription attributeDescriptions[2]) +{ + attributeDescriptions[0].location = 0; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(struct Vertex, pos); + + attributeDescriptions[1].location = 1; + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(struct Vertex, color); +} +// END c ---- As the function prototype indicates, there are going to be two of these structures. @@ -169,14 +206,30 @@ The color attribute is described in much the same way. We now need to set up the graphics pipeline to accept vertex data in this format by referencing the structures in `createGraphicsPipeline`. Find the `vertexInputInfo` struct and modify it to reference the two descriptions: -[,c++] +[source,multilang,c++,c] +.Filling in PipelineVertexInputStateCreateInfo ---- +// START c++ auto bindingDescription = Vertex::getBindingDescription(); auto attributeDescriptions = Vertex::getAttributeDescriptions(); vk::PipelineVertexInputStateCreateInfo vertexInputInfo{.vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &bindingDescription, .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), .pVertexAttributeDescriptions = attributeDescriptions.data()}; +// END c++ + +// START c +VkVertexInputBindingDescription bindingDescription = getBindingDescription(); +VkVertexInputAttributeDescription attributeDescriptions[2]; +getAttributeDescriptions(attributeDescriptions); + +VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; +vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; +vertexInputInfo.vertexBindingDescriptionCount = 1; +vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; +vertexInputInfo.vertexAttributeDescriptionCount = 2; +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions; +// END c ---- The pipeline is now ready to accept vertex data in the format of the `vertices` container and pass it on to our vertex shader. diff --git a/en/04_Vertex_buffers/02_Staging_buffer.adoc b/en/04_Vertex_buffers/02_Staging_buffer.adoc index c1c04d150..e21e1633b 100644 --- a/en/04_Vertex_buffers/02_Staging_buffer.adoc +++ b/en/04_Vertex_buffers/02_Staging_buffer.adoc @@ -32,8 +32,10 @@ It's a bit of work, but it'll teach you a lot about how resources are shared bet Because we're going to create multiple buffers in this chapter, it's a good idea to move buffer creation to a helper function. Create a new function `createBuffer` and move the code in `createVertexBuffer` (except mapping) to it. -[,c++] +[source,multilang,c++,c] +.createBuffer helper ---- +// START c++ std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties) { vk::BufferCreateInfo bufferInfo{.size = size, .usage = usage, .sharingMode = vk::SharingMode::eExclusive}; @@ -44,6 +46,36 @@ std::pair createBuffer(vk::DeviceSize buffer.bindMemory(*bufferMemory, 0); return {std::move(buffer), std::move(bufferMemory)}; } +// END c++ + +// START c +void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer* buffer, VkDeviceMemory* bufferMemory) +{ + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, buffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create buffer!"); + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(device, *buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, bufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate buffer memory!"); + } + + vkBindBufferMemory(device, *buffer, *bufferMemory, 0); +} +// END c ---- Make sure to add parameters for the buffer size, memory properties and usage so that we can use this function to create many different types of buffers. @@ -72,8 +104,10 @@ Run your program to make sure that the vertex buffer still works properly. We're now going to change `createVertexBuffer` to only use a host visible buffer as temporary buffer and use a device local one as actual vertex buffer. -[,c++] +[source,multilang,c++,c] +.createVertexBuffer using a staging buffer ---- +// START c++ void createVertexBuffer() { vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); @@ -90,6 +124,34 @@ void createVertexBuffer() copyBuffer(stagingBuffer, vertexBuffer, bufferSize); } +// END c++ + +// START c +void createVertexBuffer() +{ + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &stagingBuffer, &stagingBufferMemory); + + void* dataStaging; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &dataStaging); + memcpy(dataStaging, vertices.data(), bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &vertexBuffer, &vertexBufferMemory); + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +// END c ---- We're now using a new `stagingBuffer` with `stagingBufferMemory` for mapping and copying the vertex data. @@ -116,28 +178,66 @@ Therefore we must first allocate a temporary command buffer. You may wish to create a separate command pool for these kinds of short-lived buffers, because the implementation may be able to apply memory allocation optimizations. You should use the `vk::CommandPoolCreateFlagBits::eTransient` flag during command pool generation in that case. -[,c++] +[source,multilang,c++,c] +.Allocating a temporary command buffer for the copy ---- +// START c++ void copyBuffer(vk::raii::Buffer & srcBuffer, vk::raii::Buffer & dstBuffer, vk::DeviceSize size) { vk::CommandBufferAllocateInfo allocInfo{ .commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; vk::raii::CommandBuffer commandCopyBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); } +// END c++ + +// START c +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) +{ + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandCopyBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandCopyBuffer); +} +// END c ---- And immediately start recording the command buffer: -[,c++] +[source,multilang,c++,c] +.Beginning the one-time-submit command buffer ---- +// START c++ commandCopyBuffer.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); +// END c++ + +// START c +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; +vkBeginCommandBuffer(commandCopyBuffer, &beginInfo); +// END c ---- We're only going to use the command buffer once and wait with returning from the function until the copy operation has finished executing. It's good practice to tell the driver about our intent using `vk::CommandBufferUsageFlagBits::eOneTimeSubmit`. -[,c++] +[source,multilang,c++,c] +.Recording the copy command ---- +// START c++ commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy(0, 0, size)); +// END c++ + +// START c +VkBufferCopy copyRegion{}; +copyRegion.srcOffset = 0; +copyRegion.dstOffset = 0; +copyRegion.size = size; +vkCmdCopyBuffer(commandCopyBuffer, srcBuffer, dstBuffer, 1, ©Region); +// END c ---- Contents of buffers are transferred using the `vk:raii::CommandBuffer::copyBuffer` command. @@ -145,18 +245,38 @@ It takes the source and destination buffers as arguments, and an array of region The regions are defined in `vk::BufferCopy` structs and consist of a source buffer offset, destination buffer offset and size. It is not possible to specify `vk::WholeSize` here, unlike the `vk::raii::DeviceMemory::mapMemory` command. -[,c++] +[source,multilang,c++,c] +.Ending the copy command buffer ---- +// START c++ commandCopyBuffer.end(); +// END c++ + +// START c +vkEndCommandBuffer(commandCopyBuffer); +// END c ---- This command buffer only contains the copy command, so we can stop recording right after that. Now execute the command buffer to complete the transfer: -[,c++] +[source,multilang,c++,c] +.Submitting and waiting for the transfer to finish ---- +// START c++ queue.submit(vk::SubmitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer}, nullptr); queue.waitIdle(); +// END c++ + +// START c +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandCopyBuffer; + +vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE); +vkQueueWaitIdle(queue); +// END c ---- Unlike the draw commands, there are no events we need to wait on this time. diff --git a/en/04_Vertex_buffers/03_Index_buffer.adoc b/en/04_Vertex_buffers/03_Index_buffer.adoc index 760cbfd0a..a276d654d 100644 --- a/en/04_Vertex_buffers/03_Index_buffer.adoc +++ b/en/04_Vertex_buffers/03_Index_buffer.adoc @@ -51,18 +51,30 @@ We can stick to `uint16_t` for now because we're using less than 65535 unique ve Just like the vertex data, the indices need to be uploaded into a `vk::raii::Buffer` for the GPU to be able to access them. Define two new class members to hold the resources for the index buffer: -[,c++] +[source,multilang,c++,c] +.Index buffer class members ---- +// START c++ vk::raii::Buffer vertexBuffer = nullptr; vk::raii::DeviceMemory vertexBufferMemory = nullptr; vk::raii::Buffer indexBuffer = nullptr; vk::raii::DeviceMemory indexBufferMemory = nullptr; +// END c++ + +// START c +VkBuffer vertexBuffer = VK_NULL_HANDLE; +VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE; +VkBuffer indexBuffer = VK_NULL_HANDLE; +VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE; +// END c ---- The `createIndexBuffer` function that we'll add now is almost identical to `createVertexBuffer`: -[,c++] +[source,multilang,c++,c] +.createIndexBuffer ---- +// START c++ void initVulkan() { ... @@ -87,6 +99,42 @@ void createIndexBuffer() copyBuffer(stagingBuffer, indexBuffer, bufferSize); } +// END c++ + +// START c +void initVulkan() +{ + ... + createVertexBuffer(); + createIndexBuffer(); + ... +} + +void createIndexBuffer() +{ + VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &stagingBuffer, &stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, indices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &indexBuffer, &indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +// END c ---- There are only two notable differences. @@ -102,10 +150,20 @@ We first need to bind the index buffer, just like we did for the vertex buffer. The difference is that you can only have a single index buffer. It's unfortunately not possible to use different indices for each vertex attribute, so we do still have to completely duplicate vertex data even if just one attribute varies. -[,c++] +[source,multilang,c++,c] +.Binding the vertex and index buffers ---- +// START c++ commandBuffers[frameIndex].bindVertexBuffers(0, *vertexBuffer, {0}); commandBuffers[frameIndex].bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint16); +// END c++ + +// START c +VkBuffer vertexBuffers[] = {vertexBuffer}; +VkDeviceSize offsets[] = {0}; +vkCmdBindVertexBuffers(commandBuffers[frameIndex], 0, 1, vertexBuffers, offsets); +vkCmdBindIndexBuffer(commandBuffers[frameIndex], indexBuffer, 0, VK_INDEX_TYPE_UINT16); +// END c ---- An index buffer is bound with `vk::raii::CommandBuffer::bindIndexBuffer` which has the index buffer, a byte offset into it, and the type of index data as parameters. @@ -114,9 +172,16 @@ As mentioned before, the possible types are `vk::IndexType::eUint16` and `vk::In Just binding an index buffer doesn't change anything yet, we also need to change the drawing command to tell Vulkan to use the index buffer. Remove the `vk::raii::CommandBuffer::draw` line and replace it with `vk::raii::CommandBuffer::drawIndexed`: -[,c++] +[source,multilang,c++,c] +.The indexed draw call ---- +// START c++ commandBuffer.drawIndexed(static_cast(indices.size()), 1, 0, 0, 0); +// END c++ + +// START c +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +// END c ---- A call to this function is very similar to `vk::raii::CommandBuffer::draw`. diff --git a/en/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc b/en/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc index 7c43b218a..2d80b5c36 100644 --- a/en/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc +++ b/en/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc @@ -130,12 +130,25 @@ void createDescriptorSetLayout() Every binding needs to be described through a `vk::DescriptorSetLayoutBinding` struct. -[,c++] +[source,multilang,c++,c] +.DescriptorSetLayoutBinding ---- +// START c++ void createDescriptorSetLayout() { vk::DescriptorSetLayoutBinding uboLayoutBinding{ .binding = 0, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eVertex}; } +// END c++ + +// START c +void createDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding{}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; + uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; +} +// END c ---- The first two fields specify the `binding` used in the shader and the type of descriptor, which is a uniform buffer object. @@ -153,30 +166,69 @@ You can leave this to its default value. All the descriptor bindings are combined into a single `vk::raii::DescriptorSetLayout` object. Define a new class member above `pipelineLayout`: -[,c++] +[source,multilang,c++,c] +.Descriptor set layout class member ---- +// START c++ vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; vk::raii::PipelineLayout pipelineLayout = nullptr; +// END c++ + +// START c +VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; +VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; +// END c ---- We can then create it using `vk::raii::DescriptorSetLayout` constructor. This function accepts a simple `vk::DescriptorSetLayoutCreateInfo` with the array of bindings: -[,c++] +[source,multilang,c++,c] +.Creating the descriptor set layout ---- +// START c++ vk::DescriptorSetLayoutBinding uboLayoutBinding{ .binding = 0, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eVertex}; vk::DescriptorSetLayoutCreateInfo layoutInfo{.bindingCount = 1, .pBindings = &uboLayoutBinding}; descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); +// END c++ + +// START c +VkDescriptorSetLayoutBinding uboLayoutBinding{}; +uboLayoutBinding.binding = 0; +uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +uboLayoutBinding.descriptorCount = 1; +uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 1; +layoutInfo.pBindings = &uboLayoutBinding; + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor set layout!"); +} +// END c ---- We need to specify the descriptor set layout during pipeline creation to tell Vulkan which descriptors the shaders will be using. Descriptor set layouts are specified in the pipeline layout object. Modify the `vk::PipelineLayoutCreateInfo` to reference the layout object: -[,c++] +[source,multilang,c++,c] +.Referencing the descriptor set layout in the pipeline layout ---- +// START c++ vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, .pSetLayouts = &*descriptorSetLayout, .pushConstantRangeCount = 0 }; +// END c++ + +// START c +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; +pipelineLayoutInfo.pushConstantRangeCount = 0; +// END c ---- You may be wondering why it's possible to specify multiple descriptor set layouts here, because a single one already includes all of the bindings. @@ -193,20 +245,34 @@ Thus, we need to have as many uniform buffers as we have frames in flight, and w To that end, add new class members for `uniformBuffers`, and `uniformBuffersMemory`: -[,c++] +[source,multilang,c++,c] +.Uniform buffer class members ---- +// START c++ vk::raii::Buffer indexBuffer = nullptr; vk::raii::DeviceMemory indexBufferMemory = nullptr; std::vector uniformBuffers; std::vector uniformBuffersMemory; std::vector uniformBuffersMapped; +// END c++ + +// START c +VkBuffer indexBuffer = VK_NULL_HANDLE; +VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE; + +std::vector uniformBuffers; +std::vector uniformBuffersMemory; +std::vector uniformBuffersMapped; +// END c ---- Similarly, create a new function `createUniformBuffers` that is called after `createIndexBuffer` and allocates the buffers: -[,c++] +[source,multilang,c++,c] +.createUniformBuffers ---- +// START c++ void initVulkan() { ... @@ -230,6 +296,37 @@ void createUniformBuffers() uniformBuffersMapped.emplace_back( uniformBuffersMemory.back().mapMemory(0, bufferSize)); } } +// END c++ + +// START c +void initVulkan() +{ + ... + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + ... +} + +... + +void createUniformBuffers() +{ + uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + VkDeviceSize bufferSize = sizeof(UniformBufferObject); + createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &uniformBuffers[i], &uniformBuffersMemory[i]); + + vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]); + } +} +// END c ---- We map the buffer right after creation using `vk::raii::DeviceMemory::mapMemory` to get a pointer to which we can write the data later on. diff --git a/en/05_Uniform_buffers/01_Descriptor_pool_and_sets.adoc b/en/05_Uniform_buffers/01_Descriptor_pool_and_sets.adoc index f5fdb9ab6..509aa7210 100644 --- a/en/05_Uniform_buffers/01_Descriptor_pool_and_sets.adoc +++ b/en/05_Uniform_buffers/01_Descriptor_pool_and_sets.adoc @@ -32,17 +32,38 @@ void createDescriptorPool() We first need to describe which descriptor types our descriptor sets are going to contain and how many of them, using `vk::DescriptorPoolSize` structures. -[,c++] +[source,multilang,c++,c] +.DescriptorPoolSize ---- +// START c++ vk::DescriptorPoolSize poolSize{ .type = vk::DescriptorType::eUniformBuffer, .descriptorCount = MAX_FRAMES_IN_FLIGHT }; +// END c++ + +// START c +VkDescriptorPoolSize poolSize{}; +poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize.descriptorCount = MAX_FRAMES_IN_FLIGHT; +// END c ---- We will allocate one of these descriptors for every frame. This pool size structure is referenced by the main `vk::DescriptorPoolCreateInfo`: -[,c++] +[source,multilang,c++,c] +.DescriptorPoolCreateInfo ---- +// START c++ vk::DescriptorPoolCreateInfo poolInfo{ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, .maxSets = MAX_FRAMES_IN_FLIGHT, .poolSizeCount = 1, .pPoolSizes = &poolSize }; +// END c++ + +// START c +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; +poolInfo.maxSets = MAX_FRAMES_IN_FLIGHT; +poolInfo.poolSizeCount = 1; +poolInfo.pPoolSizes = &poolSize; +// END c ---- Aside from the maximum number of individual descriptors that are available, we also need to specify the maximum number of descriptor sets that may be allocated. @@ -50,13 +71,26 @@ Aside from the maximum number of individual descriptors that are available, we a The structure has an optional flag similar to command pools that determines if individual descriptor sets can be freed or not. As the `vk::raii::DescriptorSets` destroy the underlying `VkDescriptorSet` on destruction we need to set it to `vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet` to allow that. -[,c++] +[source,multilang,c++,c] +.Creating the descriptor pool ---- +// START c++ vk::raii::DescriptorPool descriptorPool = nullptr; ... descriptorPool = vk::raii::DescriptorPool(device, poolInfo); +// END c++ + +// START c +VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + +... + +if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor pool!"); +} +// END c ---- Add a new class member to store the handle of the descriptor pool and call the `vk::raii::DescriptorPool` constructor to create it. @@ -86,12 +120,24 @@ void createDescriptorSets() A descriptor set allocation is described with a `vk::DescriptorSetAllocateInfo` struct. You need to specify the descriptor pool to allocate from, the number of descriptor sets to allocate, and the descriptor set layout to base them on: -[,c++] +[source,multilang,c++,c] +.DescriptorSetAllocateInfo ---- +// START c++ std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); vk::DescriptorSetAllocateInfo allocInfo{.descriptorPool = descriptorPool, .descriptorSetCount = static_cast(layouts.size()), .pSetLayouts = layouts.data()}; +// END c++ + +// START c +std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); +VkDescriptorSetAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; +allocInfo.descriptorPool = descriptorPool; +allocInfo.descriptorSetCount = static_cast(layouts.size()); +allocInfo.pSetLayouts = layouts.data(); +// END c ---- In our case, we will create one descriptor set for each frame in flight, all with the same layout. @@ -99,14 +145,29 @@ Unfortunately, we do need all the copies of the layout because the next function Add a class member to hold the descriptor set handles and allocate them with `vk::raii::Device::allocateDescriptorSets`: -[,c++] +[source,multilang,c++,c] +.Allocating the descriptor sets ---- +// START c++ vk::raii::DescriptorPool descriptorPool = nullptr; std::vector descriptorSets; ... descriptorSets = device.allocateDescriptorSets(allocInfo); +// END c++ + +// START c +VkDescriptorPool descriptorPool = VK_NULL_HANDLE; +std::vector descriptorSets; + +... + +descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); +if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate descriptor sets!"); +} +// END c ---- The descriptor sets have been allocated now, but the descriptors within still need to be configured. @@ -133,8 +194,10 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the `vk::WholeSize` value for the range. The configuration of descriptors is updated using the `vk::raii::Device::updateDescriptorSets` function, which takes an array of `vk::WriteDescriptorSet` structs as parameter. -[,c++] +[source,multilang,c++,c] +.DescriptorBufferInfo and WriteDescriptorSet ---- +// START c++ vk::DescriptorBufferInfo bufferInfo{.buffer = uniformBuffers[i], .offset = 0, .range = sizeof(UniformBufferObject)}; vk::WriteDescriptorSet descriptorWrite{.dstSet = descriptorSets[i], .dstBinding = 0, @@ -142,6 +205,23 @@ vk::WriteDescriptorSet descriptorWrite{.dstSet = descriptorSets[i], .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}; +// END c++ + +// START c +VkDescriptorBufferInfo bufferInfo{}; +bufferInfo.buffer = uniformBuffers[i]; +bufferInfo.offset = 0; +bufferInfo.range = sizeof(UniformBufferObject); + +VkWriteDescriptorSet descriptorWrite{}; +descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrite.dstSet = descriptorSets[i]; +descriptorWrite.dstBinding = 0; +descriptorWrite.dstArrayElement = 0; +descriptorWrite.descriptorCount = 1; +descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrite.pBufferInfo = &bufferInfo; +// END c ---- The first two fields specify the descriptor set to update and the binding. @@ -159,9 +239,16 @@ The `pBufferInfo` field is used for descriptors that refer to buffer data. There used in this sample: `pImageInfo` is used for descriptors that refer to image data, and `pTexelBufferView` is used for descriptors that refer to buffer views. Our descriptor is based on buffers, so we're just using `pBufferInfo`. -[,c++] +[source,multilang,c++,c] +.Applying the descriptor write ---- +// START c++ device.updateDescriptorSets(descriptorWrite, {}); +// END c++ + +// START c +vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +// END c ---- The updates are applied using `vk::raii::Device::updateDescriptorSets`. @@ -173,10 +260,18 @@ The latter can be used to copy descriptors to each other, as its name implies. We now need to update the `recordCommandBuffer` function to actually bind the right descriptor set for each frame to the descriptors in the shader with `vk::raii::CommandBuffer::bindDescriptorSets`. This needs to be done before the `vk::raii::CommandBuffer::drawIndexed` call: -[,c++] +[source,multilang,c++,c] +.Binding the descriptor set before drawing ---- +// START c++ commandBuffers[frameIndex].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, *descriptorSets[frameIndex], nullptr); commandBuffer.drawIndexed(static_cast(indices.size()), 1, 0, 0, 0); +// END c++ + +// START c +vkCmdBindDescriptorSets(commandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[frameIndex], 0, nullptr); +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +// END c ---- Unlike vertex and index buffers, descriptor sets are not unique to graphics pipelines. @@ -192,8 +287,10 @@ The problem is that because of the Y-flip we did in the projection matrix, the v This causes backface culling to kick in and prevents any geometry from being drawn. Go to the `createGraphicsPipeline` function and modify the `frontFace` in `vk::PipelineRasterizationStateCreateInfo` to correct this: -[,c++] +[source,multilang,c++,c] +.Correcting frontFace for the Y-flip ---- +// START c++ vk::PipelineRasterizationStateCreateInfo rasterizer{.depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, .polygonMode = vk::PolygonMode::eFill, @@ -201,6 +298,19 @@ vk::PipelineRasterizationStateCreateInfo rasterizer{.depthClampEnable = v .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = vk::False, .lineWidth = 1.0f}; +// END c++ + +// START c +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +rasterizer.rasterizerDiscardEnable = VK_FALSE; +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.lineWidth = 1.0f; +// END c ---- Run your program again, and you should now see the following: diff --git a/en/06_Texture_mapping/00_Images.adoc b/en/06_Texture_mapping/00_Images.adoc index 8a535ada4..5508f79ae 100644 --- a/en/06_Texture_mapping/00_Images.adoc +++ b/en/06_Texture_mapping/00_Images.adoc @@ -143,10 +143,18 @@ Image objects will make it easier and faster to retrieve colors by allowing us t Pixels within an image object are known as texels, and we'll use that name from this point on. Add the following new class members: -[,c++] +[source,multilang,c++,c] +.Texture image class members ---- +// START c++ vk::raii::Image textureImage = nullptr; vk::raii::DeviceMemory textureImageMemory = nullptr; +// END c++ + +// START c +VkImage textureImage = VK_NULL_HANDLE; +VkDeviceMemory textureImageMemory = VK_NULL_HANDLE; +// END c ---- The parameters for an image are specified in a `vk::ImageCreateInfo` struct: @@ -229,8 +237,10 @@ Use the default `vk::raii::DeviceMemory` constructor, and use `bindMemory` on th This function is already getting quite large and there'll be a need to create more images in later chapters, so we should abstract image creation into a `createImage` function, like we did for buffers. Create the function and move the image object creation and memory allocation to it: -[,c++] +[source,multilang,c++,c] +.createImage helper ---- +// START c++ std::pair createImage( uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties ) { @@ -254,14 +264,56 @@ std::pair createImage( return {std::move(image), std::move(imageMemory)}; } +// END c++ + +// START c +void createImage( + uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, + VkImage* image, VkDeviceMemory* imageMemory) +{ + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = format; + imageInfo.extent.width = width; + imageInfo.extent.height = height; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = tiling; + imageInfo.usage = usage; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, image) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, *image, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, imageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); + } + + vkBindImageMemory(device, *image, *imageMemory, 0); +} +// END c ---- I've made the width, height, format, tiling mode, usage, and memory properties parameters, because these will all vary between the images we'll be creating throughout this tutorial. The `createTextureImage` function can now be simplified to: -[,c++] +[source,multilang,c++,c] +.createTextureImage ---- +// START c++ void createTextureImage() { int texWidth, texHeight, texChannels; @@ -289,6 +341,41 @@ void createTextureImage() vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal); } +// END c++ + +// START c +void createTextureImage() +{ + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &stagingBuffer, &stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); + vkUnmapMemory(device, stagingBufferMemory); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &textureImage, &textureImageMemory); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +// END c ---- == Layout transitions @@ -305,8 +392,10 @@ These transitions are performed using pipeline barriers, which not only change t The function we're going to write now involves recording and executing a command buffer again, so now's a good time to move that logic into a helper function or two: -[,c++] +[source,multilang,c++,c] +.beginSingleTimeCommands and endSingleTimeCommands ---- +// START c++ vk::raii::CommandBuffer beginSingleTimeCommands() { vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; @@ -326,19 +415,72 @@ void endSingleTimeCommands(vk::raii::CommandBuffer &&commandBuffer) queue.submit(submitInfo, nullptr); queue.waitIdle(); } +// END c++ + +// START c +VkCommandBuffer beginSingleTimeCommands() +{ + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + return commandBuffer; +} + +void endSingleTimeCommands(VkCommandBuffer commandBuffer) +{ + vkEndCommandBuffer(commandBuffer); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + + vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(queue); + + vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +} +// END c ---- The code for these functions is based on the existing code in `copyBuffer`. You can now simplify that function to: -[,c++] +[source,multilang,c++,c] +.Simplified copyBuffer ---- +// START c++ void copyBuffer(vk::raii::Buffer &srcBuffer, vk::raii::Buffer &dstBuffer, vk::DeviceSize size) { vk::raii::CommandBuffer commandCopyBuffer = beginSingleTimeCommands(); commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy{.size = size}); endSingleTimeCommands(std::move(commandCopyBuffer)); } +// END c++ + +// START c +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) +{ + VkCommandBuffer commandCopyBuffer = beginSingleTimeCommands(); + + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(commandCopyBuffer, srcBuffer, dstBuffer, 1, ©Region); + + endSingleTimeCommands(commandCopyBuffer); +} +// END c ---- If we were still using buffers, then we could now write a function to record and execute `copyBufferToImage` to finish the job, but this command requires the image to be in the right layout first. @@ -355,14 +497,32 @@ One of the most common ways to perform layout transitions is using an _image mem A pipeline barrier like that is generally used to synchronize access to resources, like ensuring that a write to a buffer completes before reading from it, but it can also be used to transition image layouts and transfer queue family ownership when `vk::SharingMode::eExclusive` is used. There is an equivalent _buffer memory barrier_ to do this for buffers. -[,c++] +[source,multilang,c++,c] +.ImageMemoryBarrier ---- +// START c++ vk::ImageMemoryBarrier barrier{.oldLayout = oldLayout, .newLayout = newLayout, .srcQueueFamilyIndex = vk::QueueFamilyIgnored, .dstQueueFamilyIndex = vk::QueueFamilyIgnored, .image = image, .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .levelCount = 1, .layerCount = 1}}; +// END c++ + +// START c +VkImageMemoryBarrier barrier{}; +barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +barrier.oldLayout = oldLayout; +barrier.newLayout = newLayout; +barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.image = image; +barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +barrier.subresourceRange.baseMipLevel = 0; +barrier.subresourceRange.levelCount = 1; +barrier.subresourceRange.baseArrayLayer = 0; +barrier.subresourceRange.layerCount = 1; +// END c ---- `oldLayout` and `newLayout` specify the the layout transition. @@ -410,14 +570,30 @@ void copyBufferToImage(vk::raii::CommandBuffer &commandBuffer, const vk::raii::B Just like with buffer copies, you need to specify which part of the buffer is going to be copied to which part of the image. This happens through `vk::BufferImageCopy` structs: -[,c++] +[source,multilang,c++,c] +.BufferImageCopy region ---- +// START c++ vk::BufferImageCopy region{.bufferOffset = 0, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = {.aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1}, .imageOffset = {0, 0, 0}, .imageExtent = {width, height, 1}}; +// END c++ + +// START c +VkBufferImageCopy region{}; +region.bufferOffset = 0; +region.bufferRowLength = 0; +region.bufferImageHeight = 0; +region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +region.imageSubresource.mipLevel = 0; +region.imageSubresource.baseArrayLayer = 0; +region.imageSubresource.layerCount = 1; +region.imageOffset = {0, 0, 0}; +region.imageExtent = {width, height, 1}; +// END c ---- Most of these fields are self-explanatory. @@ -429,9 +605,16 @@ The `imageSubresource`, `imageOffset` and `imageExtent` fields indicate to which Buffer to image copy operations are enqueued using the `vk::raii::CommandBuffer::copyBufferToImage` function: -[,c++] +[source,multilang,c++,c] +.Recording the buffer-to-image copy ---- +// START c++ commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, region); +// END c++ + +// START c +vkCmdCopyBufferToImage(commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); +// END c ---- The third parameter indicates which layout the image is currently using. @@ -481,8 +664,10 @@ There are two transitions we need to handle: These rules are specified using the following access masks and pipeline stages: -[,c++] +[source,multilang,c++,c] +.Determining access masks and pipeline stages for the transition ---- +// START c++ vk::PipelineStageFlags sourceStage; vk::PipelineStageFlags destinationStage; @@ -507,6 +692,34 @@ else throw std::invalid_argument("unsupported layout transition!"); } commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, nullptr, barrier); +// END c++ + +// START c +VkPipelineStageFlags sourceStage; +VkPipelineStageFlags destinationStage; + +if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) +{ + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; +} +else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) +{ + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; +} +else +{ + throw std::invalid_argument("unsupported layout transition!"); +} +vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +// END c ---- As you can see in the aforementioned table, transfer writes must occur in the pipeline transfer stage. diff --git a/en/06_Texture_mapping/01_Image_view_and_sampler.adoc b/en/06_Texture_mapping/01_Image_view_and_sampler.adoc index 26795ef70..030cf0f47 100644 --- a/en/06_Texture_mapping/01_Image_view_and_sampler.adoc +++ b/en/06_Texture_mapping/01_Image_view_and_sampler.adoc @@ -56,8 +56,10 @@ textureImageView = vk::raii::ImageView(device, viewInfo); Because so much of the logic is duplicated from `createImageViews`, you may wish to abstract it into a new `createImageView` function: -[,c++] +[source,multilang,c++,c] +.createImageView helper ---- +// START c++ vk::raii::ImageView createImageView(vk::Image const &image, vk::Format format) { vk::ImageViewCreateInfo viewInfo{ @@ -67,22 +69,57 @@ vk::raii::ImageView createImageView(vk::Image const &image, vk::Format format) .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}}; return vk::raii::ImageView(device, viewInfo); } +// END c++ + +// START c +VkImageView createImageView(VkImage image, VkFormat format) +{ + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + VkImageView imageView; + if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture image view!"); + } + return imageView; +} +// END c ---- The `createTextureImageView` function can now be simplified to: -[,c++] +[source,multilang,c++,c] +.Simplified createTextureImageView ---- +// START c++ void createTextureImageView() { textureImageView = createImageView(*textureImage, vk::Format::eR8G8B8A8Srgb); } +// END c++ + +// START c +void createTextureImageView() +{ + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +} +// END c ---- And `createImageViews` can be simplified to: -[,c++] +[source,multilang,c++,c] +.Simplified createImageViews ---- +// START c++ void createImageViews() { assert(swapChainImageViews.empty()); @@ -93,6 +130,20 @@ void createImageViews() swapChainImageViews.emplace_back(createImageView(image, swapChainSurfaceFormat.format)); } } +// END c++ + +// START c +void createImageViews() +{ + assert(swapChainImageViews.empty()); + + swapChainImageViews.reserve(swapChainImages.size()); + for (auto &image : swapChainImages) + { + swapChainImageViews.push_back(createImageView(image, swapChainSurfaceFormat.format)); + } +} +// END c ---- == Samplers @@ -210,8 +261,10 @@ This struct in turn has a member called `maxSamplerAnisotropy` and this is the m If we want to go for maximum quality, we can simply use that value directly. You can either query the properties at the beginning of your program and pass them around to the functions that need them, or query them in the `createTextureSampler` function itself. -[,c++] +[source,multilang,c++,c] +.The complete SamplerCreateInfo ---- +// START c++ vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear, @@ -223,20 +276,53 @@ vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear .maxAnisotropy = properties.limits.maxSamplerAnisotropy, .compareEnable = vk::False, .compareOp = vk::CompareOp::eAlways}; ----- - -[,c++] ----- +// END c++ + +// START c +VkPhysicalDeviceProperties properties{}; +vkGetPhysicalDeviceProperties(physicalDevice, &properties); + +VkSamplerCreateInfo samplerInfo{}; +samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; +samplerInfo.magFilter = VK_FILTER_LINEAR; +samplerInfo.minFilter = VK_FILTER_LINEAR; +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.anisotropyEnable = VK_TRUE; +samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +samplerInfo.compareEnable = VK_FALSE; +samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; +// END c +---- + +[source,multilang,c++,c] +.Border color +---- +// START c++ samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack; +// END c++ + +// START c +samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +// END c ---- The `borderColor` field specifies which color is returned when sampling beyond the image with clamp to border addressing mode. It is possible to return black, white or transparent in either float or int formats. You cannot specify an arbitrary color. -[,c++] +[source,multilang,c++,c] +.Coordinate normalization ---- +// START c++ samplerInfo.unnormalizedCoordinates = vk::False; +// END c++ + +// START c +samplerInfo.unnormalizedCoordinates = VK_FALSE; +// END c ---- The `unnormalizedCoordinates` field specifies which coordinate system you want to use to address texels in an image. @@ -254,12 +340,22 @@ If a comparison function is enabled, then texels will first be compared to a val This is mainly used for https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html[percentage-closer filtering] on shadow maps. We'll look at this in a future chapter. -[,c++] +[source,multilang,c++,c] +.Mipmapping fields ---- +// START c++ samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; samplerInfo.mipLodBias = 0.0f; samplerInfo.minLod = 0.0f; samplerInfo.maxLod = 0.0f; +// END c++ + +// START c +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.mipLodBias = 0.0f; +samplerInfo.minLod = 0.0f; +samplerInfo.maxLod = 0.0f; +// END c ---- All of these fields apply to mipmapping. @@ -268,8 +364,10 @@ We will look at mipmapping in a link:/Generating_Mipmaps[later chapter], but bas The functioning of the sampler is now fully defined. Add a class member to hold the handle of the sampler object and create the sampler with `vkCreateSampler`: -[,c++] +[source,multilang,c++,c] +.Sampler class member and creation ---- +// START c++ vk::raii::ImageView textureImageView = nullptr; vk::raii::Sampler textureSampler = nullptr; @@ -281,6 +379,23 @@ void createTextureSampler() textureSampler = vk::raii::Sampler(device, samplerInfo); } +// END c++ + +// START c +VkImageView textureImageView = VK_NULL_HANDLE; +VkSampler textureSampler = VK_NULL_HANDLE; + +... + +void createTextureSampler() +{ + ... + + if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture sampler!"); + } +} +// END c ---- Note the sampler does not reference a `vk::Image` anywhere. @@ -297,19 +412,41 @@ image::/images/validation_layer_anisotropy.png[] That's because anisotropic filtering is actually an optional device feature. We need to update the `createLogicalDevice` function to request it: -[,c++] +[source,multilang,c++,c] +.Requesting the anisotropic filtering feature ---- +// START c++ vk::StructureChain featureChain = { {.features = {.samplerAnisotropy = true } }, // vk::PhysicalDeviceFeatures2 {.synchronization2 = true, .dynamicRendering = true }, // vk::PhysicalDeviceVulkan13Features {.extendedDynamicState = true } // vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT }; +// END c++ + +// START c +VkPhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures{}; +extendedDynamicStateFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT; +extendedDynamicStateFeatures.extendedDynamicState = VK_TRUE; + +VkPhysicalDeviceVulkan13Features vulkan13Features{}; +vulkan13Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; +vulkan13Features.pNext = &extendedDynamicStateFeatures; +vulkan13Features.synchronization2 = VK_TRUE; +vulkan13Features.dynamicRendering = VK_TRUE; + +VkPhysicalDeviceFeatures2 featureChain{}; +featureChain.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; +featureChain.pNext = &vulkan13Features; +featureChain.features.samplerAnisotropy = VK_TRUE; +// END c ---- And even though it is very unlikely that a modern graphics card will not support it, we should update `isDeviceSuitable` to check if it is available: -[,c++] +[source,multilang,c++,c] +.Checking for the anisotropy feature in isDeviceSuitable ---- +// START c++ bool isDeviceSuitable(VkPhysicalDevice device) { ... @@ -325,16 +462,53 @@ bool isDeviceSuitable(VkPhysicalDevice device) // Return true if the physicalDevice meets all the criteria return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures; } +// END c++ + +// START c +bool isDeviceSuitable(VkPhysicalDevice device) +{ + ... + + VkPhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures{}; + extendedDynamicStateFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT; + + VkPhysicalDeviceVulkan13Features vulkan13Features{}; + vulkan13Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + vulkan13Features.pNext = &extendedDynamicStateFeatures; + + VkPhysicalDeviceFeatures2 features{}; + features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features.pNext = &vulkan13Features; + + vkGetPhysicalDeviceFeatures2(physicalDevice, &features); + + bool supportsRequiredFeatures = features.features.samplerAnisotropy && + vulkan13Features.dynamicRendering && + vulkan13Features.synchronization2 && + extendedDynamicStateFeatures.extendedDynamicState; + + // Return true if the physicalDevice meets all the criteria + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures; +} +// END c ---- The `vk::raii::PhysicalDevice::getFeatures2` repurposes the `vk::PhysicalDeviceFeatures` struct to indicate which features are supported rather than requested by setting the boolean values. Instead of enforcing the availability of anisotropic filtering, it's also possible to simply not use it by conditional setting: -[,c++] +[source,multilang,c++,c] +.Disabling anisotropic filtering ---- +// START c++ samplerInfo.anisotropyEnable = vk::False; samplerInfo.maxAnisotropy = 1.0f; +// END c++ + +// START c +samplerInfo.anisotropyEnable = VK_FALSE; +samplerInfo.maxAnisotropy = 1.0f; +// END c ---- In the xref:./02_Combined_image_sampler.adoc[next chapter] we will expose the image and sampler objects to the shaders to draw the texture onto the square. diff --git a/en/06_Texture_mapping/02_Combined_image_sampler.adoc b/en/06_Texture_mapping/02_Combined_image_sampler.adoc index f020df1c2..adc988d8b 100644 --- a/en/06_Texture_mapping/02_Combined_image_sampler.adoc +++ b/en/06_Texture_mapping/02_Combined_image_sampler.adoc @@ -18,13 +18,34 @@ After that, we're going to add texture coordinates to `Vertex`, replacing the co Browse to the `createDescriptorSetLayout` function and add a `VkDescriptorSetLayoutBinding` for a combined image sampler descriptor. We'll simply put it in the binding after the uniform buffer: -[,c++] +[source,multilang,c++,c] +.Adding the combined image sampler binding ---- +// START c++ std::array bindings{ {{.binding = 0, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eVertex}, {.binding = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment}}}; vk::DescriptorSetLayoutCreateInfo layoutInfo{.bindingCount = static_cast(bindings.size()), .pBindings = bindings.data()}; +// END c++ + +// START c +VkDescriptorSetLayoutBinding bindings[2]{}; +bindings[0].binding = 0; +bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +bindings[0].descriptorCount = 1; +bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + +bindings[1].binding = 1; +bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +bindings[1].descriptorCount = 1; +bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 2; +layoutInfo.pBindings = bindings; +// END c ---- Make sure to set the `stageFlags` to indicate that we intend to use the combined image sampler descriptor in the fragment shader. @@ -34,14 +55,32 @@ It is possible to use texture sampling in the vertex shader, for example to dyna We must also create a larger descriptor pool to make room for the allocation of the combined image sampler by adding another `vk::PoolSize` of type `vk::DescriptorType::eCombinedImageSampler` to the `vk::DescriptorPoolCreateInfo`. Go to the `createDescriptorPool` function and modify it to include a `vk::DescriptorPoolSize` for this descriptor: -[,c++] +[source,multilang,c++,c] +.Enlarging the descriptor pool ---- +// START c++ std::array poolSize{{{.type = vk::DescriptorType::eUniformBuffer, .descriptorCount = MAX_FRAMES_IN_FLIGHT}, {.type = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = MAX_FRAMES_IN_FLIGHT}}}; vk::DescriptorPoolCreateInfo poolInfo{.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, .maxSets = MAX_FRAMES_IN_FLIGHT, .poolSizeCount = static_cast(poolSize.size()), .pPoolSizes = poolSize.data()}; +// END c++ + +// START c +VkDescriptorPoolSize poolSize[2]{}; +poolSize[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize[0].descriptorCount = MAX_FRAMES_IN_FLIGHT; +poolSize[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +poolSize[1].descriptorCount = MAX_FRAMES_IN_FLIGHT; + +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; +poolInfo.maxSets = MAX_FRAMES_IN_FLIGHT; +poolInfo.poolSizeCount = 2; +poolInfo.pPoolSizes = poolSize; +// END c ---- Inadequate descriptor pools are a good example of a problem that the validation layers will not catch: As of Vulkan 1.1, `vk::raii::Device::allocateDescriptorSets` may fail and throw a `vk::OutOfPoolMemoryError` exception if the pool is not sufficiently large, but the driver may also try to solve the problem internally. @@ -55,8 +94,10 @@ However, it remains best practice to do so, and in the future, `VK_LAYER_KHRONOS The final step is to bind the actual image and sampler resources to the descriptors in the descriptor set. Go to the `createDescriptorSets` function. -[,c++] +[source,multilang,c++,c] +.DescriptorImageInfo for the combined image sampler ---- +// START c++ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { vk::DescriptorBufferInfo bufferInfo{.buffer = uniformBuffers[i], .offset = 0, .range = sizeof(UniformBufferObject)}; @@ -64,13 +105,33 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) ... } +// END c++ + +// START c +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) +{ + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); + + VkDescriptorImageInfo imageInfo{}; + imageInfo.sampler = textureSampler; + imageInfo.imageView = textureImageView; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + ... +} +// END c ---- The resources for a combined image sampler structure must be specified in a `vk::DescriptorImageInfo` struct, just like the buffer resource for a uniform buffer descriptor is specified in a `vk::DescriptorBufferInfo` struct. This is where the objects from the previous chapter come together. -[,c++] +[source,multilang,c++,c] +.Writing both descriptors ---- +// START c++ std::array descriptorWrites{{{.dstSet = descriptorSets[i], .dstBinding = 0, .dstArrayElement = 0, @@ -84,6 +145,29 @@ std::array descriptorWrites{{{.dstSet = desc .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo}}}; device.updateDescriptorSets(descriptorWrites, {}); +// END c++ + +// START c +VkWriteDescriptorSet descriptorWrites[2]{}; + +descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[0].dstSet = descriptorSets[i]; +descriptorWrites[0].dstBinding = 0; +descriptorWrites[0].dstArrayElement = 0; +descriptorWrites[0].descriptorCount = 1; +descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrites[0].pBufferInfo = &bufferInfo; + +descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[1].dstSet = descriptorSets[i]; +descriptorWrites[1].dstBinding = 1; +descriptorWrites[1].dstArrayElement = 0; +descriptorWrites[1].descriptorCount = 1; +descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +descriptorWrites[1].pImageInfo = &imageInfo; + +vkUpdateDescriptorSets(device, 2, descriptorWrites, 0, nullptr); +// END c ---- The descriptors must be updated with this image info, just like the buffer. @@ -95,8 +179,10 @@ The descriptors are now ready to be used by the shaders! There is one important ingredient for texture mapping that is still missing, and that's the actual texture coordinates for each vertex, often called "uv coordinates". The texture coordinates determine how the image is actually mapped to the geometry. -[,c++] +[source,multilang,c++,c] +.Vertex struct with texture coordinates ---- +// START c++ struct Vertex { glm::vec2 pos; @@ -115,6 +201,43 @@ struct Vertex {.location = 2, .binding = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(Vertex, texCoord)}}}; } }; +// END c++ + +// START c +struct Vertex +{ + float pos[2]; + float color[3]; + float texCoord[2]; +}; + +VkVertexInputBindingDescription getBindingDescription() +{ + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(struct Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + return bindingDescription; +} + +void getAttributeDescriptions(VkVertexInputAttributeDescription attributeDescriptions[3]) +{ + attributeDescriptions[0].location = 0; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(struct Vertex, pos); + + attributeDescriptions[1].location = 1; + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(struct Vertex, color); + + attributeDescriptions[2].location = 2; + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[2].offset = offsetof(struct Vertex, texCoord); +} +// END c ---- Modify the `Vertex` struct to include a `vec2` for texture coordinates. diff --git a/en/08_Loading_models.adoc b/en/08_Loading_models.adoc index bd4da7dd1..819a9f883 100644 --- a/en/08_Loading_models.adoc +++ b/en/08_Loading_models.adoc @@ -55,22 +55,41 @@ stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texCha We're going to load the vertices and indices from the model file now, so you should remove the global `vertices` and `indices` arrays now. Replace them with non-const containers as class members: -[,c++] +[source,multilang,c++,c] +.Vertex and index storage as class members ---- +// START c++ std::vector vertices; std::vector indices; vk::raii::Buffer vertexBuffer = nullptr; vk::raii::DeviceMemory vertexBufferMemory = nullptr; vk::raii::Buffer indexBuffer = nullptr; vk::raii::DeviceMemory indexBufferMemory = nullptr; +// END c++ + +// START c +std::vector vertices; +std::vector indices; +VkBuffer vertexBuffer = VK_NULL_HANDLE; +VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE; +VkBuffer indexBuffer = VK_NULL_HANDLE; +VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE; +// END c ---- You should change the type of the indices from `uint16_t` to `uint32_t`, because there are going to be a lot more vertices than 65535. The somewhat complex construction `vk::IndexTypeValue::value` automatically gets you the indices' type. -[,c++] +[source,multilang,c++,c] +.Binding the 32-bit index buffer ---- +// START c++ commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexTypeValue::value); +// END c++ + +// START c +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32); +// END c ---- The tinyobjloader library is included in the same way as STB libraries. diff --git a/en/09_Generating_Mipmaps.adoc b/en/09_Generating_Mipmaps.adoc index 60ef0a682..ac15dfe2c 100644 --- a/en/09_Generating_Mipmaps.adoc +++ b/en/09_Generating_Mipmaps.adoc @@ -26,12 +26,22 @@ Up until now, we have always set this value to one. We need to calculate the number of mip levels from the dimensions of the image. First, add a class member to store this number: -[,c++] +[source,multilang,c++,c] +.mipLevels class member ---- +// START c++ ... uint32_t mipLevels = 0; vk::raii::Image textureImage = nullptr; ... +// END c++ + +// START c +... +uint32_t mipLevels = 0; +VkImage textureImage = VK_NULL_HANDLE; +... +// END c ---- The value for `mipLevels` can be found once we've loaded the texture in `createTextureImage`: @@ -184,8 +194,10 @@ Each level will be transitioned to `vk::ImageLayout::eShaderReadOnlyOptimal` aft We're now going to write the function that generates the mipmaps: -[,c++] +[source,multilang,c++,c] +.generateMipmaps signature and the reusable barrier ---- +// START c++ void generateMipmaps(vk::raii::CommandBuffer &commandBuffer, vk::raii::Image &image, int32_t texWidth, @@ -201,6 +213,29 @@ void generateMipmaps(vk::raii::CommandBuffer &commandBuffer, .image = image, .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .levelCount = 1, .layerCount = 1}}; } +// END c++ + +// START c +void generateMipmaps(VkCommandBuffer commandBuffer, + VkImage image, + int32_t texWidth, + int32_t texHeight, + uint32_t mipLevels) +{ + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.layerCount = 1; +} +// END c ---- We're going to make several transitions, so we'll reuse this `vk::ImageMemoryBarrier`. @@ -220,8 +255,10 @@ for (uint32_t i = 1; i < mipLevels; i++) This loop will record each of the `vk::raii::CommandBuffer::blitImage` commands. Note that the loop variable starts at 1, not 0. -[,c++] +[source,multilang,c++,c] +.Transitioning level i-1 to transfer-src ---- +// START c++ barrier.subresourceRange.baseMipLevel = i - 1; barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal; @@ -229,18 +266,54 @@ barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead; commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, barrier); +// END c++ + +// START c +barrier.subresourceRange.baseMipLevel = i - 1; +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +// END c ---- First, we transition level `i - 1` to `vk::ImageLayout::eTransferSrcOptimal`. This transition will wait for level `i - 1` to be filled, either from the previous blit command, or from `vk::raii::CommandBuffer::copyBufferToImage`. The current blit command will wait on this transition. -[,c++] +[source,multilang,c++,c] +.ImageBlit region ---- +// START c++ vk::ImageBlit blit = {.srcSubresource = {.aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = i - 1, .layerCount = 1}, .srcOffsets = std::array({{}, {mipWidth, mipHeight, 1}}), .dstSubresource = {.aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = i, .layerCount = 1}, .dstOffsets = std::array({{}, {1 < mipWidth ? mipWidth / 2 : 1, 1 < mipHeight ? mipHeight / 2 : 1, 1}})}; +// END c++ + +// START c +VkImageBlit blit{}; +blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.srcSubresource.mipLevel = i - 1; +blit.srcSubresource.layerCount = 1; +blit.srcOffsets[0].x = 0; +blit.srcOffsets[0].y = 0; +blit.srcOffsets[0].z = 0; +blit.srcOffsets[1].x = mipWidth; +blit.srcOffsets[1].y = mipHeight; +blit.srcOffsets[1].z = 1; +blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.dstSubresource.mipLevel = i; +blit.dstSubresource.layerCount = 1; +blit.dstOffsets[0].x = 0; +blit.dstOffsets[0].y = 0; +blit.dstOffsets[0].z = 0; +blit.dstOffsets[1].x = 1 < mipWidth ? mipWidth / 2 : 1; +blit.dstOffsets[1].y = 1 < mipHeight ? mipHeight / 2 : 1; +blit.dstOffsets[1].z = 1; +// END c ---- Next, we specify the regions that will be used in the blit operation. @@ -250,9 +323,19 @@ The two elements of the `srcOffsets` array determine the 3D region that data wil The X and Y dimensions of the `dstOffsets[1]` are divided by two since each mip level is half the size of the previous level. The Z dimension of `srcOffsets[1]` and `dstOffsets[1]` must be 1, since a 2D image has a depth of 1. -[,c++] +[source,multilang,c++,c] +.Recording the blit command ---- +// START c++ commandBuffer.blitImage(image, vk::ImageLayout::eTransferSrcOptimal, image, vk::ImageLayout::eTransferDstOptimal, blit, vk::Filter::eLinear); +// END c++ + +// START c +vkCmdBlitImage(commandBuffer, + image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &blit, VK_FILTER_LINEAR); +// END c ---- Now, we record the blit command. @@ -266,14 +349,26 @@ The last parameter allows us to specify a `vk::Filter` to use in the blit. We have the same filtering options here that we had when making the `vk::raii::Sampler`. We use the `vk::Filter::eLinear` to enable interpolation. -[,c++] +[source,multilang,c++,c] +.Transitioning level i-1 to shader-read-only ---- +// START c++ barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal; barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead; barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, barrier); +// END c++ + +// START c +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; +barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +// END c ---- This barrier transitions mip level `i - 1` to `vk::ImageLayout::eShaderReadOnlyOptimal`. @@ -299,8 +394,10 @@ We check each dimension before the division to ensure that dimension never becom This handles cases where the image is not square, since one of the mip dimensions would reach 1 before the other dimension. When this happens, that dimension should remain 1 for all remaining levels. -[,c++] +[source,multilang,c++,c] +.Transitioning the last mip level ---- +// START c++ barrier.subresourceRange.baseMipLevel = mipLevels - 1; barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; @@ -309,6 +406,18 @@ When this happens, that dimension should remain 1 for all remaining levels. commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, barrier); } +// END c++ + +// START c + barrier.subresourceRange.baseMipLevel = mipLevels - 1; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} +// END c ---- Finally, we insert one more pipeline barrier. @@ -356,8 +465,10 @@ void generateMipmaps(vk::raii::CommandBuffer &commandBuffer, In the `generateMipmaps` function, use `vk::raii::PhysicalDevice::getFormatProperties` to request the properties of the texture image format: -[,c++] +[source,multilang,c++,c] +.Querying format properties for linear blit support ---- +// START c++ void generateMipmaps(vk::raii::CommandBuffer &commandBuffer, vk::raii::Image &image, vk::Format imageFormat, @@ -368,18 +479,43 @@ void generateMipmaps(vk::raii::CommandBuffer &commandBuffer, // Check if image format supports linear blit-ing vk::FormatProperties formatProperties = physicalDevice->getFormatProperties(imageFormat); ... +// END c++ + +// START c +void generateMipmaps(VkCommandBuffer commandBuffer, + VkImage image, + VkFormat imageFormat, + int32_t texWidth, + int32_t texHeight, + uint32_t mipLevels) +{ + // Check if image format supports linear blit-ing + VkFormatProperties formatProperties; + vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, &formatProperties); + ... +// END c ---- The `vk::FormatProperties` struct has three fields named `linearTilingFeatures`, `optimalTilingFeatures` and `bufferFeatures` that each describe how the format can be used depending on the way it is used. We create a texture image with the optimal tiling format, so we need to check `optimalTilingFeatures`. Support for the linear filtering feature can be checked with the `vk::FormatFeatureFlagBits::eSampledImageFilterLinear`: -[,c++] +[source,multilang,c++,c] +.Checking for linear filtering support ---- +// START c++ if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear)) { throw std::runtime_error("texture image format does not support linear blitting!"); } +// END c++ + +// START c +if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) +{ + throw std::runtime_error("texture image format does not support linear blitting!"); +} +// END c ---- There are two alternatives in this case. @@ -440,8 +576,10 @@ To see the results of this chapter, we need to choose values for our `textureSam We've already set the `minFilter` and `magFilter` to use `vk::Filter::eLinear`. We just need to choose values for `minLod`, `maxLod`, `mipLodBias`, and `mipmapMode`. -[,c++] +[source,multilang,c++,c] +.The complete sampler for mipmapped sampling ---- +// START c++ void createTextureSampler() { vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); @@ -461,6 +599,32 @@ void createTextureSampler() .maxLod = vk::LodClampNone}; ... } +// END c++ + +// START c +void createTextureSampler() +{ + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physicalDevice, &properties); + + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.anisotropyEnable = VK_TRUE; + samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = VK_LOD_CLAMP_NONE; + ... +} +// END c ---- In the code above, we've set up the sampler with linear filtering for both minification and magnification, and linear interpolation between mip levels. We've also set the mip level bias to 0.0f. @@ -483,9 +647,16 @@ Without mipmaps, the writing has harsh edges and gaps from MoirĂ© artifacts. You can play around with the sampler settings to see how they affect mipmapping. For example, by changing `minLod`, you can force the sampler to not use the lowest mip levels: -[,c++] +[source,multilang,c++,c] +.Forcing the sampler to skip the lowest mip levels ---- +// START c++ samplerInfo.minLod = static_cast(mipLevels / 2); +// END c++ + +// START c +samplerInfo.minLod = (float)(mipLevels / 2); +// END c ---- These settings will produce this image: diff --git a/en/11_Compute_Shader.adoc b/en/11_Compute_Shader.adoc index 84ca031e3..dcf2bab51 100644 --- a/en/11_Compute_Shader.adoc +++ b/en/11_Compute_Shader.adoc @@ -93,14 +93,28 @@ But that's not the case. In Vulkan, you can specify multiple usages for buffers and images. So for the particle vertex buffer to be used as a vertex buffer (in the graphics pass) and as a storage buffer (in the compute pass), you simply create the buffer with those two usage flags: -[,c++] +[source,multilang,c++,c] +.A buffer used as both vertex buffer and storage buffer ---- +// START c++ vk::BufferCreateInfo bufferInfo{}; ... bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst; ... shaderStorageBuffers[i] = vk::raii::Buffer(*device, bufferInfo); +// END c++ + +// START c +VkBufferCreateInfo bufferInfo{}; +... +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; +... + +if (vkCreateBuffer(device, &bufferInfo, nullptr, &shaderStorageBuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader storage buffer!"); +} +// END c ---- The two flags `vk::BufferUsageFlagBits::eVertexBuffer` and `vk::BufferUsageFlagBits::eStorageBuffer` set with `bufferInfo.usage` tell the implementation that we want to use this buffer for two different scenarios: as a vertex buffer in the vertex shader and as a store buffer. @@ -160,14 +174,28 @@ Typical use cases are applying image effects to textures, doing post-processing This is similar for images: -[,c++] +[source,multilang,c++,c] +.An image used as both sampled and storage image ---- +// START c++ vk::ImageCreateInfo imageInfo {}; ... imageInfo.usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eStorage; ... textureImage = std::make_unique( *device, swapChainCreateInfo ); +// END c++ + +// START c +VkImageCreateInfo imageInfo{}; +... +imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; +... + +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +// END c ---- The two flags `vk::ImageUsageFlagBits::eSampled` and `vk::ImageUsageFlagBits::eStorage` set with `imageInfo.usage` tell the implementation that we want to use this image for two different scenarios: as an image sampled in the fragment shader and as a storage image in the computer shader; @@ -211,8 +239,10 @@ This will also save us from dealing with several advanced synchronization mechan For our compute sample, we need to change the device creation code a bit: -[,c++] +[source,multilang,c++,c] +.Finding a queue family that supports both graphics and compute ---- +// START c++ std::vector queueFamilyProperties = physicalDevice->getQueueFamilyProperties(); // get the first index into queueFamilyProperties which supports graphics and compute @@ -221,15 +251,40 @@ auto graphicsAndComputeQueueFamilyProperty = queueFamilyProperties.end(), []( vk::QueueFamilyProperties const & qfp ) { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics && qfp.queueFlags & vk::QueueFlagBits::eCompute); } ); graphicsAndComputeIndex = static_cast( std::distance( queueFamilyProperties.begin(), graphicsAndComputeQueueFamilyProperty ) ); +// END c++ + +// START c +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr); +std::vector queueFamilyProperties(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyProperties.data()); + +graphicsAndComputeIndex = 0; +for (; graphicsAndComputeIndex < queueFamilyCount; ++graphicsAndComputeIndex) +{ + if ((queueFamilyProperties[graphicsAndComputeIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) && + (queueFamilyProperties[graphicsAndComputeIndex].queueFlags & VK_QUEUE_COMPUTE_BIT)) + { + break; + } +} +// END c ---- The changed queue family index selection code will now try to find a queue family that supports both graphics and compute. We can then get a compute queue from this queue family in `createLogicalDevice`: -[,c++] +[source,multilang,c++,c] +.Retrieving the compute queue handle ---- +// START c++ computeQueue = std::make_unique( *device, graphicsAndComputeIndex, 0 ); +// END c++ + +// START c +vkGetDeviceQueue(device, graphicsAndComputeIndex, 0, &computeQueue); +// END c ---- == The compute shader stage @@ -245,12 +300,26 @@ Compute also introduces a new binding point type for descriptors and pipelines n Loading compute shaders in our application is the same as loading any other shader. The only real difference is that we'll need to use the `vk::ShaderStageFlagBits::eCompute` mentioned above. -[,c++] +[source,multilang,c++,c] +.Compute shader stage info ---- +// START c++ auto computeShaderCode = readFile("shaders/slang.spv"); vk::PipelineShaderStageCreateInfo computeShaderStageInfo({}, vk::ShaderStageFlagBits::eCompute, shaderModule, "compMain"); ... +// END c++ + +// START c +auto computeShaderCode = readFile("shaders/slang.spv"); + +VkPipelineShaderStageCreateInfo computeShaderStageInfo{}; +computeShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +computeShaderStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; +computeShaderStageInfo.module = shaderModule; +computeShaderStageInfo.pName = "compMain"; +... +// END c ---- == Preparing the shader storage buffers @@ -354,8 +423,10 @@ layoutBindings[0].stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStag Here is the descriptor setup for our sample. The layout looks like this: -[,c++] +[source,multilang,c++,c] +.Compute descriptor set layout ---- +// START c++ std::array layoutBindings{ vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eCompute, nullptr), vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute, nullptr), @@ -364,6 +435,34 @@ std::array layoutBindings{ vk::DescriptorSetLayoutCreateInfo layoutInfo({}, layoutBindings.size(), layoutBindings.data()); computeDescriptorSetLayout = std::make_unique( *device, layoutInfo ); +// END c++ + +// START c +VkDescriptorSetLayoutBinding layoutBindings[3]{}; +layoutBindings[0].binding = 0; +layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +layoutBindings[0].descriptorCount = 1; +layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[1].binding = 1; +layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[1].descriptorCount = 1; +layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[2].binding = 2; +layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[2].descriptorCount = 1; +layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 3; +layoutInfo.pBindings = layoutBindings; + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute descriptor set layout!"); +} +// END c ---- Looking at this setup, you might wonder why we have two layout bindings for shader storage buffer objects, even though we'll only render a single particle system. @@ -376,8 +475,10 @@ For that, the compute shader needs to have access to the last and current frame' This is done by passing both to the compute shader in our descriptor setup. See the `storageBufferInfoLastFrame` and `storageBufferInfoCurrentFrame`: -[,c++] +[source,multilang,c++,c] +.Writing the uniform buffer and both SSBO descriptors ---- +// START c++ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { vk::DescriptorBufferInfo bufferInfo(uniformBuffers[i], 0, sizeof(UniformBufferObject)); @@ -390,16 +491,72 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { }; device->updateDescriptorSets(descriptorWrites, {}); } +// END c++ + +// START c +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); + + VkDescriptorBufferInfo storageBufferInfoLastFrame{}; + storageBufferInfoLastFrame.buffer = shaderStorageBuffers[(i - 1) % MAX_FRAMES_IN_FLIGHT]; + storageBufferInfoLastFrame.offset = 0; + storageBufferInfoLastFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + VkDescriptorBufferInfo storageBufferInfoCurrentFrame{}; + storageBufferInfoCurrentFrame.buffer = shaderStorageBuffers[i]; + storageBufferInfoCurrentFrame.offset = 0; + storageBufferInfoCurrentFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + VkWriteDescriptorSet descriptorWrites[3]{}; + + descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[0].dstSet = computeDescriptorSets[i]; + descriptorWrites[0].dstBinding = 0; + descriptorWrites[0].descriptorCount = 1; + descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descriptorWrites[0].pBufferInfo = &bufferInfo; + + descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[1].dstSet = computeDescriptorSets[i]; + descriptorWrites[1].dstBinding = 1; + descriptorWrites[1].descriptorCount = 1; + descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[1].pBufferInfo = &storageBufferInfoLastFrame; + + descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[2].dstSet = computeDescriptorSets[i]; + descriptorWrites[2].dstBinding = 2; + descriptorWrites[2].descriptorCount = 1; + descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[2].pBufferInfo = &storageBufferInfoCurrentFrame; + + vkUpdateDescriptorSets(device, 3, descriptorWrites, 0, nullptr); +} +// END c ---- Remember that we also have to request the descriptor types for the SSBOs from our descriptor pool: -[,c++] +[source,multilang,c++,c] +.Descriptor pool sizes for the compute descriptors ---- +// START c++ std::array poolSize { vk::DescriptorPoolSize( vk::DescriptorType::eUniformBuffer, MAX_FRAMES_IN_FLIGHT), vk::DescriptorPoolSize( vk::DescriptorType::eStorageBuffer, MAX_FRAMES_IN_FLIGHT * 2) }; +// END c++ + +// START c +VkDescriptorPoolSize poolSize[2]{}; +poolSize[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize[0].descriptorCount = MAX_FRAMES_IN_FLIGHT; +poolSize[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +poolSize[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * 2; +// END c ---- We need to double the number of `vk::DescriptorType::eStorageBuffer` types requested from the pool by two because our sets reference the SSBOs of the last and current frame. @@ -410,20 +567,48 @@ As compute is not a part of the graphics pipeline, we can't use `device->createG Instead, we need to create a dedicated compute pipeline with `device->createComputePipeline` for running our compute commands. Since a compute pipeline does not touch any of the rasterization state, it has a lot less state than a graphics pipeline: -[,c++] +[source,multilang,c++,c] +.Compute pipeline layout ---- +// START c++ vk::PipelineLayoutCreateInfo pipelineLayoutInfo({}, 1, &**computeDescriptorSetLayout); computePipelineLayout = std::make_unique( *device, pipelineLayoutInfo ); +// END c++ + +// START c +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &computeDescriptorSetLayout; + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &computePipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline layout!"); +} +// END c ---- The setup is a lot simpler, as we only require one shader stage and a pipeline layout. The pipeline layout works the same as with the graphics pipeline: -[,c++] +[source,multilang,c++,c] +.Creating the compute pipeline ---- +// START c++ vk::ComputePipelineCreateInfo pipelineInfo({}, computeShaderStageInfo, *computePipelineLayout); computePipeline = std::make_unique(device->createComputePipeline( nullptr, pipelineInfo)); +// END c++ + +// START c +VkComputePipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; +pipelineInfo.stage = computeShaderStageInfo; +pipelineInfo.layout = computePipelineLayout; + +if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &computePipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline!"); +} +// END c ---- == Compute space @@ -539,8 +724,10 @@ While not perfectly true, a dispatch is for compute as a draw call like `command This dispatches a given number of compute work items in at max. three dimensions. -[,c++] +[source,multilang,c++,c] +.Recording the compute dispatch ---- +// START c++ computeCommandBuffers[frameIndex]->begin({}); ... @@ -552,6 +739,23 @@ computeCommandBuffers[frameIndex]->dispatch( PARTICLE_COUNT / 256, 1, 1 ); ... computeCommandBuffers[frameIndex]->end(); +// END c++ + +// START c +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +vkBeginCommandBuffer(computeCommandBuffers[frameIndex], &beginInfo); +... + +vkCmdBindPipeline(computeCommandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline); +vkCmdBindDescriptorSets(computeCommandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 0, 1, &computeDescriptorSets[frameIndex], 0, nullptr); + +vkCmdDispatch(computeCommandBuffers[frameIndex], PARTICLE_COUNT / 256, 1, 1); + +... + +vkEndCommandBuffer(computeCommandBuffers[frameIndex]); +// END c ---- The `computeCommandBuffers[frameIndex]->dispatch` will dispatch `PARTICLE_COUNT / 256` local work groups in the x dimension. @@ -602,8 +806,10 @@ signaled state because otherwise, the first draw would time out while waiting for the fences to be signaled as detailed xref:03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc[here]: -[,c++] +[source,multilang,c++,c] +.Compute synchronization objects ---- +// START c++ std::vector> computeInFlightFences; std::vector> computeFinishedSemaphores; ... @@ -615,12 +821,38 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { computeFinishedSemaphores[i] = std::make_unique(*device, vk::SemaphoreCreateInfo()); computeInFlightFences[i] = std::make_unique(*device, vk::FenceCreateInfo(vk::FenceCreateFlagBits::eSignaled)); } +// END c++ + +// START c +std::vector computeInFlightFences; +std::vector computeFinishedSemaphores; +... +computeInFlightFences.resize(MAX_FRAMES_IN_FLIGHT); +computeFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + +VkSemaphoreCreateInfo semaphoreInfo{}; +semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + ... + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &computeFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &computeInFlightFences[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute synchronization objects!"); + } +} +// END c ---- We then use these to synchronize the compute buffer submission with the graphics submission: -[,c++] +[source,multilang,c++,c] +.Compute submission followed by the synchronized graphics submission ---- +// START c++ { // Compute submission while ( vk::Result::eTimeout == device->waitForFences(**computeInFlightFences[frameIndex], vk::True, UINT64_MAX) ) @@ -647,6 +879,53 @@ We then use these to synchronize the compute buffer submission with the graphics vk::PipelineStageFlags waitDestinationStageMask[] = { vk::PipelineStageFlagBits::eVertexInput, vk::PipelineStageFlagBits::eColorAttachmentOutput }; const vk::SubmitInfo submitInfo( waitSemaphores, waitDestinationStageMask, {**commandBuffers[frameIndex]}, {**renderFinishedSemaphore[frameIndex]} ); graphicsQueue->submit(submitInfo, **inFlightFences[frameIndex]); +// END c++ + +// START c +{ + // Compute submission + while (vkWaitForFences(device, 1, &computeInFlightFences[frameIndex], VK_TRUE, UINT64_MAX) == VK_TIMEOUT) + ; + + updateUniformBuffer(frameIndex); + vkResetFences(device, 1, &computeInFlightFences[frameIndex]); + vkResetCommandBuffer(computeCommandBuffers[frameIndex], 0); + recordComputeCommandBuffer(); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &computeCommandBuffers[frameIndex]; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &computeFinishedSemaphores[frameIndex]; + + vkQueueSubmit(computeQueue, 1, &submitInfo, computeInFlightFences[frameIndex]); +} +{ + // Graphics submission + while (vkWaitForFences(device, 1, &inFlightFences[frameIndex], VK_TRUE, UINT64_MAX) == VK_TIMEOUT) + ; +... + + vkResetFences(device, 1, &inFlightFences[frameIndex]); + vkResetCommandBuffer(commandBuffers[frameIndex], 0); + recordCommandBuffer(imageIndex); + + VkSemaphore waitSemaphores[] = {presentCompleteSemaphore[frameIndex], computeFinishedSemaphores[frameIndex]}; + VkPipelineStageFlags waitDestinationStageMask[] = {VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.waitSemaphoreCount = 2; + submitInfo.pWaitSemaphores = waitSemaphores; + submitInfo.pWaitDstStageMask = waitDestinationStageMask; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffers[frameIndex]; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &renderFinishedSemaphore[frameIndex]; + + vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[frameIndex]); +// END c ---- Similar to the sample in the @@ -800,8 +1079,10 @@ This means that we can use the shader storage buffer for drawing just as we used We first set up the vertex input state to match our particle structure: -[,c++] +[source,multilang,c++,c] +.Particle vertex input attribute descriptions ---- +// START c++ struct Particle { ... @@ -812,17 +1093,48 @@ struct Particle { }; } }; +// END c++ + +// START c +struct Particle { + ... +}; + +void getAttributeDescriptions(VkVertexInputAttributeDescription attributeDescriptions[2]) +{ + attributeDescriptions[0].location = 0; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(struct Particle, position); + + attributeDescriptions[1].location = 1; + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32A32_SFLOAT; + attributeDescriptions[1].offset = offsetof(struct Particle, color); +} +// END c ---- Note that we don't add `velocity` to the vertex input attributes, as this is only used by the compute shader. We then bind and draw it like we would with any vertex buffer: -[,c++] +[source,multilang,c++,c] +.Binding and drawing the particle buffer ---- +// START c++ commandBuffers[frameIndex]->bindVertexBuffers(0, { *shaderStorageBuffers[frameIndex] }, {0}); commandBuffers[frameIndex]->draw( PARTICLE_COUNT, 1, 0, 0 ); +// END c++ + +// START c +VkBuffer vertexBuffers[] = {shaderStorageBuffers[frameIndex]}; +VkDeviceSize offsets[] = {0}; +vkCmdBindVertexBuffers(commandBuffers[frameIndex], 0, 1, vertexBuffers, offsets); + +vkCmdDraw(commandBuffers[frameIndex], PARTICLE_COUNT, 1, 0, 0); +// END c ---- == Conclusion