{"slug": "how-to-vulkan-in-2026", "title": "How to Vulkan in 2026", "summary": "Sascha Willems released an updated tutorial, \"How to Vulkan in 2026,\" demonstrating modern Vulkan 1.3 features including dynamic rendering, buffer device address, descriptor indexing, and Synchronization2 to simplify graphics programming. The tutorial targets developers with basic C/C++ and real-time graphics knowledge, aiming to create a rasterized 3D scene with multiple illuminated and textured objects in a single file of a few hundred lines.", "body_md": "# How to Vulkan in 2026\n\nInfo\n\nLast updated 2026-07-16: Use vkQueueSubmit2 instead of vkQueueSubmit\n\n## Intro\n\n[This repository](https://github.com/SaschaWillems/HowToVulkan) and the accompanying tutorial demonstrate ways of writing a [Vulkan](https://vulkan.org/) graphics application in 2026. The goal is to leverage modern features to simplify Vulkan usage and, while doing so, create something more than just a basic colored triangle.\n\nVulkan was released almost 10 years ago, and a lot has changed. Version 1.0 had to make many concessions to support a broad range of GPUs across desktop and mobile. Some of the initial concepts like render passes turned out to be not so optimal, and have been replaced by alternatives. The API matured and new areas like raytracing, video acceleration and machine learning were added. Just as important as the API is the ecosystem, which also changed a lot, giving us new options for writing shaders in languages different than GLSL and tools to help with Vulkan.\n\nAnd so for this tutorial we will be using [Vulkan 1.3](https://docs.vulkan.org/refpages/latest/refpages/source/VK_VERSION_1_3.html) as a baseline. This gives us access to several features that make Vulkan easier to use while still supporting a wide range of GPUs and platforms. The ones we will be using are:\n\n[Dynamic rendering](https://www.khronos.org/blog/streamlining-render-passes)- Greatly simplifies render pass setup, one of the most criticized Vulkan areas[Buffer device address](https://docs.vulkan.org/guide/latest/buffer_device_address.html)- Lets us access buffers via pointers instead of going through descriptors[Descriptor indexing](https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_descriptor_indexing.html)- Simplifies descriptor management, often referred to as \"bindless\"[Synchronization2](https://docs.vulkan.org/guide/latest/extensions/VK_KHR_synchronization2.html)- Improves synchronization handling, one of the hardest areas of Vulkan\n\ntl;dr: Doing Vulkan in 2026 can be very different from doing Vulkan in 2016. That's what I hope to show with this.\n\nTip\n\nA list of devices supporting at least Vulkan 1.3 can be found [here](https://vulkan.gpuinfo.org/listdevices.php?apiversion=1.3).\n\n## Target audience\n\nThe tutorial is focused on writing actual Vulkan code and getting things up and running as fast as possible (possibly in an afternoon). It won't explain programming, software architecture, graphics concepts or how Vulkan works (in detail). Instead it'll often contain links to relevant information like the [Vulkan specification](https://docs.vulkan.org/). You should bring at least basic knowledge of C/C++ and real-time graphics concepts.\n\n## Goal\n\nWe focus on rasterization, other parts of Vulkan like compute or raytracing are not covered. At the end of this, we'll have multiple illuminated and textured 3D objects on screen that can be rotated using the mouse ([screenshot](images/screenshot.png)). The source comes in a single file with a few hundred lines of code, no abstractions, no hard-to-read modern C++ language constructs, and no object-oriented shenanigans. I believe that being able to follow source code from top to bottom without having to go through multiple layers of abstractions makes it much easier to follow.\n\n## License\n\nCopyright (c) 2025-2026, [Sascha Willems](https://www.saschawillems.de). The contents of this document are licensed under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) license. Source code listings and files are licensed under the MIT license.\n\n## Libraries\n\nVulkan is an explicit low-level API. Writing code for it can be very verbose. To concentrate on the interesting parts we'll be using the following libraries:\n\n[SDL](https://www.libsdl.org/)- Windowing and input (and other features not used in this tutorial). Without a library like this we would have to write a lot of platform-specific code. Alternatives are[GLFW](https://www.glfw.org/)and[SFML](https://www.sfml-dev.org/). SDL has the broadest platform support of these[Volk](https://github.com/zeux/volk)- Meta-loader that simplifies loading of Vulkan functions[VMA](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator)- Simplifies dealing with memory allocations. Removes some of the verbosity around memory management[glm](https://github.com/g-truc/glm)- A mathematics library with support for things like matrices and vectors[tinyobjloader](https://github.com/tinyobjloader/tinyobjloader)- Single header loader for the obj 3D format[KTX-Software](https://github.com/KhronosGroup/KTX-Software)- Khronos KTX GPU texture image file loader\n\nTip\n\nNone of these are required to work with Vulkan. But they make working with Vulkan easier and some of them like VMA and Volk are widely used, even in commercial applications.\n\n## Programming language\n\nWe'll use C++ 20, mostly for its designated initializers. They help with Vulkan's verbosity and improve code readability. Other than that we won't be using modern C++ features and also work with the C Vulkan headers instead of the [C++](https://github.com/KhronosGroup/Vulkan-Hpp) ones. Aside from personal preferences this is done to make this tutorial as approachable as possible, including people that use other programming languages.\n\n## Shading language\n\nVulkan consumes shaders in an intermediate format called [SPIR-V](https://www.khronos.org/spirv/). This decouples the API from the actual shading language. Initially only GLSL was supported, but in 2026 there are more and better options. One of those is [Slang](https://github.com/shader-slang), and that's what we'll be using for this tutorial. The language itself is more modern than GLSL and offers some convenient features.\n\n## Vulkan SDK\n\nWhile it's not required for developing Vulkan applications, the [LunarG Vulkan SDK](https://vulkan.lunarg.com/sdk/home) provides a convenient way to install commonly used libraries and tools, some of which are used in this tutorial. It's therefore recommended to install this. When installing, be sure to select the optional GLM, Volk, SDL3 and Vulkan Memory Allocator components. Alternatively, you can download these from their respective repositories and adjust the include paths in the CMakeLists.txt file.\n\n## Validation layers\n\nVulkan was designed to minimize driver overhead. While that *can* result in better performance, it also removes many of the safeguards that APIs like OpenGL had and puts that responsibility into your hands. If you misuse Vulkan the driver is free to crash. So even if your app works on one GPU, it doesn't guarantee that it works on others. On the other hand, the Vulkan specification defines valid usages for all functionality. And with the [validation layers](https://github.com/KhronosGroup/Vulkan-ValidationLayers), an easy-to-use tool to check for that exists.\n\nValidation layers can be enabled in code, but the easier option is to enable the layers via the [Vulkan Configurator GUI](https://vulkan.lunarg.com/doc/view/latest/windows/vkconfig.html) provided by the [Vulkan SDK](#vulkan-sdk). Once they're enabled, any improper use of the API will be logged to the command line window of our application.\n\nNote\n\nYou should always have the validation layers enabled when developing with Vulkan. This makes sure you write spec-compliant code that properly works on other systems.\n\n## Graphics debugger\n\nAnother indispensable tool is a graphics debugger. Similar to the CPU debugger available in IDEs like Visual Studio, these help you debug runtime issues on the GPU side of things. A commonly used cross-platform and cross-vendor graphics debugger with Vulkan support is [RenderDoc](https://renderdoc.org/). While using such a debugger isn’t required for this tutorial, it’s invaluable if you want to build upon what you’ve learned and encounter issues while doing so.\n\n## Development environment\n\nOur build system will be [CMake](https://cmake.org/). Similar to my approach to writing code, things will be kept as simple as possible with the added benefit of being able to follow this tutorial with a wide variety of C++ compilers and IDEs.\n\nTo create build files for your C++ IDE, run CMake in the source folder of the project like this:\n\n```\ncmake -B build -G \"Visual Studio 17 2022\"\n```\n\nThis will write a Visual Studio 2022 solution file to the `build`\n\nfolder. As an alternative to the command line, you can use [cmake-gui](https://cmake.org/cmake/help/latest/manual/cmake-gui.1.html). The generator (-G) depends on your IDE, you can find a list of those [here](https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html).\n\n## Source\n\nNow that everything is properly set up we can start digging into the code. The following chapters will walk you through the [main source file](https://github.com/SaschaWillems/HowToVulkan/blob/main/source/main.cpp) from top to bottom.\n\nWarning\n\nSome of the less interesting initialization, declaration and boilerplate code is omitted from this document. It's advised to also have the main source file open while doing this tutorial.\n\n## Instance setup\n\nThe first thing we need is a Vulkan instance. It connects the application to Vulkan and as such is the base for everything that follows.\n\nSetting up the instance consists of passing information about your application:\n\n```\nVkApplicationInfo appInfo{\n    .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,\n    .pApplicationName = \"How to Vulkan\",\n    .apiVersion = VK_API_VERSION_1_3\n};\n```\n\nMost important is the `apiVersion`\n\n, which tells Vulkan that we want to use Vulkan 1.3. Using a higher API version gives us more features out of the box that otherwise would have to be used via extensions. [Vulkan 1.3](https://docs.vulkan.org/refpages/latest/refpages/source/VK_VERSION_1_3.html) is widely supported and adds a lot of features to the Vulkan core that make it easier to use. `pApplicationName`\n\ncan be used to identify your application.\n\nInfo\n\nA structure member you'll see a lot is `sType`\n\n. The driver needs to know what structure type it has to deal with, and with Vulkan being a C-API there is no other way than specifying it via structure member.\n\nThe instance also needs to know about the extensions you want to use. As the name implies, these are used to extend the API. As instance creation (and some other things) are platform-specific, the instance needs to know what platform-specific extensions you want to use. For Windows e.g. you'd use [VK_KHR_win32_surface](https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_win32_surface.html) and [VK_KHR_android_surface](https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_android_surface.html) for Android and so on for other platforms.\n\nNote\n\nThere are two extension types in Vulkan. Instance and device extensions. The former are mostly global, often platform specific extensions independent of your GPU, the latter are based on your GPU's capabilities.\n\nThat would mean we'd have to write platform-specific code. **But** with a library like SDL we don't have to do that; instead, we ask SDL for the platform-specific instance extensions:\n\n```\nuint32_t instanceExtensionsCount{ 0 };\nchar const* const* instanceExtensions{ SDL_Vulkan_GetInstanceExtensions(&instanceExtensionsCount) };\n```\n\nSo no more need to worry about platform specific things. With the application info and the required extensions set up, we can create our instance:\n\n```\nVkInstanceCreateInfo instanceCI{\n    .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,\n    .pApplicationInfo = &appInfo,\n    .enabledExtensionCount = instanceExtensionsCount,\n    .ppEnabledExtensionNames = instanceExtensions,\n};\nchk(vkCreateInstance(&instanceCI, nullptr, &instance));\n```\n\nThis is very simple. We pass our application info and both the names and number of instance extensions that SDL gave us (for the platform we're compiling for). Calling [ vkCreateInstance](https://docs.vulkan.org/refpages/latest/refpages/source/vkCreateInstance.html) creates our instance.\n\nTip\n\nMost Vulkan functions can fail in different ways and return a [ VkResult](https://docs.vulkan.org/refpages/latest/refpages/source/VkResult.html) value. We use a small inline function called\n\n`chk`\n\nto check that return code and in case of an error we exit the application. In a real-world application you should do more sophisticated error handling.## Device selection\n\nWe now have to choose the device we want to use for rendering. Although this isn't typical, it’s possible to have several Vulkan-capable devices within a single system. For instance, if you have multiple GPUs installed or if you have both an integrated and a discrete GPU:\n\nInfo\n\nWhen dealing with Vulkan a commonly used term is implementation. This refers to something that implements the Vulkan API. Usually it's the driver for your GPU, but it also could be a CPU based software implementation. To keep things simple we'll be using the term GPU for the rest of the tutorial.\n\nFor that, we get a list of all available physical devices supporting Vulkan:\n\n```\nuint32_t deviceCount{ 0 };\nchk(vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr));\nstd::vector<VkPhysicalDevice> devices(deviceCount);\nchk(vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()));\n```\n\nAfter the second call to [ vkEnumeratePhysicalDevices](https://docs.vulkan.org/refpages/latest/refpages/source/vkEnumeratePhysicalDevices.html) we have a list of all available Vulkan capable devices.\n\nInfo\n\nHaving to call functions that return some sort of list twice is common in the Vulkan C-API. The first call will return the number of elements, which is then used to properly size the result list. The second call then fills the actual result list.\n\nBecause most systems only have one device, we just implement a simple and optional way of selecting devices by passing the desired device index as a command line argument:\n\n```\nuint32_t deviceIndex{ 0 };\nif (argc > 1) {\n    deviceIndex = std::stoi(argv[1]);\n    assert(deviceIndex < deviceCount);\n}\n```\n\nWe also want to display information of the selected device. For that we call [ vkGetPhysicalDeviceProperties2](https://docs.vulkan.org/refpages/latest/refpages/source/vkGetPhysicalDeviceProperties2.html) and output the name of the device to the console:\n\n```\nVkPhysicalDeviceProperties2 deviceProperties{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2 };\nvkGetPhysicalDeviceProperties2(devices[deviceIndex], &deviceProperties);\nstd::cout << \"Selected device: \" << deviceProperties.properties.deviceName <<  \"\\n\";\n```\n\nInfo\n\nYou might have noticed that `VkPhysicalDeviceProperties2`\n\nand `vkGetPhysicalDeviceProperties2`\n\nare suffixed with a `2`\n\n. This is done to [address](https://docs.vulkan.org/spec/latest/appendices/legacy.html) shortcomings from previous versions. Fixing the original functions and structures isn't an option, as that would break API compatibility.\n\n## Queues\n\nIn Vulkan, work is not directly submitted to a device but rather to a queue. A queue abstracts access to a piece of hardware (graphics, compute, transfer, video, etc.). They are organized in queue families, with each family describing a set of queues with common functionality. Available queue types differ between GPUs. As we'll only do graphics operations, we need to just find one queue family with graphics support. This is done by checking for the [ VK_QUEUE_GRAPHICS_BIT](https://docs.vulkan.org/refpages/latest/refpages/source/VkQueueFlagBits.html) flag:\n\n```\nuint32_t queueFamilyCount{ 0 };\nvkGetPhysicalDeviceQueueFamilyProperties(devices[deviceIndex], &queueFamilyCount, nullptr);\nstd::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);\nvkGetPhysicalDeviceQueueFamilyProperties(devices[deviceIndex], &queueFamilyCount, queueFamilies.data());\nuint32_t queueFamily{ 0 };\nfor (size_t i = 0; i < queueFamilies.size(); i++) {\n    if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {\n        queueFamily = i;\n        break;\n    }\n}\n```\n\nAs we want to [present](#present-image) something to the screen using that graphics queue, we also check if said queue supports presentation:\n\n```\nchk(SDL_Vulkan_GetPresentationSupport(instance, devices[deviceIndex], queueFamily));\n```\n\nTip\n\nDevices that don't have a queue family supporting graphics are very rare in reality. Also on most devices, the first queue family supports graphics, compute and presentation. It's still a good practice to check this like we do above, esp. when you want to use other queue types like compute. If you run into devices where graphics, compute and/or presentation require different queue families, you'd have to do additional synchronization between these queues.\n\nFor our next step we need to reference that queue family using a [ VkDeviceQueueCreateInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkDeviceQueueCreateInfo.html). It is possible to request multiple queues from the same family, but we won't require that. That's why we need to specify priorities in\n\n`pQueuePriorities`\n\n(in our case just one). With multiple queues from the same family, a driver might use that information to prioritize work:\n\n``` js\nconst float qfpriorities{ 1.0f };\nVkDeviceQueueCreateInfo queueCI{\n    .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,\n    .queueFamilyIndex = queueFamily,\n    .queueCount = 1,\n    .pQueuePriorities = &qfpriorities\n};\n```\n\n## Device setup\n\nNow that we have a connection to the Vulkan library, selected a physical device and know what queue family we want to use, we need to get a handle to the GPU. This is called a **device** in Vulkan. Vulkan distinguishes between physical and logical devices. The former presents the actual device (usually the GPU), the latter presents a handle to that device's Vulkan implementation which the application will interact with.\n\nAn important part of device creation is requesting features and extensions we want to use. Our instance was created with Vulkan 1.3 as a baseline, which gives us almost all the features we want to use. So we only have to request the [ VK_KHR_swapchain](https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_swapchain.html) extension in order to be able to present something to the screen:\n\n``` js\nconst std::vector<const char*> deviceExtensions{ VK_KHR_SWAPCHAIN_EXTENSION_NAME };\n```\n\nTip\n\nThe Vulkan header defines constants for all extensions like `VK_KHR_SWAPCHAIN_EXTENSION_NAME`\n\n. You can use these instead of writing their name as a string. This helps to avoid typos in extension names.\n\nUsing Vulkan 1.3 as a baseline, we can use the features mentioned [earlier on](#about) without resorting to extensions. When using extensions, that would require more code, plus checks and fallback paths if an extension were not present. With core features, we can instead simply enable them:\n\n```\nVkPhysicalDeviceVulkan12Features enabledVk12Features{\n    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,\n    .descriptorIndexing = true,\n    .shaderSampledImageArrayNonUniformIndexing = true,\n    .descriptorBindingVariableDescriptorCount = true,\n    .runtimeDescriptorArray = true,\n    .bufferDeviceAddress = true\n};\nVkPhysicalDeviceVulkan13Features enabledVk13Features{\n    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,\n    .pNext = &enabledVk12Features,\n    .synchronization2 = true,\n    .dynamicRendering = true,\n};\nVkPhysicalDeviceFeatures enabledVk10Features{\n    .samplerAnisotropy = VK_TRUE\n};\n```\n\n`shaderSampledImageArrayNonUniformIndexing`\n\n, `descriptorBindingVariableDescriptorCount`\n\nand `runtimeDescriptorArray`\n\nare related to descriptor indexing, the rest of the names match the actual feature. We also enable [anisotropic filtering](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFeatures.html#_members) for better texture filtering.\n\nInfo\n\nAnother Vulkan struct member you're going to see often is `pNext`\n\n. This can be used to create a linked list of structures that are passed into a function call. The driver then uses the `sType`\n\nmember of each structure in that list to identify said structure's type.\n\nWith everything in place, we can create a logical device with the core features, extensions and the queue families we want to use:\n\n```\nVkDeviceCreateInfo deviceCI{\n    .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,\n    .pNext = &enabledVk13Features,\n    .queueCreateInfoCount = 1,\n    .pQueueCreateInfos = &queueCI,\n    .enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size()),\n    .ppEnabledExtensionNames = deviceExtensions.data(),\n    .pEnabledFeatures = &enabledVk10Features\n};\nchk(vkCreateDevice(devices[deviceIndex], &deviceCI, nullptr, &device));\n```\n\nWe also need a queue to submit our graphics commands to, which we can now request from the device we just created:\n\n```\nvkGetDeviceQueue(device, queueFamily, 0, &queue);\n```\n\n## Setting up VMA\n\nVulkan is an explicit API, which also applies to memory management. As noted in the list of libraries we will be using the [Vulkan Memory Allocator (VMA)](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) to significantly simplify this area.\n\nVMA provides an allocator used to allocate memory from. This needs to be set up for your project once. For that we pass in pointers to some common Vulkan functions, our Vulkan instance and device, we also enable support for buffer device address (`flags`\n\n):\n\n```\nVmaVulkanFunctions vkFunctions{\n    .vkGetInstanceProcAddr = vkGetInstanceProcAddr,\n    .vkGetDeviceProcAddr = vkGetDeviceProcAddr,\n    .vkCreateImage = vkCreateImage\n};\nVmaAllocatorCreateInfo allocatorCI{\n    .flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, \n    .physicalDevice = devices[deviceIndex],\n    .device = device,\n    .pVulkanFunctions = &vkFunctions,\n    .instance = instance\n};\nchk(vmaCreateAllocator(&allocatorCI, &allocator));\n```\n\nTip\n\nVMA also uses [ VkResult](https://docs.vulkan.org/refpages/latest/refpages/source/VkResult.html) return codes, we can use the same\n\n`chk`\n\nfunction to check VMA's function results.## Window and surface\n\nTo \"draw\" something in Vulkan (the correct term would be \"present an image\", more on that later) we need a surface. Most of the time, a surface is taken from a window. Creating both is platform-specific, as mentioned in the [instance chapter](#instance-setup). So in theory, this *would* require us to write different code paths for all platforms we want to support (Windows, Linux, Android, etc.).\n\nBut that's where libraries like SDL come into play. They take care of all the platform specifics for us, so that part becomes dead simple.\n\nTip\n\nLibraries like SDL, GLFW and SFML also take care of other platform specific functionality like input, audio and networking (to a varying degree).\n\nFirst we create a window with Vulkan support:\n\n```\nSDL_Window* window = SDL_CreateWindow(\"How to Vulkan\", 1280u, 720u, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);\n```\n\nAnd then request a Vulkan surface for that window:\n\n```\nchk(SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface));\n```\n\nFor the following chapter(s) we'll need to know the properties of the surface we just created, so we get them via [ vkGetPhysicalDeviceSurfaceCapabilitiesKHR](https://docs.vulkan.org/refpages/latest/refpages/source/vkGetPhysicalDeviceSurfaceCapabilitiesKHR.html) and store them for future reference:\n\n```\nVkSurfaceCapabilitiesKHR surfaceCaps{};\nchk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(devices[deviceIndex], surface, &surfaceCaps));\n```\n\n## Swapchain\n\nTo visually present something to a surface (in our case, the window) we need to create a swapchain. It's basically a series of images, storing color information, that you enqueue to the presentation engine of the operating system. The [ VkSwapchainCreateInfoKHR](https://docs.vulkan.org/refpages/latest/refpages/source/VkSwapchainCreateInfoKHR.html) is pretty extensive and requires some explanation.\n\nFirst step is getting the correct extent of the swapchain. Usually we can get this from the surface capabilities we just queried, but on [Wayland](https://docs.vulkan.org/spec/latest/chapters/VK_KHR_surface/wsi.html#platformCreateSurface_wayland) this has the special value `0xFFFFFFFF`\n\n, which indicate that the surface size will basically be determined by the size of the window:\n\n```\nVkExtent2D swapchainExtent{ surfaceCaps.currentExtent };\nif (surfaceCaps.currentExtent.width == 0xFFFFFFFF) {\n    swapchainExtent = { .width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y) };\n}\n```\n\nWith the correct extent, we can now set up the information required to create the swapchain:\n\n``` js\nconst VkFormat imageFormat{ VK_FORMAT_B8G8R8A8_SRGB };\nVkSwapchainCreateInfoKHR swapchainCI{\n    .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,\n    .surface = surface,\n    .minImageCount = surfaceCaps.minImageCount,\n    .imageFormat = imageFormat,\n    .imageColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR,\n    .imageExtent{.width = swapchainExtent.width, .height = swapchainExtent.height },\n    .imageArrayLayers = 1,\n    .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,\n    .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,\n    .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,\n    .presentMode = VK_PRESENT_MODE_FIFO_KHR\n};\nchk(vkCreateSwapchainKHR(device, &swapchainCI, nullptr, &swapchain));\n```\n\nWe're using the 4 component color format `VK_FORMAT_B8G8R8A8_SRGB`\n\nwith a non-linear sRGB [color space](https://docs.vulkan.org/refpages/latest/refpages/source/VkColorSpaceKHR.html) `VK_COLORSPACE_SRGB_NONLINEAR_KHR`\n\n. This combination is guaranteed to be available everywhere. Different combinations would require checking for support. `minImageCount`\n\nwill be the minimum number of images we get from the swapchain. This value varies between GPUs, hence why we use the information we earlier requested from the surface. `presentMode`\n\ndefines the way in which images are presented to the screen. [ VK_PRESENT_MODE_FIFO_KHR](https://docs.vulkan.org/refpages/latest/refpages/source/VkPresentModeKHR.html#) is a v-synced mode and the only mode guaranteed to be available everywhere.\n\nNote\n\nThe swapchain setup shown here is a bare minimum. In a real-world application this part can be quite complicated, as you might have to adjust it based on user settings. One example would be HDR capable devices, where you'd need to use a different image format and color space.\n\nSomething special about the swapchain is that its images are not owned by the application, but rather by the swapchain. So instead of explicitly creating these on our own, we request them from the swapchain. This will give us at least as many images as set by `minImageCount`\n\n:\n\n```\nuint32_t imageCount{ 0 };\nchk(vkGetSwapchainImagesKHR(device, swapchain, &imageCount, nullptr));\nswapchainImages.resize(imageCount);\nchk(vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapchainImages.data()));\nswapchainImageViews.resize(imageCount);\n```\n\n## Depth attachment\n\nAs we're rendering three-dimensional objects, we want to make sure they're properly displayed, no matter from what perspective you look at them, or in which order their triangles are rasterized. That's done via [depth testing](https://docs.vulkan.org/spec/latest/chapters/fragops.html#fragops-depth) and to use that we need to have a depth attachment.\n\nFirst we need to check what depth format is actually available on the current GPU using [vkGetPhysicalDeviceFormatProperties2](https://docs.vulkan.org/refpages/latest/refpages/source/vkGetPhysicalDeviceFormatProperties2.html):\n\n```\nstd::vector<VkFormat> depthFormatList{ VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT };\nVkFormat depthFormat{ VK_FORMAT_UNDEFINED };\nfor (VkFormat& format : depthFormatList) {\n    VkFormatProperties2 formatProperties{ .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2 };\n    vkGetPhysicalDeviceFormatProperties2(devices[deviceIndex], format, &formatProperties);\n    if (formatProperties.formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {\n        depthFormat = format;\n        break;\n    }\n}\n```\n\nNote\n\nThe Vulkan spec [guarantees](https://docs.vulkan.org/spec/latest/chapters/formats.html#features-required-format-support) certain format and usage combinations to be supported on all devices. One such guarantee is for depth formats, where either `VK_FORMAT_D32_SFLOAT_S8_UINT`\n\nor `VK_FORMAT_D24_UNORM_S8_UINT`\n\nmust be supported for use as a depth attachment.\n\nThe properties of the depth image are then defined in a [ VkImageCreateInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageCreateInfo.html) structure. Some of these are similar to those found at swapchain creation:\n\n```\nVkImageCreateInfo depthImageCI{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,\n    .imageType = VK_IMAGE_TYPE_2D,\n    .format = depthFormat,\n    .extent{.width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y), .depth = 1 },\n    .mipLevels = 1,\n    .arrayLayers = 1,\n    .samples = VK_SAMPLE_COUNT_1_BIT,\n    .tiling = VK_IMAGE_TILING_OPTIMAL,\n    .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,\n    .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,\n};\n```\n\n[makes sure the image is stored in a format best suited for the GPU. We also need to state our desired usage case for the image, which is](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageTiling.html)\n\n`VK_IMAGE_TILING_OPTIMAL`\n\n[, as we'll use it as the depth attachment for our render output (more on that later). The initial layout defines the image's content, which we don't have to care about, so we set that to](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageUsageFlagBits.html)\n\n`VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT`\n\n[.](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageLayout.html)\n\n`VK_IMAGE_LAYOUT_UNDEFINED`\n\nThis is also the first time we'll use VMA to allocate something in Vulkan. Memory allocation for buffers and images in Vulkan is verbose yet often very similar. With VMA we can do away with a lot of that. VMA also takes care of selecting the correct memory types and usage flags, something that would otherwise require a lot of code to get right.\n\n```\nVmaAllocationCreateInfo allocCI{\n    .flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT,\n    .usage = VMA_MEMORY_USAGE_AUTO\n};\nchk(vmaCreateImage(allocator, &depthImageCI, &allocCI, &depthImage, &depthImageAllocation, nullptr));\n```\n\nOne thing that makes VMA so convenient is [ VMA_MEMORY_USAGE_AUTO](https://gpuopen-librariesandsdks.github.io/VulkanMemoryAllocator/html/choosing_memory_type.html). This usage flag will have VMA select the required usage flags automatically based on the other values you pass in for the allocation and/or buffer create info. There are some cases where you might be better off explicitly stating usage flags, but in most cases the auto flag is the perfect fit. The\n\n`VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT`\n\nflag tells VMA to create a separate memory allocation for this resource, which is recommended for e.g. large image attachments.Tip\n\nWe only need a single image, even if we do double buffering in other places. That's because the image is only ever accessed by the GPU and the GPU can only ever write to a single depth image at a time. This differs from resources shared by the CPU and the GPU, but more on that later.\n\nImages in Vulkan are not accessed directly, but rather through [views](https://docs.vulkan.org/spec/latest/chapters/resources.html#VkImageView), a common concept in programming. This adds flexibility and allows different access patterns for the same image.\n\n```\nVkImageViewCreateInfo depthViewCI{ \n    .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,\n    .image = depthImage,\n    .viewType = VK_IMAGE_VIEW_TYPE_2D,\n    .format = depthFormat,\n    .subresourceRange{ .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, .levelCount = 1, .layerCount = 1 }\n};\nchk(vkCreateImageView(device, &depthViewCI, nullptr, &depthImageView));\n```\n\nWe need a view to the image we just created and we want to access it as a 2D view. The `subresourceRange`\n\nspecifies the part of the image we want to access via this view. For images with multiple layers or (mip) levels you could create separate image views for any of these and access an image in different ways. The `aspectMask`\n\nrefers to the aspect of the image that we want to access via the view. This can be color, stencil, or (in our case) the depth part of the image.\n\nWith both the image and the image view created, our depth attachment is now ready to be used later on for rendering.\n\n## Loading meshes\n\nFrom Vulkan's perspective there is no technical difference between drawing a single triangle or a complex mesh with thousands of triangles. Both result in some sort of buffer that the GPU will read data from. The GPU does not care where that data comes from. We could start by displaying a single triangle with hardcoded vertex data, but for a learning experience it's much better to load an actual 3D object instead. That's our next step.\n\nThere are plenty of formats around for storing 3D models. [glTF](https://www.khronos.org/Gltf) for example offers a lot of features and is extensible in a way similar to Vulkan. But we want to keep things simple, so we'll be using the [Wavefront .obj format](https://en.wikipedia.org/wiki/Wavefront_.obj_file) instead. As far as 3D formats go, it won't get more plain than this. And it's supported by many tools like [Blender](https://www.blender.org/).\n\nFirst we declare a struct for the vertex data we plan to use in our application. That's vertex positions, vertex normals (used for lighting), and texture coordinates. These are colloquially abbreviated as [uv](https://en.wikipedia.org/wiki/UV_mapping):\n\n```\nstruct Vertex {\n    glm::vec3 pos;\n    glm::vec3 normal;\n    glm::vec2 uv;\n};\n```\n\nWe're using the [tinyobjloader library](https://github.com/tinyobjloader/tinyobjloader) to load .obj files. It does all the parsing and gives us structured access to the data contained in that file:\n\n```\n// Mesh data\ntinyobj::attrib_t attrib;\nstd::vector<tinyobj::shape_t> shapes;\nstd::vector<tinyobj::material_t> materials;\nchk(tinyobj::LoadObj(&attrib, &shapes, &materials, nullptr, nullptr, \"assets/suzanne.obj\"));\n```\n\nAfter a successful call to `LoadObj`\n\n, we can access the vertex data stored in the selected .obj file. `attrib`\n\ncontains a linear array of the vertex data, `shapes`\n\ncontains indices into that data. `materials`\n\nwon't be used, we'll do our own shading.\n\nWarning\n\nThe .obj format is a bit dated and doesn't match modern 3D pipelines in all aspects. One such aspect is indexing of the vertex data. Due to how .obj files are structured we end up with one index per vertex, which limits the effectiveness of indexed rendering. In a real-world application you'd use formats that work well with indexed rendering like glTF.\n\nWe'll be using interleaved vertex attributes. Interleaved means, that in memory, for every vertex three floats for the position are followed by three floats for the normal vector (used for lighting), which in turn is followed by two floats for texture coordinates.\n\nTo make that work, we need to convert the position, normal, and texture coordinate data provided by tinyobj:\n\n``` js\nconst VkDeviceSize indexCount{shapes[0].mesh.indices.size()};   \nstd::vector<Vertex> vertices{};\nstd::vector<uint16_t> indices{};\n// Load vertex and index data\nfor (auto& index : shapes[0].mesh.indices) {\n    Vertex v{\n        .pos = { attrib.vertices[index.vertex_index * 3], -attrib.vertices[index.vertex_index * 3 + 1], attrib.vertices[index.vertex_index * 3 + 2] },\n        .normal = { attrib.normals[index.normal_index * 3], -attrib.normals[index.normal_index * 3 + 1], attrib.normals[index.normal_index * 3 + 2] },\n        .uv = { attrib.texcoords[index.texcoord_index * 2], 1.0 - attrib.texcoords[index.texcoord_index * 2 + 1] }\n    };\n    vertices.push_back(v);\n    indices.push_back(indices.size());\n}\n```\n\nTip\n\nThe value of the position's and normal's y-axis, and the texture coordinate's v-axis are flipped. This is done to accommodate for Vulkan's coordinate system. Otherwise the model and the texture image would appear upside down.\n\nWith the data stored in an interleaved way we can now upload it to the GPU. For that we need to create a buffer that's going to hold the vertex and index data:\n\n```\nVkDeviceSize vBufSize{ sizeof(Vertex) * vertices.size() };\nVkDeviceSize iBufSize{ sizeof(uint16_t) * indices.size() };\nVkBufferCreateInfo bufferCI{\n    .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,\n    .size = vBufSize + iBufSize,\n    .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT\n};\n```\n\nInstead of having separate buffers for vertices and indices, we'll put both into the same buffer. Hence why the `size`\n\nof the buffer is calculated from the size of both vertices and index vectors. The buffer [ usage](https://docs.vulkan.org/refpages/latest/refpages/source/VkBufferUsageFlagBits.html) bit mask combination of\n\n`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`\n\nand `VK_BUFFER_USAGE_INDEX_BUFFER_BIT`\n\nsignals that intended use case to the driver.Similar to creating images earlier on we use VMA to allocate the buffer for storing vertex and index data:\n\n```\nVmaAllocationCreateInfo vBufferAllocCI{\n    .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,\n    .usage = VMA_MEMORY_USAGE_AUTO\n};\nVmaAllocationInfo vBufferAllocInfo{};\nchk(vmaCreateBuffer(allocator, &bufferCI, &vBufferAllocCI, &vBuffer, &vBufferAllocation, &vBufferAllocInfo));\n```\n\nWe again use `VMA_MEMORY_USAGE_AUTO`\n\nto have VMA select the correct usage flags for the buffer. The specific `flags`\n\ncombination of `VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT`\n\nand `VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT`\n\nused here make sure we get a memory type that's located on the GPU (in VRAM) and accessible by the host. While it's possible to store vertices and indices in CPU memory, GPU access to them will be much slower. Early on, CPU accessible VRAM memory types were only available on systems with a unified memory architecture, like mobiles or integrated GPUs. But thanks to [(Re)BAR/SAM](https://en.wikipedia.org/wiki/PCI_configuration_space#Resizable_BAR) even dedicated GPUs can now map most of their VRAM into host address space and make it accessible via the CPU.\n\nNote\n\nWithout this we'd have to create a so-called \"staging\" buffer on the host, copy data to that buffer and then submit a buffer copy from staging to the GPU side buffer using a command buffer. That would require a lot more code.\n\nThe `VMA_ALLOCATION_CREATE_MAPPED_BIT`\n\ngets us a persistently mapped buffer, which in turn lets us directly copy data into VRAM:\n\n```\nmemcpy(vBufferAllocInfo.pMappedData, vertices.data(), vBufSize);\nmemcpy(((char*)vBufferAllocInfo.pMappedData) + vBufSize, indices.data(), iBufSize);\n```\n\n## CPU and GPU parallelism\n\nIn graphics heavy applications, the CPU is mostly used to feed work to the GPU. When OpenGL was invented, computers had one CPU with a single core. But today, even mobile devices have multiple cores and Vulkan gives us more explicit control over how work is distributed across these and the GPU.\n\nThis lets us have CPU and GPU work in parallel where possible. So while the GPU is still busy, we can already start creating the next \"work package\" on the CPU. The naive approach would be having the GPU always wait on the CPU (and vice versa), but that would kill any chance of parallelism.\n\nTip\n\nKeeping this in mind will help understand why things like [command buffers](#command-buffers) exist in Vulkan and why we duplicate certain resources.\n\nA prerequisite for that is to multiply resources shared by the CPU and GPU. That way the CPU can start updating resource *n+1* while the GPU is still using resource *n*. That's basically double (or multi) buffering and is often referred to as \"frames in flight\" in Vulkan.\n\nWhile in theory we could have many frames in flight, each added frame in flight also adds latency. So usually you have no more than 2 or 3 frames in flight. We define this at the very top of our code:\n\n```\nconstexpr uint32_t maxFramesInFlight{ 2 };\n```\n\nAnd use it to dimension all resources that are shared by the CPU and GPU:\n\n```\nstd::array<ShaderDataBuffer, maxFramesInFlight> shaderDataBuffers;\nstd::array<VkCommandBuffer, maxFramesInFlight> commandBuffers;\n```\n\nNote\n\nThe concept of frames in flight only applies to resource shared by CPU and GPU. Resources that are only used by the GPU don't have to be multiplied. This applies to e.g. images.\n\n## Shader data buffers\n\nWe also want to pass values to our [shaders](#the-shader) that can change on the CPU side, e.g. from user input. For that we are going to create buffers that can be written by the CPU and read by the GPU. The data in these buffers stays constant (uniform) across all shader invocations for a draw call. This is an important guarantee for the GPU.\n\nThe data we want to pass is stored in a single structure and laid out consecutively, so we can easily copy it over to the matching GPU structure:\n\n```\nstruct ShaderData {\n    glm::mat4 projection;\n    glm::mat4 view;\n    glm::mat4 model[3];\n    glm::vec4 lightPos{ 0.0f, -10.0f, 10.0f, 0.0f };\n    uint32_t selected{1};\n} shaderData{};\n```\n\nWarning\n\nIt's important to match structure layouts between CPU-side and GPU-side. Depending on the data types and arrangement used, layouts might look the same but will actually be different due to how shading languages align struct members. One way to avoid this, aside from manually aligning or padding structures, is to use Vulkan's [VK_EXT_scalar_block_layout](https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_scalar_block_layout.html) or the corresponding Vulkan 1.2 core feature (both are optional).\n\nIf we were to use older Vulkan versions we now *would* have to deal with descriptors, a fundamental but partially limiting and hard to manage part of Vulkan.\n\nBut by using Vulkan 1.3's [buffer device address](https://docs.vulkan.org/guide/latest/buffer_device_address.html) feature, we can do away with descriptors (for buffers). Instead of having to access them through descriptors, we can access buffers via their address using pointer syntax in the shader. Not only does that make things easier to understand, it also removes some coupling and requires less code.\n\nAs mentioned in [the previous chapter](#cpu-and-gpu-parallelism), we create one shader data buffer per the maximum number of frames in flight. That way we can update one buffer on the CPU while the GPU reads from another one. This ensures we don't run into any read/write hazards where the CPU starts updating values while the GPU is still reading them:\n\n```\nfor (auto i = 0; i < maxFramesInFlight; i++) {\n    VkBufferCreateInfo uBufferCI{\n        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,\n        .size = sizeof(ShaderData),\n        .usage = VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT\n    };\n    VmaAllocationCreateInfo uBufferAllocCI{\n        .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,\n        .usage = VMA_MEMORY_USAGE_AUTO\n    };\n    chk(vmaCreateBuffer(allocator, &uBufferCI, &uBufferAllocCI, &shaderDataBuffers[i].buffer, &shaderDataBuffers[i].allocation, &shaderDataBuffers[i].allocationInfo));\n```\n\nCreating these buffers is similar to creating the vertex/index buffers for our mesh. The create info structure states that we want to access this buffer via its device address (`VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT`\n\n). The buffer size must (at least) match that of our CPU data structure. We again use VMA to handle the allocation, using the same flags as for the vertex/index buffer to make sure we get a buffer that's accessible by both the CPU and GPU. Using the `VMA_ALLOCATION_CREATE_MAPPED_BIT`\n\nflag makes sure the buffer is persistently mapped and gives us a pointer to the buffer in a `VmaAllocationInfo`\n\nstruct. Unlike older APIs, this is perfectly fine in Vulkan and makes it easier to update the buffers later on, as we can just keep a permanent pointer to the buffer (memory).\n\nTip\n\nUnlike larger, static buffers, buffers for passing small amounts of data to shaders don't have to be stored in the GPU's VRAM. While we still ask VMA for such a memory type, falling back to CPU-side memory wouldn't be an issue.\n\n```\n    VkBufferDeviceAddressInfo uBufferBdaInfo{\n        .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO,\n        .buffer = shaderDataBuffers[i].buffer\n    };\n    shaderDataBuffers[i].deviceAddress = vkGetBufferDeviceAddress(device, &uBufferBdaInfo);\n}\n```\n\nTo be able to access the buffer in our shader, we then get its device address and store it for later access.\n\n## Synchronization objects\n\nAnother area where Vulkan is very explicit is [synchronization](https://docs.vulkan.org/spec/latest/chapters/synchronization.html). Other APIs like OpenGL did this implicitly for us. But here, we need to make sure that access to GPU resources is properly guarded to avoid any write/read hazards that could happen by e.g. the CPU starting to write to memory still in use by the GPU. This is somewhat similar to doing multithreading on the CPU, but more complicated because we need to make this work between the CPU and GPU, both being very different types of processing units, and also on the GPU itself.\n\nWarning\n\nGetting synchronization right in Vulkan can be very hard, especially since wrong or missing synchronization might not be visible on all GPUs or in all situations. Sometimes it only shows with low frame rates or on mobile devices. The [validation layers](#validation-layers) include a way to check this with the synchronization validation preset. Make sure to enable it from time to time and check for any hazards reported.\n\nWe'll be using different means of synchronization during this tutorial:\n\n[Fences](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-fences)are used to signal work completion from GPU to CPU. We use them when we need to make sure that a resource used by both GPU and CPU is free to be modified on the CPU.[Binary Semaphores](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-semaphores)are used to control access to resources on the GPU-side (only). We use them to ensure proper ordering for things like presentation.[Pipeline barriers](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-pipeline-barriers)are used to control resource access within a GPU queue. We use them for layout transitions of images.\n\nFences and binary semaphores are objects that we have to create and store, barriers instead are issued as commands and will be discussed later:\n\n```\nVkSemaphoreCreateInfo semaphoreCI{\n    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO\n};\nVkFenceCreateInfo fenceCI{\n    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,\n    .flags = VK_FENCE_CREATE_SIGNALED_BIT\n};\nfor (auto i = 0; i < maxFramesInFlight; i++) {\n    chk(vkCreateFence(device, &fenceCI, nullptr, &fences[i]));\n    chk(vkCreateSemaphore(device, &semaphoreCI, nullptr, &imageAcquiredSemaphores[i]));\n}\nrenderCompleteSemaphores.resize(swapchainImages.size());\nfor (auto& semaphore : renderCompleteSemaphores) {\n    chk(vkCreateSemaphore(device, &semaphoreCI, nullptr, &semaphore));\n}\n```\n\nThere aren't a lot of options for creating these objects. Fences will be created in a signalled state by setting the `VK_FENCE_CREATE_SIGNALED_BIT`\n\nflag. Otherwise the first wait for such a fence would run into a timeout. We need one fence per [frame-in-flight](#cpu-and-gpu-parallelism) to sync between GPU and CPU. Same for the semaphore used to signal presentation. The number of semaphores used to signal rendering needs to match that of the swapchain's images. The reason for this is explained later on in [command buffer submission](#submit-command-buffer).\n\nTip\n\nFor more complex sync setups, [timeline semaphores](https://www.khronos.org/blog/vulkan-timeline-semaphores) can help reduce the verbosity. They add a semaphore type with a counter value that can be increased and waited on and also can be queried by the CPU to replace fences.\n\n## Command buffers\n\nUnlike older APIs like OpenGL, you can't arbitrarily issue commands to the GPU in Vulkan. Instead we have to record these into [command buffers](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html) and then submit them to a [queue](#queues).\n\nWhile this makes things a bit more complicated from the application's point-of-view, it helps the driver to optimize things and also enables applications to record command buffers on separate threads. That's another spot where Vulkan allows us to better utilize CPU and GPU resources.\n\nCommand buffers have to be allocated from a [command pool](https://docs.vulkan.org/refpages/latest/refpages/source/VkCommandPool.html), an object that helps the driver optimize allocations:\n\n```\nVkCommandPoolCreateInfo commandPoolCI{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,\n    .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,\n    .queueFamilyIndex = queueFamily\n};\nchk(vkCreateCommandPool(device, &commandPoolCI, nullptr, &commandPool));\n```\n\nAs we explicitly reset command buffers before [recording them](#record-command-buffer), we need to set the [ VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT](https://docs.vulkan.org/refpages/latest/refpages/source/VkCommandPoolCreateFlagBits.html) flag. We also have to specify the queue family that the command buffers allocated from this pool will be submitted to.\n\nTip\n\nIt's not uncommon to have multiple command pools in a more complex application. They're cheap to create and if you want to record command buffers from multiple threads you require one such pool per thread.\n\nCommand buffers will be recorded on the CPU and executed on the GPU, so we create one per max. [frames in flight](#cpu-and-gpu-parallelism):\n\n```\nVkCommandBufferAllocateInfo cbAllocCI{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,\n    .commandPool = commandPool,\n    .commandBufferCount = maxFramesInFlight\n};\nchk(vkAllocateCommandBuffers(device, &cbAllocCI, commandBuffers.data()));\n```\n\nA call to [vkAllocateCommandBuffers](https://docs.vulkan.org/refpages/latest/refpages/source/vkAllocateCommandBuffers.html) will allocate `commandBufferCount`\n\ncommand buffers from our just created pool.\n\n## Loading textures\n\nWe are now going to load the textures used for rendering the 3D models. In Vulkan, textures are images, just like the swapchain or depth image. From a GPU's perspective, images are more complex than buffers, something that's reflected in the verbosity around getting them uploaded to the GPU.\n\nThere are a lot of image file formats around, but we'll go with [KTX](https://www.khronos.org/ktx/), a container format by Khronos. Unlike file formats such as JPEG or PNG, it stores images in native GPU layout, meaning we can directly upload them without having to decompress or convert. It also supports GPU specific features like storing mip maps, 3D textures and cubemaps. One tool for creating KTX image files is [PVRTexTool](https://developer.imaginationtech.com/solutions/pvrtextool/).\n\nWith the help of that library, loading such a file from disk is trivial:\n\n```\nfor (auto i = 0; i < textures.size(); i++) {\n    ktxTexture* ktxTexture{ nullptr };\n    std::string filename = \"assets/suzanne\" + std::to_string(i) + \".ktx\";\n    ktxTexture_CreateFromNamedFile(filename.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTexture);\n    ...\n```\n\nWarning\n\nThe textures we load use an 8-bit per channel RGBA format, even though we don't use the alpha channel. You might be tempted to use RGB instead to save memory, but RGB isn't widely supported. If you used such formats in OpenGL the driver often secretly converted them to RGBA. In Vulkan trying to use an unsupported format instead would just fail.\n\nCreating the image (object) is very similar to how we created the [depth attachment](#depth-attachment):\n\n```\nVkImageCreateInfo texImgCI{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,\n    .imageType = VK_IMAGE_TYPE_2D,\n    .format = ktxTexture_GetVkFormat(ktxTexture),\n    .extent = {.width = ktxTexture->baseWidth, .height = ktxTexture->baseHeight, .depth = 1 },\n    .mipLevels = ktxTexture->numLevels,\n    .arrayLayers = 1,\n    .samples = VK_SAMPLE_COUNT_1_BIT,\n    .tiling = VK_IMAGE_TILING_OPTIMAL,\n    .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,\n    .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED\n};\nVmaAllocationCreateInfo texImageAllocCI{ .usage = VMA_MEMORY_USAGE_AUTO };\nchk(vmaCreateImage(allocator, &texImgCI, &texImageAllocCI, &textures[i].image, &textures[i].allocation, nullptr));\n```\n\nThe format is read from the texture using `ktxTexture_GetVkFormat`\n\n, width, height and the number of [mip levels](https://docs.vulkan.org/spec/latest/chapters/textures.html#textures-level-of-detail-operation) also come from that. Our desired `usage`\n\ncombination means that we want to transfer data loaded from disk to this image (`VK_IMAGE_USAGE_TRANSFER_DST_BIT`\n\n) and (at a later point) want to sample from it in a shader (`VK_IMAGE_USAGE_SAMPLED_BIT`\n\n). We again use VK_IMAGE_LAYOUT_UNDEFINED, as that's the only one allowed in this case (the only other allowed format is VK_IMAGE_LAYOUT_PREINITIALIZED but that only works with linear tiled images). Once again `vmaCreateImage`\n\nis used to create the image, with `VMA_MEMORY_USAGE_AUTO`\n\nmaking sure we get the most fitting memory type (GPU VRAM).\n\nWe also create a view through which the image (texture) will be accessed. In our case we want to access the whole image, including all mip levels:\n\n```\nVkImageViewCreateInfo texVewCI{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,\n    .image = textures[i].image,\n    .viewType = VK_IMAGE_VIEW_TYPE_2D,\n    .format = texImgCI.format,\n    .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = ktxTexture->numLevels, .layerCount = 1 }\n};\nchk(vkCreateImageView(device, &texVewCI, nullptr, &textures[i].view));\n```\n\nWith the empty image created it's time to upload data. Unlike buffers, we can't simply memcpy data to an image. That's because [optimal tiling](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageTiling.html) stores texels in a hardware-specific layout and we have no way to convert to that. Instead we have to create an intermediate buffer that we copy the data to, and then issue a command to the GPU that copies this buffer to the image, doing the conversion in turn.\n\nCreating that buffer is very much the same as creating the [shader data buffers](#shader-data-buffers) with some minor differences:\n\n```\nVkBuffer imgSrcBuffer{};\nVmaAllocation imgSrcAllocation{};\nVkBufferCreateInfo imgSrcBufferCI{\n    .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,\n    .size = (uint32_t)ktxTexture->dataSize,\n    .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT\n};\nVmaAllocationCreateInfo imgSrcAllocCI{\n    .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,\n    .usage = VMA_MEMORY_USAGE_AUTO\n};\nchk(vmaCreateBuffer(allocator, &imgSrcBufferCI, &imgSrcAllocCI, &imgSrcBuffer, &imgSrcAllocation, &imgSrcAllocInfo));\n```\n\nThis buffer will be used as a temporary source for a buffer-to-image copy, so the only flag we need is [ VK_BUFFER_USAGE_TRANSFER_SRC_BIT](https://docs.vulkan.org/refpages/latest/refpages/source/VkBufferUsageFlagBits.html). The allocation is handled by VMA, once again.\n\nAs the buffer was created with the mappable bit, getting the image data into that buffer is again just a matter of a simple `memcpy`\n\n:\n\n``` php\nmemcpy(imgSrcAllocInfo.pMappedData, ktxTexture->pData, ktxTexture->dataSize);\n```\n\nNext we need to copy the image data from that buffer to the optimal tiled image on the GPU. For that we have to create a command buffer. We'll get into the detail on how they work [later on](#record-command-buffer). We also create a fence that's used to wait for the command buffer to finish execution:\n\n```\nVkFenceCreateInfo fenceOneTimeCI{\n    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO\n};\nVkFence fenceOneTime{};\nchk(vkCreateFence(device, &fenceOneTimeCI, nullptr, &fenceOneTime));\nVkCommandBuffer cbOneTime{};\nVkCommandBufferAllocateInfo cbOneTimeAI{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,\n    .commandPool = commandPool,\n    .commandBufferCount = 1\n};\nchk(vkAllocateCommandBuffers(device, &cbOneTimeAI, &cbOneTime));\n```\n\nWe can then start recording the commands required to get image data to its destination:\n\n```\nVkCommandBufferBeginInfo cbOneTimeBI{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,\n    .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT\n};\nchk(vkBeginCommandBuffer(cbOneTime, &cbOneTimeBI));\nVkImageMemoryBarrier2 barrierTexImage{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,\n    .srcStageMask = VK_PIPELINE_STAGE_2_NONE,\n    .srcAccessMask = VK_ACCESS_2_NONE,\n    .dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT,\n    .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,\n    .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,\n    .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,\n    .image = textures[i].image,\n    .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = ktxTexture->numLevels, .layerCount = 1 }\n};\nVkDependencyInfo barrierTexInfo{\n    .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,\n    .imageMemoryBarrierCount = 1,\n    .pImageMemoryBarriers = &barrierTexImage\n};\nvkCmdPipelineBarrier2(cbOneTime, &barrierTexInfo);\nstd::vector<VkBufferImageCopy> copyRegions{};\nfor (auto j = 0; j < ktxTexture->numLevels; j++) {\n    ktx_size_t mipOffset{0};\n    KTX_error_code ret = ktxTexture_GetImageOffset(ktxTexture, j, 0, 0, &mipOffset);\n    copyRegions.push_back({\n        .bufferOffset = mipOffset,\n        .imageSubresource{.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = (uint32_t)j, .layerCount = 1},\n        .imageExtent{.width = ktxTexture->baseWidth >> j, .height = ktxTexture->baseHeight >> j, .depth = 1 },\n    });\n}\nvkCmdCopyBufferToImage(cbOneTime, imgSrcBuffer, textures[i].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<uint32_t>(copyRegions.size()), copyRegions.data());\nVkImageMemoryBarrier2 barrierTexRead{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,\n    .srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT,\n    .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,\n    .dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,\n    .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,\n    .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,\n    .newLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL,\n    .image = textures[i].image,\n    .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = ktxTexture->numLevels, .layerCount = 1 }\n};\nbarrierTexInfo.pImageMemoryBarriers = &barrierTexRead;\nvkCmdPipelineBarrier2(cbOneTime, &barrierTexInfo);\nchk(vkEndCommandBuffer(cbOneTime));\nVkCommandBufferSubmitInfo cbOneTimeSubmitInfo{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO,\n    .commandBuffer = cbOneTime\n};\nVkSubmitInfo2 oneTimeSI{\n    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2,\n    .commandBufferInfoCount = 1,\n    .pCommandBufferInfos = &cbOneTimeSubmitInfo\n};\nchk(vkQueueSubmit2(queue, 1, &oneTimeSI, fenceOneTime));\nchk(vkWaitForFences(device, 1, &fenceOneTime, VK_TRUE, UINT64_MAX));\n```\n\nIt might look a bit overwhelming at first but it's easily explained. Earlier on we learned about optimal tiled images, where texels are stored in a hardware-specific layout for optimal access by the GPU. That [layout](https://docs.vulkan.org/spec/latest/chapters/resources.html#resources-image-layouts) also defines what operations are possible with an image. That's why we need to change said layout depending on what we want to do next with our image. That's done via a pipeline barrier issued by [vkCmdPipelineBarrier2](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdPipelineBarrier2.html). The first one transitions all mip levels of the texture image from the initial undefined layout to a layout that allows us to transfer data to it (`VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`\n\n). We then copy over all the mip levels from our temporary buffer to the image using [vkCmdCopyBufferToImage](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdCopyBufferToImage.html). Finally we transition the mip levels from transfer destination to a layout we can read from in our shader (`VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL`\n\n). Submitting this command buffer to the graphics queue then executes all these commands. Command buffer submissions will be explained in depth [later on](#submit-command-buffer).\n\nTip\n\nExtensions that would make this easier are [VK_EXT_host_image_copy](https://www.khronos.org/blog/copying-images-on-the-host-in-vulkan), allowing for copying image data directly from the CPU without having to use a command buffer and [VK_KHR_unified_image_layouts](https://www.khronos.org/blog/so-long-image-layouts-simplifying-vulkan-synchronisation), simplifying image layouts. These aren't widely supported yet, but future candidates for making Vulkan easier to use.\n\nLater on we'll sample these textures in our shader, and sampling parameters used there are defined by a sampler object. We want smooth linear filtering, so we enable [anisotropic filter](https://docs.vulkan.org/spec/latest/chapters/textures.html#textures-texel-anisotropic-filtering) to reduce blur and aliasing. We also set the max. LOD to use all mip levels:\n\n```\nVkSamplerCreateInfo samplerCI{\n    .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,\n    .magFilter = VK_FILTER_LINEAR,\n    .minFilter = VK_FILTER_LINEAR,\n    .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,\n    .anisotropyEnable = VK_TRUE,\n    .maxAnisotropy = 8.0f, // 8 is a widely supported value for max anisotropy\n    .maxLod = (float)ktxTexture->numLevels,\n};\nchk(vkCreateSampler(device, &samplerCI, nullptr, &textures[i].sampler));\n```\n\nAt last we clean up and store descriptor related information for that texture to be used later:\n\n```\nktxTexture_Destroy(ktxTexture);\ntextureDescriptors.push_back({\n    .sampler = textures[i].sampler,\n    .imageView = textures[i].view,\n    .imageLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL\n});\n```\n\nNow that we have uploaded the texture images, put them into the correct layout and know how to sample them, we need a way for the GPU to access them in the shader. From the GPU's point of view, images are more complicated than buffers as the GPU needs more information on what they look like and how they're accessed. This is where [descriptors](https://docs.vulkan.org/spec/latest/chapters/descriptorsets.html) are required, handles that represent (describe, hence the name) shader resources.\n\nIn earlier Vulkan versions we would also have to use them for buffers, but as noted in the [shader data buffers](#shader-data-buffers) chapter, buffer device address saves us from doing that. There's no easy to use or widely available equivalent to that for images yet.\n\nAnd while descriptor handling is still one of the most verbose parts, using [descriptor indexing](https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_descriptor_indexing.html) simplifies this significantly and makes it easier to scale. With that feature we can go for a \"bindless\" setup, where all image descriptors for the textures are put into one large array and indexed in the [shader](#the-shader) rather than having to create and bind descriptor sets for every texture. To demonstrate how this works, we'll be loading multiple textures. This approach scales up no matter how many textures you use (within the limits of what the GPU supports).\n\nFirst we define the interface between our application and the shader in the form of a descriptor set layout:\n\n```\nVkDescriptorBindingFlags descVariableFlag{ VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT };\nVkDescriptorSetLayoutBindingFlagsCreateInfo descBindingFlags{\n    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO,\n    .bindingCount = 1,\n    .pBindingFlags = &descVariableFlag\n};\nVkDescriptorSetLayoutBinding descLayoutBindingTex{\n    .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,\n    .descriptorCount = static_cast<uint32_t>(textures.size()),\n    .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT\n};\nVkDescriptorSetLayoutCreateInfo descLayoutTexCI{\n    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,\n    .pNext = &descBindingFlags,\n    .bindingCount = 1,\n    .pBindings = &descLayoutBindingTex\n};\nchk(vkCreateDescriptorSetLayout(device, &descLayoutTexCI, nullptr, &descriptorSetLayoutTex));\n```\n\nAs we're only using descriptors for images, we just have a single binding. The [VkDescriptorSetLayoutBindingFlagsCreateInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorSetLayoutBindingFlagsCreateInfo.html) is used to enable a variable number of descriptors in that binding as part of descriptor indexing and is passed via `pNext`\n\n. We combine texture images and samplers (see below), so the binding's type needs to be [ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER](https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorType.html). There will be as many descriptors in that layout as we have textures loaded and we only need to access this from the fragment shader, so we set\n\n`stageFlags`\n\nto `VK_SHADER_STAGE_FRAGMENT_BIT`\n\n. A call to [vkCreateDescriptorSetLayout](https://docs.vulkan.org/refpages/latest/refpages/source/vkCreateDescriptorSetLayout.html)will then create a descriptor set layout with this configuration. We need this to allocate the descriptor and for defining the shader interface at\n\n[pipeline creation](#graphics-pipeline).\n\nTip\n\nThere are scenarios where you could separate images and samplers, e.g. if you have a lot of images and don't want to waste memory on having samplers for each or if you want to dynamically use different sampling options. In that case you'd use two pool sizes, one for `VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE`\n\nand one for `VK_DESCRIPTOR_TYPE_SAMPLER`\n\n.\n\nSimilar to command buffers, descriptors are allocated from a descriptor pool:\n\n```\nVkDescriptorPoolSize poolSize{\n    .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,\n    .descriptorCount = static_cast<uint32_t>(textures.size())\n};\nVkDescriptorPoolCreateInfo descPoolCI{\n    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,\n    .maxSets = 1,\n    .poolSizeCount = 1,\n    .pPoolSizes = &poolSize\n};\nchk(vkCreateDescriptorPool(device, &descPoolCI, nullptr, &descriptorPool));\n```\n\nThe number of descriptor types we want to allocate must be specified here upfront. We need as many descriptors for combined image and samplers as we load textures. We also have to specify how many descriptor sets (**not** descriptors) we want to allocate via `maxSets`\n\n. That's one, because with descriptor indexing, we use an array of combined image and samplers. It's also only ever accessed by the GPU, so there is no need to duplicate it per max. frames in flight. Getting pool size right is important, as allocations beyond the requested counts will fail.\n\nNext we allocate the descriptor set from that pool. While the descriptor set layout defines the interface, the descriptor contains the actual descriptor data. The reason that layouts and sets are split is because you can mix layouts and re-use them for different descriptor sets.\n\n```\nuint32_t variableDescCount{ static_cast<uint32_t>(textures.size()) };\nVkDescriptorSetVariableDescriptorCountAllocateInfo variableDescCountAI{\n    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO_EXT,\n    .descriptorSetCount = 1,\n    .pDescriptorCounts = &variableDescCount\n};\nVkDescriptorSetAllocateInfo texDescSetAlloc{\n    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,\n    .pNext = &variableDescCountAI,\n    .descriptorPool = descriptorPool,\n    .descriptorSetCount = 1,\n    .pSetLayouts = &descriptorSetLayoutTex\n};\nchk(vkAllocateDescriptorSets(device, &texDescSetAlloc, &descriptorSetTex));\n```\n\nSimilar to descriptor set layout creation, we have to pass the descriptor indexing setup to the allocation via [VkDescriptorSetVariableDescriptorCountAllocateInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorSetVariableDescriptorCountAllocateInfo.html) in `pNext`\n\n.\n\nThe descriptor set allocated by [vkAllocateDescriptorSets](https://docs.vulkan.org/refpages/latest/refpages/source/vkAllocateDescriptorSets.html) is largely uninitialized and needs to be backed with actual data before we can access it in a shader:\n\n```\nVkWriteDescriptorSet writeDescSet{\n    .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,\n    .dstSet = descriptorSetTex,\n    .dstBinding = 0,\n    .descriptorCount = static_cast<uint32_t>(textureDescriptors.size()),\n    .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, \n    .pImageInfo = textureDescriptors.data()\n};\nvkUpdateDescriptorSets(device, 1, &writeDescSet, 0, nullptr);\n```\n\nThe [VkDescriptorImageInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorImageInfo.html) refers to an array of descriptors for the textures we loaded above combined with samplers in `pImageInfo`\n\n. Calling [vkUpdateDescriptorSets](https://docs.vulkan.org/refpages/latest/refpages/source/vkUpdateDescriptorSets.html) will put that information in the first (and in our case only) binding slot of the descriptor set.\n\n## Loading shaders\n\nAs mentioned earlier we'll be using the [Slang language](https://github.com/shader-slang) to write shaders running on the GPU. Vulkan can't directly load shaders written in such a language though (or GLSL or HLSL). It expects them in the SPIR-V intermediate format. For that we need to compile from Slang to SPIR-V first. There are two approaches to do that: Compile offline using Slang's command line compiler or compile at runtime using Slang's library.\n\nWe'll go for the latter as that makes updating shaders a bit easier. With offline compilation you'd have to recompile the shaders every time you change them or find a way to have the build system do that for you. With runtime compilation we'll always use the latest shader version when running our code.\n\nTo compile Slang shaders, we first create a global Slang session, which is the connection between our application and the Slang library:\n\n```\nslang::createGlobalSession(slangGlobalSession.writeRef());\n```\n\nNext we create a session to define our compilation scope. We want to compile to SPIR-V, so we set the target `format`\n\nto `SLANG_SPIRV`\n\n. We use [SPIR-V 1.4](https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_spirv_1_4.html) as the baseline for shader features. This has been core since Vulkan 1.2, so it's guaranteed to be supported in our case. We also change the `defaultMatrixLayoutMode`\n\nto a column-major layout to match the layout of the GLM library we use to construct matrices later on:\n\n```\nauto slangTargets{ std::to_array<slang::TargetDesc>({ {\n    .format{SLANG_SPIRV},\n    .profile{slangGlobalSession->findProfile(\"spirv_1_4\")}\n} })};\nauto slangOptions{ std::to_array<slang::CompilerOptionEntry>({ {\n    slang::CompilerOptionName::EmitSpirvDirectly,\n    {slang::CompilerOptionValueKind::Int, 1}\n} })};\nslang::SessionDesc slangSessionDesc{\n    .targets{slangTargets.data()},\n    .targetCount{SlangInt(slangTargets.size())},\n    .defaultMatrixLayoutMode = SLANG_MATRIX_LAYOUT_COLUMN_MAJOR,\n    .compilerOptionEntries{slangOptions.data()},\n    .compilerOptionEntryCount{uint32_t(slangOptions.size())}\n};\nSlang::ComPtr<slang::ISession> slangSession;\nslangGlobalSession->createSession(slangSessionDesc, slangSession.writeRef());\n```\n\nThe session created by `createSession`\n\ncan then be used to get the SPIR-V representation of the Slang shader. For that, we first load the textual shader from a file using `loadModuleFromSource`\n\nand then use `getTargetCode`\n\nto compile all entry points in our shader to SPIR-V:\n\n``` php\nSlang::ComPtr<slang::IModule> slangModule{\n    slangSession->loadModuleFromSource(\"triangle\", \"assets/shader.slang\", nullptr, nullptr)\n};\nSlang::ComPtr<ISlangBlob> spirv;\nslangModule->getTargetCode(0, spirv.writeRef());\n```\n\nTo use our shader in the graphics pipeline (see below), we need to create a shader module. These are containers for compiled SPIR-V shaders. To create such a module, we pass the SPIR-V compiled by Slang to [ vkCreateShaderModule](https://docs.vulkan.org/refpages/latest/refpages/source/vkCreateShaderModule.html):\n\n```\nVkShaderModuleCreateInfo shaderModuleCI{\n    .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,\n    .codeSize = spirv->getBufferSize(),\n    .pCode = (uint32_t*)spirv->getBufferPointer()\n};\nVkShaderModule shaderModule{};\nchk(vkCreateShaderModule(device, &shaderModuleCI, nullptr, &shaderModule));\n```\n\nTip\n\nThe [VK_KHR_maintenance5](https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_maintenance5.html) extension, which became core with Vulkan 1.4, deprecated shader modules. It allows direct passing of `VkShaderModuleCreateInfo`\n\nto the pipeline's shader stage create info.\n\n## The shader\n\nAlthough shading languages aren’t as powerful as CPU programming languages, they can still handle complex scenarios. Our shader is intentionally kept simple:\n\n```\nstruct VSInput {\n    float3 Pos;\n    float3 Normal;\n    float2 UV;\n};\n\nSampler2D textures[];\n\nstruct ShaderData {\n    float4x4 projection;\n    float4x4 view;\n    float4x4 model[3];\n    float4 lightPos;\n    uint32_t selected;\n};\n\nstruct VSOutput {\n    float4 Pos : SV_POSITION;\n    float3 Normal;\n    float2 UV;\n    float3 Factor;\n    float3 LightVec;\n    float3 ViewVec;\n    uint32_t InstanceIndex;\n};\n\n[shader(\"vertex\")]\nVSOutput main(VSInput input, uniform ShaderData *shaderData, uint instanceIndex : SV_VulkanInstanceID) {\n    VSOutput output;\n    float4x4 modelMat = shaderData->model[instanceIndex];\n    output.Normal = mul((float3x3)mul(shaderData->view, modelMat), input.Normal);\n    output.UV = input.UV;\n    output.Pos = mul(shaderData->projection, mul(shaderData->view, mul(modelMat, float4(input.Pos.xyz, 1.0))));\n    output.Factor = (shaderData->selected == instanceIndex ? 3.0f : 1.0f);\n    output.InstanceIndex = instanceIndex;\n    // Calculate view vectors required for lighting\n    float4 fragPos = mul(mul(shaderData->view, modelMat), float4(input.Pos.xyz, 1.0));\n    output.LightVec = shaderData->lightPos.xyz - fragPos.xyz;\n    output.ViewVec = -fragPos.xyz;\n    return output;\n}\n\n[shader(\"fragment\")]\nfloat4 main(VSOutput input) {\n    // Phong lighting\n    float3 N = normalize(input.Normal);\n    float3 L = normalize(input.LightVec);\n    float3 V = normalize(input.ViewVec);\n    float3 R = reflect(-L, N);\n    float3 diffuse = max(dot(N, L), 0.0025);\n    float3 specular = pow(max(dot(R, V), 0.0), 16.0) * 0.75;\n    // Sample from texture\n    float3 color = textures[NonUniformResourceIndex(input.InstanceIndex)].Sample(input.UV).rgb * input.Factor;\n    return float4(diffuse * color.rgb + specular, 1.0);\n}\n```\n\nTip\n\nSlang lets us put all shader stages into a single file. That removes the need to duplicate the shader interface or having to put that into shared includes. It also makes it easier to read (and edit) the shader.\n\nIt contains two shading stages and starts with defining structures that are used by the different stages. The `ShaderData`\n\nstructure matches the layout of the shader data structure defined on the [CPU side](#shader-data-buffers).\n\nFirst is the vertex shader, marked by the `[shader(\"vertex\")]`\n\nattribute. It takes in vertices defined by `VSInput`\n\n, matching the vertex layout from the [graphics pipeline](#graphics-pipeline). The vertex shader will be invoked for every vertex [drawn](#record-command-buffer). As we use buffer device address, we pass and access the UBO as a pointer. Slang will translate this to a PushConstant, that is why we needed one in the pipeline layout. As we draw multiple instances of our 3D model and want to use different matrices for every instance, we use the built-in `SV_VulkanInstanceID`\n\nsystem value to index into the model matrices. We also want to highlight the selected model, so if the current instance matches that selection, we pass a different color factor to the fragment shader.\n\nSecond is the fragment shader, marked by the `[shader(\"fragment\")]`\n\nattribute. First we calculate some basic lighting using the [phong reflection model](https://en.wikipedia.org/wiki/Phong_reflection_model) with values passed from the vertex shader. Then, to demonstrate descriptor indexing, we read from the array of textures (`Sampler2D textures[]`\n\n) using the instance index, and finally combine that with the lighting calculation. This is written to the current color attachment.\n\n## Graphics pipeline\n\nAnother area where Vulkan strongly differs from OpenGL is state management. OpenGL was a huge state machine, and that state could be changed at any time. This made it hard for drivers to optimize things. Vulkan fundamentally changes that by introducing pipeline state objects. They provide a full set of pipeline state in a \"compiled\" pipeline object, giving the driver a chance to optimize them. These objects also allow for pipeline object creation in e.g. a separate thread. If you need different pipeline state you have to create a new pipeline state object.\n\nNote\n\nThere is *some* state in Vulkan that can be dynamic. Mostly basic state like viewport and scissor setup. Having it be dynamic is not an issue for drivers. There are several extensions that make additional state dynamic, but we're not going to use them here.\n\nVulkan supports [pipeline types](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineBindPoint.html) specific to use cases like graphics, compute, and raytracing. As such, setting up a pipeline depends on what we want to achieve. In our case, that's graphics (aka [rasterization](https://en.wikipedia.org/wiki/Rasterisation)), so we'll be creating a graphics pipeline.\n\nFirst we create a pipeline layout. This defines the interface between the pipeline and our shaders. Pipeline layouts are separate objects as you can mix and match them for use with other pipelines:\n\n```\nVkPushConstantRange pushConstantRange{\n    .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,\n    .size = sizeof(VkDeviceAddress)\n};\nVkPipelineLayoutCreateInfo pipelineLayoutCI{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,\n    .setLayoutCount = 1,\n    .pSetLayouts = &descriptorSetLayoutTex,\n    .pushConstantRangeCount = 1,\n    .pPushConstantRanges = &pushConstantRange\n};\nchk(vkCreatePipelineLayout(device, &pipelineLayoutCI, nullptr, &pipelineLayout));\n```\n\nThe [ pushConstantRange](https://docs.vulkan.org/refpages/latest/refpages/source/VkPushConstantRange.html) defines a range of values that we can directly push to the shader without having to go through a buffer. We use these to pass a pointer to the shader data buffer buffer (more on that later). The descriptor set layouts (\n\n`pSetLayouts`\n\n) define the interface to the shader resources. In our case that's only one layout for passing the texture image descriptors. The call to [will create the pipeline layout we can then use for our pipeline.](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineLayoutCreateInfo.html)\n\n`vkCreatePipelineLayout`\n\nAnother part of our interface between the pipeline and the shader is the layout of the vertex data. In the [mesh loading chapter](#loading-meshes) we defined a basic vertex structure that we now need to specify in Vulkan terms. We use a single vertex buffer, so we require one [vertex binding point](https://docs.vulkan.org/refpages/latest/refpages/source/VkVertexInputBindingDescription.html). The `stride`\n\nmatches the size of our vertex structure as our vertices are stored directly adjacent in memory. The `inputRate`\n\nis per-vertex, meaning that the data pointer advances for every vertex read:\n\n```\nVkVertexInputBindingDescription vertexBinding{\n     .binding = 0,\n     .stride = sizeof(Vertex),\n     .inputRate = VK_VERTEX_INPUT_RATE_VERTEX\n};\n```\n\nNext we specify how [vertex attributes](https://docs.vulkan.org/refpages/latest/refpages/source/VkVertexInputAttributeDescription.html) for position, normals and texture coordinates are laid out in memory. This exactly matches our CPU-side vertex structure:\n\n```\nstd::vector<VkVertexInputAttributeDescription> vertexAttributes{\n    { .location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT },\n    { .location = 1, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = offsetof(Vertex, normal) },\n    { .location = 2, .binding = 0, .format = VK_FORMAT_R32G32_SFLOAT, .offset = offsetof(Vertex, uv) },\n};\n```\n\nTip\n\nAnother option for accessing vertices in the shader is buffer device address. That way we would skip the traditional vertex attributes and manually fetch them in the shader using pointers. That's called \"vertex pulling\". On some devices that can be slower though, so we stick with the traditional way.\n\nNow we start filling in the many `VkPipeline*CreateInfo`\n\nstructures required to create a pipeline. We won't explain all of these in detail, you can read up on them in the [spec](https://docs.vulkan.org/refpages/latest/refpages/source/VkGraphicsPipelineCreateInfo.html). They're all kinda similar, and describe a particular part of the pipeline.\n\nFirst up is the pipeline state for the [vertex input](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineVertexInputStateCreateInfo.html) we just defined above:\n\n```\nVkPipelineVertexInputStateCreateInfo vertexInputState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,\n    .vertexBindingDescriptionCount = 1,\n    .pVertexBindingDescriptions = &vertexBinding,\n    .vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexAttributes.size()),\n    .pVertexAttributeDescriptions = vertexAttributes.data(),\n};\n```\n\nAnother structure directly connected to our vertex data is the [input assembly state](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineInputAssemblyStateCreateInfo.html). It defines how [primitives](https://docs.vulkan.org/refpages/latest/refpages/source/VkPrimitiveTopology.html) are assembled. We want to render a list of separate triangles, so we use [ VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST](https://docs.vulkan.org/refpages/latest/refpages/source/VkPrimitiveTopology.html):\n\n```\nVkPipelineInputAssemblyStateCreateInfo inputAssemblyState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,\n    .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST\n};\n```\n\nAn important part of any pipeline are the [shaders](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineShaderStageCreateInfo.html) we want to use and the pipeline stages they map to. Having only a single set of shaders is the reason why we only need a single pipeline. Thanks to Slang we get all stages in a single shader module:\n\n```\nstd::vector<VkPipelineShaderStageCreateInfo> shaderStages{\n    { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,\n      .stage = VK_SHADER_STAGE_VERTEX_BIT,\n      .module = shaderModule, .pName = \"main\"},\n    { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,\n      .stage = VK_SHADER_STAGE_FRAGMENT_BIT,\n      .module = shaderModule, .pName = \"main\" }\n};\n```\n\nTip\n\nIf you'd wanted to use different shaders (or shader combinations), you'd have to create multiple pipelines. The [VK_EXT_shader_objects](https://www.khronos.org/blog/you-can-use-vulkan-without-pipelines-today) makes those shader stages into separate objects and adds more flexibility to this part of the API.\n\nNext we configure the [viewport state](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineViewportStateCreateInfo.html). We use one viewport and one scissor, and we also want them to be dynamic state so we don't have to recreate the pipeline if any of those changes, e.g. when resizing the window. It's one of the few dynamic states that have been there since Vulkan 1.0:\n\n```\nVkPipelineViewportStateCreateInfo viewportState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,\n    .viewportCount = 1,\n    .scissorCount = 1\n};\nstd::vector<VkDynamicState> dynamicStates{ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };\nVkPipelineDynamicStateCreateInfo dynamicState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,\n    .dynamicStateCount = 2,\n    .pDynamicStates = dynamicStates.data()\n};\n```\n\nAs we want to use [depth buffering](#depth-attachment), we configure the [depth/stencil state](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineDepthStencilStateCreateInfo.html) to enable both depth tests and writes and set the compare operation so that fragments closer to the viewer are passing depth tests:\n\n```\nVkPipelineDepthStencilStateCreateInfo depthStencilState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,\n    .depthTestEnable = VK_TRUE,\n    .depthWriteEnable = VK_TRUE,\n    .depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL\n};\n```\n\nThe following state tells the pipeline that we want to use dynamic rendering instead of the cumbersome render pass objects. Unlike render passes, setting this up is fairly trivial and also removes a tight coupling between the pipeline and a render pass. For dynamic rendering we just have to specify the number and formats of the attachments we plan to use (later on):\n\n```\nVkPipelineRenderingCreateInfo renderingCI{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO,\n    .colorAttachmentCount = 1,\n    .pColorAttachmentFormats = &imageFormat,\n    .depthAttachmentFormat = depthFormat\n};\n```\n\nNote\n\nAs this functionality was added at a later point in Vulkan's life, there is no dedicated member for it in the pipeline create info. We instead pass this to `pNext`\n\n(see below)\n\nWe don't make use of the following state, but they must be specified and also need to have some sane default values. So we set [blending](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineColorBlendStateCreateInfo.html), [rasterization](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineRasterizationStateCreateInfo.html) and [multisampling](https://docs.vulkan.org/refpages/latest/refpages/source/VkPipelineMultisampleStateCreateInfo.html) to default values:\n\n```\nVkPipelineColorBlendAttachmentState blendAttachment{\n    .colorWriteMask = 0xF\n};\nVkPipelineColorBlendStateCreateInfo colorBlendState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,\n    .attachmentCount = 1,\n    .pAttachments = &blendAttachment\n};\nVkPipelineRasterizationStateCreateInfo rasterizationState{\n     .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,\n     .lineWidth = 1.0f\n};\nVkPipelineMultisampleStateCreateInfo multisampleState{\n    .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,\n    .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT\n};\n```\n\nWith all relevant pipeline state create structures properly set up, we wire them up to finally create our graphics pipeline:\n\n```\nVkGraphicsPipelineCreateInfo pipelineCI{\n    .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,\n    .pNext = &renderingCI,\n    .stageCount = 2,\n    .pStages = shaderStages.data(),\n    .pVertexInputState = &vertexInputState,\n    .pInputAssemblyState = &inputAssemblyState,\n    .pViewportState = &viewportState,\n    .pRasterizationState = &rasterizationState,\n    .pMultisampleState = &multisampleState,\n    .pDepthStencilState = &depthStencilState,\n    .pColorBlendState = &colorBlendState,\n    .pDynamicState = &dynamicState,\n    .layout = pipelineLayout\n};\nchk(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCI, nullptr, &pipeline));\n```\n\nAfter a successful call to [ vkCreateGraphicsPipelines](https://docs.vulkan.org/refpages/latest/refpages/source/vkCreateGraphicsPipelines.html), our graphics pipeline is ready to be used for rendering.\n\n## Render loop\n\nGetting to this point took quite an effort but we're now ready to actually \"draw\" something to the screen. Like so much before, this is both explicit and indirect in Vulkan. Getting something displayed on screen nowadays is a complex matter compared to how early computer graphics worked. Esp. with an API that has to support so many different platforms and devices.\n\nThis brings us to the render loop, in which we'll take user-input, render our scene, update shader values and make sure all of this is properly synchronized between CPU and GPU and on the GPU itself:\n\n```\nuint64_t lastTime{ SDL_GetTicks() };\nbool quit{ false };\nwhile (!quit) {\n    // Wait on fence\n    // Acquire next image\n    // Update shader data\n    // Record command buffer\n    // Submit command buffer\n    // Present image\n    // Poll events\n}\n```\n\nThe loop will be executed as long as the window stays open. SDL also gives us precise [timing functions](https://wiki.libsdl.org/SDL3/SDL_GetTicks) that we use to measure elapsed time for framerate-independent calculations later on.\n\nThere's a lot happening inside the loop, so we'll now look at each part separately.\n\n### Wait on fence\n\nAs discussed in [CPU and GPU parallelism](#cpu-and-gpu-parallelism), one area where we can overlap CPU and GPU work is command buffer recording. We want to have the CPU start recording the next command buffer while the GPU is still working on the previous one.\n\nTo do that, we wait for the fence of the last frame the GPU has worked on to finish execution:\n\n```\nchk(vkWaitForFences(device, 1, &fences[frameIndex], true, UINT64_MAX));\nchk(vkResetFences(device, 1, &fences[frameIndex]));\n```\n\nThe call to [vkWaitForFences](https://docs.vulkan.org/refpages/latest/refpages/source/vkWaitForFences.html) will wait on the CPU until the GPU has signalled it has finished all work submitted with that fence. We use a deliberately large timeout value of `UINT64_MAX`\n\n. As the fence is still in the signaled state, we also need to [reset](https://docs.vulkan.org/refpages/latest/refpages/source/vkResetFences.html) it for the next submission.\n\nNote\n\nWe have no requirements regarding how long the graphics operations are allowed to take, so we essentially don’t care about a timeout. We don’t perform any particularly complex tasks, and the fence will usually be signaled within a few milliseconds. Also, most operating systems implement features to reset the GPU if a graphics task takes too long.\n\n### Acquire next image\n\nAs we don't have direct control over the [swapchain images](#swapchain), we need to \"ask\" (acquire) the swapchain for the next index to be used in this frame:\n\n```\nchkSwapchain(vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imageAcquiredSemaphores[frameIndex], VK_NULL_HANDLE, &imageIndex));\n```\n\nIt's important to use the image index returned by [vkAcquireNextImageKHR](https://docs.vulkan.org/refpages/latest/refpages/source/vkAcquireNextImageKHR.html) to access the swapchain images. That's because there is no guarantee that images are acquired in consecutive order. That's one of the reasons we have two indices.\n\nWe also pass a [semaphore](#synchronization-objects) to this function which will be used later on at command buffer submission.\n\nNote\n\nWe are using a variation of the `chk`\n\nfunction to check return values of calls related to presentation. This is due to [VK_ERROR_OUT_OF_DATE_KHR](https://docs.vulkan.org/spec/latest/chapters/fundamentals.html#VkResult), which is returned when the surface is no longer compatible with the swapchain. This can occur on certain platforms, e.g. if display orientation changes. To prevent the application from exiting in these cases, we explicitly handle this error in `chkSwapchain`\n\n. Instead of exiting, we recreate the swapchain for the next frame.\n\n### Update shader data\n\nWe want the next frame to use up-to-date user inputs. This is safe to do now after waiting for the fence. For that we update matrices from current data using glm:\n\n```\nshaderData.projection = glm::perspective(glm::radians(45.0f), (float)windowSize.x / (float)windowSize.y, 0.1f, 32.0f);\nshaderData.view = glm::translate(glm::mat4(1.0f), camPos);\nfor (auto i = 0; i < 3; i++) {\n    auto instancePos = glm::vec3((float)(i - 1) * 3.0f, 0.0f, 0.0f);\n    shaderData.model[i] = glm::translate(glm::mat4(1.0f), instancePos) * glm::mat4_cast(glm::quat(objectRotations[i]));\n}\n```\n\nA simple `memcpy`\n\nto the shader data buffer's persistently mapped pointer is sufficient to make this available to the GPU (and with that our shader):\n\n```\nmemcpy(shaderDataBuffers[frameIndex].allocationInfo.pMappedData, &shaderData, sizeof(ShaderData));\n```\n\nThis works because the [shader data buffers](#shader-data-buffers) are stored in a memory type accessible by both the CPU (for writing) and the GPU (for reading). With the preceding fence synchronization we also made sure that the CPU won't start writing to that shader data buffer before the GPU has finished reading from it.\n\n### Record command buffer\n\nNow we can finally start recording actual GPU work items. A lot of the things we need for that have been discussed earlier, so even though this will be a lot of code, it should be easy to follow. As mentioned in [command buffers](#command-buffers), commands are not directly issued to the GPU in Vulkan but rather recorded to command buffers. That's exactly what we are going to do: record the commands for a single render frame.\n\nYou might be tempted to pre-record command buffers and reuse them until something changes that would require re-recording. This makes things unnecessarily complicated however, as you'd have to implement update logic that works with CPU/GPU parallelism. And since recording command buffers is relatively fast and can be offloaded to other CPU threads if needed, recording them every frame is perfectly fine.\n\nNote\n\nCommands that are recorded into a command buffer start with `vkCmd`\n\n. They are not directly executed, but only when the command buffer is submitted to a queue (GPU timeline). This also explains why these commands don't return a result. A common mistake for beginners is to mix those commands with commands that are instantly executed on the CPU timeline. It's important to remember that these two different timelines exist.\n\nCommand buffers have a [lifecycle](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html#commandbuffers-lifecycle) that we have to adhere to. For example we can't record commands to it while it's in the executable state. This is also checked by [validation layers](#validation-layers) that would let us know if we misused things.\n\nFirst we need to move the command buffer into the initial state. That's done by [resetting it](https://docs.vulkan.org/refpages/latest/refpages/source/vkResetCommandBuffer.html) and is safe to do now as we have waited on the fence earlier on to make sure it's no longer in the pending state:\n\n```\nauto cb = commandBuffers[frameIndex];\nchk(vkResetCommandBuffer(cb, 0));\n```\n\nOnce reset, we can start recording the command buffer:\n\n```\nVkCommandBufferBeginInfo cbBI {\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,\n    .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT\n};\nchk(vkBeginCommandBuffer(cb, &cbBI));\n```\n\nThe [ VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT](https://docs.vulkan.org/refpages/latest/refpages/source/VkCommandBufferUsageFlagBits.html) flag affects how lifecycle moves to invalid state after execution and can be used as an optimization hint by drivers. After calling\n\n[vkBeginCommandBuffer](https://docs.vulkan.org/refpages/latest/refpages/source/vkBeginCommandBuffer.html), which moves the command buffer into recording state, we can start recording the actual commands.\n\nDuring rendering, color information will be written to the current [swapchain image](#swapchain), depth information will be written to the [depth image](#depth-attachment). As we learned in [loading textures](#loading-textures), optimal tiled images need to be in the correct layout for their itended use case. As such, the first step is to issue layout transitions for both of them:\n\n```\nstd::array<VkImageMemoryBarrier2, 2> outputBarriers{\n    VkImageMemoryBarrier2{\n        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,\n        .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n        .srcAccessMask = 0,\n        .dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,\n        .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,\n        .newLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,\n        .image = swapchainImages[imageIndex],\n        .subresourceRange{.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1 }\n    },\n    VkImageMemoryBarrier2{\n        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,\n        .srcStageMask = VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT,\n        .srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,\n        .dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT,\n        .dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,\n        .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,\n        .newLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,\n        .image = depthImage,\n        .subresourceRange{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, .levelCount = 1, .layerCount = 1 }\n    }\n        };\nVkDependencyInfo barrierDependencyInfo{\n    .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,\n    .imageMemoryBarrierCount = 2,\n    .pImageMemoryBarriers = outputBarriers.data()\n};\nvkCmdPipelineBarrier2(cb, &barrierDependencyInfo);\n```\n\nNot only do [image memory barriers](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-dependencies) transition layouts, they also make sure that this happens at the right [pipeline stage](https://docs.vulkan.org/spec/latest/chapters/pipelines.html#pipelines-block-diagram), enforcing ordering inside a command buffer. Similar to other synchronization primitives we used, these are necessary to make sure the GPU e.g. doesn't start writing to an image in one pipeline stage while a previous pipeline stage is still reading from it. They also make writes visible to following stages. The `srcStageMask`\n\nis the pipeline stage(s) to wait on, `srcAccessMask`\n\nand defines writes to be made available. `dstStageMask`\n\nand `dstAccessMask`\n\ndefine where and what writes to be made visible.\n\nNote\n\nAvailable and visible might sound like the same thing, but they aren't. That's due to how CPU/GPUs work and how they interact with caches. Available means data is ready for future memory operations (e.g. cache flushes). Visible means that data is actually visible to reads from the consuming stages.\n\nThe *first barrier* transitions the current swapchain image *to* a [layout](https://docs.vulkan.org/refpages/latest/refpages/source/VkImageLayout.html) (`newLayout`\n\n) so that we can use it as a color attachment for rendering. Similarly the *second barrier* transitions the depth image *to* a layout so that we can use it as a depth attachment for rendering. Both transition *from* and undefined layout (`oldLayout`\n\n). That's because we don't need the previous content of any of these images.\n\nTip\n\nThe `VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL`\n\nlayout is a core feature of Vulkan 1.3 that combines all types of attachment layouts into a single one. This simplifies image barriers.\n\nA call to [vkCmdPipelineBarrier2](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdPipelineBarrier2.html) will then insert those two barriers into the current command buffer.\n\nWith the attachments in the correct layout, it's time to define how we are going to use them. As noted early on, we'll use [Dynamic rendering](https://www.khronos.org/blog/streamlining-render-passes) for that, instead of the complicated and cumbersome render pass objects from Vulkan 1.0.\n\n```\nVkRenderingAttachmentInfo colorAttachmentInfo{\n    .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,\n    .imageView = swapchainImageViews[imageIndex],\n    .imageLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,\n    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,\n    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,\n    .clearValue{.color{ 0.0f, 0.0f, 0.2f, 1.0f }}\n};\nVkRenderingAttachmentInfo depthAttachmentInfo{\n    .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,\n    .imageView = depthImageView,\n    .imageLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,\n    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,\n    .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,\n    .clearValue = {.depthStencil = {1.0f,  0}}\n};\n```\n\nWe set up one [VkRenderingAttachmentInfo](https://docs.vulkan.org/refpages/latest/refpages/source/VkRenderingAttachmentInfo.html) for the swapchain image used as the color attachment and the depth image used as the depth attachment. Both will be cleared to their respective `clearValue`\n\nat the start of a render pass with `loadOp`\n\nset to `VK_ATTACHMENT_LOAD_OP_CLEAR`\n\n. `storeOp`\n\nfor the color attachment is configured to keep its contents, as we still need them to be presented to the screen. We don't need the depth information once we're done rendering, so we literally don't care what happens with its contents after the render pass. Layouts for both must match what we transitioned them earlier on.\n\nCalling [vkCmdBeginRendering](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdBeginRendering.html) will then start our dynamic render pass instance with above attachment configuration:\n\n```\nVkRenderingInfo renderingInfo{\n    .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,\n    .renderArea{.extent{.width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y) }},\n    .layerCount = 1,\n    .colorAttachmentCount = 1,\n    .pColorAttachments = &colorAttachmentInfo,\n    .pDepthAttachment = &depthAttachmentInfo\n};\nvkCmdBeginRendering(cb, &renderingInfo);\n```\n\nInside this render pass instance we can finally start recording GPU commands. Remember that these aren't issued to the GPU just yet, but are only recorded into the current command buffer.\n\nWe start by setting up the [viewport](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-viewport) to define our rendering area. We always want this to be the whole window. Same for the [scissor](https://docs.vulkan.org/spec/latest/chapters/fragops.html#fragops-scissor) area. Both are part of the dynamic state we enabled at [pipeline creation](#graphics-pipeline), so we can adjust them inside the command buffer instead of having to recreate the graphics pipeline on each window resize:\n\n```\nVkViewport vp{\n    .width = static_cast<float>(windowSize.x),\n    .height = static_cast<float>(windowSize.y),\n    .minDepth = 0.0f,\n    .maxDepth = 1.0f\n};\nvkCmdSetViewport(cb, 0, 1, &vp);\nVkRect2D scissor{ .extent{ .width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y) } };\nvkCmdSetScissor(cb, 0, 1, &scissor);\n```\n\nNext up is binding the resources involved in rendering our 3D objects. The [graphics pipeline](#graphics-pipeline), that also includes our vertex and fragment shaders, as well as the descriptor set for the array of our [texture images](#loading-textures) and the vertex and index buffers of our [3D mesh](#loading-meshes):\n\n```\nvkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);\nVkDeviceSize vOffset{ 0 };\nvkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSetTex, 0, nullptr);\nvkCmdBindVertexBuffers(cb, 0, 1, &vBuffer, &vOffset);\nvkCmdBindIndexBuffer(cb, vBuffer, vBufSize, VK_INDEX_TYPE_UINT16);\n```\n\nWe also want to access data in the [shader data buffer](#shader-data-buffer). We opted for using buffer device address instead of going through descriptors, so instead we pass the address of the current frame's shader data buffer via a push constant to the shaders:\n\n```\nvkCmdPushConstants(cb, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(VkDeviceAddress), &shaderDataBuffers[frameIndex].deviceAddress);\n```\n\nNote\n\nThese `vkCmd*`\n\ncalls (and many others) set the current command buffer state. That means they persist across multiple draw calls inside this command buffer. So if you e.g. wanted to issue a second draw call with the same pipeline but a different descriptor sets you'd only have to call `vkCmdBindDescriptorSets`\n\nwith another set, while keeping the rest of the state.\n\nAnd with that we are *finally* ready to issue an actual draw command. With all the work we did up to this point, that's just a single command:\n\n```\nvkCmdDrawIndexed(cb, indexCount, 3, 0, 0, 0);\n```\n\nThis call to [vkCmdDrawIndexed](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdDrawIndexed.html) will draw indexCount / 3 triangles from the currently bound index and vertex buffer. We also want to draw multiple instances of our 3D mesh, so we set the instance count (third argument) to 3, which we use in the [vertex shader](#the-shader) to index into the [model matrices](#shader-data-buffers).\n\nWe now [finish](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdEndRendering.html) the current render pass:\n\n```\nvkCmdEndRendering(cb);\n```\n\nAnd transition the swapchain image that we just used as an attachment (to output color values) to a layout required for [presentation](#present-image):\n\n```\nVkImageMemoryBarrier2 barrierPresent{\n    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,\n    .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n    .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,\n    .dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n    .dstAccessMask = 0,\n    .oldLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,\n    .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,\n    .image = swapchainImages[imageIndex],\n    .subresourceRange{.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1 }\n};\nVkDependencyInfo barrierPresentDependencyInfo{\n    .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,\n    .imageMemoryBarrierCount = 1,\n    .pImageMemoryBarriers = &barrierPresent\n};\nvkCmdPipelineBarrier2(cb, &barrierPresentDependencyInfo);\n```\n\nWe don't need a barrier for the depth attachment, as we don't use that outside of this render pass.\n\nAt last we [end recording](https://docs.vulkan.org/refpages/latest/refpages/source/vkEndCommandBuffer.html) the command buffer:\n\n```\nvkEndCommandBuffer(cb);\n```\n\nThis moves it to the [executable state](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html#commandbuffers-lifecycle). That's a requirement for the next step.\n\n### Submit command buffer\n\nIn order to execute the commands we just recorded we need to submit the command buffer to a matching queue. In a real-world application it's not uncommon to have multiple queues of different types and also more complex submission patterns. But we only use graphics commands (no compute or ray tracing) and as such also only have a single graphics queue to which we submit our current frame's command buffer:\n\n```\nVkSemaphoreSubmitInfo waitSemaphoreInfo{\n    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,\n    .semaphore = imageAcquiredSemaphores[frameIndex],\n    .stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n};\nVkCommandBufferSubmitInfo commandBufferSubmitInfo{\n    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO,\n    .commandBuffer = cb\n};\nVkSemaphoreSubmitInfo signalSemaphoreInfo{\n    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,\n    .semaphore = renderCompleteSemaphores[imageIndex],\n    .stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,\n};\nVkSubmitInfo2 submitInfo{\n    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2,\n    .waitSemaphoreInfoCount = 1,\n    .pWaitSemaphoreInfos = &waitSemaphoreInfo,\n    .commandBufferInfoCount = 1,\n    .pCommandBufferInfos = &commandBufferSubmitInfo,\n    .signalSemaphoreInfoCount = 1,\n    .pSignalSemaphoreInfos = &signalSemaphoreInfo,\n};\nchk(vkQueueSubmit2(queue, 1, &submitInfo, fences[frameIndex]));\n```\n\nThe [ VkSubmitInfo2](https://docs.vulkan.org/refpages/latest/refpages/source/VkSubmitInfo2.html) structure needs some explanation, esp. in regards to synchronization. Earlier on we learned about the\n\n[synchronization primitives](#synchronization-objects)that we need to properly synchronize work between CPU and GPU and the GPU itself. And this is where it all comes together.\n\nThe wait semaphore in `pWaitSemaphoreInfos`\n\nensures that execution of the command buffer does not begin until the swapchain image we want to render to has been acquired. This means presentation has finished and the image has been released by the presentation engine. This is required because swapchain images are owned by the presentation engine rather than by our application. The stage specified in `stageMask`\n\nmakes that wait happen at the color attachment output stage, so in theory the GPU may already begin work on earlier pipeline stages, such as vertex fetching. The signal semaphore in `pSignalSemaphoreInfos`\n\n, on the other hand, is signaled by the GPU once command buffer execution has completed and ensures that presentation does not begin until the command buffer has finished execution. Together, these guarantees prevent read/write hazards that could cause the GPU to read from or write to resources that are still in use.\n\nNotice the distinction between using `frameIndex`\n\nfor the image-acquired semaphore and `imageIndex`\n\nfor the render-complete semaphore. This is because `vkQueuePresentKHR`\n\n(see below) cannot signal a semaphore without a certain extension, which is not yet available everywhere. To work around this, we decouple the two semaphore types and use one render-complete semaphore per swapchain image instead. An in-depth explanation can be found in the [Vulkan Guide](https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html).\n\nWarning\n\nSubmissions can have multiple wait and signal semaphores and wait stages. In a more complex application (than ours) which might mix graphics with compute, it's important to keep synchronization scope as narrow as possible to allow for the GPU to overlap work. This is one of the hardest parts to get right in Vulkan. Errors can be detected with the [validation layers](#validation-layers), performance can be checked with vendor-specific graphics profilers.\n\nOnce work has been submitted, we can calculate the frame index for the next render loop iteration:\n\n```\nframeIndex = (frameIndex + 1) % maxFramesInFlight;\n```\n\n### Present image\n\nThe final step to get our rendering results to the screen is presenting the current swapchain image we used as the color attachment:\n\n```\nVkPresentInfoKHR presentInfo{\n    .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,\n    .waitSemaphoreCount = 1,\n    .pWaitSemaphores = &renderCompleteSemaphores[imageIndex],\n    .swapchainCount = 1,\n    .pSwapchains = &swapchain,\n    .pImageIndices = &imageIndex\n};\nchkSwapchain(vkQueuePresentKHR(queue, &presentInfo));\n```\n\nCalling [vkQueuePresentKHR](https://docs.vulkan.org/refpages/latest/refpages/source/vkQueuePresentKHR.html) will enqueue the image for presentation and waits on the render finish semaphore. This guarantees the image won't be presented until all rendering commands have finished.\n\n### Poll events\n\nLast but not least, we work through the event queue of the operating system. Thanks to SDL, this code is platform-independent. Processing events is done in an additional loop (inside the render loop) where we call SDL's [event polling function](https://wiki.libsdl.org/SDL3/SDL_PollEvent) until all events have been processed. We only handle event types we're interested in:\n\n```\nfloat elapsedTime{ (SDL_GetTicks() - lastTime) / 1000.0f };\nlastTime = SDL_GetTicks();\nfor (SDL_Event event; SDL_PollEvent(&event);) {\n\n    // Exit loop if the application is about to close\n    if (event.type == SDL_EVENT_QUIT) {\n        quit = true;\n        break;\n    }\n\n    // Rotate the selected object with mouse drag\n    if (event.type == SDL_EVENT_MOUSE_MOTION) {\n        if (event.button.button == SDL_BUTTON_LEFT) {\n            objectRotations[shaderData.selected].x -= (float)event.motion.yrel * elapsedTime;\n            objectRotations[shaderData.selected].y += (float)event.motion.xrel * elapsedTime;\n        }\n    }\n\n    // Zooming with the mouse wheel\n    if (event.type == SDL_EVENT_MOUSE_WHEEL) {\n        camPos.z += (float)event.wheel.y * elapsedTime * 10.0f;\n    }\n\n    // Select active model instance\n    if (event.type == SDL_EVENT_KEY_DOWN) {\n        if (event.key.key == SDLK_PLUS || event.key.key == SDLK_KP_PLUS) {\n            shaderData.selected = (shaderData.selected < 2) ? shaderData.selected + 1 : 0;\n        }\n        if (event.key.key == SDLK_MINUS || event.key.key == SDLK_KP_MINUS) {\n            shaderData.selected = (shaderData.selected > 0) ? shaderData.selected - 1 : 2;\n        }\n    }\n\n    // Window resize\n    if (event.type == SDL_EVENT_WINDOW_RESIZED) {\n        updateSwapchain = true;\n    }\n}\n```\n\nWe want to have some interactivity in our application. For that we calculate rotation for the currently selected model instance based on mouse movement when the left button is down in the `SDL_EVENT_MOUSE_MOTION`\n\nevent. Same with the mousewheel in `SDL_EVENT_MOUSE_WHEEL`\n\nto allow zooming the camera in and out. The `SDL_EVENT_KEY_DOWN`\n\nevent lets us toggle between the model instances using the plus and minus keys.\n\nThe `SDL_EVENT_QUIT`\n\nevent is called when our application is to be closed, no matter how. We set `quit`\n\nto true in that case, exit outer render loop and jump to the [clean up](#cleaning-up) part of the code.\n\nAlthough it's optional, and something games often don't implement, we also handle resizing via the `SDL_EVENT_WINDOW_RESIZED`\n\nevent, which requires recreating the swapchain and associated resources.\n\n### Recreate swapchain\n\nThe swapchain needs to be recreated when the window is resized or if its surface becomes [out-of-date](#acquire-next-image). If any of these operations request an update to the swapchain, we recreate it:\n\n```\nif (updateSwapchain) {\n    updateSwapchain = false;\n    chk(vkDeviceWaitIdle(device));\n    chk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(devices[deviceIndex], surface, &surfaceCaps));\n    swapchainCI.oldSwapchain = swapchain;\n    swapchainCI.imageExtent = { .width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y) };\n    chk(vkCreateSwapchainKHR(device, &swapchainCI, nullptr, &swapchain));\n    for (auto i = 0; i < imageCount; i++) {\n        vkDestroyImageView(device, swapchainImageViews[i], nullptr);\n    }\n    chk(vkGetSwapchainImagesKHR(device, swapchain, &imageCount, nullptr));\n    swapchainImages.resize(imageCount);\n    chk(vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapchainImages.data()));\n    swapchainImageViews.resize(imageCount);\n    for (auto i = 0; i < imageCount; i++) {\n        VkImageViewCreateInfo viewCI{\n            .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,\n            .image = swapchainImages[i],\n            .viewType = VK_IMAGE_VIEW_TYPE_2D,\n            .format = imageFormat,\n            .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1}\n        };\n        chk(vkCreateImageView(device, &viewCI, nullptr, &swapchainImageViews[i]));\n    }\n    for (auto& semaphore : renderCompleteSemaphores) {\n        vkDestroySemaphore(device, semaphore, nullptr);\n    }\n    renderCompleteSemaphores.resize(imageCount);\n    for (auto& semaphore : renderCompleteSemaphores) {\n        chk(vkCreateSemaphore(device, &semaphoreCI, nullptr, &semaphore));\n    }   \n    vkDestroySwapchainKHR(device, swapchainCI.oldSwapchain, nullptr);\n    vmaDestroyImage(allocator, depthImage, depthImageAllocation);\n    vkDestroyImageView(device, depthImageView, nullptr);\n    depthImageCI.extent = { .width = static_cast<uint32_t>(windowSize.x), .height = static_cast<uint32_t>(windowSize.y), .depth = 1 };\n    VmaAllocationCreateInfo allocCI{\n        .flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT,\n        .usage = VMA_MEMORY_USAGE_AUTO\n    };\n    chk(vmaCreateImage(allocator, &depthImageCI, &allocCI, &depthImage, &depthImageAllocation, nullptr));\n    VkImageViewCreateInfo viewCI{\n        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,\n        .image = depthImage,\n        .viewType = VK_IMAGE_VIEW_TYPE_2D,\n        .format = depthFormat,\n        .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, .levelCount = 1, .layerCount = 1 }\n    };\n    chk(vkCreateImageView(device, &viewCI, nullptr, &depthImageView));\n}\n```\n\nThis looks intimidating at first, but it's mostly code we already used earlier on to create the swapchain and the depth image. Before we can recreate these, we call [vkDeviceWaitIdle](https://docs.vulkan.org/refpages/latest/refpages/source/vkDeviceWaitIdle.html) to wait until the GPU has completed all outstanding operations. This makes sure that none of those objects are still in use by the GPU.\n\nAn important difference here is setting the `oldSwapchain`\n\nmember of the swapchain create info. This is necessary during recreation to allow the application to continue presenting any already acquired image. Remember we don't have control over those, as they're owned by the swapchain (and the operating system). Other than that it's a simple matter of destroying existing objects (`vkDestroy*`\n\n) and recreating them just like we did earlier on using the new size of the window.\n\n## Cleaning up\n\nDestroying Vulkan resources is just as explicit as creating them. In theory you could exit the application without doing that and have the operating system clean up for you instead. But properly cleaning up after you is common sense and so we do that. We once again call vkDeviceWaitIdle to make sure none of the GPU resources we want to destroy are still in use. Once that call has successfully finished, we can start cleaning up all the Vulkan GPU objects we created in our application:\n\n```\nchk(vkDeviceWaitIdle(device));\nfor (auto i = 0; i < maxFramesInFlight; i++) {\n    vkDestroyFence(device, fences[i], nullptr);\n    vkDestroySemaphore(device, imageAcquiredSemaphores[i], nullptr);\n    ...\n}\nvmaDestroyImage(allocator, depthImage, depthImageAllocation);\n...\nvkDestroyCommandPool(device, commandPool, nullptr);\nvmaDestroyAllocator(allocator);\nvkDestroyDevice(device, nullptr);\nvkDestroyInstance(instance, nullptr);\n```\n\nOrdering of commands only matters for the VMA allocator, device and instance. These should only be destroyed after all objects created from them. The instance should be deleted last, that way we'll be notified by the validation layers (when enabled) of every object we forgot to properly delete. One resource you don't have to explicitly destroy are the command buffers. Calling [vkDestroyCommandPool](https://docs.vulkan.org/refpages/latest/refpages/source/vkDestroyCommandPool.html) will implicitly free all command buffers allocated from that pool.\n\n## Closing words\n\nBy now, you should have a basic understanding of how to create a Vulkan application that performs rasterization and leverages recent API versions and features. Vulkan remains a relatively verbose API, which is inherent to its explicit, low-level design. But though we still need a lot of code to get something up and running in Vulkan, it's easier to understand and more flexible, making it a solid foundation for more complex applications.\n\nLooking at the broader picture, Vulkan in 2026 supports a wider range of use cases than ever. In addition to rasterization and compute, it also offers functionality for hardware-accelerated raytracing, video encoding and decoding, machine learning, and safety-critical areas.\n\nIf you're looking for further resources, check the [Vulkan Docs Site](https://docs.vulkan.org/). It combines multiple Vulkan documentation resources like the spec, a tutorial and samples into a convenient single site.", "url": "https://wpnews.pro/news/how-to-vulkan-in-2026", "canonical_source": "https://www.howtovulkan.com/", "published_at": "2026-07-23 15:37:50+00:00", "updated_at": "2026-07-23 15:52:53.797560+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Sascha Willems", "Vulkan", "Vulkan 1.3", "SDL", "Khronos Group"], "alternates": {"html": "https://wpnews.pro/news/how-to-vulkan-in-2026", "markdown": "https://wpnews.pro/news/how-to-vulkan-in-2026.md", "text": "https://wpnews.pro/news/how-to-vulkan-in-2026.txt", "jsonld": "https://wpnews.pro/news/how-to-vulkan-in-2026.jsonld"}}