From b9a069be05dd0ca41fda8a7e7ac9fe44ba734f81 Mon Sep 17 00:00:00 2001 From: Powei Feng Date: Tue, 5 Mar 2024 11:05:57 -0800 Subject: [PATCH] vk: Initial draft for descriptor set refactoring (#7620) - Add cache for ds layouts and ds - Abstract descriptors API into VulkanDescriptorSetManager - Note that this is just a draft and not hooked into the current implementation. --- filament/backend/CMakeLists.txt | 2 + filament/backend/src/vulkan/VulkanDriver.cpp | 2 +- .../src/vulkan/VulkanPipelineCache.cpp | 8 +- .../backend/src/vulkan/VulkanPipelineCache.h | 2 - .../backend/src/vulkan/VulkanResources.cpp | 6 +- filament/backend/src/vulkan/VulkanResources.h | 60 +- filament/backend/src/vulkan/VulkanUtility.h | 94 ++- .../vulkan/caching/VulkanDescriptorSet.cpp | 647 ++++++++++++++++++ .../src/vulkan/caching/VulkanDescriptorSet.h | 111 +++ 9 files changed, 866 insertions(+), 66 deletions(-) create mode 100644 filament/backend/src/vulkan/caching/VulkanDescriptorSet.cpp create mode 100644 filament/backend/src/vulkan/caching/VulkanDescriptorSet.h diff --git a/filament/backend/CMakeLists.txt b/filament/backend/CMakeLists.txt index 4edf04af14..fc985aa819 100644 --- a/filament/backend/CMakeLists.txt +++ b/filament/backend/CMakeLists.txt @@ -166,6 +166,8 @@ endif() if (FILAMENT_SUPPORTS_VULKAN) list(APPEND SRCS include/backend/platforms/VulkanPlatform.h + src/vulkan/caching/VulkanDescriptorSet.cpp + src/vulkan/caching/VulkanDescriptorSet.h src/vulkan/platform/VulkanPlatform.cpp src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp src/vulkan/platform/VulkanPlatformSwapChainImpl.h diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index 4b479eb36d..876007a627 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -1757,7 +1757,7 @@ void VulkanDriver::draw(PipelineState pipelineState, Handle r VulkanTexture* samplerTextures[VulkanPipelineCache::SAMPLER_BINDING_COUNT] = {nullptr}; auto const& bindingToSamplerIndex = program->getBindingToSamplerIndex(); - VulkanPipelineCache::UsageFlags usage = program->getUsage(); + VulkanPipelineCache::UsageFlags usage = program->getUsage(); UTILS_NOUNROLL for (uint8_t binding = 0; binding < VulkanPipelineCache::SAMPLER_BINDING_COUNT; binding++) { diff --git a/filament/backend/src/vulkan/VulkanPipelineCache.cpp b/filament/backend/src/vulkan/VulkanPipelineCache.cpp index 659de0092c..8c19a12ad1 100644 --- a/filament/backend/src/vulkan/VulkanPipelineCache.cpp +++ b/filament/backend/src/vulkan/VulkanPipelineCache.cpp @@ -581,12 +581,8 @@ void VulkanPipelineCache::unbindImageView(VkImageView imageView) noexcept { void VulkanPipelineCache::bindUniformBufferObject(uint32_t bindingIndex, VulkanBufferObject* bufferObject, VkDeviceSize offset, VkDeviceSize size) noexcept { - bindUniformBuffer(bindingIndex, bufferObject->buffer.getGpuBuffer(), offset, size); - mPipelineBoundResources.acquire(bufferObject); -} + VkBuffer buffer = bufferObject->buffer.getGpuBuffer(); -void VulkanPipelineCache::bindUniformBuffer(uint32_t bindingIndex, VkBuffer buffer, - VkDeviceSize offset, VkDeviceSize size) noexcept { ASSERT_POSTCONDITION(bindingIndex < UBUFFER_BINDING_COUNT, "Uniform bindings overflow: index = %d, capacity = %d.", bindingIndex, UBUFFER_BINDING_COUNT); @@ -602,6 +598,8 @@ void VulkanPipelineCache::bindUniformBuffer(uint32_t bindingIndex, VkBuffer buff key.uniformBufferOffsets[bindingIndex] = offset; key.uniformBufferSizes[bindingIndex] = size; + + mPipelineBoundResources.acquire(bufferObject); } void VulkanPipelineCache::bindSamplers(VkDescriptorImageInfo samplers[SAMPLER_BINDING_COUNT], diff --git a/filament/backend/src/vulkan/VulkanPipelineCache.h b/filament/backend/src/vulkan/VulkanPipelineCache.h index 26f17b0751..de271c4b7a 100644 --- a/filament/backend/src/vulkan/VulkanPipelineCache.h +++ b/filament/backend/src/vulkan/VulkanPipelineCache.h @@ -153,8 +153,6 @@ public: void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept; void bindUniformBufferObject(uint32_t bindingIndex, VulkanBufferObject* bufferObject, VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) noexcept; - void bindUniformBuffer(uint32_t bindingIndex, VkBuffer buffer, - VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) noexcept; void bindSamplers(VkDescriptorImageInfo samplers[SAMPLER_BINDING_COUNT], VulkanTexture* textures[SAMPLER_BINDING_COUNT], UsageFlags flags) noexcept; void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc, diff --git a/filament/backend/src/vulkan/VulkanResources.cpp b/filament/backend/src/vulkan/VulkanResources.cpp index 4cd88f2bb0..bbc8472239 100644 --- a/filament/backend/src/vulkan/VulkanResources.cpp +++ b/filament/backend/src/vulkan/VulkanResources.cpp @@ -14,10 +14,10 @@ * limitations under the License. */ - #include "VulkanResources.h" #include "VulkanHandles.h" #include "VulkanResourceAllocator.h" +#include "caching/VulkanDescriptorSet.h" namespace filament::backend { @@ -62,6 +62,10 @@ void deallocateResource(VulkanResourceAllocator* allocator, VulkanResourceType t case VulkanResourceType::RENDER_PRIMITIVE: allocator->destruct(Handle(id)); break; + case VulkanResourceType::DESCRIPTOR_SET: + allocator->destruct(Handle(id)); + break; + // If the resource is heap allocated, then the resource manager just skip refcounted // destruction. case VulkanResourceType::FENCE: diff --git a/filament/backend/src/vulkan/VulkanResources.h b/filament/backend/src/vulkan/VulkanResources.h index c5b7440ffc..8364c232b0 100644 --- a/filament/backend/src/vulkan/VulkanResources.h +++ b/filament/backend/src/vulkan/VulkanResources.h @@ -17,6 +17,8 @@ #ifndef TNT_FILAMENT_BACKEND_VULKANRESOURCES_H #define TNT_FILAMENT_BACKEND_VULKANRESOURCES_H +#include "VulkanUtility.h" + #include #include @@ -44,6 +46,7 @@ enum class VulkanResourceType : uint8_t { TIMER_QUERY, VERTEX_BUFFER, VERTEX_BUFFER_INFO, + DESCRIPTOR_SET, // Below are resources that are managed manually (i.e. not ref counted). FENCE, @@ -162,62 +165,7 @@ namespace { // When the size of the resource set is known to be small, (for example for VulkanRenderPrimitive), // we just use a std::array to back the set. template -class FixedCapacityResourceSet { -private: - using FixedSizeArray = std::array; - -public: - using const_iterator = typename FixedSizeArray::const_iterator; - - inline ~FixedCapacityResourceSet() { - clear(); - } - - inline const_iterator begin() { - if (mInd == 0) { - return mArray.cend(); - } - return mArray.cbegin(); - } - - inline const_iterator end() { - if (mInd == 0) { - return mArray.cend(); - } - if (mInd < SIZE) { - return mArray.begin() + mInd; - } - return mArray.cend(); - } - - inline const_iterator find(VulkanResource* resource) { - return std::find(begin(), end(), resource); - } - - inline void insert(VulkanResource* resource) { - assert_invariant(mInd < SIZE); - mArray[mInd++] = resource; - } - - inline void erase(VulkanResource* resource) { - assert_invariant(false && "FixedCapacityResourceSet::erase should not be called"); - } - - inline void clear() { - if (mInd == 0) { - return; - } - mInd = 0; - } - - inline size_t size() { - return mInd; - } - -private: - FixedSizeArray mArray{nullptr}; - size_t mInd = 0; -}; +using FixedCapacityResourceSet = CappedArray; // robin_set/map are useful for sets that are acquire only and the set will be iterated when the set // is cleared. diff --git a/filament/backend/src/vulkan/VulkanUtility.h b/filament/backend/src/vulkan/VulkanUtility.h index 3ba35c75e6..1b2ddc2e26 100644 --- a/filament/backend/src/vulkan/VulkanUtility.h +++ b/filament/backend/src/vulkan/VulkanUtility.h @@ -23,6 +23,8 @@ #include +#include + namespace filament::backend { VkFormat getVkFormat(ElementType type, bool normalized, bool integer); @@ -88,10 +90,100 @@ utils::FixedCapacityVector enumerate( #undef EXPAND_ENUM_NO_ARGS #undef EXPAND_ENUM_ARGS - // Useful shorthands using VkFormatList = utils::FixedCapacityVector; +// An Array that will be fixed capacity, but the "size" (as in user added elements) is variable. +// Note that this class is movable. +template +class CappedArray { +private: + using FixedSizeArray = std::array; +public: + using const_iterator = typename FixedSizeArray::const_iterator; + using iterator = typename FixedSizeArray::iterator; + + CappedArray() = default; + + // Delete copy constructor/assignment. + CappedArray(CappedArray const& rhs) = delete; + CappedArray& operator=(CappedArray& rhs) = delete; + + CappedArray(CappedArray&& rhs) noexcept { + this->swap(rhs); + } + + CappedArray& operator=(CappedArray&& rhs) noexcept { + this->swap(rhs); + return *this; + } + + inline ~CappedArray() { + clear(); + } + + inline const_iterator begin() { + if (mInd == 0) { + return mArray.cend(); + } + return mArray.cbegin(); + } + + inline const_iterator end() { + if (mInd > 0 && mInd < CAPACITY) { + return mArray.begin() + mInd; + } + return mArray.cend(); + } + + inline T back() { + assert_invariant(mInd > 0); + return *(mArray.begin() + mInd); + } + + inline void pop_back() { + assert_invariant(mInd > 0); + mInd--; + } + + inline const_iterator find(T item) { + return std::find(begin(), end(), item); + } + + inline void insert(T item) { + assert_invariant(mInd < CAPACITY); + mArray[mInd++] = item; + } + + inline void erase(T item) { + PANIC_PRECONDITION("CappedArray::erase should not be called"); + } + + inline void clear() { + if (mInd == 0) { + return; + } + mInd = 0; + } + + inline T& operator[](uint16_t ind) { + return mArray[ind]; + } + + inline size_t size() { + return mInd; + } + +private: + void swap(CappedArray& rhs) { + std::swap(mArray, rhs.mArray); + std::swap(mInd, rhs.mInd); + } + + FixedSizeArray mArray; + size_t mInd = 0; +}; + } // namespace filament::backend #endif // TNT_FILAMENT_BACKEND_VULKANUTILITY_H diff --git a/filament/backend/src/vulkan/caching/VulkanDescriptorSet.cpp b/filament/backend/src/vulkan/caching/VulkanDescriptorSet.cpp new file mode 100644 index 0000000000..78ed4ae46e --- /dev/null +++ b/filament/backend/src/vulkan/caching/VulkanDescriptorSet.cpp @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 "VulkanDescriptorSet.h" + +#include +#include + +#include + +namespace filament::backend { + +namespace { + +template +using LayoutMap = tsl::robin_map; +using LayoutArray = VulkanDescriptorSetManager::LayoutArray; +using UniformBufferBitmask = VulkanDescriptorSetManager::UniformBufferBitmask; +using SamplerBitmask = VulkanDescriptorSetManager::SamplerBitmask; + +constexpr uint8_t EXPECTED_IN_FLIGHT_FRAMES = 3; // Asssume triple buffering + +#define PORT_CONSTANT(NameSpace, K) constexpr decltype(NameSpace::K) K = NameSpace::K; + +PORT_CONSTANT(VulkanDescriptorSetManager, MAX_SUPPORTED_SHADER_STAGE) +PORT_CONSTANT(VulkanDescriptorSetManager, VERTEX_STAGE) +PORT_CONSTANT(VulkanDescriptorSetManager, FRAGMENT_STAGE) + +PORT_CONSTANT(Program, UNIFORM_BINDING_COUNT) +PORT_CONSTANT(Program, SAMPLER_BINDING_COUNT) + +#undef PORT_CONSTANT + +// Use constexpr to statically generate a bit count table for 8-bit numbers. +struct BitCountHelper { + constexpr BitCountHelper() : data{} { + for (uint16_t i = 0; i < 256; ++i) { + data[i] = 0; + for (auto j = i; j > 0; j /= 2) { + if (j & 1) { + data[i]++; + } + } + } + } + uint8_t data[256]; +} BitCountImpl; + +inline uint8_t countBits(uint32_t num) { + static constexpr uint8_t const* BIT_COUNT = BitCountImpl.data; + return BIT_COUNT[num & 0xFF] + BIT_COUNT[(num >> 8) & 0xFF] + BIT_COUNT[(num >> 16) & 0xFF] + + BIT_COUNT[(num >> 24) & 0xFF]; +} + +struct SamplerSet { + using Mask = UniformBufferBitmask; + struct Key { + Mask mask; + static_assert(sizeof(Mask) == 4); + uint32_t padding;// We need a padding to ensure 8-byte alignement. + VkDescriptorSetLayout layout; + VkSampler sampler[SAMPLER_BINDING_COUNT]; + VkImageView imageView[SAMPLER_BINDING_COUNT]; + VkImageLayout imageLayout[SAMPLER_BINDING_COUNT]; + }; + + struct Equal { + bool operator()(Key const& k1, Key const& k2) const { + if (k1.mask != k2.mask) return false; + if (k1.layout != k2.layout) return false; + + for (uint8_t i = 0, bitCount = countBits(k1.mask); i < bitCount; ++i) { + if (k1.sampler[i] != k2.sampler[i] || k1.imageView[i] != k2.imageView[i] || + k1.imageLayout[i] != k2.imageLayout[i]) { + return false; + } + } + return true; + } + }; + using HashFn = utils::hash::MurmurHashFn; + using Cache = std::unordered_map; +}; + +struct UBOSet { + using Mask = SamplerBitmask; + + struct Key { + Mask mask; + static_assert(sizeof(Mask) == 4); + uint32_t padding;// We need a padding to ensure 8-byte alignement. + VkDescriptorSetLayout layout; + VkBuffer buffers[UNIFORM_BINDING_COUNT]; // 80 0 + VkDeviceSize offsets[UNIFORM_BINDING_COUNT];// 40 1592 + VkDeviceSize sizes[UNIFORM_BINDING_COUNT]; // 40 1632 + }; + + struct Equal { + bool operator()(Key const& k1, Key const& k2) const { + if (k1.mask != k2.mask) return false; + if (k1.layout != k2.layout) return false; + + for (uint8_t i = 0, bitCount = countBits(k1.mask); i < bitCount; ++i) { + if (k1.buffers[i] != k2.buffers[i] || k1.offsets[i] != k2.offsets[i] || + k1.sizes[i] != k2.sizes[i]) { + return false; + } + } + return true; + } + }; + using HashFn = utils::hash::MurmurHashFn; + using Cache = std::unordered_map; +}; + +constexpr uint8_t MAX_BINDINGS = UNIFORM_BINDING_COUNT * MAX_SUPPORTED_SHADER_STAGE; +static_assert(MAX_BINDINGS >= SAMPLER_BINDING_COUNT * MAX_SUPPORTED_SHADER_STAGE); + +template +class DescriptorPool { +private: + static constexpr uint32_t COUNT_MULTIPLIER = + MAX_SUPPORTED_SHADER_STAGE * EXPECTED_IN_FLIGHT_FRAMES; + static constexpr uint32_t CAPACITY = + std::is_same_v ? Program::UNIFORM_BINDING_COUNT * COUNT_MULTIPLIER + : Program::SAMPLER_BINDING_COUNT * COUNT_MULTIPLIER; + +public: + DescriptorPool() + : mDevice(VK_NULL_HANDLE), + mResourceAllocator(nullptr) {} + + DescriptorPool(VkDevice device, VulkanResourceAllocator* allocator) + : mDevice(device), + mResourceAllocator(allocator) { + VkDescriptorPoolSize sizes[1] = { + { + .type = std::is_same_v ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER + : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = CAPACITY, + }, + }; + VkDescriptorPoolCreateInfo info{ + .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = EXPECTED_IN_FLIGHT_FRAMES, + .poolSizeCount = 1, + .pPoolSizes = sizes, + }; + vkCreateDescriptorPool(mDevice, &info, VKALLOC, &mPool); + } + + DescriptorPool(DescriptorPool const&) = delete; + DescriptorPool& operator=(DescriptorPool const&) = delete; + + ~DescriptorPool() { + vkDestroyDescriptorPool(mDevice, mPool, VKALLOC); + } + + VulkanDescriptorSet* obtainSet(VkDescriptorSetLayout layout) { + if (mCount == CAPACITY) { + return nullptr; + } + + if (auto unused = mUnused.find(layout); + unused != mUnused.end() && !unused->second.empty()) { + auto& unusedList = unused->second; + auto set = unusedList.back(); + unusedList.pop_back(); + mCount++; + return set; + } + + // Creating a new set + VkDescriptorSetLayout layouts[1] = {layout}; + VkDescriptorSetAllocateInfo allocInfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = mPool, + .descriptorSetCount = 1, + .pSetLayouts = layouts, + }; + VkDescriptorSet vkSet; + UTILS_UNUSED VkResult result = vkAllocateDescriptorSets(mDevice, &allocInfo, &vkSet); + ASSERT_POSTCONDITION(result == VK_SUCCESS, "Cannot allocate descriptor set. error=%d", + (int) result); + + mCount++; + return createSet(layout, vkSet); + } + +private: + inline VulkanDescriptorSet* createSet(VkDescriptorSetLayout layout, VkDescriptorSet vkSet) { + return VulkanDescriptorSet::create(mResourceAllocator, vkSet, layout, + [this](VulkanDescriptorSet* set) { + auto const layout = set->layout; + auto const vkSet = set->vkSet; + auto listIter = mUnused.find(layout); + assert_invariant(listIter != mUnused.end()); + auto& unusedList = listIter->second; + + // At this point, we know that the pointer will be stale, and we need to remove + // any non-ref-counted references to it. + // TODO: this will be inefficient + auto iter = std::find(unusedList.begin(), unusedList.end(), set); + unusedList.erase(iter); + + // We are recycling - release the vk resource back into the pool. Note that the + // vk handle has not changed, but we need to change the backend handle to allow + // for proper refcounting of resources referenced in this set. + unusedList.push_back(createSet(layout, vkSet)); + mCount--; + }); + } + + VkDevice mDevice; + VulkanResourceAllocator* mResourceAllocator; + VkDescriptorPool mPool; + + uint32_t mCount = 0; + std::unordered_map> mUnused; +}; + +template +class DescriptorInfinitePool { +public: + DescriptorInfinitePool(VkDevice device, VulkanResourceAllocator* allocator) + : mDevice(device), + mResourceAllocator(allocator) {} + + ~DescriptorInfinitePool() { + for (auto pool: mPools) { + delete pool; + } + } + + VulkanDescriptorSet* obtainSet(VkDescriptorSetLayout layout) { + for (auto pool: mPools) { + auto set = pool->obtainSet(layout); + if (set) { + return set; + } + } + // We need to increase the number of pools + mPools.push_back(new DescriptorPool{mDevice, mResourceAllocator}); + auto pool = mPools.back(); + return pool->obtainSet(layout); + } + +private: + VkDevice mDevice; + VulkanResourceAllocator* mResourceAllocator; + std::vector*> mPools; +}; + +// This holds a cache of descriptor sets of a TYPE (UBO or Sampler). The Key is defined as the +// layout and the content (for example specific samplers). +template +class DescriptorSetCache { +public: + DescriptorSetCache(VkDevice device, VulkanResourceAllocator* allocator) + : mPool(device, allocator), + mResourceManager(allocator) {} + + std::pair obtainSet(Key const& key) { + mAge++; + if (auto result = mCache.find(key); result != mCache.end()) { + auto set = result->second; + auto const oldAge = mReverseHistory[set]; + mHistory.erase(oldAge); + mHistory[mAge] = set; + mReverseHistory[set] = mAge; + return {set, true}; + } + + VulkanDescriptorSet* set = mPool.obtainSet(key.layout); + mCache[key] = set; + mReverseCache[set] = key; + mHistory[mAge] = set; + mReverseHistory[set] = mAge; + mResourceManager.acquire(set); + return {set, false}; + } + + // We need to periodically purge the descriptor sets so that we're not holding on to resources + // unnecessarily. The strategy for purging needs to be examined more. + void gc() noexcept { + constexpr uint32_t ALLOWED_ENTRIES = EXPECTED_IN_FLIGHT_FRAMES * 3; + int32_t const toCutCount = mHistory.size() - ALLOWED_ENTRIES; + + if (toCutCount <= 0) { + return; + } + + std::vector remove; + for (auto entry = mHistory.begin(); entry != mHistory.end(); entry++) { + auto const& set = entry->second; + Key const& key = mReverseCache[set]; + mCache.erase(key); + mReverseCache.erase(set); + mReverseHistory.erase(set); + + remove.push_back(entry->first); + mResourceManager.release(set); + } + for (auto removeAge: remove) { + mHistory.erase(removeAge); + } + } + +private: + DescriptorInfinitePool mPool; + + // TODO: combine some of these data structures + Cache mCache; + std::unordered_map mReverseCache; + + // Use the ordering for purging if needed; + std::map mHistory; + std::unordered_map mReverseHistory; + uint64_t mAge; + + // Note that instead of owning the resources (i.e. descriptor set) in the pools, we keep them + // here since all the allocated sets have to pass through this class, and this class has better + // knowledge to make decisions about gc. + VulkanResourceManager mResourceManager; +}; + +using UBOSetCache = DescriptorSetCache; +using SamplerSetCache = DescriptorSetCache; + +template +class LayoutCache { +public: + explicit LayoutCache(VkDevice device) + : mDevice(device) {} + + ~LayoutCache() { + for (auto [mask, layout]: mLayouts) { + vkDestroyDescriptorSetLayout(mDevice, layout, VKALLOC); + } + mLayouts.clear(); + } + + VkDescriptorSetLayout getLayout(MaskType stageMask) noexcept { + if (stageMask == 0) { + return VK_NULL_HANDLE; + } + if (auto iter = mLayouts.find(stageMask); iter != mLayouts.end()) { + return iter->second; + } + VkDescriptorType descriptorType; + if constexpr (std::is_same_v) { + descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + } else if constexpr (std::is_same_v) { + descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + } + + VkDescriptorSetLayoutBinding toBind[MAX_BINDINGS]; + uint32_t count = 0; + for (uint8_t i = 0, maxBindings = sizeof(stageMask) * 4; i < maxBindings; i++) { + VkShaderStageFlags stages = 0; + if ((stageMask & (VERTEX_STAGE << i)) != 0) { + stages |= VK_SHADER_STAGE_VERTEX_BIT; + } + if ((stageMask & (FRAGMENT_STAGE << i)) == 0) { + stages |= VK_SHADER_STAGE_FRAGMENT_BIT; + } + if (stages == 0) { + continue; + } + + auto& bindInfo = toBind[count++]; + bindInfo = { + .binding = i, + .descriptorType = descriptorType, + .descriptorCount = 1, + .stageFlags = stages, + }; + } + VkDescriptorSetLayoutCreateInfo dlinfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = count, + .pBindings = toBind, + }; + + VkDescriptorSetLayout handle; + vkCreateDescriptorSetLayout(mDevice, &dlinfo, VKALLOC, &handle); + mLayouts[stageMask] = handle; + + return handle; + } + +private: + VkDevice mDevice; + LayoutMap mLayouts; +}; + +using UBOSetLayoutCache = LayoutCache; +using SamplerSetLayoutCache = LayoutCache; + +} // anonymous namespace + +VulkanDescriptorSet::VulkanDescriptorSet(VulkanResourceAllocator* allocator, VkDescriptorSet rawSet, + VkDescriptorSetLayout layout, OnRecycle&& onRecycleFn) + : VulkanResource(VulkanResourceType::DESCRIPTOR_SET), + resources(allocator), + vkSet(rawSet), + layout(layout), + mOnRecycleFn(std::move(onRecycleFn)) {} + +VulkanDescriptorSet::~VulkanDescriptorSet() { + if (mOnRecycleFn) { + mOnRecycleFn(this); + } +} + +VulkanDescriptorSet* VulkanDescriptorSet::create(VulkanResourceAllocator* allocator, + VkDescriptorSet rawSet, VkDescriptorSetLayout layout, OnRecycle&& onRecycleFn) { + auto handle = allocator->allocHandle(); + auto set = allocator->construct(handle, allocator, rawSet, layout, + std::move(onRecycleFn)); + return set; +} + +class VulkanDescriptorSetManager::Impl { +public: + Impl(VkDevice device, VulkanResourceAllocator* allocator) + : mDevice(device), + mUBOLayoutCache(device), + mUBOCache(device, allocator), + mSamplerLayoutCache(device), + mSamplerCache(device, allocator), + mResources(allocator) {} + + void setUniformBufferObject(uint32_t bindingIndex, VulkanBufferObject* bufferObject, + VkDeviceSize offset, VkDeviceSize size) noexcept { + mUbos.push_back({bindingIndex, bufferObject, offset, size}); + + // Between "set" and "commit", we need to ref the buffer object to avoid it being collected. + mResources.acquire(bufferObject); + } + + void setSamplers(SamplerArray&& samplers) { + mSamplers = std::move(samplers); + for (auto const& sampler: mSamplers) { + mResources.acquire(sampler.texture); + } + } + + void gc() noexcept { + mUBOCache.gc(); + mSamplerCache.gc(); + } + + // This will write/update all of the descriptor set (and create a set if a one of the same + // layout is not available). + void bind(VulkanCommandBuffer* commands, GetPipelineLayoutFunction& getPipelineLayoutFn) { + LayoutArray layouts = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + std::array descSets = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + uint8_t descSetCount = 0; + + VkWriteDescriptorSet descriptorWrites[UNIFORM_BINDING_COUNT + SAMPLER_BINDING_COUNT]; + VkDescriptorBufferInfo uboWrite[UNIFORM_BINDING_COUNT]; + VkDescriptorImageInfo samplerWrite[SAMPLER_BINDING_COUNT]; + uint32_t nwrites = 0; + + UBOSet::Key uboKey = {}; + auto& uboMask = uboKey.mask; + uint8_t uboCount = 0; + for (auto const& ubo: mUbos) { + auto const& binding = std::get<0>(ubo); + + uboKey.buffers[uboCount] = std::get<1>(ubo)->buffer.getGpuBuffer(); + uboKey.offsets[uboCount] = std::get<2>(ubo); + uboKey.sizes[uboCount] = std::get<3>(ubo); + uboCount++; + + // Currently we let ubo be visible in all stages. + uboMask |= (VERTEX_STAGE << binding); + uboMask |= (FRAGMENT_STAGE << binding); + } + + if (uboMask) { + layouts[UBO_SET_INDEX] = uboKey.layout = mUBOLayoutCache.getLayout(uboMask); + + auto [descriptorSet, cached] = mUBOCache.obtainSet(uboKey); + descSets[descSetCount++] = descriptorSet->vkSet; + + // We need to write to the descriptor set since it wasn't cached. + if (!cached) { + // If it wasn't cached before, we need to ref the resources that it touches. + for (auto const& ubo: mUbos) { + auto const buffer = std::get<1>(ubo); + descriptorSet->resources.acquire(buffer); + } + + for (uint8_t i = 0; i < uboCount; ++i) { + auto const& binding = std::get<0>(mUbos[i]); + VkWriteDescriptorSet& descriptorWrite = descriptorWrites[nwrites++]; + auto& writeInfo = uboWrite[i]; + writeInfo = { + .buffer = uboKey.buffers[i], + .offset = uboKey.offsets[i], + .range = uboKey.sizes[i], + }; + descriptorWrite = { + .dstSet = descriptorSet->vkSet, + .dstBinding = binding, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pBufferInfo = &writeInfo, + }; + } + } + commands->acquire(descriptorSet); + } + + SamplerSet::Key samplerKey = {}; + auto& samplerMask = samplerKey.mask; + uint8_t samplerCount = 0; + for (auto const sampler: mSamplers) { + samplerMask |= (sampler.stage << sampler.binding); + + samplerKey.sampler[samplerCount] = sampler.info.sampler; + samplerKey.imageView[samplerCount] = sampler.info.imageView; + samplerKey.imageLayout[samplerCount] = sampler.info.imageLayout; + samplerCount++; + } + + if (samplerMask) { + layouts[SAMPLER_SET_INDEX] = samplerKey.layout = + mSamplerLayoutCache.getLayout(samplerMask); + + auto [descriptorSet, cached] = mSamplerCache.obtainSet(samplerKey); + descSets[descSetCount++] = descriptorSet->vkSet; + + if (!cached) { + for (auto const& sampler: mSamplers) { + descriptorSet->resources.acquire(sampler.texture); + } + + for (uint8_t i = 0; i < samplerCount; ++i) { + auto const& binding = mSamplers[i].binding; + VkWriteDescriptorSet& descriptorWrite = descriptorWrites[nwrites++]; + auto& writeInfo = samplerWrite[i]; + writeInfo = mSamplers[i].info; + descriptorWrite = { + .dstSet = descriptorSet->vkSet, + .dstBinding = binding, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .pImageInfo = &writeInfo, + }; + } + } + commands->acquire(descriptorSet); + } + if (nwrites) { + vkUpdateDescriptorSets(mDevice, nwrites, descriptorWrites, 0, nullptr); + } + + VkPipelineLayout const pipelineLayout = getPipelineLayoutFn(layouts); + VkCommandBuffer const cmdbuffer = commands->buffer(); + + BoundState state { + .cmdbuf = cmdbuffer, + .pipelineLayout = pipelineLayout, + }; + + if (state == mPreviousBoundState) { + return; + } + + vkCmdBindDescriptorSets(cmdbuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, + descSetCount, descSets.data(), 0, nullptr); + + mPreviousBoundState = state; + + // Once bound, the resources are now ref'd in the descriptor set and the references in this + // class can be released and the descriptor set is ref'd by the command buffer. + mResources.clear(); + } + +private: + struct BoundState { + VkCommandBuffer cmdbuf; + VkPipelineLayout pipelineLayout; + + bool operator==(BoundState const& b) const { + return cmdbuf == b.cmdbuf && pipelineLayout == b.pipelineLayout; + } + }; + + VkDevice mDevice; + + UBOSetLayoutCache mUBOLayoutCache; + UBOSetCache mUBOCache; + + SamplerSetLayoutCache mSamplerLayoutCache; + SamplerSetCache mSamplerCache; + + std::vector> mUbos; + SamplerArray mSamplers; + VulkanAcquireOnlyResourceManager mResources; + + BoundState mPreviousBoundState; +}; + +VulkanDescriptorSetManager::VulkanDescriptorSetManager(VkDevice device, + VulkanResourceAllocator* resourceAllocator) + : mImpl(new Impl(device, resourceAllocator)) {} + +void VulkanDescriptorSetManager::terminate() noexcept { + assert_invariant(mImpl); + delete mImpl; + mImpl = nullptr; +} + +void VulkanDescriptorSetManager::gc() noexcept { mImpl->gc(); } + +void VulkanDescriptorSetManager::bind(VulkanCommandBuffer* commands, + GetPipelineLayoutFunction& getPipelineLayoutFn) { + mImpl->bind(commands, getPipelineLayoutFn); +} + +void VulkanDescriptorSetManager::setUniformBufferObject(uint32_t bindingIndex, + VulkanBufferObject* bufferObject, VkDeviceSize offset, VkDeviceSize size) noexcept { + mImpl->setUniformBufferObject(bindingIndex, bufferObject, offset, size); +} + +void VulkanDescriptorSetManager::setSamplers(SamplerArray&& samplers) { + mImpl->setSamplers(std::move(samplers)); +} + +} // namespace filament::backend diff --git a/filament/backend/src/vulkan/caching/VulkanDescriptorSet.h b/filament/backend/src/vulkan/caching/VulkanDescriptorSet.h new file mode 100644 index 0000000000..f19812ad7b --- /dev/null +++ b/filament/backend/src/vulkan/caching/VulkanDescriptorSet.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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. + */ + +#ifndef TNT_FILAMENT_BACKEND_CACHING_VULKANDESCRIPTORSET_H +#define TNT_FILAMENT_BACKEND_CACHING_VULKANDESCRIPTORSET_H + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +namespace filament::backend { + +// We need to make this class public to enable allocation on the HandleAllocator. +struct VulkanDescriptorSet : public VulkanResourceBase { +public: + // Because we need to recycle descriptor set not used, we allow for a callback that the "Pool" + // can use to repackage the vk handle. + using OnRecycle = std::function; + + VulkanDescriptorSet(VulkanResourceAllocator* allocator, VkDescriptorSet rawSet, + VkDescriptorSetLayout layout, OnRecycle&& onRecycleFn); + + ~VulkanDescriptorSet(); + + static VulkanDescriptorSet* create(VulkanResourceAllocator* allocator, VkDescriptorSet rawSet, + VkDescriptorSetLayout layout, OnRecycle&& onRecycleFn); + + // TODO: maybe change to fixed size for performance. + VulkanAcquireOnlyResourceManager resources; + + VkDescriptorSet const vkSet; + VkDescriptorSetLayout const layout; + +private: + OnRecycle mOnRecycleFn; +}; + +// Abstraction over the pool and the layout cache. +class VulkanDescriptorSetManager { +public: + using StageBitMask = uint32_t; + using UniformBufferBitmask = StageBitMask; + using SamplerBitmask = StageBitMask; + + static constexpr uint8_t CONCURRENT_DESCRIPTOR_SET_COUNT = 2;// UBO and samplers + static constexpr uint8_t MAX_SUPPORTED_SHADER_STAGE = 2; // Vertex and fragment. + + static_assert(sizeof(UniformBufferBitmask) * 8 >= + (Program::UNIFORM_BINDING_COUNT) *MAX_SUPPORTED_SHADER_STAGE); + static_assert(sizeof(SamplerBitmask) * 8 >= + Program::SAMPLER_BINDING_COUNT * MAX_SUPPORTED_SHADER_STAGE); + + static constexpr StageBitMask VERTEX_STAGE = 0x1; + static constexpr StageBitMask FRAGMENT_STAGE = (0x1 << (sizeof(StageBitMask) / 4)); + static constexpr uint8_t UBO_SET_INDEX = 0; + static constexpr uint8_t SAMPLER_SET_INDEX = 1; + + using LayoutArray = std::array; + struct SamplerBundle { + VkDescriptorImageInfo info = {}; + VulkanTexture* texture = nullptr; + uint8_t binding = 0; + SamplerBitmask stage = 0; + }; + using SamplerArray = CappedArray; + using GetPipelineLayoutFunction = std::function; + + VulkanDescriptorSetManager(VkDevice device, VulkanResourceAllocator* resourceAllocator); + + void terminate() noexcept; + + void gc() noexcept; + + // This will write/update all of the descriptor set. + void bind(VulkanCommandBuffer* commands, GetPipelineLayoutFunction& getPipelineFn); + + void setUniformBufferObject(uint32_t bindingIndex, VulkanBufferObject* bufferObject, + VkDeviceSize offset, VkDeviceSize size) noexcept; + + void setSamplers(SamplerArray&& samplers); + +private: + class Impl; + Impl* mImpl; +}; + +} // namespace filament::backend + +#endif// TNT_FILAMENT_BACKEND_CACHING_VULKANDESCRIPTORSET_H