diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 9f9b795ec..62bf3ecb1 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -57,6 +57,7 @@ ** xref:samples/extensions/conservative_rasterization/README.adoc[Conservative rasterization] ** xref:samples/extensions/debug_utils/README.adoc[Debug utils] ** xref:samples/extensions/descriptor_buffer_basic/README.adoc[Descriptor buffer basic] +** xref:samples/extensions/descriptor_heap/README.adoc[Descriptor heap] ** xref:samples/extensions/descriptor_indexing/README.adoc[Descriptor indexing] ** xref:samples/extensions/dynamic_line_rasterization/README.adoc[Dynamic line rasterization] ** xref:samples/extensions/dynamic_primitive_clipping/README.adoc[Dynamic primitive clipping] diff --git a/framework/api_vulkan_sample.h b/framework/api_vulkan_sample.h index 45954d1d0..0621a36df 100644 --- a/framework/api_vulkan_sample.h +++ b/framework/api_vulkan_sample.h @@ -299,7 +299,7 @@ class ApiVulkanSample : public vkb::VulkanSampleC /** * @brief Creates a new (graphics) command pool object storing command buffers */ - void create_command_pool(); + virtual void create_command_pool(); /** * @brief Setup default depth and stencil views diff --git a/samples/extensions/README.adoc b/samples/extensions/README.adoc index 66dd39982..7102c8a82 100644 --- a/samples/extensions/README.adoc +++ b/samples/extensions/README.adoc @@ -250,11 +250,19 @@ Demonstrate how to use patch control points dynamically, which can reduce the nu Demonstrate how to use fragment shader barycentric feature, which allows accessing barycentric coordinates for each processed fragment. -=== xref:./{extension_samplespath}descriptor_buffer_basic/README.adoc[Basic descriptor buffer] +=== xref:./{extension_samplespath}descriptor_buffer_basic/README.adoc[Descriptor buffer] *Extension*: https://www.khronos.org/registry/vulkan/specs/latest/html/vkspec.html#VK_ext_descriptor_buffer[`VK_EXT_descriptor_buffer`] -Demonstrate how to use the new extension to replace descriptor sets with resource descriptor buffers +**Note:** If available, `VK_EXT_descriptor_heap` should be preferred. + +Demonstrate how to use the resource descriptor buffers to replace descriptor sets + +=== xref:./{extension_samplespath}descriptor_heap/README.adoc[Descriptor heap] + +*Extension*: https://www.khronos.org/registry/vulkan/specs/latest/html/vkspec.html#VK_ext_descriptor_heap[`VK_EXT_descriptor_heap`] + +Demonstrate how to use descriptor heaps that completely rework how descriptors are handled in Vulkan === xref:./{extension_samplespath}color_write_enable/README.adoc[Color write enable] diff --git a/samples/extensions/descriptor_heap/CMakeLists.txt b/samples/extensions/descriptor_heap/CMakeLists.txt new file mode 100644 index 000000000..9ef8d3000 --- /dev/null +++ b/samples/extensions/descriptor_heap/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Sascha Willems +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 the "License"; +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH) +get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME) + +add_sample( + ID ${FOLDER_NAME} + CATEGORY ${CATEGORY_NAME} + AUTHOR "Sascha Willems" + NAME "Descriptor Heap" + DESCRIPTION "Demonstrates the descriptor heap extension to streamline descriptor setup" + SHADER_FILES_GLSL + "descriptor_heap/glsl/cube.vert" + "descriptor_heap/glsl/cube.frag" + SHADER_FILES_HLSL + "descriptor_heap/hlsl/cube.vert.hlsl" + "descriptor_heap/hlsl/cube.frag.hlsl" + SHADER_FILES_SLANG + "descriptor_heap/slang/cube.vert.slang" + "descriptor_heap/slang/cube.frag.slang") \ No newline at end of file diff --git a/samples/extensions/descriptor_heap/README.adoc b/samples/extensions/descriptor_heap/README.adoc new file mode 100644 index 000000000..d1565e0a5 --- /dev/null +++ b/samples/extensions/descriptor_heap/README.adoc @@ -0,0 +1,303 @@ +//// +Copyright (c) 2026, Sascha Willems + + SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +//// += Descriptor heap + +ifdef::site-gen-antora[] +TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/descriptor_heap[Khronos Vulkan samples github repository]. +endif::[] + +== Overview + +Managing descriptors is one of the harder parts of Vulkan. The link:https://docs.vulkan.org/features/latest/features/proposals/VK_EXT_descriptor_buffer.html[VK_EXT_descriptor_buffer] extension aimed to simplify this, but it came with a few caveats. + +The link:https://docs.vulkan.org/features/latest/features/proposals/VK_EXT_descriptor_heap.html[VK_EXT_descriptor_heap] extension addresses these caveats and completely overhauls Vulkan's descriptor. + +This extension is intended to completely replace Vulkan's existing descriptor set mechanism and, if available, supersedes descriptor buffers. It is meant to be the way forward for passing data to shaders. + +In Vulkan, descriptor state is built around several explicit objects and binding steps. Applications define descriptor set layouts, create compatible pipeline layouts, allocate descriptor sets from descriptor pools, update those sets with resource information, and bind them during command buffer generation. This model is very explicit and works well, but it also means that descriptor management tends to become one of the more rigid and verbose parts of using Vulkan. + +Descriptor heaps replace most of that object-centric workflow with a memory-centric one. Instead of allocating descriptor sets and binding them per draw or dispatch, the application writes descriptor data directly into heap memory and binds the heaps themselves. Shaders then access descriptors either through set-and-binding mappings, which can be used with existing shaders, or more directly via untyped pointers. In practice, this moves descriptor handling closer to how applications already think about buffers: allocate memory, write data into it, and provide an addressable view to the GPU. + +So the shift is not only about reducing API objects. It is also about changing the mental model from "create and bind descriptor objects" to "manage descriptor memory". + +To sum it up: descriptor heaps greatly simplify descriptor management and make it much more flexible. + +== Key benefits + +Understanding why descriptor heaps matter in practice comes down to a handful of concrete advantages over the traditional model. + +=== No descriptor pools or descriptor sets + +In traditional Vulkan, applications must pre-allocate descriptor pools, specify counts per descriptor type, and allocate descriptor sets from those pools. Getting pool sizes wrong leads to out-of-memory errors, and fragmentation can become a problem over time. With descriptor heaps, there are no pools and no sets - just heap buffers. Allocation is as straightforward as sizing a buffer for the number of descriptors you need. + +=== No pipeline layout compatibility constraints + +Traditional descriptor sets are bound against a specific pipeline layout, and the layout of the bound sets must be compatible with the pipeline being used. With descriptor heaps, there are no pipeline layouts for descriptor data. The same heap bindings work across all pipelines. + +=== Descriptors as plain memory + +Updating descriptors in the traditional model requires a `vkUpdateDescriptorSets` call, and the set must not be in use by the GPU at the time. With descriptor heaps, updating a descriptor is just a memory write via the `vkWrite*DescriptorsEXT` functions. This removes the "in-use" constraint at the descriptor object level and reduces the CPU-side API overhead per update. + +=== Natural fit for bindless rendering + +Descriptor indexing (link:https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_descriptor_indexing.html[VK_EXT_descriptor_indexing]) made bindless rendering possible in Vulkan by treating descriptor sets as large arrays. Descriptor heaps take that idea further: an offset into heap memory is all that is needed to address any resource. There is no large descriptor set to maintain or bind — just a heap and an index. + +=== Simplified command buffer state management + +With heaps bound once per command buffer, there is no need to rebind descriptor sets when switching pipelines that share the same resource layout. This reduces the number of binding commands recorded per frame, which can meaningfully lower CPU overhead in draw-call-heavy workloads. + +== Heap types + +Implementations require two distinct heap types, so this extension differentiates between: + +- Sampler heaps for storing samplers +- Resource heaps for storing all other resource types like buffers and images + +So in a typical scenario where we want to use e.g. uniform buffers, storage buffers, images and samplers, we need to create two distinct heaps. + +== Creating heaps + +As noted above, heaps are basically just memory that you can copy descriptor-related information to. Creating a heap therefore is no different from creating any other buffer in Vulkan and requires the `VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT` usage flag: + +[,cpp] +---- +const VkDeviceSize heap_buffer_size = aligned_size(image_heap_offset + cube_count * image_descriptor_size + descriptor_heap_properties.minResourceHeapReservedRange, descriptor_heap_properties.resourceHeapAlignment); + +descriptor_heap_resources = + std::make_unique( + get_device(), + heap_buffer_size, + VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); +---- + +== Uploading data to heaps + +What information is required to store a descriptor type in a heap varies. The destination where this information is stored in the heap is passed using link:https://docs.vulkan.org/refpages/latest/refpages/source/VkHostAddressRangeEXT.html[host address ranges]. + +For samplers, we pass `VkSamplerCreateInfo` (no need to create an actual `VkSampler`): + +[,cpp] +---- +VkSamplerCreateInfo sampler_ci{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + ...}; +VkHostAddressRangeEXT sampler_har{ + .address = (uint8_t *) (descriptor_heap_samplers->get_data()) + sampler_heap_offset, + .size = sampler_descriptor_size}; +vkWriteSamplerDescriptorsEXT(get_device().get_handle(), 1, &sampler_ci, &sampler_har); +---- + +For images, we pass `VkImageViewCreateInfo` (pointing to the actual `VkImage`): + +[,cpp] +---- +VkImageViewCreateInfo image_view_ci{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = texture.image->get_vk_image().get_handle(), + .viewType = VK_IMAGE_VIEW_TYPE_2D + ...}; +VkImageDescriptorInfoEXT image_descriptor_info{ + .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, + .pView = &image_view_ci, + ...}; +VkResourceDescriptorInfoEXT image_resource_desc{ + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .data = {.pImage = &image_descriptor_info}}; +VkHostAddressRangeEXT image_host_address{ + .address = (uint8_t *) (descriptor_heap_resources->get_data()) + image_heap_offset, + .size = image_descriptor_size}; +vkWriteResourceDescriptorsEXT(get_device().get_handle(), 1, &image_resource_desc, &image_host_address); +---- + +For buffers, we just pass their device address and range: + +[,cpp] +---- +VkDeviceAddressRangeEXT buffer_device_addr_range{ + .address = uniform_buffer->get_device_address(), + .size = uniform_buffer->get_size()}; +VkResourceDescriptorInfoEXT buffer_resource_desc{ + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .data = {.pAddressRange = &buffer_device_addr_range}}; +VkHostAddressRangeEXT buffer_host_address{ + .address = (uint8_t *) (descriptor_heap_resources->get_data()), + .size = buffer_descriptor_size}; +vkWriteResourceDescriptorsEXT(get_device().get_handle(), 1, &buffer_resource_desc, &buffer_host_address); +---- + +link:https://docs.vulkan.org/refpages/latest/refpages/source/vkWriteResourceDescriptorsEXT.html[vkWriteResourceDescriptorsEXT] and link:https://docs.vulkan.org/refpages/latest/refpages/source/vkWriteSamplerDescriptorsEXT.html[vkWriteSamplerDescriptorsEXT] are then used to write the descriptors to heap memory. + +**Note:** The above examples are simplified to show a basic setup. The `vkWrite*DescriptorsEXT` functions can be used to write multiple descriptors to a heap at once. + +== Binding heaps + +Heaps are bound per heap type using link:https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdBindResourceHeapEXT.html[vkCmdBindResourceHeapEXT], so if you use a resource heap and a sampler heap, you need to issue two binding calls: + +[,cpp] +---- +// Bind the resource heap (buffers and images) +VkBindHeapInfoEXT bind_heap_info_res{ + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange{ + .address = descriptor_heap_resources->get_device_address(), + .size = descriptor_heap_resources->get_size()}, + .reservedRangeOffset = descriptor_heap_resources->get_size() - descriptor_heap_properties.minResourceHeapReservedRange, + .reservedRangeSize = descriptor_heap_properties.minResourceHeapReservedRange, +}; +vkCmdBindResourceHeapEXT(draw_cmd_buffer, &bind_heap_info_res); + +// Bind the sampler heap +VkBindHeapInfoEXT bind_heap_info_samplers{ + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange{ + .address = descriptor_heap_samplers->get_device_address(), + .size = descriptor_heap_samplers->get_size()}, + .reservedRangeOffset = descriptor_heap_samplers->get_size() - descriptor_heap_properties.minSamplerHeapReservedRange, + .reservedRangeSize = descriptor_heap_properties.minSamplerHeapReservedRange}; +vkCmdBindSamplerHeapEXT(draw_cmd_buffer, &bind_heap_info_samplers); + +... + +// All draw calls will source from heaps (until non-heap bindings are issued) +vkCmdDrawIndexed(draw_cmd_buffer, cube->vertex_indices, 2, 0, 0, 0); +---- + +**Note:** One important difference, compared to other buffers, is the **reserved range** (e.g. `reservedRangeOffset` and `reservedRangeSize`). Implementations, to varying degrees, need additional space for some descriptor-related operations. This space must not be used by the application, so we need to both allocate it and mark those regions as reserved. For this sample, we put the reserved range after the space where we have copied the descriptors. + +== Push data (instead of push constants) + +This extension also adds a more direct way to push state data to the command buffer. With no need for pipeline layouts, this is now much simpler and is done via a single call to link:https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdPushDataEXT.html[vkCmdPushDataEXT]: + +[,cpp] +---- +PushData push_data{ ... }; +VkPushDataInfoEXT push_data_info{ + .sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT, + .data = {.address = &push_data, .size = sizeof(PushData)}}; +vkCmdPushDataEXT(draw_cmd_buffer, &push_data_info); +---- + +== Accessing heaps + +The extension lets us access descriptors in shaders in two different ways: + +=== Descriptor set and binding mappings + +link:https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorSetAndBindingMappingEXT.html[VkDescriptorSetAndBindingMappingEXT] can be used to simplify porting existing codebases to descriptor heaps. By using this, existing shaders can be used without having to modify them. The syntax for binding descriptors, regardless of shading language, stays the same. + +[,glsl] +---- +// Vertex shader +layout (set = 0, binding = 0) uniform UBO { + mat4 projection; + mat4 view; + mat4 model; +} ubo[2]; + +// Fragment shader +layout (set = 1, binding = 0) uniform texture2D textureImage[2]; +layout (set = 2, binding = 0) uniform sampler textureSampler[2]; +---- + +The app then defines how the heaps are mapped to those bindings: + +[,cpp] +---- +std::array set_binding_mappings{}; + +// Maps "layout (set = 0, binding = 0) uniform UBO" +set_binding_mappings[0] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 0, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_UNIFORM_BUFFER_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapArrayStride = static_cast(buffer_descriptor_size)}}}; + +// Maps "layout (set = 1, binding = 0) uniform texture2D" +set_binding_mappings[1] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 1, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_SAMPLED_IMAGE_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapOffset = static_cast(image_heap_offset), .heapArrayStride = static_cast(image_descriptor_size)}}}; + +// Maps "layout (set = 2, binding = 0) uniform sampler" +set_binding_mappings[2] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 2, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_SAMPLER_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapOffset = static_cast(sampler_heap_offset), .heapArrayStride = static_cast(sampler_descriptor_size)}}}; +---- + +These mappings need to be passed to the corresponding shader stages: + +[,cpp] +---- +VkShaderDescriptorSetAndBindingMappingInfoEXT descriptor_set_binding_mapping_info{ + .sType = VK_STRUCTURE_TYPE_SHADER_DESCRIPTOR_SET_AND_BINDING_MAPPING_INFO_EXT, + .mappingCount = static_cast(set_binding_mappings.size()), + .pMappings = set_binding_mappings.data()}; + +shader_stages[0].pNext = &descriptor_set_binding_mapping_info; +shader_stages[1].pNext = &descriptor_set_binding_mapping_info; +---- + +=== Untyped pointers + +**Note:** This is not yet demonstrated in this sample. Support also varies between shading languages and might not be available everywhere. + +As a more forward-looking approach, descriptors can be accessed directly from a shader using SPIR-V untyped pointers, which are exposed in Vulkan via link:https://docs.vulkan.org/refpages/latest/refpages/source/VK_KHR_shader_untyped_pointers.html[VK_KHR_shader_untyped_pointers]. + +This requires shaders to be adapted accordingly and also requires the shading language to support this feature. In GLSL, for example, this is provided via link:https://github.com/KhronosGroup/GLSL/blob/main/extensions/ext/GLSL_EXT_descriptor_heap.txt[GLSL_EXT_descriptor_heap]: + +[,glsl] +---- +#extension GL_EXT_descriptor_heap: require +... +layout(descriptor_heap) uniform texture2D textureImage[]; +layout(descriptor_heap) uniform sampler textureSampler[]; +... +vec4 color = texture(sampler2D(textureImage[someIndex])); +---- + +== Links + +* https://docs.vulkan.org/features/latest/features/proposals/VK_EXT_descriptor_heap.html[Extension proposal] +* https://docs.vulkan.org/guide/latest/descriptor_heap.html[Vulkan Guide entry] +* https://www.khronos.org/blog/vulkan-introduces-roadmap-2026-and-new-descriptor-heap-extension[Blog entry] +* https://vulkan.gpuinfo.org/displayextensiondetail.php?extension=VK_EXT_descriptor_heap[Extension device coverage] + + diff --git a/samples/extensions/descriptor_heap/descriptor_heap.cpp b/samples/extensions/descriptor_heap/descriptor_heap.cpp new file mode 100644 index 000000000..7522a978e --- /dev/null +++ b/samples/extensions/descriptor_heap/descriptor_heap.cpp @@ -0,0 +1,573 @@ +/* + * Copyright (c) 2026 Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "descriptor_heap.h" + +DescriptorHeap::DescriptorHeap() +{ + title = "Descriptor heap"; + + add_device_extension(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + add_device_extension(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME); + add_device_extension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); + add_device_extension(VK_EXT_DESCRIPTOR_HEAP_EXTENSION_NAME); +} + +DescriptorHeap::~DescriptorHeap() +{ + if (has_device()) + { + vkDestroyPipeline(get_device().get_handle(), pipeline, nullptr); + for (auto &cube : cubes) + { + cube.texture.image.reset(); + vkDestroySampler(get_device().get_handle(), cube.texture.sampler, nullptr); + } + cube.reset(); + descriptor_heap_resources.reset(); + descriptor_heap_samplers.reset(); + } +} + +bool DescriptorHeap::prepare(const vkb::ApplicationOptions &options) +{ + if (!ApiVulkanSample::prepare(options)) + { + return false; + } + + camera.type = vkb::CameraType::LookAt; + camera.set_position({0.f, 0.f, -4.f}); + camera.set_rotation({0.f, 180.f, 0.f}); + camera.set_perspective(60.f, static_cast(width) / static_cast(height), 256.f, 0.1f); + + load_assets(); + prepare_uniform_buffers(); + create_descriptor_heaps(); + create_pipeline(); + prepared = true; + + return true; +} + +void DescriptorHeap::request_gpu_features(vkb::core::PhysicalDeviceC &gpu) +{ + // Enable features required for this example + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDeviceBufferDeviceAddressFeatures, bufferDeviceAddress); + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDeviceDynamicRenderingFeaturesKHR, dynamicRendering); + + // We need to enable the descriptor heap feature to make use of them + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDeviceDescriptorHeapFeaturesEXT, descriptorHeap); + + if (gpu.get_features().samplerAnisotropy) + { + gpu.get_mutable_requested_features().samplerAnisotropy = true; + } +} + +void DescriptorHeap::setup_render_pass() +{ + // We use dynamic rendering, so we skip render pass setup +} + +void DescriptorHeap::setup_framebuffer() +{ + // We use dynamic rendering, so we skip framebuffer setup +} + +void DescriptorHeap::on_update_ui_overlay(vkb::Drawer &drawer) +{ + drawer.combo_box("Sampler type", &selected_sampler, sampler_names); +} + +uint32_t DescriptorHeap::get_api_version() const +{ + return VK_API_VERSION_1_2; +} + +void DescriptorHeap::load_assets() +{ + cube = load_model("scenes/textured_unit_cube.gltf"); + cubes[0].texture = load_texture("textures/crate01_color_height_rgba.ktx", vkb::sg::Image::Color); + cubes[1].texture = load_texture("textures/crate02_color_height_rgba.ktx", vkb::sg::Image::Color); +} + +inline static VkDeviceSize aligned_size(VkDeviceSize value, VkDeviceSize alignment) +{ + return (value + alignment - 1) & ~(alignment - 1); +} + +void DescriptorHeap::prepare_uniform_buffers() +{ + uniform_buffers.resize(draw_cmd_buffers.size()); + for (auto &uniform_buffer : uniform_buffers) + { + uniform_buffer = std::make_unique(get_device(), sizeof(UniformData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + } +} + +void DescriptorHeap::update_uniform_buffers(float delta_time) +{ + if (animate) + { + cubes[0].rotation.x += 2.5f * delta_time; + if (cubes[0].rotation.x > 360.0f) + { + cubes[0].rotation.x -= 360.0f; + } + cubes[1].rotation.y += 2.0f * delta_time; + if (cubes[1].rotation.y > 360.0f) + { + cubes[1].rotation.y -= 360.0f; + } + } + + uniform_data.projection_matrix = camera.matrices.perspective; + uniform_data.view_matrix = camera.matrices.view; + + std::array positions = {glm::vec3(-2.0f, 0.0f, 0.0f), glm::vec3(1.5f, 0.5f, 0.0f)}; + for (auto i = 0; i < cube_count; i++) + { + glm::mat4 cubeMat = glm::translate(glm::mat4(1.0f), positions[i]); + cubeMat = glm::rotate(cubeMat, glm::radians(cubes[i].rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + cubeMat = glm::rotate(cubeMat, glm::radians(cubes[i].rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + cubeMat = glm::rotate(cubeMat, glm::radians(cubes[i].rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + uniform_data.model_matrix[i] = cubeMat; + } + + uniform_buffers[current_buffer]->convert_and_update(uniform_data); +} + +void DescriptorHeap::create_descriptor_heaps() +{ + // Descriptor heaps have varying offset, size and alignment requirements, so we store it's properties for later user + descriptor_heap_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 device_props_2{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &descriptor_heap_properties}; + vkGetPhysicalDeviceProperties2(get_device().get_gpu().get_handle(), &device_props_2); + + // Resource heap (buffers and images) + buffer_descriptor_size = aligned_size(descriptor_heap_properties.bufferDescriptorSize, descriptor_heap_properties.bufferDescriptorAlignment); + // Images are stored after the last buffer (aligned) + image_heap_offset = aligned_size(buffer_descriptor_size * cube_count, descriptor_heap_properties.imageDescriptorAlignment); + image_descriptor_size = aligned_size(descriptor_heap_properties.imageDescriptorSize, descriptor_heap_properties.imageDescriptorAlignment); + // Samplers will be stored in a separate heap + sampler_descriptor_size = aligned_size(descriptor_heap_properties.samplerDescriptorSize, descriptor_heap_properties.samplerDescriptorAlignment); + + // There are two descriptor heap types: One that can store resources (buffers, images) and one that can store samplers + const VkDeviceSize heap_buffer_size = aligned_size(image_heap_offset + cube_count * image_descriptor_size + descriptor_heap_properties.minResourceHeapReservedRange, descriptor_heap_properties.resourceHeapAlignment); + + descriptor_heap_resources = std::make_unique(get_device(), + heap_buffer_size, + VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + + const VkDeviceSize heap_sampler_size = aligned_size(sampler_count * sampler_descriptor_size + descriptor_heap_properties.minSamplerHeapReservedRange, descriptor_heap_properties.samplerHeapAlignment); + + descriptor_heap_samplers = std::make_unique(get_device(), + heap_sampler_size, + VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + + // Sampler heap + std::array host_address_ranges_samplers{}; + + // No need to create an actual VkSampler, we can simply pass the create info that describes the sampler + std::array sampler_create_infos{ + VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .maxAnisotropy = 16.0f, + .maxLod = (float) cubes[0].texture.image.get()->get_mipmaps().size(), + }, + VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_NEAREST, + .minFilter = VK_FILTER_NEAREST, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .maxAnisotropy = 16.0f, + .maxLod = (float) cubes[1].texture.image.get()->get_mipmaps().size(), + }}; + + for (auto i = 0; i < static_cast(sampler_create_infos.size()); i++) + { + host_address_ranges_samplers[i] = { + .address = const_cast(descriptor_heap_samplers->get_data()) + sampler_heap_offset + sampler_descriptor_size * i, + .size = sampler_descriptor_size}; + } + + VK_CHECK(vkWriteSamplerDescriptorsEXT(get_device().get_handle(), static_cast(host_address_ranges_samplers.size()), sampler_create_infos.data(), host_address_ranges_samplers.data())); + + auto vector_size{cubes.size() + uniform_buffers.size()}; + std::vector host_address_ranges_resources(vector_size); + std::vector resource_descriptor_infos(vector_size); + + size_t resource_heap_index{0}; + + // Buffers + std::vector device_address_ranges_uniform_buffer(uniform_buffers.size()); + for (auto i = 0; i < uniform_buffers.size(); i++) + { + device_address_ranges_uniform_buffer[i] = {.address = uniform_buffers[i]->get_device_address(), .size = uniform_buffers[i]->get_size()}; + resource_descriptor_infos[resource_heap_index] = { + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .data = { + .pAddressRange = &device_address_ranges_uniform_buffer[i]}}; + host_address_ranges_resources[resource_heap_index] = { + .address = const_cast(descriptor_heap_resources->get_data()) + buffer_descriptor_size * i, + .size = buffer_descriptor_size}; + + resource_heap_index++; + } + + // Images (one per cube) + std::array image_view_create_infos{}; + std::array image_descriptor_infos{}; + + for (auto i = 0; i < cubes.size(); i++) + { + image_view_create_infos[i] = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = cubes[i].texture.image->get_vk_image().get_handle(), + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = cubes[i].texture.image->get_format(), + .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = static_cast(cubes[i].texture.image->get_mipmaps().size()), .baseArrayLayer = 0, .layerCount = 1}, + }; + + image_descriptor_infos[i] = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, + .pView = &image_view_create_infos[i], + .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + + resource_descriptor_infos[resource_heap_index] = { + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .data = { + .pImage = &image_descriptor_infos[i]}}; + + host_address_ranges_resources[resource_heap_index] = { + .address = const_cast(descriptor_heap_resources->get_data()) + image_heap_offset + image_descriptor_size * i, + .size = image_descriptor_size}; + + resource_heap_index++; + } + + VK_CHECK(vkWriteResourceDescriptorsEXT(get_device().get_handle(), static_cast(resource_descriptor_infos.size()), resource_descriptor_infos.data(), host_address_ranges_resources.data())); +} + +void DescriptorHeap::create_pipeline() +{ + VkPipelineInputAssemblyStateCreateInfo input_assembly_state = vkb::initializers::pipeline_input_assembly_state_create_info(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE); + VkPipelineRasterizationStateCreateInfo rasterization_state = vkb::initializers::pipeline_rasterization_state_create_info(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, 0); + VkPipelineColorBlendAttachmentState blend_attachment_state = vkb::initializers::pipeline_color_blend_attachment_state(0xf, VK_FALSE); + VkPipelineColorBlendStateCreateInfo color_blend_state = vkb::initializers::pipeline_color_blend_state_create_info(1, &blend_attachment_state); + VkPipelineDepthStencilStateCreateInfo depth_stencil_state = vkb::initializers::pipeline_depth_stencil_state_create_info(VK_TRUE, VK_TRUE, VK_COMPARE_OP_GREATER); + VkPipelineViewportStateCreateInfo viewport_state = vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0); + VkPipelineMultisampleStateCreateInfo multisample_state = vkb::initializers::pipeline_multisample_state_create_info(VK_SAMPLE_COUNT_1_BIT, 0); + std::vector dynamic_state_enables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic_state = vkb::initializers::pipeline_dynamic_state_create_info(dynamic_state_enables); + + // Vertex bindings and attributes + const std::vector vertex_input_bindings = { + vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX), + }; + const std::vector vertex_input_attributes = { + vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos)), + vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv)), + }; + VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info(); + vertex_input_state.vertexBindingDescriptionCount = static_cast(vertex_input_bindings.size()); + vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data(); + vertex_input_state.vertexAttributeDescriptionCount = static_cast(vertex_input_attributes.size()); + vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data(); + + std::array shader_stages{}; + shader_stages[0] = load_shader("descriptor_heap", "cube.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); + shader_stages[1] = load_shader("descriptor_heap", "cube.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); + + // Descriptor heaps can be used without having to explicitly change the shaders + // This is done by specifiying the bindings and their types at the shader stage level + // As samplers require a different heap (than images), we can't use combined images + + std::array set_binding_mappings{}; + + // Buffer binding + set_binding_mappings[0] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 0, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_UNIFORM_BUFFER_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapArrayStride = static_cast(buffer_descriptor_size)}}}; + + // We are using multiple images, which requires us to set heapArrayStride to let the implementation know where image n+1 starts + set_binding_mappings[1] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 1, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_SAMPLED_IMAGE_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapOffset = static_cast(image_heap_offset), .heapArrayStride = static_cast(image_descriptor_size)}}}; + + // As samplers require a different heap (than images), we can't use combined images but split image and sampler + set_binding_mappings[2] = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT, + .descriptorSet = 2, + .firstBinding = 0, + .bindingCount = 1, + .resourceMask = VK_SPIRV_RESOURCE_TYPE_SAMPLER_BIT_EXT, + .source = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT, + .sourceData = { + .constantOffset = { + .heapOffset = static_cast(sampler_heap_offset), .heapArrayStride = static_cast(sampler_descriptor_size)}}}; + + VkShaderDescriptorSetAndBindingMappingInfoEXT descriptor_set_binding_mapping_info{ + .sType = VK_STRUCTURE_TYPE_SHADER_DESCRIPTOR_SET_AND_BINDING_MAPPING_INFO_EXT, + .mappingCount = static_cast(set_binding_mappings.size()), + .pMappings = set_binding_mappings.data()}; + + shader_stages[0].pNext = &descriptor_set_binding_mapping_info; + shader_stages[1].pNext = &descriptor_set_binding_mapping_info; + + // Create graphics pipeline for dynamic rendering + VkFormat color_rendering_format = get_render_context().get_format(); + + // Provide information for dynamic rendering + VkPipelineRenderingCreateInfoKHR pipeline_rendering_create_info{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &color_rendering_format, + .depthAttachmentFormat = depth_format}; + if (!vkb::is_depth_only_format(depth_format)) + { + pipeline_rendering_create_info.stencilAttachmentFormat = depth_format; + } + + // Use the pNext to point to the rendering create struct + VkGraphicsPipelineCreateInfo pipeline_create_info{ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .stageCount = static_cast(shader_stages.size()), + .pStages = shader_stages.data(), + .pVertexInputState = &vertex_input_state, + .pInputAssemblyState = &input_assembly_state, + .pViewportState = &viewport_state, + .pRasterizationState = &rasterization_state, + .pMultisampleState = &multisample_state, + .pDepthStencilState = &depth_stencil_state, + .pColorBlendState = &color_blend_state, + .pDynamicState = &dynamic_state, + }; + + // With descriptor heaps we no longer need a pipeline layout + // This struct must be chained into pipeline creation to enable the use of heaps (allowing us to leave pipelineLayout empty) + VkPipelineCreateFlags2CreateInfo pipeline_create_flags_2{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO, + .pNext = &pipeline_rendering_create_info, + .flags = VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT}; + pipeline_create_info.pNext = &pipeline_create_flags_2; + + VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), VK_NULL_HANDLE, 1, &pipeline_create_info, VK_NULL_HANDLE, &pipeline)); +} + +void DescriptorHeap::draw(float delta_time) +{ + ApiVulkanSample::prepare_frame(); + update_uniform_buffers(delta_time); + build_command_buffer(); + submit_info.commandBufferCount = 1; + submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer]; + VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE)); + ApiVulkanSample::submit_frame(); +} + +void DescriptorHeap::build_command_buffers() +{ + // This sample doesn't use prebuilt command buffers +} + +void DescriptorHeap::build_command_buffer() +{ + VkCommandBuffer draw_cmd_buffer = draw_cmd_buffers[current_buffer]; + vkResetCommandBuffer(draw_cmd_buffer, 0); + + std::array clear_values{}; + clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}}; + clear_values[1].depthStencil = {0.0f, 0}; + + auto command_begin = vkb::initializers::command_buffer_begin_info(); + VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffer, &command_begin)); + + VkImageSubresourceRange range{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS}; + + VkImageSubresourceRange depth_range{range}; + depth_range.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + + vkb::image_layout_transition(draw_cmd_buffer, + swapchain_buffers[current_buffer].image, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + range); + + vkb::image_layout_transition(draw_cmd_buffer, + depth_stencil.image, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + depth_range); + + VkRenderingAttachmentInfoKHR color_attachment_info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR, + .imageView = swapchain_buffers[current_buffer].view, + .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .resolveMode = VK_RESOLVE_MODE_NONE, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = clear_values[0]}; + + VkRenderingAttachmentInfoKHR depth_attachment_info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR, + .imageView = depth_stencil.view, + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + .resolveMode = VK_RESOLVE_MODE_NONE, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .clearValue = clear_values[1]}; + + auto render_area = VkRect2D{VkOffset2D{}, VkExtent2D{width, height}}; + auto render_info = vkb::initializers::rendering_info(render_area, 1, &color_attachment_info); + render_info.layerCount = 1; + render_info.pDepthAttachment = &depth_attachment_info; + if (!vkb::is_depth_only_format(depth_format)) + { + render_info.pStencilAttachment = &depth_attachment_info; + } + + vkCmdBeginRenderingKHR(draw_cmd_buffer, &render_info); + + VkViewport viewport = vkb::initializers::viewport(static_cast(width), static_cast(height), 0.0f, 1.0f); + vkCmdSetViewport(draw_cmd_buffer, 0, 1, &viewport); + + VkRect2D scissor = vkb::initializers::rect2D(static_cast(width), static_cast(height), 0, 0); + vkCmdSetScissor(draw_cmd_buffer, 0, 1, &scissor); + + vkCmdBindPipeline(draw_cmd_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + // Pass options as push data + struct PushData + { + int32_t sampler_index; + int32_t frame_index; + } push_data = { + .sampler_index = selected_sampler, + // Samples do not support frames-in-flight yet, so frameIndex never changes + .frame_index = 0}; + VkPushDataInfoEXT push_data_info{ + .sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT, + .data = {.address = &push_data, .size = sizeof(PushData)}}; + vkCmdPushDataEXT(draw_cmd_buffer, &push_data_info); + + // Bind the heap containing resources (buffers and images) + VkBindHeapInfoEXT bind_heap_info_res{ + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange{ + .address = descriptor_heap_resources->get_device_address(), + .size = descriptor_heap_resources->get_size()}, + .reservedRangeOffset = descriptor_heap_resources->get_size() - descriptor_heap_properties.minResourceHeapReservedRange, + .reservedRangeSize = descriptor_heap_properties.minResourceHeapReservedRange, + }; + vkCmdBindResourceHeapEXT(draw_cmd_buffer, &bind_heap_info_res); + + // Bind the heap containing samplers + VkBindHeapInfoEXT bind_heap_info_samplers{ + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange{ + .address = descriptor_heap_samplers->get_device_address(), + .size = descriptor_heap_samplers->get_size()}, + .reservedRangeOffset = descriptor_heap_samplers->get_size() - descriptor_heap_properties.minSamplerHeapReservedRange, + .reservedRangeSize = descriptor_heap_properties.minSamplerHeapReservedRange}; + vkCmdBindSamplerHeapEXT(draw_cmd_buffer, &bind_heap_info_samplers); + + VkDeviceSize offsets[1] = {0}; + + const auto &vertex_buffer = cube->vertex_buffers.at("vertex_buffer"); + const auto &index_buffer = cube->index_buffer; + + vkCmdBindVertexBuffers(draw_cmd_buffer, 0, 1, vertex_buffer.get(), offsets); + vkCmdBindIndexBuffer(draw_cmd_buffer, index_buffer->get_handle(), 0, cube->index_type); + vkCmdDrawIndexed(draw_cmd_buffer, cube->vertex_indices, cube_count, 0, 0, 0); + + vkCmdEndRenderingKHR(draw_cmd_buffer); + + draw_ui(draw_cmd_buffer, current_buffer); + + vkb::image_layout_transition(draw_cmd_buffer, + swapchain_buffers[current_buffer].image, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + range); + + VK_CHECK(vkEndCommandBuffer(draw_cmd_buffer)); +} + +void DescriptorHeap::render(float delta_time) +{ + if (!prepared) + { + return; + } + draw(delta_time); +} + +void DescriptorHeap::create_command_pool() +{ + VkCommandPoolCreateInfo command_pool_info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT, 0).get_family_index()}; + VK_CHECK(vkCreateCommandPool(get_device().get_handle(), &command_pool_info, nullptr, &cmd_pool)); +} + +std::unique_ptr create_descriptor_heap() +{ + return std::make_unique(); +} diff --git a/samples/extensions/descriptor_heap/descriptor_heap.h b/samples/extensions/descriptor_heap/descriptor_heap.h new file mode 100644 index 000000000..40eaa7c05 --- /dev/null +++ b/samples/extensions/descriptor_heap/descriptor_heap.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "api_vulkan_sample.h" + +class DescriptorHeap : public ApiVulkanSample +{ + public: + DescriptorHeap(); + ~DescriptorHeap() override; + + bool prepare(const vkb::ApplicationOptions &options) override; + + void render(float delta_time) override; + void create_command_pool() override; + void build_command_buffers() override; + void build_command_buffer(); + void request_gpu_features(vkb::core::PhysicalDeviceC &gpu) override; + void setup_render_pass() override; + void setup_framebuffer() override; + void on_update_ui_overlay(vkb::Drawer &drawer) override; + + private: + bool animate = true; + + static const size_t cube_count{2}; + static const size_t sampler_count{2}; + + struct Cube + { + Texture texture; + glm::vec3 rotation; + }; + std::array cubes; + + struct UniformData + { + glm::mat4 projection_matrix; + glm::mat4 view_matrix; + glm::mat4 model_matrix[cube_count]; + } uniform_data; + + VkPhysicalDeviceDescriptorHeapPropertiesEXT descriptor_heap_properties{}; + std::unique_ptr descriptor_heap_resources; + std::unique_ptr descriptor_heap_samplers; + std::vector> uniform_buffers; + + int32_t selected_sampler{0}; + + std::unique_ptr cube; + + // Size and offset values for heap objects + VkDeviceSize buffer_heap_offset{0}; + VkDeviceSize buffer_descriptor_size{0}; + VkDeviceSize image_heap_offset{0}; + VkDeviceSize image_descriptor_size{0}; + VkDeviceSize sampler_heap_offset{0}; + VkDeviceSize sampler_descriptor_size{0}; + + std::vector sampler_names{"Linear", "Nearest"}; + + VkPipeline pipeline{nullptr}; + + uint32_t get_api_version() const override; + + void load_assets(); + void prepare_uniform_buffers(); + void update_uniform_buffers(float delta_time); + void create_descriptor_heaps(); + void create_pipeline(); + void draw(float delta_time); +}; + +std::unique_ptr create_descriptor_heap(); diff --git a/shaders/descriptor_heap/glsl/cube.frag b/shaders/descriptor_heap/glsl/cube.frag new file mode 100644 index 000000000..7d2cc2fb4 --- /dev/null +++ b/shaders/descriptor_heap/glsl/cube.frag @@ -0,0 +1,35 @@ +#version 450 +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +layout (set = 1, binding = 0) uniform texture2D textureImage[2]; +layout (set = 2, binding = 0) uniform sampler textureSampler[2]; + +layout (location = 0) in vec2 inUV; +layout (location = 1) flat in int inInstanceIndex; + +layout(push_constant) uniform PushConsts { + int samplerIndex; + int frameIndex; +} pushConsts; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = texture(sampler2D(textureImage[inInstanceIndex], textureSampler[pushConsts.samplerIndex]), inUV); +} \ No newline at end of file diff --git a/shaders/descriptor_heap/glsl/cube.frag.spv b/shaders/descriptor_heap/glsl/cube.frag.spv new file mode 100644 index 000000000..33867fdb4 Binary files /dev/null and b/shaders/descriptor_heap/glsl/cube.frag.spv differ diff --git a/shaders/descriptor_heap/glsl/cube.vert b/shaders/descriptor_heap/glsl/cube.vert new file mode 100644 index 000000000..d07aebf18 --- /dev/null +++ b/shaders/descriptor_heap/glsl/cube.vert @@ -0,0 +1,41 @@ +#version 450 +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec2 inUV; + +layout(push_constant) uniform PushConsts { + int samplerIndex; + int frameIndex; +} pushConsts; + +layout (set = 0, binding = 0) uniform UBO { + mat4 projection; + mat4 view; + mat4 model[2]; +} ubo[2]; + +layout (location = 0) out vec2 outUV; +layout (location = 1) flat out int outInstanceIndex; + +void main() +{ + outUV = inUV; + gl_Position = ubo[pushConsts.frameIndex].projection * ubo[pushConsts.frameIndex].view * ubo[pushConsts.frameIndex].model[gl_InstanceIndex] * vec4(inPos.xyz, 1.0); + outInstanceIndex = gl_InstanceIndex; +} \ No newline at end of file diff --git a/shaders/descriptor_heap/glsl/cube.vert.spv b/shaders/descriptor_heap/glsl/cube.vert.spv new file mode 100644 index 000000000..450a8e893 Binary files /dev/null and b/shaders/descriptor_heap/glsl/cube.vert.spv differ diff --git a/shaders/descriptor_heap/hlsl/cube.frag.hlsl b/shaders/descriptor_heap/hlsl/cube.frag.hlsl new file mode 100644 index 000000000..771da7a14 --- /dev/null +++ b/shaders/descriptor_heap/hlsl/cube.frag.hlsl @@ -0,0 +1,40 @@ +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +[[vk::binding(0, 1)]] +Texture2D textureImage[2] : register(t1, space2); +[[vk::binding(0, 2)]] +SamplerState textureSampler[2] : register(s1, space2); + +struct VSOutput +{ + float4 Pos : SV_POSITION; + [[vk::location(0)]] float2 UV : TEXCOORD0; + [[vk::location(1)]] int InstanceIndex : TEXCOORD1; +}; + +struct PushConsts +{ + int samplerIndex; + int frameIndex; +}; +[[vk::push_constant]] PushConsts pushConsts; + +float4 main(VSOutput input) : SV_TARGET0 +{ + return textureImage[input.InstanceIndex].Sample(textureSampler[pushConsts.samplerIndex], input.UV); +} \ No newline at end of file diff --git a/shaders/descriptor_heap/hlsl/cube.frag.spv b/shaders/descriptor_heap/hlsl/cube.frag.spv new file mode 100644 index 000000000..42b35c3ab Binary files /dev/null and b/shaders/descriptor_heap/hlsl/cube.frag.spv differ diff --git a/shaders/descriptor_heap/hlsl/cube.vert.hlsl b/shaders/descriptor_heap/hlsl/cube.vert.hlsl new file mode 100644 index 000000000..e2096ff60 --- /dev/null +++ b/shaders/descriptor_heap/hlsl/cube.vert.hlsl @@ -0,0 +1,53 @@ +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +struct VSInput +{ + [[vk::location(0)]] float3 Pos : POSITION0; + [[vk::location(1)]] float2 UV : TEXCOORD0; +}; + +struct UBOCamera +{ + float4x4 projection; + float4x4 view; + float4x4 model[2]; +}; +[[vk::binding(0, 0)]] ConstantBuffer ubo[2] : register(b0, space0); + +struct VSOutput +{ + float4 Pos : SV_POSITION; + [[vk::location(0)]] float2 UV : TEXCOORD0; + [[vk::location(1)]] int InstanceIndex : TEXCOORD1; +}; + +struct PushConsts +{ + int samplerIndex; + int frameIndex; +}; +[[vk::push_constant]] PushConsts pushConsts; + +VSOutput main(VSInput input, uint InstanceIndex: SV_InstanceID) +{ + VSOutput output; + output.UV = input.UV; + output.Pos = mul(ubo[pushConsts.frameIndex].projection, mul(ubo[pushConsts.frameIndex].view, mul(ubo[pushConsts.frameIndex].model[InstanceIndex], float4(input.Pos.xyz, 1.0)))); + output.InstanceIndex = InstanceIndex; + return output; +}; \ No newline at end of file diff --git a/shaders/descriptor_heap/hlsl/cube.vert.spv b/shaders/descriptor_heap/hlsl/cube.vert.spv new file mode 100644 index 000000000..50c0e79cc Binary files /dev/null and b/shaders/descriptor_heap/hlsl/cube.vert.spv differ diff --git a/shaders/descriptor_heap/slang/cube.frag.slang b/shaders/descriptor_heap/slang/cube.frag.slang new file mode 100644 index 000000000..2e90807f2 --- /dev/null +++ b/shaders/descriptor_heap/slang/cube.frag.slang @@ -0,0 +1,32 @@ +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float2 UV; + int InstanceIndex; +}; + +[[vk::binding(0, 1)]] Texture2D textureImage[2]; +[[vk::binding(0, 2)]] SamplerState textureSampler[2]; + +[shader("fragment")] +float4 main(VSOutput input, uniform int samplerIndex, uniform int frameIndex) +{ + return textureImage[input.InstanceIndex].Sample(textureSampler[samplerIndex], input.UV); +} \ No newline at end of file diff --git a/shaders/descriptor_heap/slang/cube.frag.spv b/shaders/descriptor_heap/slang/cube.frag.spv new file mode 100644 index 000000000..51b98e56e Binary files /dev/null and b/shaders/descriptor_heap/slang/cube.frag.spv differ diff --git a/shaders/descriptor_heap/slang/cube.vert.slang b/shaders/descriptor_heap/slang/cube.vert.slang new file mode 100644 index 000000000..4ab1e6072 --- /dev/null +++ b/shaders/descriptor_heap/slang/cube.vert.slang @@ -0,0 +1,46 @@ +/* Copyright (c) 2026, Sascha Willems + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +struct VSInput +{ + float3 Pos; + float2 UV; +}; + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float2 UV; + int InstanceIndex; +}; + +struct UBO { + float4x4 projection; + float4x4 view; + float4x4 model[2]; +}; +[[vk::binding(0, 0)]] ConstantBuffer ubo[2]; + +[shader("vertex")] +VSOutput main(VSInput input, uint InstanceIndex: SV_VulkanInstanceID, uniform int samplerIndex, uniform int frameIndex) +{ + VSOutput output; + output.UV = input.UV; + output.Pos = mul(ubo[frameIndex].projection, mul(ubo[frameIndex].view, mul(ubo[frameIndex].model[InstanceIndex], float4(input.Pos.xyz, 1.0)))); + output.InstanceIndex = InstanceIndex; + return output; +} \ No newline at end of file diff --git a/shaders/descriptor_heap/slang/cube.vert.spv b/shaders/descriptor_heap/slang/cube.vert.spv new file mode 100644 index 000000000..f844342b5 Binary files /dev/null and b/shaders/descriptor_heap/slang/cube.vert.spv differ