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 6e20ad54..9711439a 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 @@ -2,25 +2,25 @@ = Swap chain -Vulkan does not have the concept of a "default framebuffer," hence it -requires an infrastructure that will own the buffers we will render to -before we visualize them on the screen. This infrastructure is -known as the *swap chain* and must be created explicitly in Vulkan. The swap -chain is essentially a queue of images that are waiting to be presented to the -screen. Our application will acquire such an image to draw to it, and then -return it to the queue. How exactly the queue works. The conditions for -presenting an image from the queue depend on how the swap chain is set up. However, -the general purpose of the swap chain is to synchronize the presentation of -images with the refresh rate of the screen. +Vulkan does not have the concept of a "default framebuffer," hence it requires +an infrastructure that will own the buffers we will render to before we +visualize them on the screen. This infrastructure is known as the *swap chain* +and must be created explicitly in Vulkan. The swap chain is essentially a queue +of images that are waiting to be presented to the screen. Our application will +acquire such an image to draw to it, and then return it to the queue. How +exactly the queue works. The conditions for presenting an image from the queue +depend on how the swap chain is set up. However, the general purpose of the +swap chain is to synchronize the presentation of images with the refresh rate of +the screen. == Checking for swap chain support Not all graphics cards are capable of presenting images directly to a screen for various reasons, for example, because they are designed for servers and don't have any display outputs. Secondly, since image presentation is heavily tied -into the window system and the surfaces associated with windows, it is not - part of the Vulkan core. You have to enable the `VK_KHR_swapchain` -device extension after querying for its support. +into the window system and the surfaces associated with windows, it is not part +of the Vulkan core. You have to enable the `VK_KHR_swapchain` device extension +after querying for its support. For that purpose we'll first extend the `createLogicalDevice` function to check if this extension is supported. We've previously seen how to list the @@ -30,55 +30,90 @@ macro `VK_KHR_SWAPCHAIN_EXTENSION_NAME` that is defined as `VK_KHR_swapchain`. The advantage of using this macro is that the compiler will catch misspellings. -First declare a list of required device extensions, similar to the list of -validation layers to enable. +First, declare a list of required device extensions, similar to the list of +validation layers to be enabled: -[,c++] +[source,multilang,c++,c] ---- +// START c++ std::vector deviceExtensions = { vk::KHRSwapchainExtensionName, vk::KHRSpirv14ExtensionName, vk::KHRSynchronization2ExtensionName, vk::KHRCreateRenderpass2ExtensionName }; +// END c++ + +// START c +std::vector deviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_SPIRV_1_4_EXTENSION_NAME, + VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, + VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, +}; +// END c ---- -It should be noted that the availability of a presentation queue, -as we checked in the previous chapter, implies that the swap chain extension -must be supported. However, the extension does have to be explicitly enabled. +It should be noted that the availability of a presentation queue, as we checked +in the previous chapter, implies that the swap chain extension must be +supported. However, the extension does have to be explicitly enabled. == Enabling device extensions -Using a swapchain requires enabling the `VK_KHR_swapchain` extension first. +Using a swap chain requires enabling the `VK_KHR_swapchain` extension first. Enabling the extension just requires a small change to the logical device creation structure: -[,c++] +[source,multilang,c++,c] ---- -deviceCreateInfo.enabledExtensionCount = deviceExtensions.size(); +// START c++ +deviceCreateInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); +// END c++ + +// START c +deviceCreateInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); +deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); +// END c ---- -Alternatively, we can do this at the construction and keep this very succinct: +Alternatively, we can do this using the provided VulkanHpp constructors and keep +this very succinct (note that for this to work, the `VULKAN_HPP_NO_CONSTRUCTORS` +and `VULKAN_HPP_NO_STRUCT_CONSTRUCTORS` should not be defined): -[,c++] ----- -std::vector deviceExtensions = { vk::KHRSwapchainExtensionName }; +[source,multilang,c++,c] +---- +// START c++ +std::vector deviceExtensions = { + vk::KHRSwapchainExtensionName, + vk::KHRSpirv14ExtensionName, + vk::KHRSynchronization2ExtensionName, + vk::KHRCreateRenderpass2ExtensionName, +}; float queuePriority = 0.0f; -vk::DeviceQueueCreateInfo deviceQueueCreateInfo( {}, graphicsIndex, 1, &queuePriority ); -vk::DeviceCreateInfo deviceCreateInfo( {}, deviceQueueCreateInfo, {}, deviceExtensions ); +vk::DeviceQueueCreateInfo deviceQueueCreateInfo({}, graphicsIndex, 1, &queuePriority); +vk::DeviceCreateInfo deviceCreateInfo({}, deviceQueueCreateInfo, {}, deviceExtensions); +// END c++ + +// START c +/* there are, obviously, no Vulkan C-API constructors */ +// END c ---- +Notice that using VulkanHpp constructors, we don't have to explicitly provide +the containers count and the pointer to its underlying data - we can just supply +the extensions vector directly. + == Querying details of swap chain support -Just checking if a swap chain is available is not enough because it may not - be compatible with our window surface. Creating a swap chain also -involves a lot more settings than instance and device creation, so we need to -query for some more details before we're able to proceed. +Just checking if a swap chain is available is not enough because it may not be +compatible with our window surface. Creating a swap chain also involves a lot +more settings than instance and device creation, so we need to query for some +more details before we're able to proceed. There are basically three kinds of properties we need to check: -* Basic surface capabilities (min/max number of images in swap chain, min/max +* Basic surface capabilities (min/max number of images in swapchain, min/max width and height of images) * Surface formats (pixel format, color space) * Available presentation modes @@ -89,23 +124,43 @@ next section. Let's start with the basic surface capabilities. These properties are straightforward to query and are returned into a single -`VkSurfaceCapabilitiesKHR` struct. +`VkSurfaceCapabilitiesKHR` struct: -[,c++] +[source,multilang,c++,c] ---- -auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( surface ); +// START c++ +vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface); +// END c++ + +// START c +VkSurfaceCapabilitiesKHR surfaceCapabilities; +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities); +// END c ---- -This function takes the specified `VkPhysicalDevice` and `VkSurfaceKHR` window -surface into account when determining the supported capabilities. All the -support querying functions have these two as first parameters because they are -the core components of the swap chain. +This physical device member function takes the `VkPhysicalDevice` and specified +`VkSurfaceKHR` window surface into account when determining the supported +capabilities. All the support querying functions have these two as first +parameters because they are the core components of the swap chain. -The next step is about querying the supported surface formats. +The next step is about querying the supported surface formats: -[,c++] +[source,multilang,c++,c] ---- -std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR( surface ); +// START c++ +std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR(surface); +// END c++ + +// START c +/* first get the number of available formats */ +uint32_t formatsCount; +vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatsCount, nullptr); + +/* then fill the container with the available formats */ +std::vector availableFormats(formatsCount); +vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatsCount, + availableFormats.data()); +// END c ---- Make sure that the vector is resized to hold all the available formats. @@ -113,26 +168,40 @@ Make sure that the vector is resized to hold all the available formats. Finally, querying the supported presentation modes works exactly the same way with `vkGetPhysicalDeviceSurfacePresentModesKHR`: -[,c++] +[source,multilang,c++,c] ---- -std::vector availablePresentModes = physicalDevice.getSurfacePresentModesKHR( surface ); +// START c++ +std::vector availablePresentModes = + physicalDevice.getSurfacePresentModesKHR(surface); +// END c++ + +// START c +/* first get the number of available presentation modes */ +uint32_t presentModesCount; +vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModesCount, nullptr); + +/* then fill the container with the available presentation modes */ +std::vector availablePresentModes(presentModesCount); +vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModesCount, + availablePresentModes.data()); +// END c ---- All the details are available now. Swap chain support is enough for this tutorial if there is at least one supported image format and one supported -presentation mode given the window surface we have. -It is important that we only try to query for swap chain support after verifying -that the extension is available. +presentation mode given the window surface we have. It is important that we only +try to query for swap chain support after verifying that the extension is +available. == Choosing the right settings for the swap chain -There may still be many different modes of varying optimality in the swap -chain. We'll now write a couple of functions to find the right settings for the best +There may still be many different modes of varying optimality in the swap chain. +We'll now write a couple of functions to find the right settings for the best possible swap chain. There are three types of settings to determine: * Surface format (color depth) * Presentation mode (conditions for "swapping" images to the screen) -* Swap extent (resolution of images in swapchain) +* Swap extent (resolution of images in swap chain) For each of these settings, we'll have an ideal value in mind that we'll go with if it's available, and otherwise we'll create some logic to find the next best @@ -141,52 +210,106 @@ thing. === Surface format The function for this setting starts out like this. We'll later pass the -`formats` member of the `SwapChainSupportDetails` struct as argument. +`formats` member of the `SwapChainSupportDetails` struct as argument: -[,c++] +[source,multilang,c++,c] ---- -vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { - return availableFormats[0]; +// START c++ +vk::SurfaceFormatKHR + chooseSwapSurfaceFormat(const std::vector &availableFormats) +{ + return availableFormats[0]; } +// END c++ + +// START c +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats) +{ + return availableFormats[0]; +} +// END c ---- -Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. The -`format` member specifies the color channels and types. For example, +Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. +The `format` member specifies the color channels and types. For example, `VK_FORMAT_B8G8R8A8_SRGB` means that we store the B, G, R and alpha channels in that order with an 8-bit unsigned integer for a total of 32 bits per pixel. The `colorSpace` member indicates if the SRGB color space is supported or not using the `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` flag. Note that this flag used to be called `VK_COLORSPACE_SRGB_NONLINEAR_KHR` in old versions of the specification. -For the color space we'll use SRGB if it is available, because it link:http://stackoverflow.com/questions/12524623/[results in more accurate perceived colors]. It is also pretty much the standard color space for images, like the textures we'll use later on. -Because of that we should also use an SRGB color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. +For the color space, we'll use SRGB if it is available, because it +link:http://stackoverflow.com/questions/12524623/[results in more accurate +perceived colors]. It is also pretty much the standard color space for images, +like the textures we'll use later on. Because of that we should also use an SRGB +color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. Let's go through the list and see if the preferred combination is available: -[,c++] +[source,multilang,c++,c] ---- -for (const auto& availableFormat : availableFormats) { - if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { - return availableFormat; - } +// START c++ +for (const auto &availableFormat : availableFormats) +{ + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && + availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) + { + return availableFormat; + } +} +// END c++ + +// START c +for (const auto &availableFormat : availableFormats) +{ + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && + availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + return availableFormat; + } } +// END c ---- If that also 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++] ----- -vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { - for (const auto& availableFormat : availableFormats) { - if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { - return availableFormat; - } - } - - return availableFormats[0]; +format that is available. The `chooseSwapSurfaceFormat` should now look +something like this: + +[source,multilang,c++,c] +---- +// START c++ +vk::SurfaceFormatKHR + chooseSwapSurfaceFormat(const std::vector &availableFormats) +{ + for (const auto &availableFormat : availableFormats) + { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && + availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) + { + return availableFormat; + } + } + + return availableFormats[0]; } +// END c++ + +// START c +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats) +{ + for (const auto &availableFormat : availableFormats) + { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && + availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + return availableFormat; + } + } + + return availableFormats[0]; +} +// END c ---- === Presentation mode @@ -212,47 +335,90 @@ Instead of blocking the application when the queue is full, the images that are already queued are simply replaced with the newer ones. This mode can be used to render frames as fast as possible while still avoiding tearing, resulting in fewer latency issues than standard vertical sync. This is commonly known as -"triple buffering," although the existence of three buffers alone does not +"triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. Only the `VK_PRESENT_MODE_FIFO_KHR` mode is guaranteed to be available, so we'll again have to write a function that looks for the best mode that is available: -[,c++] +[source,multilang,c++,c] ---- -vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { - return vk::PresentModeKHR::eFifo; +// START c++ +vk::PresentModeKHR + chooseSwapPresentMode(const std::vector &availablePresentModes) +{ + return vk::PresentModeKHR::eFifo; } ----- - -I think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if -energy usage is not a concern. It allows us to avoid tearing while still -maintaining fairly low latency by rendering new images that are as -up to date as possible right until the vertical blank. On mobile devices, -where energy usage is more important, you will probably want to use -`VK_PRESENT_MODE_FIFO_KHR` instead. Now, let's look through the list to see -if `VK_PRESENT_MODE_MAILBOX_KHR` is available: +// END c++ -[,c++] ----- -vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { - for (const auto& availablePresentMode : availablePresentModes) { - if (availablePresentMode == vk::PresentModeKHR::eMailbox) { - return availablePresentMode; - } - } - return vk::PresentModeKHR::eFifo; +// START c +VkPresentModeKHR + chooseSwapPresentMode(const std::vector &availablePresentModes) +{ + return VK_PRESENT_MODE_FIFO_KHR; +} +// END c +---- + +I think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if energy +usage is not a concern. It allows us to avoid tearing while still maintaining +fairly low latency by rendering new images that are as up-to-date as possible +right until the vertical blank. On mobile devices, where energy usage is more +important, you will probably want to use `VK_PRESENT_MODE_FIFO_KHR` instead. +Now, let's look through the list to see if `VK_PRESENT_MODE_MAILBOX_KHR` is +available: + +[source,multilang,c++,c] +---- +// START c++ +vk::PresentModeKHR + chooseSwapPresentMode(const std::vector &availablePresentModes) +{ + for (const auto &availablePresentMode : availablePresentModes) + { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) + { + return availablePresentMode; + } + } + return vk::PresentModeKHR::eFifo; } +// END c++ + +// START c +VkPresentModeKHR chooseSwapPresentMode(const std::vector &availablePresentModes) +{ + for (const auto &availablePresentMode : availablePresentModes) + { + if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) + { + return availablePresentMode; + } + } + return VK_PRESENT_MODE_FIFO_KHR; +} +// END c ---- === Swap extent That leaves only one major property, for which we'll add one last function: -[,c++] +[source,multilang,c++,c] ---- -vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { +// START c++ +vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabilities) +{ + +} +// END c++ + +// START c +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities) +{ + } +// END c ---- The swap extent is the resolution of the swap chain images, and it's almost @@ -260,9 +426,9 @@ always exactly equal to the resolution of the window that we're drawing to _in pixels_ (more on that in a moment). The range of the possible resolutions is defined in the `VkSurfaceCapabilitiesKHR` structure. Vulkan tells us to match the resolution of the window by setting the width and height in the -`currentExtent` member. However, some window managers do allow us to differ here, -and this is indicated by setting the width and height in `currentExtent` to a -special value: the maximum value of `uint32_t`. In that case we'll pick the +`currentExtent` member. However, some window managers do allow us to differ +here, and this is indicated by setting the width and height in `currentExtent` +to a special value: the maximum value of `uint32_t`. In that case we'll pick the resolution that best matches the window within the `minImageExtent` and `maxImageExtent` bounds. But we must specify the resolution in the correct unit. @@ -279,105 +445,260 @@ 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] ---- -#include // Necessary for uint32_t -#include // Necessary for std::numeric_limits -#include // Necessary for std::clamp +// START c++ +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp -... +/* ... */ -vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { - if (capabilities.currentExtent.width != std::numeric_limits::max()) { - return capabilities.currentExtent; - } - int width, height; - glfwGetFramebufferSize(window, &width, &height); +vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabilities) +{ + if (capabilities.currentExtent.width != std::numeric_limits::max()) + { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); - return { - std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), - std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) - }; + return {std::clamp(width, capabilities.minImageExtent.width, + capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, + capabilities.maxImageExtent.height)}; } +// END c++ + +// START c +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp + +/* ... */ + +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities) +{ + if (capabilities.currentExtent.width != std::numeric_limits::max()) + { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + return {std::clamp(width, capabilities.minImageExtent.width, + capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, + capabilities.maxImageExtent.height)}; +} +// END c ---- -The `clamp` function is used here to bound the values of `width` and -`height` between the allowed minimum and maximum extents that are supported -by the implementation. +The `clamp` function is used here to bound the values of `width` and `height` +between the allowed minimum and maximum extents that are supported by the +implementation. == Creating the swap chain Now that we have all of these helper functions helping us with the choices we -have to make at runtime, we finally have all the information necessary to -create a working swap chain. +have to make at runtime, we finally have all the information necessary to 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. +Before proceeding, let's add class members to store the `VkSwapchainKHR` object, +its images, its image format, and its extent (these will be needed in future +chapters): -[,c++] +[source,multilang,c++,c] ---- -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); +// START c++ +vk::raii::SwapchainKHR swapChain = nullptr; +std::vector swapChainImages; +vk::Format swapChainImageFormat = vk::Format::eUndefined; +vk::Extent2D swapChainExtent; +// END c++ + +// START c +VkSwapchainKHR swapChain = VK_NULL_HANDLE; +std::vector swapChainImages; +VkFormat swapChainImageFormat = VK_FORMAT_UNDEFINED; +VkExtent2D swapChainExtent; +// END c +---- + +Then 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. Within it, we get the surface capabilities from the physical device +and the surface and we assign both the `swapChainExtent` and the +`swapChainSurfaceFormat` class members: + +[source,multilang,c++,c] +---- +// START c++ +void initVulkan() +{ + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); +} + +void createSwapChain() +{ + auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + swapChainSurfaceFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(surface)); +} +// END c++ + +// START c +void initVulkan() +{ + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); } -void createSwapChain() { - auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( surface ); - swapChainSurfaceFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR( surface )); - swapChainExtent = chooseSwapExtent(surfaceCapabilities); - auto minImageCount = std::max( 3u, surfaceCapabilities.minImageCount ); - minImageCount = ( surfaceCapabilities.maxImageCount > 0 && minImageCount > surfaceCapabilities.maxImageCount ) ? surfaceCapabilities.maxImageCount : minImageCount; +void createSwapChain() +{ + auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + swapChainSurfaceFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(surface)); } +// END c ---- -Aside from these properties, we also have to decide how many images we -would like to have in the swap chain. The implementation specifies the -minimum number that it requires to function: +Aside from these properties, we also have to decide how many images we would +like to have in the swap chain. The implementation specifies the minimum number +that it requires to function. For that, we'll create the following static +`chooseSwapMinImageCount` function: -[,c++] +[source,multilang,c++,c] ---- -uint32_t imageCount = surfaceCapabilities.minImageCount; +// START c++ +static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount; + return minImageCount; +} +// END c++ + +// START c +static uint32_t chooseSwapMinImageCount(VkSurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount; + return minImageCount; +} +// END c ---- -However, simply sticking to this minimum means that we may sometimes have -to wait on the driver to complete internal operations before we can acquire -another image to render to. Therefore, it is recommended to request at least -one more image than the minimum: +However, simply sticking to the minimum specified by the implementation means +that we may sometimes have to wait on the driver to complete internal operations +before we can acquire another image to render to. Therefore, it is recommended +to request at least one more image than the minimum: -[,c++] +[source,multilang,c++,c] ---- -uint32_t imageCount = surfaceCapabilities.minImageCount + 1; +// START c++ +static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount + 1; + return minImageCount; +} +// END c++ + +// START c +static uint32_t chooseSwapMinImageCount(VkSurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount + 1; + return minImageCount; +} +// END c ---- -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: +We should also make sure to not exceed the maximum number of images while doing +this by checking the `maxImageCount` field of the surface capabilities, where +`0` is a special value that means that there is no maximum: -[,c++] +[source,multilang,c++,c] ---- -if (surfaceCapabilities.maxImageCount > 0 && imageCount > surfaceCapabilities.maxImageCount) { - imageCount = surfaceCapabilities.maxImageCount; +// START c++ +static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount + 1; + if ((0 < surfaceCapabilities.maxImageCount) && + (surfaceCapabilities.maxImageCount < minImageCount)) + { + minImageCount = surfaceCapabilities.maxImageCount; + } + return minImageCount; } +// END c++ + +// START c +static uint32_t chooseSwapMinImageCount(VkSurfaceCapabilitiesKHR const &surfaceCapabilities) +{ + uint32_t minImageCount = surfaceCapabilities.minImageCount + 1; + 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 +filling in a large structure, to be fair, the swap chain is a fairly complex object so it is among the larger createInfo structures in Vulkan: -[,c++] +[source,multilang,c++,c] ---- +// START c++ vk::SwapchainCreateInfoKHR swapChainCreateInfo{ - .flags = vk::SwapchainCreateFlagsKHR(), . - surface = surface, .minImageCount = minImageCount, - .imageFormat = swapChainSurfaceFormat.format, .imageColorSpace = swapChainSurfaceFormat.colorSpace, - .imageExtent = swapChainExtent, .imageArrayLayers =1, - .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, .imageSharingMode = vk::SharingMode::eExclusive, - .preTransform = surfaceCapabilities.currentTransform, .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, - .presentMode = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR( surface )), - .clipped = true, .oldSwapchain = nullptr }; + .surface = surface, + .minImageCount = chooseSwapMinImageCount(surfaceCapabilities), + .imageFormat = swapChainSurfaceFormat.format, + .imageColorSpace = swapChainSurfaceFormat.colorSpace, + .imageExtent = swapChainExtent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, + /* .imageSharingMode will be set in subsequent if statement */ + /* .queueFamilyIndexCount will be set in subsequent if statement */ + /* .pQueueFamilyIndices will be set in subsequent if statement */ + .preTransform = surfaceCapabilities.currentTransform, + .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, + .presentMode = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(surface)), + .clipped = true, + .oldSwapchain = nullptr}; +// END c++ + +// START c +VkSwapchainCreateInfoKHR swapChainCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .pNext = NULL, + .flags = static_cast(0), + .surface = surface, + .minImageCount = chooseSwapMinImageCount(surfaceCapabilities), + .imageFormat = swapChainSurfaceFormat.format, + .imageColorSpace = swapChainSurfaceFormat.colorSpace, + .imageExtent = swapChainExtent, + .imageArrayLayers = 1, + .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + /* .imageSharingMode will be set in subsequent if statement */ + /* .queueFamilyIndexCount will be set in subsequent if statement */ + /* .pQueueFamilyIndices will be set in subsequent if statement */ + .preTransform = surfaceCapabilities.currentTransform, + .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + .presentMode = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(surface)), + .clipped = VK_TRUE, + .oldSwapchain = VK_NULL_HANDLE}; +// END c ---- The `imageArrayLayers` specifies the number of layers each image consists of. @@ -390,27 +711,78 @@ post-processing. In that case you may use a value like `VK_IMAGE_USAGE_TRANSFER_DST_BIT` instead and use a memory operation to transfer the rendered image to a swap chain image. -[,c++] ----- -uint32_t queueFamilyIndices[] = {graphicsFamily, presentFamily}; +The `supportedTransforms` field is used to specify that a certain transform +should be applied to images in the swap chain if it is supported +(`supportedTransforms` in `capabilities`), like a 90-degree clockwise rotation +or horizontal flip. Sine we do not want any transformation, we simply specify +the current transformation from the surface capabilities. -if (graphicsFamily != presentFamily) { - swapChainCreateInfo.imageSharingMode = vk::SharingMode::eConcurrent; - swapChainCreateInfo.queueFamilyIndexCount = 2; - swapChainCreateInfo.pQueueFamilyIndices = queueFamilyIndices; -} else { - swapChainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive; - swapChainCreateInfo.queueFamilyIndexCount = 0; // Optional - swapChainCreateInfo.pQueueFamilyIndices = nullptr; // Optional -} ----- +The `compositeAlpha` field specifies if the alpha channel should be used for +blending with other windows in the window system. You'll almost always want to +simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. + +The `presentMode` field is the presentation mode that was described in detail +above. We use the previously defined `chooseSwapPresentMode` to set it. + +The `clipped` field is used to specify whether we care about the color of pixels +that are obscured, for example, because another window is in front of them. +Unless you really need to be able to read these pixels back and get predictable +results, you'll get the best performance by enabling clipping, hence the +`VK_TRUE`. + +That leaves one last field, `oldSwapChain`. With Vulkan, it's possible that your +swap chain becomes invalid or unoptimized while your application is running, for +example, because the window was resized. In that case, the swap chain actually +needs to be recreated from scratch, and a reference to the old one must be +specified in this field. This is a complex topic that we'll learn more about +in xref:03_Drawing_a_triangle/04_Swap_chain_recreation.adoc[a future chapter]. +For now, we'll assume that we'll only ever create one swap chain, hence the +`VK_NULL_HANDLE`. Next, we need to specify how to handle swap chain images that will be used across multiple queue families. That will be the case in our application if the graphics queue family is different from the presentation queue. We'll be drawing on the images in the swap chain from the graphics queue and then submitting them -on the presentation queue. There are two ways to handle images that are -accessed from multiple queues: +on the presentation queue: + +[source,multilang,c++,c] +---- +// START c++ +uint32_t queueFamilyIndices[] = {graphicsFamily, presentFamily}; + +if (graphicsFamily != presentFamily) +{ + swapChainCreateInfo.imageSharingMode = vk::SharingMode::eConcurrent; + swapChainCreateInfo.queueFamilyIndexCount = 2; + swapChainCreateInfo.pQueueFamilyIndices = queueFamilyIndices; +} +else +{ + swapChainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive; + swapChainCreateInfo.queueFamilyIndexCount = 0; // Optional + swapChainCreateInfo.pQueueFamilyIndices = nullptr; // Optional +} +// END c++ + +// START c +uint32_t queueFamilyIndices[] = {graphicsFamily, presentFamily}; + +if (graphicsFamily != presentFamily) +{ + swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + swapChainCreateInfo.queueFamilyIndexCount = 2; + swapChainCreateInfo.pQueueFamilyIndices = queueFamilyIndices; +} +else +{ + swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + swapChainCreateInfo.queueFamilyIndexCount = 0; // Optional + swapChainCreateInfo.pQueueFamilyIndices = nullptr; // Optional +} +// END c +---- + +There are two ways to handle images that are accessed from multiple queues: * `VK_SHARING_MODE_EXCLUSIVE`: An image is owned by one queue family at a time, and ownership must be explicitly transferred before using it in another queue @@ -425,113 +797,51 @@ requires you to specify in advance between which queue families ownership will be shared using the `queueFamilyIndexCount` and `pQueueFamilyIndices` parameters. If the graphics queue family and presentation queue family are the same, which will be the case on most hardware, then we should stick to exclusive -mode. Concurrent mode requires you to specify at least two distinct -queue families. +mode. Concurrent mode requires you to specify at least two distinct queue +families. -[,c++] ----- -swapChainCreateInfo.preTransform = surfaceCapabilities.currentTransform; ----- +Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`. +We can also get the swap chain images using `vkGetSwapchainImagesKHR`: -We can specify that a certain transform should be applied to images in the swap -chain if it is supported (`supportedTransforms` in `capabilities`), like a -90-degree clockwise rotation or horizontal flip. To specify that you do not want -any transformation, simply specify the current transformation. - -[,c++] ----- -swapChainCreateInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; +[source,multilang,c++,c] ---- +// START c++ +swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); +swapChainImages = swapChain.getImages(); +// END c++ -The `compositeAlpha` field specifies if the alpha channel should be used for -blending with other windows in the window system. You'll almost always want to -simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. +// START c +vkCreateSwapchainKHR(device, &swapChainCreateInfo, nullptr, &swapChain); -[,c++] ----- -swapChainCreateInfo.presentMode = presentMode; -swapChainCreateInfo.clipped = vk::True; ----- - -The `presentMode` member speaks for itself. If the `clipped` member is set to -`VK_TRUE` then that means that we don't care about the color of pixels that are -obscured, for example, because another window is in front of them. Unless you -really need to be able to read these pixels back and get predictable results, -you'll get the best performance by enabling clipping. - -[,c++] ----- -swapChainCreateInfo.oldSwapchain = VK_NULL_HANDLE; ----- - -That leaves one last field, `oldSwapChain`. With Vulkan, it's possible that -your swap chain becomes invalid or unoptimized while your application is -running, for example, because the window was resized. In that case, the swap chain -actually needs to be recreated from scratch, and a reference to the old one must -be specified in this field. This is a complex topic that we'll learn more about -in xref:03_Drawing_a_triangle/04_Swap_chain_recreation.adoc[a future chapter]. For now, we'll assume that we'll only ever create -one swap chain. - -Now add class members to store the `VkSwapchainKHR` object and its images: +/* first get the number of swap chain images */ +uint32_t swapChainImagesCount; +vkGetSwapchainImagesKHR(device, swapChain, &swapChainImagesCount, nullptr); -[,c++] ----- -VkSwapchainKHR swapChain; -std::vector swapChainImages; ----- - -Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`: - -[,c++] ----- -swapChain = vk::raii::SwapchainKHR( device, swapChainCreateInfo ); -swapChainImages = swapChain.getImages(); +/* then resize the swap chain images container to fit all the swap chain images */ +swapChainImages.resize(swapChainImagesCount); +vkGetSwapchainImagesKHR(device, swapChain, &swapChainImagesCount, swapChainImages.data()); +// END c ---- -The parameters are the logical device, swap chain creation info, optional custom -allocators and a pointer to the variable to store the handle in. +The parameters are the logical device, swap chain creation info, and an optional +custom allocators and, in case of the C-API, a pointer to the variable to store +the handle in. -Now run the application to ensure that the swap chain is created -successfully! If at this point you get an access violation error in -`vkCreateSwapchainKHR` or see a message like `Failed to find -'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`, then see -the xref:90_FAQ.adoc[FAQ entry] about the Steam overlay layer. +Now run the application to ensure that the swap chain is created successfully! +If at this point you get an access violation error in `vkCreateSwapchainKHR` or +see a message like `Failed to find 'vkGetInstanceProcAddress' in layer +SteamOverlayVulkanLayer.dll`, then see the xref:90_FAQ.adoc[FAQ entry] about the +Steam overlay layer. -Try removing the `swapChainCreateInfo.imageExtent = extent;` line with validation layers -enabled. You'll see that one of the validation layers immediately catches the -mistake and a helpful message is printed: +Try removing the `swapChainCreateInfo.imageExtent = extent;` line with +validation layers enabled. You'll see that one of the validation layers +immediately catches the mistake and a helpful message is printed: image::/images/swap_chain_validation_layer.png[] -== Retrieving the swap chain images -The swap chain has been created now, so all that remains is retrieving the -handles of the `VkImage` objects it contains. We'll reference these during rendering -operations in later chapters. - -[,c++] ----- -std::vector swapChainImages = swapChainImages = swapChain->getImages(); ----- - -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++] ----- -vk::raii::SwapchainKHR swapChain = nullptr; -std::vector swapChainImages; -vk::Format swapChainImageFormat = vk::Format::eUndefined; -vk::Extent2D swapChainExtent; - -... - -swapChainImageFormat = surfaceFormat.format; -swapChainExtent = extent; ----- - We now have a set of images that can be drawn onto and can be presented to the -window. The xref:./02_Image_views.adoc[next chapter] will begin to cover how we can set up the images as -render targets, and then we start looking into the actual graphics pipeline and -drawing commands! +window. The xref:./02_Image_views.adoc[next chapter] will begin to cover how we +can set up the images as render targets, and then we start looking into the +actual graphics pipeline and drawing commands! link:/attachments/06_swap_chain_creation.cpp[C{pp} code] 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 539ebb8e..cc711fac 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 @@ -2,11 +2,19 @@ = Image views -To use any `VkImage`, including those in the swap chain, in the render pipeline -we have to create a `VkImageView` object. An image view is quite literally a -view into an image. It describes how to access the image and which part of the -image to access, for example, if it should be treated as a 2D texture depth -texture without any mipmapping levels. +In Vulkan, accessing and manipulating resources isn't done directly but rather +through *views* - these describe how to look at (or *view*) a subset of the +underlying data in a desired way. For instance, `VkBuffer` refers to a buffer +object which represents a linear array of data. To use a `VkBuffer` you have to +create a `VkBufferView` object which describes, among other things, the +accessible contiguous range of the underlying data (this is done by providing +an `offset` and a `range`). + +Similarly, to use any `VkImage`, including those in the swap chain, in the +render pipeline we have to create a `VkImageView` object. The `VkImageView` +describes how to interpret the underlying image and which part of it to access, +for example, if it should be treated as a 2D texture depth texture without any +mipmapping levels. In this chapter we'll write a `createImageViews` function that creates a basic image view for every image in the swap chain so that we can use them as color @@ -14,16 +22,39 @@ targets later on. First, add a class member to store the image views in: -[,c++] +[source,multilang,c++,c] ---- +// 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 -creation. +creation: -[,c++] +[source,multilang,c++,c] ---- +// START c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); +} + +void createImageViews() { + +} +// END c++ + +// START c void initVulkan() { createInstance(); setupDebugMessenger(); @@ -37,64 +68,102 @@ void initVulkan() { void createImageViews() { } +// END c ---- The parameters for image view creation are specified in a -`VkImageViewCreateInfo` structure. The first few parameters are the flags, -this isn't needed in our case, we'll add the images in the upcoming for loop. -Next, we specify that we're rendering to a 2d screen. If we were wanting -to render to a 3d screen or cube map, those are also options as is a 1d -screen. As you can probably guess, we'd want a 2d render target in most +`VkImageViewCreateInfo` structure. The first field is `flags` which is not +needed in our case. + +Next is the `image` field which will be set to each `VkImage` from the swap +chain in the upcoming loop over the swap chain images. + +Next, in the `viewType` field, we specify that we're rendering to a 2D screen. +If we wanted to render to a 3D screen or cube map, those are also options as is +a 1D screen. As you can probably guess, we'd want a 2D render target in most cases when we're rendering to a screen. -Next, we specify the image format; this is how the colorspace -components are configured so you get the right color format in your -renders. Next, components aren't needed for our swap chain; we'll talk -about them in a bit though. The last variable is the SubResource range, -which is necessary, and we'll talk about shortly. +Next, in the `format` field, we specify the image format; this is how the +colorspace components are configured so you get the right color format in your +renders. In the case of swap chain image views, we want to use the same format +as the underlying swap chain `VkImage` objects. + +The `subresourceRange` field describes the purpose of the image and which part +of it can be accessed. Our images will be used as color targets without any +mipmapping levels or multiple layers. -[,c++] +The `createImageViews` function should now look something like this: + +[source,multilang,c++,c] ---- +// START c++ void createImageViews() { swapChainImageViews.clear(); - vk::ImageViewCreateInfo imageViewCreateInfo{ .viewType = vk::ImageViewType::e2D, .format = swapChainImageFormat, - .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } }; + vk::ImageViewCreateInfo imageViewCreateInfo{ + /* .image = will be overwritten in the subsequent loop */ + .viewType = vk::ImageViewType::e2D, + .format = swapChainImageFormat, + .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1} + }; } ----- +// END c++ -The `components` field allows you to swizzle the color channels around. For -example, you can map all the channels to the red channel for a monochrome -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: +// START c +void createImageViews() { + swapChainImageViews.clear(); -[,c++] ----- -createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + VkImageViewCreateInfo imageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = NULL, + /* .image = will be overwritten in the subsequent loop */ + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = swapChainImageFormat, + .components = { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}, + }; +} +// END c ---- -The `subresourceRange` field describes what the image's purpose is and which -part of the image should be accessed. Our images will be used as color targets -without any mipmapping levels or multiple layers. +The `components` field (which is kept to default in the C++ RAII snippet above) +allows you to swizzle the color channels around. For example, you can map all +the channels to the red channel for a monochrome texture. You can also map +constant values of `0` and `1` to a channel. The concept of swizzling will +become much clearer once we get to the Shader Modules chapter: -[,c++] ----- -createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; -createInfo.subresourceRange.baseMipLevel = 0; -createInfo.subresourceRange.levelCount = 1; -createInfo.subresourceRange.baseArrayLayer = 0; -createInfo.subresourceRange.layerCount = 1; +[source,multilang,c++,c] ---- +// START c++ +imageViewCreateInfo.components = { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity, +}; +// END c++ -An easy way to specify all of that is this single line: - -[,c++] ----- -vk::ImageViewCreateInfo imageViewCreateInfo( {}, {}, vk::ImageViewType::e2D, swapChainImageFormat, {}, { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } ); +// START c +imageViewCreateInfo.components = { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, +}; +// END c ---- If you were working on a stereographic 3D application, then you would create a @@ -112,27 +181,45 @@ tutorial, but even those use cases can be rendered to with these same structures and techniques we describe here. Next, set up the loop that iterates over all the swap chain images and add -them to our structure. +them to our structure: -[,c++] +[source,multilang,c++,c] ---- +// START c++ for (auto image : swapChainImages) { imageViewCreateInfo.image = image; } +// END c++ + +// START c +for (auto image : swapChainImages) { + imageViewCreateInfo.image = image; +} +// END c ---- Creating the image view is now a matter of calling `vkCreateImageView`: -[,c++] +[source,multilang,c++,c] ---- +// 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; swapChainImageViews.emplace_back( device, imageViewCreateInfo ); } +// 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. -That requires one more step, known as a framebuffer. +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. That requires one more step, +known as a *framebuffer*. In the xref:/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[next chapters,] we'll have to set up the graphics pipeline. link:/attachments/07_image_views.cpp[C{pp} code] 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 c27cbe05..de2e8298 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 @@ -194,7 +194,7 @@ Now we just need to pass these per-vertex colors to the fragment shader so it can output their interpolated values to the framebuffer. Add an output for color to the vertex shader and write to it in the `vertMain` function: -[,glsl] +[,slang] ---- struct VertexOutput { float3 color; @@ -212,7 +212,7 @@ VertexOutput vertMain(uint vid : SV_VertexID) { Next, we need to add a matching parameter in the fragment shader: -[,glsl] +[,slang] ---- [shader("fragment")] float4 fragMain(VertexOutput inVert) : SV_Target @@ -349,7 +349,7 @@ Then you can add the Slang build step to your target like this: [,cmake] ---- -add_slang_shader_target( foo SOURCES ${SHADER_SLANG_SOURCES}) +add_slang_shader_target(foo SOURCES ${SHADER_SLANG_SOURCES}) add_dependencies(bar foo) ---- @@ -363,14 +363,16 @@ write a simple helper function to load the binary data from the files. ---- #include -... +/* ... */ -static std::vector readFile(const std::string& filename) { - std::ifstream file(filename, std::ios::ate | std::ios::binary); +static std::vector readFile(const std::string &filename) +{ + std::ifstream file(filename, std::ios::ate | std::ios::binary); - if (!file.is_open()) { - throw std::runtime_error("failed to open file!"); - } + if (!file.is_open()) + { + throw std::runtime_error("failed to open file!"); + } } ---- @@ -412,8 +414,9 @@ of the two shaders: [,c++] ---- -void createGraphicsPipeline() { - auto shaderCode = readFile("shaders/slang.spv"); +void createGraphicsPipeline() +{ + auto shaderCode = readFile("shaders/slang.spv"); } ---- @@ -428,36 +431,72 @@ Before we can pass the code to the pipeline, we have to wrap it in a `VkShaderModule` object. Let's create a helper function `createShaderModule` to do that. -[,c++] +[source,multilang,c++,c] ---- -[[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector& code) const { +// START c++ +[[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector &code) const +{ + +} +// END c++ + +// START c +[[nodiscard]] VkShaderModule createShaderModule(const std::vector &code) const +{ } +// END c ---- The function will take a buffer with the bytecode as parameter and create a `VkShaderModule` from it. -Creating a shader module is straightforward, we only need to specify a pointer to the -buffer with the bytecode and the length of it. This information is specified in -a `VkShaderModuleCreateInfo` structure. The one catch is that the size of the -bytecode is specified in bytes, but the bytecode pointer is a `uint32_t` pointer -rather than a `char` pointer. Therefore, we will need to cast the pointer with -`reinterpret_cast` as shown below. When you perform a cast like this, you also -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. +Creating a shader module is straightforward, we only need to specify a pointer +to the buffer with the bytecode and the length of it. This information is +specified in a `VkShaderModuleCreateInfo` structure. The one catch is that the +size of the bytecode is specified in bytes, but the bytecode pointer is a +`uint32_t` pointer rather than a `char` pointer. Therefore, we will need to cast +the pointer with `reinterpret_cast` as shown below. When you perform a cast like +this, you also 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. The `VkShaderModuleCreateInfo` struct should look +something like this: + +[source,multilang,c++,c] +---- +// START c++ +vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size() * sizeof(char), + .pCode = reinterpret_cast(code.data()), +}; -[,c++] ----- -vk::ShaderModuleCreateInfo createInfo{ .codeSize = code.size() * sizeof(char), .pCode = reinterpret_cast(code.data()) }; +// END c++ + +// START c +VkShaderModuleCreateInfo createInfo{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .codeSize = code.size() * sizeof(char), + .pCode = reinterpret_cast(code.data()), +}; +// END c ---- The `VkShaderModule` can then be created with a call to `vkCreateShaderModule`: -[,c++] +[source,multilang,c++,c] ---- -vk::raii::ShaderModule shaderModule{ device, createInfo }; +// 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 @@ -466,19 +505,40 @@ allocators and handle output variable. The buffer with the code can be freed immediately after creating the shader module. Remember to return the created shader module: -[,c++] +[source,multilang,c++,c] ---- +// START c++ return shaderModule; +// END c++ + +// START c +return shaderModule; +// END c ---- -Shader modules are just a thin wrapper around the shader bytecode that we've previously loaded from a file and the functions defined in it. -The compilation and linking of the SPIR-V bytecode to machine code for execution by the GPU doesn't happen until the graphics pipeline is created. -That means that we're allowed to destroy the shader modules again as soon as pipeline creation is finished, which is why we'll make them local variables in the `createGraphicsPipeline` function instead of class members: +Shader modules are just a thin wrapper around the shader bytecode that we've +previously loaded from a file and the functions defined in it. The compilation +and linking of the SPIR-V bytecode to machine code for execution by the GPU +doesn't happen until the graphics pipeline is created. That means that we're +allowed to destroy the shader modules again as soon as pipeline creation is +finished, which is why we'll make them local variables in the +`createGraphicsPipeline` function instead of class members: -[,c++] +[source,multilang,c++,c] ---- -void createGraphicsPipeline() { - vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); +// START c++ +void createGraphicsPipeline() +{ + vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); +} +// END c++ + +// START c +void createGraphicsPipeline() +{ + VkShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); +} +// END c ---- == Shader stage creation @@ -488,47 +548,83 @@ pipeline stage through `VkPipelineShaderStageCreateInfo` structures as part of the actual pipeline creation process. We'll start by filling in the structure for the vertex shader, again in the -`createGraphicsPipeline` function. +`createGraphicsPipeline` function: -[,c++] +[source,multilang,c++,c] ---- -vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, .module = shaderModule, .pName = "vertMain" }; +// START c++ +vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = shaderModule, + .pName = "vertMain", +}; +// END c++ + +// START c +VkPipelineShaderStageCreateInfo vertShaderStageInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = shaderModule, + .pName = "vertMain", +}; +// END c ---- -The first two parameters are the flags and the stage that we're operating -in. The next two parameters specify the shader module containing the code, and -the function to invoke, known as the _entrypoint_. -That means that it's possible to combine multiple fragment shaders into a -single shader module and use different entry points to differentiate between - their behaviors. +The first two parameters are the flags and the stage that we're operating in. +The next two parameters specify the shader module containing the code, and the +function to invoke, known as the _entrypoint_. That means that it's possible to +combine multiple fragment shaders into a single shader module and use different +entry points to differentiate between their behaviors. -There is one more (optional) member, `pSpecializationInfo`, which we won't - be using here, but is worth discussing. It allows you to specify values for - shader constants. You can use a single shader module where its behavior can - be configured in pipeline creation by specifying different values for the - constants used in it. -This is more efficient than configuring the shader using variables at render - time, because the compiler can do optimizations like eliminating `if` - statements that depend on these values. -If you don't have any constants like that, then you can set the member to -`nullptr`, which our struct initialization does automatically. +There is one more (optional) member, `pSpecializationInfo`, which we won't be +using here, but is worth discussing. It allows you to specify values for shader +constants. You can use a single shader module where its behavior can be +configured in pipeline creation by specifying different values for the constants +used in it. This is more efficient than configuring the shader using variables +at render time, because the compiler can do optimizations like eliminating `if` +statements that depend on these values. If you don't have any constants like +that, then you can set the member to `nullptr`, which our struct initialization +does automatically. Modifying the structure to suit the fragment shader is easy: -[,c++] +[source,multilang,c++,c] ---- -vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, .module = shaderModule, .pName = "fragMain" }; +// START c++ +vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = shaderModule, + .pName = "fragMain", +}; +// END c++ + +// START c +VkPipelineShaderStageCreateInfo fragShaderStageInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_FRAGMENT_BIT, + .module = shaderModule, + .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. +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] ---- +// 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. -In the xref:./02_Fixed_functions.adoc[next chapter,] we'll look at the fixed-function stages. +In the xref:./02_Fixed_functions.adoc[next chapter,] we'll look at the +fixed-function stages. link:/attachments/09_shader_modules.cpp[C{pp} code] / link:/attachments/09_shader_base.slang[Slang shader] / 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 3df936c9..080f8d75 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 @@ -3,343 +3,568 @@ = Fixed functions The older graphics APIs provided the default state for most of the stages of the -graphics pipeline. In Vulkan, you have to be explicit about most pipeline states as -it'll be baked into an immutable pipeline state object. In this chapter, we'll fill -in all the structures to configure these fixed-function operations. - -== Dynamic state - -While *most* of the pipeline state needs to be baked into the pipeline state, -a limited amount of the state *can* actually be changed without recreating the -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 `VkPipelineDynamicStateCreateInfo` structure like this: - -[,c++] ----- -std::vector dynamicStates = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor -}; - -vk::PipelineDynamicStateCreateInfo dynamicState{ .dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data() }; ----- - -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. -This results in a more flexible setup and is widespread for things like viewport and scissor state, which would result in a more complex setup when being baked into the pipeline state. +graphics pipeline. In Vulkan, you have to be explicit about most pipeline states +as it'll be baked into an immutable pipeline state object. In this chapter, +we'll fill in all the structures to configure these fixed-function operations. == Vertex input -The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the vertex data that will be passed to the vertex shader. -It describes this in roughly two ways: +The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the +vertex data that will be passed to the vertex shader. It describes this in +roughly two ways: -* Bindings: spacing between data and whether the data is per-vertex or per-instance (see https://en.wikipedia.org/wiki/Geometry_instancing[instancing]) -* Attribute descriptions: type of the attributes passed to the vertex shader, which binding to load them from and at which offset +* Bindings: spacing between data and whether the data is per-vertex or +per-instance (see https://en.wikipedia.org/wiki/Geometry_instancing[instancing]) +* Attribute descriptions: type of the attributes passed to the vertex shader, +which binding to load them from and at which offset -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. +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++] +Add this structure to the `createGraphicsPipeline` function right after the +`shaderStages` array: + +[source,multilang,c++,c] ---- +// START c++ vk::PipelineVertexInputStateCreateInfo vertexInputInfo; +// END c++ + +// START c +VkPipelineVertexInputStateCreateInfo vertexInputInfo{ + .sType = 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. -Add this structure to the `createGraphicsPipeline` function right after the `shaderStages` array. +The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members +point to an array of structs that describe the aforementioned details for +loading vertex data. == Input assembly -The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what kind of geometry will be drawn from the vertices and if primitive restart should be enabled. -The former is specified in the `topology` member and can have values like: +The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what +kind of geometry will be drawn from the vertices and if primitive restart should +be enabled. The former is specified in the `topology` member and can have values +like: * `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: points from vertices * `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: line from every two vertices without reuse -* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as start vertex for the next line -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every three vertices without reuse -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP`: the second and third vertex of every triangle is used as first two vertices of the next triangle - -Normally, the vertices are loaded from the vertex buffer by index in sequential order, but with an _element buffer_ you can specify the indices to use yourself. -This allows you to perform optimizations like reusing vertices. -If you set the `primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up lines and triangles in the `_STRIP` topology modes by using a special index of `0xFFFF` or `0xFFFFFFFF`. - -We intend to draw triangles throughout this tutorial, so we'll stick to the following data for the structure: +* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as +start vertex for the next line +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every three vertices +without reuse +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP`: the second and third vertex of every +triangle is used as first two vertices of the next triangle + +Normally, the vertices are loaded from the vertex buffer by index in sequential +order, but with an _element buffer_ you can specify the indices to use yourself. +This allows you to perform optimizations like reusing vertices. If you set the +`primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up +lines and triangles in the `_STRIP` topology modes by using a special index of +`0xFFFF` or `0xFFFFFFFF`. + +We intend to draw triangles throughout this tutorial, so we'll stick to the +following data for the structure: + +[source,multilang,c++,c] +---- +// START c++ +vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, +}; +// END c++ -[,c++] ----- -vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ .topology = vk::PrimitiveTopology::eTriangleList }; +// START c +VkPipelineInputAssemblyStateCreateInfo inputAssembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = VK_FALSE, +}; +// END c ---- -== Viewports and scissors +== Dynamic state -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. +Before jumping into the viewport configuration, the concept of _dynamice state_ +has to be introduced. While *most* of the pipeline state needs to be baked, a +limited amount of the state *can* actually be changed without recreating the +pipeline at draw time. Examples are the size of the viewport, line width, and +blend constants. Such state is referred to as *dynamic state*. -[,c++] ----- -vk::Viewport{ 0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f }; ----- +Particularly, 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. +While the former is more in line with the other states, it's often convenient to +make viewport and scissor state dynamic as it gives you a lot more flexibility. +This is widespread and all implementations can handle this dynamic state without +a performance penalty. -Remember that the size of the swap chain and its images may differ from the `WIDTH` and `HEIGHT` of the window. -The swap chain images will be used as framebuffers later on, so we should stick to their size. +With dynamic state, it's even possible to specify different viewports and/or +scissor rectangles within a single _command buffer_ (this will be explained +in a future chapter). -The `minDepth` and `maxDepth` values specify the range of depth values to use for the framebuffer. -These values must be within the `[0.0f, 1.0f]` range, but `minDepth` may be higher than `maxDepth`. -If you aren't doing anything special, then you should stick to the standard values of `0.0f` and `1.0f`. +Since, in this tutorial, we will opt for setting the viewport and its scissor +rectangle as dynamic states, a `VkPipelineDynamicStateCreateInfo` structure in +the `createGraphicsPipeline` function has to be filled as such: -While viewports define the transformation from the image to the framebuffer, scissor rectangles define in which region pixels will actually be stored. -The rasterizer will discard any pixels outside the scissored rectangles. -They function like a filter rather than a transformation. -The difference is illustrated below. -Note that the left scissored rectangle is just one of the many possibilities that would result in that image, as long as it's larger than the viewport. +[source,multilang,c++,c] +---- +// START c++ +std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, +}; -image::/images/viewports_scissors.png[] +vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data(), +}; +// END c++ -So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: +// START c +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, +} -[,c++] ----- -vk::Rect2D{ vk::Offset2D{ 0, 0 }, swapChainExtent } +VkPipelineDynamicStateCreateInfo dynamicState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data(), +}; +// 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. -While the former is more in line with the other states, it's often convenient to make viewport and scissor state dynamic as it gives you a lot more flexibility. -This is widespread and all implementations can handle this dynamic state without a performance penalty. +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. This results in a more +flexible setup and is widespread for things like viewport and scissor state, +which would result in a more complex setup when being baked into the pipeline +state (recreating a new pipeline for each viewport change). -When opting for dynamic viewport(s) and scissor rectangle(s), you need to -enable the respective dynamic states for the pipeline: +This dynamic state struct variable will be passed, later on, to a +`VkGraphicsPipelineCreateInfo` which is used to create the pipeline. -[,c++] ----- -std::vector dynamicStates = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor +== Viewports and scissors + +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. Since we have set the viewport +as a dynamic state, we don't need to declare a `VkViewport` struct variable. But +in case you want to bake the viewport into the pipeline state, you can, at this +point, declare a `VkViewport` struct as such: + +[source,multilang,c++,c] +---- +// START c++ +vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChainExtent.width), + .height = static_cast(swapChainExtent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, +}; +// END c++ + +// START c +VkViewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChainExtent.width), + .height = static_cast(swapChainExtent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, }; -vk::PipelineDynamicStateCreateInfo dynamicState({}, dynamicStates.size(), dynamicStates.data()); +// END c ---- -And then you only need to specify their count at pipeline creation time: +Remember that the size of the swap chain and its images may differ from the +`WIDTH` and `HEIGHT` of the window. The swap chain images will be used as +framebuffers later on, so we should stick to their size. -[,c++] ----- -vk::PipelineViewportStateCreateInfo viewportState({}, 1, {}, 1); ----- +The `minDepth` and `maxDepth` fields specify the range of depth values to use +for the framebuffer. These values must be within the `[0.0f, 1.0f]` range, but +`minDepth` may be higher than `maxDepth`. If you aren't doing anything special, +then you should stick to the standard values of `0.0f` and `1.0f`. -The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. +While viewports define the transformation from the image to the framebuffer, +scissor rectangles define in which region pixels will actually be stored. The +rasterizer will discard any pixels outside the scissored rectangles. They +function like a filter rather than a transformation. The difference is +illustrated below. Note that the left scissored rectangle is just one of the +many possibilities that would result in that image, as long as it's larger than +the viewport. -With dynamic state, it's even possible to specify different viewports and or scissor rectangles within a single command buffer. +image::/images/viewports_scissors.png[] -Without dynamic state, the viewport and scissor rectangle need to be set in the pipeline using the `VkPipelineViewportStateCreateInfo` struct. -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. +So if we wanted to draw to the entire framebuffer, we would specify a scissor +rectangle that covers it entirely. Since we are going to set the scissor as a +dynamic state, we don't need to declare a `VkRect2D` struct variable. But in +case you want to bake it into the pipeline state, you can, at this point, +declare a `VkRect2D` struct as such: -[,c++] ----- -vk::PipelineViewportStateCreateInfo viewportState{ .viewportCount = 1, .scissorCount = 1 }; +[source,multilang,c++,c] ---- +// START c++ +vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChainExtent, +}; +// 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. -Using multiple requires enabling a GPU feature (see logical device creation). - -== Rasterizer +// START c +VkRect2D scissor{ + .offset = {0, 0}, + .extent = swapChainExtent, +}; +// END c +---- -The rasterizer takes the geometry shaped by the vertices from the vertex shader and turns it into fragments to be colored by the fragment 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 `VkPipelineRasterizationStateCreateInfo` structure. +Next, we have to fill a `VkPipelineViewportStateCreateInfo` as such: -[,c++] +[source,multilang,c++,c] ---- -vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False); +// START c++ +vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + /* .pViewports = &viewport, omitted because of dynamic state */ + .scissorCount = 1, + /* .pScissors = &scissor, omitted because of dynamic state */ +}; +// END c++ + +// START c +VkPipelineViewportStateCreateInfo viewportState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .viewportCount = 1, + .scissorCount = 1, +}; +// END c ---- -If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond -the near and far planes are clamped to them as opposed to discarding them. -This is useful in some special cases like shadow maps. -Using this requires enabling a GPU feature. +Without dynamic state, the viewport and scissor rectangle need to be set in the +pipeline using the struct's `pViewports` and `pScissors` fields, respectively. +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. Since we are opting for using the dynamic state, we omit the +`pViewports` and `pScissors` fields. -[,c++] ----- -vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False); ----- +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. Using multiple viewports and scissor rectangles requires enabling +a GPU feature (see logical device creation). -If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes through the rasterizer stage. -This basically disables any output to the framebuffer. +== Rasterizer -[,c++] ----- -vk::PipelineRasterizationStateCreateInfo rasterizer{ .depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, - .polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eBack, - .frontFace = vk::FrontFace::eClockwise, .depthBiasEnable = vk::False, - .depthBiasSlopeFactor = 1.0f, .lineWidth = 1.0f }; +The rasterizer takes the geometry shaped by the vertices from the vertex shader +and turns it into fragments to be colored by the fragment 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 +`VkPipelineRasterizationStateCreateInfo` structure: + +[source,multilang,c++,c] +---- +// START c++ +vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = vk::False, + .rasterizerDiscardEnable = vk::False, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eClockwise, + .depthBiasEnable = vk::False, + .depthBiasSlopeFactor = 1.0f, + .lineWidth = 1.0f, +}; +// END c++ + +// START c +VkPipelineRasterizationStateCreateInfo rasterizer{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_BACK_BIT, + .frontFace = VK_FRONT_FACE_CLOCKWISE, + .depthBiasEnable = VK_FALSE, + .depthBiasSlopeFactor = 1.0f, + .lineWidth = 1.0f, +}; +// END c ---- -The `polygonMode` determines how fragments are generated for geometry. -The following modes are available: +If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond the +near and far planes are clamped to them as opposed to discarding them. This is +useful in some special cases like shadow maps. Using this requires enabling a +GPU feature. + +If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes +through the rasterizer stage. This basically disables any output to the +framebuffer. + +The `polygonMode` field determines how fragments are generated for geometry. The +following modes are available: * `VK_POLYGON_MODE_FILL`: fill the area of the polygon with fragments * `VK_POLYGON_MODE_LINE`: polygon edges are drawn as lines * `VK_POLYGON_MODE_POINT`: polygon vertices are drawn as points - ++ Using any mode other than fill requires enabling a GPU feature. -[,c++] ----- -rasterizer.lineWidth = 1.0f; ----- +The `cullMode` field determines the type of face culling to use. You can disable +culling, cull the front faces, cull the back faces or both. -The `lineWidth` member is straightforward, it describes the thickness of lines in terms of number of fragments. -The maximum line width that is supported depends on the hardware and any line thicker than `1.0f` requires you to enable the `wideLines` GPU feature. +The `frontFace` field specifies the vertex order for the faces to be considered +front-facing and can be clockwise or counterclockwise. -[,c++] ----- -vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False, vk::PolygonMode::eFill, - vk::CullModeFlagBits::eBack, vk::FrontFace::eClockwise); ----- - -The `cullMode` variable determines the type of face culling to use. -You can disable culling, cull the front faces, cull the back faces or both. -The `frontFace` variable specifies the vertex order for the faces to be considered front-facing and can be clockwise or counterclockwise. +The rasterizer can alter the depth values by adding a constant value or biasing +them based on a fragment's slope. This is sometimes used for shadow mapping, but +we won't be using it. Just set `depthBiasEnable` to `VK_FALSE`. -[,c++] ----- -vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False, vk::PolygonMode::eFill, - vk::CullModeFlagBits::eBack, vk::FrontFace::eClockwise, vk::False); ----- - -The rasterizer can alter the depth values by adding a constant value or biasing them based on a fragment's slope. -This is sometimes used for shadow mapping, but we won't be using it. -Just set `depthBiasEnable` to `VK_FALSE`. +The `lineWidth` field is straightforward, it describes the thickness of lines in +terms of number of fragments. The maximum line width that is supported depends +on the hardware and any line thicker than `1.0f` requires you to enable the +`wideLines` GPU feature. == Multisampling -The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, which is one of the ways to perform https://en.wikipedia.org/wiki/Multisample_anti-aliasing[antialiasing]. -It works by combining the fragment shader results of multiple polygons that rasterize to the same pixel. -This mainly occurs along edges, which is also where the most noticeable aliasing artifacts occur. -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. +The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, +which is one of the ways to perform +https://en.wikipedia.org/wiki/Multisample_anti-aliasing[antialiasing]. It works +by combining the fragment shader results of multiple polygons that rasterize to +the same pixel. This mainly occurs along edges, which is also where the most +noticeable aliasing artifacts occur. 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. For the moment, the +struct can be filled as such: + +[source,multilang,c++,c] +---- +// START c++ +vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = vk::False, +}; +// END c++ -[,c++] ----- -vk::PipelineMultisampleStateCreateInfo multisampling{.rasterizationSamples = vk::SampleCountFlagBits::e1, .sampleShadingEnable = vk::False}; +// START c +VkPipelineMultisampleStateCreateInfo multisampling{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = VK_FALSE, +}; +// END c ---- We'll revisit multisampling in later chapter, for now let's keep it disabled. == Depth and stencil testing -If you are using a depth and/or stencil buffer, then you also need to configure the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. -We don't have one right now, so we can simply pass a `nullptr` instead of a pointer to such a struct. -We'll get back to it in the depth buffering chapter. +If you are using a depth and/or stencil buffer, then you also need to configure +the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. +We don't have one right now, so we can simply pass a `nullptr` instead of a +pointer to such a struct. We'll get back to it in the depth buffering chapter. == Color blending -After a fragment shader has returned a color, it needs to be combined with the color that is already in the framebuffer. -This transformation is known as color blending, and there are two ways to do it: +After a fragment shader has returned a color, it needs to be combined with the +color that is already in the framebuffer. This transformation is known as color +blending, and there are two ways to do it: * Mix the old and new value to produce a final color * Combine the old and new value using a bitwise operation -There are two types of structs to configure color blending. -The first struct, `VkPipelineColorBlendAttachmentState` contains the configuration per attached framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` contains the _global_ color blending settings. -In our case, we only have one framebuffer: +There are two types of structs to configure color blending. The first struct, +`VkPipelineColorBlendAttachmentState` contains the configuration per attached +framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` +contains the _global_ color blending settings. In our case, we only have one +framebuffer: -[,c++] +[source,multilang,c++,c] ---- -vk::PipelineColorBlendAttachmentState colorBlendAttachment; -colorBlendAttachment.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; -colorBlendAttachment.blendEnable = vk::False; +// START c++ +vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = vk::False, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, +}; +// END c++ + +// START c +VkPipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .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. -The operations that will be performed are best demonstrated using the following pseudocode: +This per-framebuffer struct allows you to configure the first way of color +blending. The operations that will be performed are best demonstrated using the +following pseudocode: [,c++] ---- -if (blendEnable) { - finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb); - finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a); -} else { - finalColor = newColor; +if (blendEnable) +{ + finalColor.rgb = + (srcColorBlendFactor * newColor.rgb)(dstColorBlendFactor * oldColor.rgb); + finalColor.a = + (srcAlphaBlendFactor * newColor.a)(dstAlphaBlendFactor * oldColor.a); +} +else +{ + finalColor = newColor; } finalColor = finalColor & colorWriteMask; ---- -If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment shader is passed through unmodified. -Otherwise, the two mixing operations are performed to compute a new color. -The resulting color is AND'd with the `colorWriteMask` to determine which channels are actually passed through. +If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment +shader is passed through unmodified. Otherwise, the two mixing operations are +performed to compute a new color. The resulting color is AND'd with the +`colorWriteMask` to determine which channels are actually passed through. In +our case, we don't want to filter any channels hence why we passed an OR'd set +of RGBA bits. -The most common way to use color blending is to implement alpha blending, where we want the new color to be blended with the old color based on its opacity. +The most common way to use color blending is to implement alpha blending, where +we want the new color to be blended with the old color based on its opacity. The `finalColor` should then be computed as follows: [,c++] ---- finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; -finalColor.a = newAlpha.a; +finalColor.a = newAlpha.a; ---- This can be achieved with the following parameters: -[,c++] +[source,multilang,c++,c] ---- -colorBlendAttachment.blendEnable = vk::True; +// START c++ +colorBlendAttachment.blendEnable = vk::True; colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; -colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; +colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eZero; -colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; ----- - -You can find all the possible operations in the `VkBlendFactor` and `VkBlendOp` 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. +colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; +// END c++ + +// START c +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; +// END c +---- + +You can find all the possible operations in the `VkBlendFactor` and `VkBlendOp` +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: + +[source,multilang,c++,c] +---- +// START c++ +vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = vk::False, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, +}; +// END c++ -[,c++] ----- -vk::PipelineColorBlendStateCreateInfo colorBlending{.logicOpEnable = vk::False, .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment }; +// START c +VkPipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .attachmentCount = 1, + .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`. -The bitwise operation can then be specified in the `logicOp` field. -Note that this will automatically disable the first method, as if you had set `blendEnable` to `VK_FALSE` for every attached framebuffer! -The `colorWriteMask` will also be used in this mode to determine which channels in the framebuffer will actually be affected. -It is also possible to disable both modes, as we've done here, in which case the fragment colors will be written to the framebuffer unmodified. +If you want to use the second method of blending (a bitwise combination), then +you should set `logicOpEnable` to `VK_TRUE`. The bitwise operation can then be +specified in the `logicOp` field. Note that this will automatically disable the +first method, as if you had set `blendEnable` to `VK_FALSE` for every attached +framebuffer! The `colorWriteMask` will also be used in this mode to determine +which channels in the framebuffer will actually be affected. It is also possible +to disable both modes, as we've done here, in which case the fragment colors +will be written to the framebuffer unmodified. == Pipeline layout -You can use `uniform` values in shaders, which are globals similar to dynamic state variables that can be changed at drawing time to alter the behavior of your shaders without having to recreate them. -They are commonly used to pass the transformation matrix to the vertex shader, or to create texture samplers in the fragment shader. +You can use `uniform` values in shaders, which are globals similar to dynamic +state variables that can be changed at drawing time to alter the behavior of +your shaders without having to recreate them. They are commonly used to pass the +transformation matrix to the vertex shader, or to create texture samplers in the +fragment shader. -These uniform values need to be specified during pipeline creation by creating a `VkPipelineLayout` object. -Even though we won't be using them until a future chapter, we are still required to create an empty pipeline layout. +These uniform values need to be specified during pipeline creation by creating +a `VkPipelineLayout` object. Even though we won't be using them until a future +chapter, we are still required to create an empty pipeline layout. -Create a class member to hold this object because we'll refer to it from other functions at a later point in time: +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] ---- +// 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] ---- -vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 0, .pushConstantRangeCount = 0 }; +// START c++ +vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 0, + .pushConstantRangeCount = 0, +}; + +pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); +// END c++ -pipelineLayout = vk::raii::PipelineLayout( device, pipelineLayoutInfo ); +// START c +VkPipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 0, + .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. +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. == Conclusion -That's it for all the fixed-function state! -It's a lot of work to set all of this up from scratch, but the advantage is that we're now nearly fully aware of everything that is going on in the graphics pipeline! -This reduces the chance of running into unexpected behavior because the default state of certain components is not what you expect. +That's it for all the fixed-function state! It's a lot of work to set all of +this up from scratch, but the advantage is that we're now nearly fully aware of +everything that is going on in the graphics pipeline! This reduces the chance of +running into unexpected behavior because the default state of certain components +is not what you expect. -There is, however, one more object to create before we can finally create the graphics pipeline, and that is a xref:./03_Render_passes.adoc[render pass]. +There is, however, one more object to create before we can finally create the +graphics pipeline, and that is a xref:./03_Render_passes.adoc[render pass]. link:/attachments/10_fixed_functions.cpp[C{pp} code] / link:/attachments/09_shader_base.slang[Slang shader] / diff --git a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.adoc b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.adoc index 22ee9e00..b370ca4b 100644 --- a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.adoc +++ b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.adoc @@ -4,40 +4,96 @@ == Introduction -In previous versions of Vulkan, before we could finish creating the pipeline, we needed to tell Vulkan about the framebuffer attachments that would be used while rendering through a render pass object. However, with the introduction of dynamic rendering in Vulkan 1.3, we can now specify this information directly when creating the graphics pipeline and when recording command buffers. +In previous versions of Vulkan, before we could finish creating the pipeline, we +needed to tell Vulkan about the framebuffer attachments that would be used while +rendering through a render pass object. However, with the introduction of +dynamic rendering in Vulkan 1.3, we can now specify this information directly +when creating the graphics pipeline and when recording command buffers. -Dynamic rendering simplifies the rendering process by eliminating the need for render pass and framebuffer objects. Instead, we can specify the color, depth, and stencil attachments directly when we begin rendering. +Dynamic rendering simplifies the rendering process by eliminating the need for +render pass and framebuffer objects. Instead, we can specify the color, depth, +and stencil attachments directly when we begin rendering. == Pipeline Rendering Create Info -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: +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] ---- -vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat }; +// START c++ +vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, +}; +// END c++ + +// START c +VkPipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, +}; +// 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 `pNext` chain of the `vk::GraphicsPipelineCreateInfo` structure: +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 `pNext` +chain of the `vk::GraphicsPipelineCreateInfo` structure: -[,c++] +[source,multilang,c++,c] ---- -vk::GraphicsPipelineCreateInfo pipelineInfo{ .pNext = &pipelineRenderingCreateInfo, - .stageCount = 2, .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, .layout = pipelineLayout, .renderPass = nullptr }; +// START c++ +vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = nullptr, +}; +// END c++ + +// START c +VkGraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = VK_NULL_HANDLE, +}; +// END c ---- -Note that the `renderPass` parameter is set to `nullptr` because we're using dynamic rendering instead of a traditional render pass. +Note that the `renderPass` parameter is set to `nullptr` because we're using +dynamic rendering instead of a traditional render pass. == Command Buffer Recording -When recording command buffers, we'll use the `vk::CommandBuffer::beginRendering` function to start rendering to the specified attachments. We'll cover this in more detail in the Drawing chapter. +When recording command buffers, we'll use the +`vk::CommandBuffer::beginRendering` function to start rendering to the specified +attachments. We'll cover this in more detail in the Drawing chapter. -The advantage of dynamic rendering is that it simplifies the rendering process by eliminating the need for render pass and framebuffer objects. It also provides more flexibility by allowing us to change the attachments we're rendering to without creating new render pass objects. +The advantage of dynamic rendering is that it simplifies the rendering process +by eliminating the need for render pass and framebuffer objects. It also +provides more flexibility by allowing us to change the attachments we're +rendering to without creating new render pass objects. -In the xref:./04_Conclusion.adoc[next chapter], we'll put everything together to finally create the graphics pipeline object! +In the xref:./04_Conclusion.adoc[next chapter], we'll put everything together to +finally create the graphics pipeline object! link:/attachments/12_graphics_pipeline_complete.cpp[C{pp} code] / link:/attachments/09_shader_base.slang[Slang shader] / diff --git a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.adoc b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.adoc index 3fa74e82..a709be0b 100644 --- a/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.adoc +++ b/en/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.adoc @@ -2,94 +2,171 @@ = Conclusion -We can now combine all the structures and objects from the previous chapters to create the graphics pipeline! -Here are the types of objects we have now, as a quick recap: - -* Shader stages: the shader modules that define the functionality of the programmable stages of the graphics pipeline -* Fixed-function state: all the structures that define the fixed-function stages of the pipeline, like input assembly, rasterizer, viewport and color blending -* Pipeline layout: the uniform and push values referenced by the shader that can be updated at draw time -* Dynamic rendering: the formats of the attachments that will be used during rendering - -All of these combined fully define the functionality of the graphics pipeline, so we can now begin filling in the `VkGraphicsPipelineCreateInfo` structure at the end of the `createGraphicsPipeline` function. -But before the calls to `vkDestroyShaderModule` because these are still to be used during the creation. - -[,c++] +We can now combine all the structures and objects from the previous chapters to +create the graphics pipeline! Here are the types of objects we have now, as a +quick recap: + +* Shader stages: the shader modules that define the functionality of the +programmable stages of the graphics pipeline +* Fixed-function state: all the structures that define the fixed-function stages +of the pipeline, like input assembly, rasterizer, viewport and color blending +* Pipeline layout: the uniform and push values referenced by the shader that can +be updated at draw time +* Dynamic rendering: the formats of the attachments that will be used during +rendering + +All of these combined fully define the functionality of the graphics pipeline, +so we can now finally create the graphics pipeline object at the end of the +`createGraphicsPipeline` function (but, if you are using the C-API, before the +calls to `vkDestroyShaderModule` because these are still needed during the +creation). + +Recall from the previous chapter the `VkGraphicsPipelineCreateInfo` we filled: + +[source,multilang,c++,c] ---- -vk::GraphicsPipelineCreateInfo pipelineInfo({}, 2, shaderStages); +// START c++ +vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = nullptr, +}; +// END c++ + +// START c +VkGraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = VK_NULL_HANDLE, +}; +// END c ---- -We start by referencing the array of `VkPipelineShaderStageCreateInfo` structs. +The `pNext` field is used to chain structs and is assigned with a pointer to a +`VkPipelineRenderingCreateInfo` struct because, as discussed in earlier chapter, +we are using dynamic rendering instead of a traditional render pass. This +`VkPipelineRenderingCreateInfo` structure specifies the formats of the +attachments that will be used during rendering. -[,c++] ----- -vk::GraphicsPipelineCreateInfo pipelineInfo({}, 2, shaderStages, &vertexInputInfo, &inputAssembly, {}, &viewportState, &rasterizer, &multisampling, {}, &colorBlending, - &dynamicState); ----- +Then we reference the array of `VkPipelineShaderStageCreateInfo` structs and its +count in the `pStages` and `stageCount`, respectively. -Then we reference all the structures describing the fixed-function stage. +Then we reference all the structures describing the fixed-function stages in +the `pVertexInputState`, `pInputAssemblyState`, `pViewportState`, +`pRasterizationState`, `pMultisampleState`, and `pColorBlendState` fields. -[,c++] ----- -vk::GraphicsPipelineCreateInfo pipelineInfo({}, 2, shaderStages, &vertexInputInfo, &inputAssembly, {}, &viewportState, &rasterizer, &multisampling, {}, &colorBlending, - &dynamicState, *pipelineLayout); ----- +The `pDynamicState` field is set to pointer of the +`VkPipelineDynamicStateCreateInfo` that describes the pipeline's dynamic states +(viewport and its scissor rectangle). -After that comes the pipeline layout, which is a Vulkan handle rather than a struct pointer. +The `layout` field is the pipeline layout which is simply a Vulkan handle rather +than a struct pointer. -[,c++] ----- -vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat }; -vk::GraphicsPipelineCreateInfo pipelineInfo{ .pNext = &pipelineRenderingCreateInfo, - .stageCount = 2, .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, .layout = pipelineLayout, .renderPass = nullptr }; ----- +And the `renderPass` field is set to `nullptr` because we are using dynamic +rendering. -Note that we're using dynamic rendering instead of a traditional render pass, so we set the `renderPass` parameter to `nullptr` and include a `vk::PipelineRenderingCreateInfo` structure in the `pNext` chain. This structure specifies the formats of the attachments that will be used during rendering. +There are actually two more parameters that we have omitted: +`basePipelineHandle` and `basePipelineIndex`. These are default initialized as +such: [,c++] ---- -pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional -pipelineInfo.basePipelineIndex = -1; // Optional +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional +pipelineInfo.basePipelineIndex = -1; // Optional ---- -There are actually two more parameters: `basePipelineHandle` and `basePipelineIndex`. -Vulkan allows you to create a new graphics pipeline by deriving from an existing pipeline. -The idea of pipeline derivatives is that it is less expensive to set up pipelines when they have much functionality in common with an existing pipeline and switching between pipelines from the same parent can also be done quicker. -You can either specify the handle of an existing pipeline with `basePipelineHandle` or reference another pipeline that is about to be created by index with `basePipelineIndex`. -Right now there is only a single pipeline, so we'll simply specify a null handle and an invalid index. -These values are only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` flag is also specified in the `flags` field of `VkGraphicsPipelineCreateInfo`. - -Now prepare for the final step by creating a class member to hold the `VkPipeline` object: - -[,c++] +Vulkan allows you to create a new graphics pipeline by deriving from an existing +pipeline. The idea of pipeline derivatives is that it is less expensive to set +up pipelines when they have much functionality in common with an existing +pipeline and switching between pipelines from the same parent can also be done +quicker. You can either specify the handle of an existing pipeline with +`basePipelineHandle` or reference another pipeline that is about to be created +by index with `basePipelineIndex`. Right now there is only a single pipeline, +so we'll simply specify a null handle and an invalid index. These values are +only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` flag is also specified in +the `flags` field of `VkGraphicsPipelineCreateInfo`. + +Now prepare for the final step by creating a class member to hold the +`VkPipeline` object: + +[source,multilang,c++,c] ---- +// START c++ vk::raii::Pipeline graphicsPipeline = nullptr; +// END c++ + +// START c +VkPipeline graphicsPipeline = VK_NULL_HANDLE; +// END c ---- And finally, create the graphics pipeline: -[,c++] +[source,multilang,c++,c] ---- +// START c++ graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); +// END c++ + +// START c +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, + &graphicsPipeline) != VK_SUCCESS) +{ + throw std::runtime_error("failed to create graphics pipeline!"); +} + +/* ... */ + +/* The graphics pipeline is required for all common drawing operations, so it should also only be + * destroyed at the end of the program */ + +void cleanup() +{ + vkDestroyPipeline(device, graphicsPipeline, nullptr); + + /* ... */ +} +// END c ---- -The `vkCreateGraphicsPipelines` function actually has more parameters than the usual object creation functions in Vulkan. -It is designed to take multiple `VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects in a single call. +The `vkCreateGraphicsPipelines` function actually has more parameters than the +usual object creation functions in Vulkan. It is designed to take multiple +`VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects +in a single call. -The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, references an optional `VkPipelineCache` object. -A pipeline cache can be used to store and reuse data relevant to pipeline creation across multiple calls to `vkCreateGraphicsPipelines` and even across program executions if the cache is stored to a file. -This makes it possible to significantly speed up pipeline creation at a later time. -We'll get into this in the pipeline cache chapter. +The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, +references an optional `VkPipelineCache` object. A pipeline cache can be used to +store and reuse data relevant to pipeline creation across multiple calls to +`vkCreateGraphicsPipelines` and even across program executions if the cache is +stored to a file. This makes it possible to significantly speed up pipeline +creation at a later time. We'll get into this in the pipeline cache chapter. The graphics pipeline is required for all common drawing operations. -Now run your program to confirm that all this hard work has resulted in a successful pipeline creation! -We are already getting quite close to seeing something pop up on the screen. +Now run your program to confirm that all this hard work has resulted in a +successful pipeline creation! We are already getting quite close to seeing +something pop up on the screen. In the xref:/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.adoc[next couple of chapters,] -we'll set up the actual framebuffers from the swap chain images and prepare the drawing commands. +we'll set up the actual framebuffers from the swap chain images and prepare the +drawing commands. link:/attachments/12_graphics_pipeline_complete.cpp[C{pp} code] / link:/attachments/09_shader_base.slang[Slang shader] /