From 27e6ad1ad2210c17d3ef290b11b966fcc1beb34a Mon Sep 17 00:00:00 2001 From: Zheorgyan Date: Thu, 8 Oct 2026 15:26:29 +0200 Subject: [PATCH] feat(agc): bind storage buffers update-after-bind above the per-stage limit --- .../Execution/src/VulkanDevice.cpp | 13 ++- .../Graphics/include/Context.hpp | 1 + .../Graphics/include/ShaderResources.hpp | 6 +- .../Graphics/src/ShaderResources.cpp | 95 ++++++++++++------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 32 +++++++ 5 files changed, 108 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 42e909ea7..ed4468ae3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -201,6 +201,8 @@ struct VulkanDevice::State { // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; + bool storageBufferUpdateAfterBind = false; + VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT}; bool imageInt64Atomics = false; bool bufferInt64Atomics = false; bool primitiveListRestart = false; @@ -1008,17 +1010,23 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &descriptorIndexingFeatures}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; + state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE; + if (state->storageBufferUpdateAfterBind) { + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + } } descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); if (state->descriptorIndexing) { descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; - deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityShaderNonUniform); state->capabilities.push_back(spv::CapabilitySampledImageArrayNonUniformIndexing); state->capabilities.push_back(spv::CapabilityStorageImageArrayNonUniformIndexing); state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); } + descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE; state->samplerAnisotropy = true; state->textureCompressionBC = true; deviceInfo.pEnabledFeatures = &enabled; @@ -1074,7 +1082,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u imageRobustnessFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageRobustnessFeatures; } - if (state->descriptorIndexing) { + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) { descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } @@ -2554,6 +2562,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.copiedWriters = state->copiedWriters.get(); context.functions = state->functionsReady ? &state->deviceFunctions : nullptr; context.descriptorIndexing = state->descriptorIndexing; + context.descriptorIndexingLimits = state->descriptorIndexingProperties; context.imageInt64Atomics = state->imageInt64Atomics; context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 807440feb..ed079a89b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -137,6 +137,7 @@ struct Context { // VK_EXT_descriptor_indexing with non-uniform sampled/storage image array indexing enabled // (bindless image tables in graphics stages). bool descriptorIndexing = false; + VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingLimits{}; bool imageInt64Atomics = false; bool geometryShader = false; bool sampleRateShading = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 4931ee944..47474d8a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -68,12 +68,12 @@ public: // The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds // it from `bindings`), created on first use. - VkDescriptorSetLayout Layout(std::span key, std::span bindings); + VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false); struct SetAllocation { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE; }; - SetAllocation Allocate(VkDescriptorSetLayout layout, std::span sizes); + SetAllocation Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false); void Free(const SetAllocation& allocation) noexcept; // APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened. struct Stats { @@ -93,6 +93,7 @@ private: mutable std::mutex mutex; std::map, VkDescriptorSetLayout> layouts; std::vector pools; + std::vector updateAfterBindPools; std::vector dedicated; Stats stats; }; @@ -458,6 +459,7 @@ private: }; std::vector deferredImages; std::uint64_t storageBuffers = 0; + bool updateAfterBind = false; std::uint32_t plannedSampledImages = 0; std::uint32_t plannedStorageImages = 0; // The compute constructor's shader and captured regions: the caller's objects, valid only until diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 59daae04e..a49bb55ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -965,6 +965,33 @@ void ShaderResources::build(std::span shaders, const Color namespace { +VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind) { + std::vector flags; + for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; + bindingFlags.bindingCount = static_cast(flags.size()); + bindingFlags.pBindingFlags = flags.data(); + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout"); + return layout; +} + +VkDescriptorPool CreateSetPool(const Context& context, std::span sizes, bool updateAfterBind) { + VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; + info.maxSets = 1; + info.poolSizeCount = static_cast(sizes.size()); + info.pPoolSizes = sizes.data(); + VkDescriptorPool pool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool"); + return pool; +} + // APS5_PROFILE_DRAW: the [resources] phase totals. Each thread accumulates its builds' phases in // arrays of its own and merges them into the shared totals every 1000 of its builds (and when it // ends), so no build takes the shared mutex per phase; the totals lag by up to 999 builds per @@ -1045,7 +1072,8 @@ double ShaderResources::phase(BuildPhase which) { namespace { -void ValidateDescriptorLayout(const Context& context, std::span bindings) { +void ValidateDescriptorLayout(const Context& context, std::span bindings, bool updateAfterBind) { + const auto maxStageResources = updateAfterBind ? context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; std::map heapTotals; std::map, std::uint64_t> stageTotals; std::map stageResources; @@ -1056,7 +1084,7 @@ void ValidateDescriptorLayout(const Context& context, std::span shaders, cons std::int64_t shaderData = -1; const auto firstSampler = samplers.size(); const auto firstDeferred = deferredImages.size(); + const auto requireStageLimits = [&] { + if (stageStorageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageResources <= context.limits.maxPerStageResources) return; + const auto& limits = context.descriptorIndexingLimits; + Require(stageStorageBuffers <= limits.maxPerStageDescriptorUpdateAfterBindStorageBuffers && stageResources <= limits.maxPerStageUpdateAfterBindResources, "shader descriptors exceed per-stage limits"); + updateAfterBind = true; + }; for (const auto& binding : shader.program->bindings) { Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); Require(occupied.insert(binding.binding).second, "duplicate shader binding"); @@ -1126,7 +1160,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; if (imageRole) { if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage || binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) stageResources += binding.count; - Require(stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + requireStageLimits(); addImageBinding(binding, flags); continue; } @@ -1137,7 +1171,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons stageStorageBuffers += binding.count; stageResources += binding.count; storageBuffers += binding.count; - Require(stageStorageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + requireStageLimits(); Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, binding.count, flags, nullptr}, {}}; if (binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); @@ -1184,7 +1218,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons deferredImages[index].samplerCount = samplers.size() - firstSampler; } } - Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits"); + if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) { + Require(storageBuffers <= context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers, "pipeline descriptors exceed device limits"); + updateAfterBind = true; + } timing.bindingsMs = phase(BuildPhase::Bindings); // For every build, locked ones included: their stage B then takes the fast path too, and the // collects cost the same wherever they run. @@ -1197,16 +1234,14 @@ void ShaderResources::buildPrepare(std::span shaders, cons description.push_back(binding.layout); layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); } + if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description); + _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind); } else { - ValidateDescriptorLayout(context, description); - VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.bindingCount = static_cast(description.size()); - info.pBindings = description.data(); - Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &_layout), "vkCreateDescriptorSetLayout"); + ValidateDescriptorLayout(context, description, updateAfterBind); + _layout = CreateSetLayout(context, description, updateAfterBind); ownsLayout = true; } if (!bindings.empty()) { @@ -1218,16 +1253,12 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages}); if (!samplers.empty()) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLER, static_cast(samplers.size())}); if (context.descriptorCache != nullptr && !noPoolCache) { - const auto allocated = context.descriptorCache->Allocate(_layout, sizes); + const auto allocated = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); _set = allocated.set; cachePool = allocated.pool; } if (_set == VK_NULL_HANDLE) { - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = static_cast(sizes.size()); - poolInfo.pPoolSizes = sizes.data(); - Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool"); + pool = CreateSetPool(context, sizes, updateAfterBind); VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; allocation.descriptorPool = pool; allocation.descriptorSetCount = 1; @@ -2356,24 +2387,21 @@ DescriptorCache::DescriptorCache(const Context& context) : context(context), des DescriptorCache::~DescriptorCache() { for (const auto pool : pools) destroyPool(context.device, pool, nullptr); + for (const auto pool : updateAfterBindPools) destroyPool(context.device, pool, nullptr); for (const auto pool : dedicated) destroyPool(context.device, pool, nullptr); for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr); } -VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings) { +VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind) { std::lock_guard lock(mutex); std::vector keyCopy(key.begin(), key.end()); if (const auto found = layouts.find(keyCopy); found != layouts.end()) { ++stats.layoutHits; return found->second; } - ValidateDescriptorLayout(context, bindings); + ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; - VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.bindingCount = static_cast(bindings.size()); - info.pBindings = bindings.data(); - VkDescriptorSetLayout layout = VK_NULL_HANDLE; - Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout"); + const auto layout = CreateSetLayout(context, bindings, updateAfterBind); layouts.emplace(std::move(keyCopy), layout); return layout; } @@ -2395,10 +2423,11 @@ constexpr std::array ChainPoolSizes{{{VK_DESCRIPTOR_TYP constexpr std::array ChainPoolCreateSizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096 + 2}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_SAMPLER, 512}}}; } -DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span sizes) { +DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind) { bool fits = true; for (const auto& size : sizes) { - Require(size.descriptorCount <= SetDescriptorLimit(context.limits, size.type), "descriptor set exceeds the device's per-set descriptor limit"); + const auto limit = updateAfterBind && size.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers : SetDescriptorLimit(context.limits, size.type); + Require(size.descriptorCount <= limit, "descriptor set exceeds the device's per-set descriptor limit"); const auto capacity = std::find_if(ChainPoolSizes.begin(), ChainPoolSizes.end(), [&](const auto& item) { return item.type == size.type; }); if (capacity == ChainPoolSizes.end() || size.descriptorCount > capacity->descriptorCount) fits = false; } @@ -2409,12 +2438,7 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l allocation.pSetLayouts = &layout; if (!fits) { dedicated.reserve(dedicated.size() + 1); - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = static_cast(sizes.size()); - poolInfo.pPoolSizes = sizes.data(); - VkDescriptorPool pool = VK_NULL_HANDLE; - Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool"); + const auto pool = CreateSetPool(context, sizes, updateAfterBind); allocation.descriptorPool = pool; VkDescriptorSet set = VK_NULL_HANDLE; const auto result = allocate(context.device, &allocation, &set); @@ -2429,7 +2453,8 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l } // Newest pool first: it has the most room; a full or fragmented pool is left for its sets to // drain and tried again later. - for (auto it = pools.rbegin(); it != pools.rend(); ++it) { + auto& chain = updateAfterBind ? updateAfterBindPools : pools; + for (auto it = chain.rbegin(); it != chain.rend(); ++it) { allocation.descriptorPool = *it; VkDescriptorSet set = VK_NULL_HANDLE; const auto result = allocate(context.device, &allocation, &set); @@ -2440,13 +2465,13 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l if (result != VK_ERROR_OUT_OF_POOL_MEMORY && result != VK_ERROR_FRAGMENTED_POOL) Check(result, "vkAllocateDescriptorSets"); } VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT | (updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0); poolInfo.maxSets = ChainPoolSets; poolInfo.poolSizeCount = static_cast(ChainPoolCreateSizes.size()); poolInfo.pPoolSizes = ChainPoolCreateSizes.data(); VkDescriptorPool pool = VK_NULL_HANDLE; Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool"); - pools.push_back(pool); + chain.push_back(pool); ++stats.pools; allocation.descriptorPool = pool; VkDescriptorSet set = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 487e18f4d..95d57c04c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1198,6 +1198,8 @@ struct MockVulkan { std::optional memoryLimit; std::uint64_t allocationAttempts = 0; std::vector layoutBindings; + VkDescriptorSetLayoutCreateFlags layoutFlags = 0; + std::vector layoutBindingFlags; std::vector poolSizes; std::uint32_t poolMaxSets = 0; VkDescriptorPoolCreateFlags poolFlags = 0; @@ -1280,6 +1282,10 @@ VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { *layout = makeHandle(); mock.layoutBindings.assign(info->pBindings, info->pBindings + info->bindingCount); + mock.layoutFlags = info->flags; + const auto* bindingFlags = static_cast(info->pNext); + if (bindingFlags != nullptr) mock.layoutBindingFlags.assign(bindingFlags->pBindingFlags, bindingFlags->pBindingFlags + bindingFlags->bindingCount); + else mock.layoutBindingFlags.clear(); ++mock.live; return VK_SUCCESS; } @@ -1686,6 +1692,32 @@ void resourceTests() { expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); + { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + std::vector words; + for (int index = 0; index < 17; ++index) words = join(words, vsharp(reinterpret_cast(0x1000), 8)); + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 17, words)); + auto updateAfterBind = mockContext(); + updateAfterBind.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; + updateAfterBind.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + updateAfterBind.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + mock = MockVulkan{}; + { + AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); + Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, "buffers above the per-stage limit did not get an update-after-bind layout"); + Require(mock.layoutBindingFlags.size() == 1 && mock.layoutBindingFlags[0] == VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, "the guest buffers binding is not update-after-bind"); + Require(mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, "the update-after-bind set did not come from an update-after-bind pool"); + } + Require(mock.live == 0, "update-after-bind shader resources leaked Vulkan objects"); + updateAfterBind.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 16; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); }, "shader descriptors exceed per-stage limits"); + Require(mock.live == 0, "failed update-after-bind shader resources leaked Vulkan objects"); + mock = MockVulkan{}; + vertex.bindings.front() = makeBinding(Role::GuestBuffers, 0, 1, vsharp(reinterpret_cast(0x1000), 8)); + { AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); } + Require(mock.layoutFlags == 0 && mock.layoutBindingFlags.empty() && mock.poolFlags == 0, "buffers within the per-stage limit took the update-after-bind path"); + } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment;