From 070a3f12c65372280d7ae2c95c0acd786c33571c Mon Sep 17 00:00:00 2001 From: Jon Daniel Date: Thu, 15 Oct 2026 21:28:03 -0400 Subject: [PATCH] fix(agc): bind image and sampler tables through update-after-bind limits Image and sampler table capacity came only from the regular descriptor limits. MoltenVK reports Metal's argument table limits there (256 sampled images and 16 samplers per stage), below the typed heaps the tables are reserved on top of, so both capacities were 0 and every table draw or dispatch was refused as if the device lacked descriptor indexing. Its update-after-bind limits are 1000000 sampled images and 500000 samplers. - descriptorBindingSampledImageUpdateAfterBind is queried and enabled, and the update-after-bind limits are read when either it or the storage buffer feature is present. - A table whose regular capacity is 0 takes its capacity from the update-after-bind limits. Its binding is update-after-bind and its set layout and pool are update-after-bind ones, in ShaderResources and in DescribeSetLayouts alike. A table that fits the regular limits is unchanged. - An update-after-bind set layout and its allocation are checked against the update-after-bind sampled image and sampler limits when the device has descriptorBindingSampledImageUpdateAfterBind. Vulkan counts the descriptors of such sets against those limits in place of the regular ones, and requires them to be at least the regular ones only when the feature is supported, so without it, and for storage images, the regular limits are kept as before. Storage buffers keep their own update-after-bind flag. - Compute pipeline capture and replay accept update-after-bind sampled image and sampler bindings. --- .../Execution/src/ComputePipelineCapture.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 15 +- .../Graphics/include/Context.hpp | 5 + .../Graphics/src/ShaderResources.cpp | 100 ++++++++++---- .../prx/libSceAgcDriver/tests/Graphics.cpp | 129 ++++++++++++++++++ 5 files changed, 222 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 9d9fa84300..72880ca3b3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -155,7 +155,7 @@ void ComputePipelineCapture::Validate() const { RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags"); constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags"); - RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || ((binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER) && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding"); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3326b66c8d..a4fb4cfb54 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -318,6 +318,7 @@ struct VulkanDevice::State { // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; bool storageBufferUpdateAfterBind = false; + bool sampledImageUpdateAfterBind = false; bool descriptorBindingPartiallyBound = false; bool descriptorBindingVariableDescriptorCount = false; bool runtimeDescriptorArray = false; @@ -906,13 +907,14 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip for (const auto& layout : capture.sets) { const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0; hasUpdateAfterBindLayout |= updateAfterBind; - require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); + require(!updateAfterBind || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); std::vector bindings; std::vector flags; std::uint32_t variableCount = 0; for (const auto& binding : layout.bindings) { require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported"); require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported"); + require((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? state->storageBufferUpdateAfterBind : state->sampledImageUpdateAfterBind), "captured update-after-bind descriptors are unsupported"); if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count; bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr}); flags.push_back(binding.flags); @@ -1667,16 +1669,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE; + state->sampledImageUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE; state->descriptorBindingPartiallyBound = descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE; state->descriptorBindingVariableDescriptorCount = descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE; state->runtimeDescriptorArray = descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE; - if (state->storageBufferUpdateAfterBind) { + if (state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind) { 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 || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); if (state->descriptorIndexing) { descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; @@ -1686,6 +1689,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); } descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind = state->sampledImageUpdateAfterBind ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE; @@ -1781,7 +1785,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : imageRobustnessFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageRobustnessFeatures; } - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } @@ -3691,8 +3695,11 @@ Graphics::Context VulkanDevice::buildContext() const { context.runtimeDescriptorArray = state->runtimeDescriptorArray; context.partiallyBound = state->descriptorBindingPartiallyBound; context.variableDescriptorCount = state->descriptorBindingVariableDescriptorCount; + context.sampledImageUpdateAfterBind = state->sampledImageUpdateAfterBind; context.imageTableCapacity = ImageTableCapacity(context); context.samplerTableCapacity = SamplerTableCapacity(context); + context.imageTableUpdateAfterBind = ImageTableUpdateAfterBind(context); + context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context); context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; context.storageImageMultisample = state->storageImageMultisample; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index eb28fe2922..b9e48fa16a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -211,9 +211,12 @@ struct Context { bool runtimeDescriptorArray = false; bool partiallyBound = false; bool variableDescriptorCount = false; + bool sampledImageUpdateAfterBind = false; std::uint32_t imageTableCapacity = 0; std::uint32_t samplerTableCapacity = 0; bool individuallyFreeDescriptorSets = false; + bool imageTableUpdateAfterBind = false; + bool samplerTableUpdateAfterBind = false; bool geometryShader = false; bool sampleRateShading = false; bool storageImageMultisample = false; @@ -336,6 +339,8 @@ inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { std::uint32_t ImageTableCapacity(const Context& context); std::uint32_t StorageTableCapacity(const Context& context); std::uint32_t SamplerTableCapacity(const Context& context); +bool ImageTableUpdateAfterBind(const Context& context); +bool SamplerTableUpdateAfterBind(const Context& context); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7d3a64775f..75ab4242bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1720,7 +1720,13 @@ std::string DescriptorLimitError(std::string_view description, std::uint64_t req } void ValidateDescriptorLayout(const Context& context, std::span bindings, bool updateAfterBind) { - const auto maxStageResources = updateAfterBind ? context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; + const auto& indexed = context.descriptorIndexingLimits; + const bool sampledUpdateAfterBind = updateAfterBind && context.sampledImageUpdateAfterBind; + const auto maxStageResources = updateAfterBind ? indexed.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; + const auto maxStageSampledImages = sampledUpdateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSampledImages : context.limits.maxPerStageDescriptorSampledImages; + const auto maxStageSamplers = sampledUpdateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSamplers : context.limits.maxPerStageDescriptorSamplers; + const auto maxSetSampledImages = sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : context.limits.maxDescriptorSetSampledImages; + const auto maxSetSamplers = sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; std::map heapTotals; std::map, std::uint64_t> stageTotals; std::map stageResources; @@ -1732,13 +1738,13 @@ void ValidateDescriptorLayout(const Context& context, std::span context.limits.maxPerStageDescriptorSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed per-stage sampler capacity", count, context.limits.maxPerStageDescriptorSamplers)); + if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER && count > maxStageSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed per-stage sampler capacity", count, maxStageSamplers)); } } - Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages, "typed heaps exceed descriptor set capacity"); - if (heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] > context.limits.maxDescriptorSetSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed descriptor set sampler capacity", heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER], context.limits.maxDescriptorSetSamplers)); + Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= maxSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages, "typed heaps exceed descriptor set capacity"); + if (heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] > maxSetSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed descriptor set sampler capacity", heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER], maxSetSamplers)); } void ValidateRuntimeResources(const CompiledShader& shader, std::span words) { @@ -1931,7 +1937,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (std::uint32_t set = 0; set < tableFlags.size(); ++set) { if (tableFlags[set] == 0) continue; Require(context.imageTableCapacity != 0, "image tables need runtimeDescriptorArray, descriptorBindingPartiallyBound and descriptorBindingVariableDescriptorCount, which the device lacks"); - bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr}, {}, {}, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast(set)}); + const VkDescriptorBindingFlags imageTableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT | (context.imageTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr}, {}, {}, imageTableFlags, static_cast(set)}); tableBindings[set] = bindings.size() - 1u; } if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) { @@ -1947,13 +1954,16 @@ void ShaderResources::buildPrepare(std::span shaders, cons timing.uploadMs = phase(BuildPhase::Upload); splitSets = SplitsSets(context, shaders); std::array, 2> descriptions; + bool tablesUpdateAfterBind = false; for (const auto& binding : bindings) { const auto set = setOf(binding); descriptions[set].push_back(binding.layout); (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(binding.flags | (updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0)); + tablesUpdateAfterBind |= (binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) != 0; auto& key = set == 0 ? layoutKey : fragmentLayoutKey; key.insert(key.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); } + updateAfterBind = updateAfterBind || tablesUpdateAfterBind; if (updateAfterBind) { layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); @@ -2080,6 +2090,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span tableFlags{}; std::array storageTableFlags{}; bool updateAfterBind = false; + bool tablesUpdateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); @@ -2098,7 +2109,8 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true; for (std::size_t set = 0; set < descriptions.size(); ++set) { @@ -2129,6 +2142,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span(binding.descriptorType), binding.descriptorCount, binding.stageFlags}); @@ -3819,12 +3833,15 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span key } namespace { -std::uint32_t SetDescriptorLimit(const VkPhysicalDeviceLimits& limits, VkDescriptorType type) { +std::uint32_t SetDescriptorLimit(const Context& context, VkDescriptorType type, bool updateAfterBind) { + const auto& limits = context.limits; + const auto& indexed = context.descriptorIndexingLimits; + const bool sampledUpdateAfterBind = updateAfterBind && context.sampledImageUpdateAfterBind; switch (type) { - case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return limits.maxDescriptorSetStorageBuffers; - case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return limits.maxDescriptorSetSampledImages; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindStorageBuffers : limits.maxDescriptorSetStorageBuffers; + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : limits.maxDescriptorSetSampledImages; case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return limits.maxDescriptorSetStorageImages; - case VK_DESCRIPTOR_TYPE_SAMPLER: return limits.maxDescriptorSetSamplers; + case VK_DESCRIPTOR_TYPE_SAMPLER: return sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : limits.maxDescriptorSetSamplers; default: throw std::runtime_error("AGC graphics: descriptor set uses an unsupported descriptor type " + std::to_string(type)); } } @@ -3838,7 +3855,7 @@ constexpr std::array ChainPoolCreateSizes{{{VK_DESCRIPT DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind, std::optional variableCount) { bool fits = true; for (const auto& size : sizes) { - const auto limit = updateAfterBind && size.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers : SetDescriptorLimit(context.limits, size.type); + const auto limit = SetDescriptorLimit(context, size.type, updateAfterBind); 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; @@ -4547,28 +4564,61 @@ std::uint32_t StorageTableCapacity(const Context& context) { return std::min({context.imageTableCapacity, 4096u, context.limits.maxPerStageDescriptorStorageImages / 4u}); } -std::uint32_t ImageTableCapacity(const Context& context) { +namespace { + +std::uint64_t Room(std::uint64_t limit, std::uint64_t reserve) { + return limit > reserve ? limit - reserve : 0u; +} + +std::uint32_t ImageTableCapacityFor(const Context& context, bool updateAfterBind) { if (!context.runtimeDescriptorArray || !context.partiallyBound || !context.variableDescriptorCount || !context.descriptorIndexing) return 0u; + if (updateAfterBind && !context.sampledImageUpdateAfterBind) return 0u; constexpr std::uint64_t sampledHeaps = static_cast(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity; constexpr std::uint64_t storageHeaps = static_cast(static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers) - ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) * ShaderRecompiler::RuntimeAbi::StorageHeapCapacity; constexpr std::uint64_t buffers = ShaderRecompiler::RuntimeAbi::BufferCapacity + 8u; - const auto room = [](std::uint64_t limit, std::uint64_t reserve) { return limit > reserve ? limit - reserve : 0u; }; + const auto& indexed = context.descriptorIndexingLimits; + const std::uint64_t stageImages = updateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSampledImages : context.limits.maxPerStageDescriptorSampledImages; + const std::uint64_t stageResources = updateAfterBind ? indexed.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; + const std::uint64_t setImages = updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : context.limits.maxDescriptorSetSampledImages; std::uint64_t capacity = static_cast(ShaderRecompiler::RuntimeAbi::StageCount) * ShaderRecompiler::ImageTableAbi::ImageElementBudget; - capacity = std::min(capacity, room(context.limits.maxPerStageDescriptorSampledImages, sampledHeaps)); - capacity = std::min(capacity, room(context.limits.maxPerStageResources, sampledHeaps + storageHeaps + buffers)); - capacity = std::min(capacity, room(context.limits.maxDescriptorSetSampledImages, sampledHeaps * ShaderRecompiler::RuntimeAbi::StageCount)); + capacity = std::min(capacity, Room(stageImages, sampledHeaps)); + capacity = std::min(capacity, Room(stageResources, sampledHeaps + storageHeaps + buffers)); + capacity = std::min(capacity, Room(setImages, sampledHeaps * ShaderRecompiler::RuntimeAbi::StageCount)); return static_cast(capacity); } -std::uint32_t SamplerTableCapacity(const Context& context) { +std::uint32_t SamplerTableCapacityFor(const Context& context, bool updateAfterBind) { if (!context.runtimeDescriptorArray || !context.partiallyBound) return 0u; - const auto room = [](std::uint64_t limit, std::uint64_t reserve) { return limit > reserve ? limit - reserve : 0u; }; + if (updateAfterBind && !context.sampledImageUpdateAfterBind) return 0u; + const auto& indexed = context.descriptorIndexingLimits; + const std::uint64_t stageSamplers = updateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSamplers : context.limits.maxPerStageDescriptorSamplers; + const std::uint64_t setSamplers = updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; std::uint64_t capacity = ShaderRecompiler::ImageTableAbi::SamplerElementBudget; - capacity = std::min(capacity, room(context.limits.maxPerStageDescriptorSamplers, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); - capacity = std::min(capacity, room(context.limits.maxDescriptorSetSamplers / ShaderRecompiler::RuntimeAbi::StageCount, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); + capacity = std::min(capacity, Room(stageSamplers, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); + capacity = std::min(capacity, Room(setSamplers / ShaderRecompiler::RuntimeAbi::StageCount, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); return static_cast(capacity - capacity % ShaderRecompiler::ImageTableAbi::SamplerEntryElements); } +} + +std::uint32_t ImageTableCapacity(const Context& context) { + const auto capacity = ImageTableCapacityFor(context, false); + return capacity != 0u ? capacity : ImageTableCapacityFor(context, true); +} + +std::uint32_t SamplerTableCapacity(const Context& context) { + const auto capacity = SamplerTableCapacityFor(context, false); + return capacity != 0u ? capacity : SamplerTableCapacityFor(context, true); +} + +bool ImageTableUpdateAfterBind(const Context& context) { + return ImageTableCapacityFor(context, false) == 0u && ImageTableCapacityFor(context, true) != 0u; +} + +bool SamplerTableUpdateAfterBind(const Context& context) { + return SamplerTableCapacityFor(context, false) == 0u && SamplerTableCapacityFor(context, true) != 0u; +} + ShaderResources::TableStage& ShaderResources::tableStage(const CompiledShader& shader) { for (auto& table : tableStages) { if (table.program == shader.program) return table; @@ -4597,7 +4647,8 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding& namespace Abi = ShaderRecompiler::ImageTableAbi; Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler && binding.guestDescriptor.size() == static_cast(binding.count) * 4u && binding.samplerDepthCompare.size() == binding.count && binding.count % Abi::SamplerEntryElements == 0u, "sampler table binding is malformed"); Require(context.samplerTableCapacity != 0, "sampler tables need runtimeDescriptorArray and descriptorBindingPartiallyBound, which the device lacks"); - Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr}, {}, {}, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}; + const VkDescriptorBindingFlags samplerTableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | (context.samplerTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr}, {}, {}, samplerTableFlags}; static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; for (std::uint32_t entry = 0; entry < binding.count / Abi::SamplerEntryElements; ++entry) { const auto first = entry * Abi::SamplerEntryElements; @@ -4637,7 +4688,8 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding& item.imageAllocations.push_back(samplers.size() - 1u); } } - Require(samplers.size() <= context.limits.maxDescriptorSetSamplers, "pipeline sampler descriptors exceed device limits"); + const auto setSamplers = context.samplerTableUpdateAfterBind ? context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; + Require(samplers.size() <= setSamplers, "pipeline sampler descriptors exceed device limits"); bindings.push_back(std::move(item)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1713796fb9..73d0c8d2a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6115,6 +6115,134 @@ void hostImportBudgetTests() { Require(HostImportBudget(memory, 16 * GiB) == 8 * GiB, "a host heap past memoryHeapCount limits the import budget"); } +void tableUpdateAfterBindTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using namespace AgcDriver::Graphics; + auto argumentLimits = mockContext(); + argumentLimits.runtimeDescriptorArray = true; + argumentLimits.partiallyBound = true; + argumentLimits.variableDescriptorCount = true; + argumentLimits.descriptorIndexing = true; + argumentLimits.limits.maxPerStageDescriptorSamplers = 16; + argumentLimits.limits.maxPerStageDescriptorSampledImages = 256; + argumentLimits.limits.maxPerStageResources = 287; + argumentLimits.limits.maxDescriptorSetSamplers = 80; + argumentLimits.limits.maxDescriptorSetSampledImages = 1280; + argumentLimits.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSamplers = 500000; + argumentLimits.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSampledImages = 1000000; + argumentLimits.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 1000000; + argumentLimits.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers = 500000; + argumentLimits.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSampledImages = 1000000; + Require(ImageTableCapacity(argumentLimits) == 0u && SamplerTableCapacity(argumentLimits) == 0u && !ImageTableUpdateAfterBind(argumentLimits) && !SamplerTableUpdateAfterBind(argumentLimits), "tables beyond the regular descriptor limits were enabled without update-after-bind sampled images"); + argumentLimits.sampledImageUpdateAfterBind = true; + Require(ImageTableCapacity(argumentLimits) == ShaderRecompiler::RuntimeAbi::StageCount * Abi::ImageElementBudget && ImageTableUpdateAfterBind(argumentLimits), "an image table beyond the regular descriptor limits did not take the update-after-bind limits"); + Require(SamplerTableCapacity(argumentLimits) == Abi::SamplerElementBudget && SamplerTableUpdateAfterBind(argumentLimits), "a sampler table beyond the regular descriptor limits did not take the update-after-bind limits"); + auto regularLimits = argumentLimits; + regularLimits.limits.maxPerStageDescriptorSamplers = 1048576; + regularLimits.limits.maxPerStageDescriptorSampledImages = 1048576; + regularLimits.limits.maxPerStageResources = 1048576; + regularLimits.limits.maxDescriptorSetSamplers = 1048576; + regularLimits.limits.maxDescriptorSetSampledImages = 1048576; + Require(ImageTableCapacity(regularLimits) == ShaderRecompiler::RuntimeAbi::StageCount * Abi::ImageElementBudget && !ImageTableUpdateAfterBind(regularLimits), "an image table within the regular descriptor limits took the update-after-bind path"); + Require(SamplerTableCapacity(regularLimits) == Abi::SamplerElementBudget && !SamplerTableUpdateAfterBind(regularLimits), "a sampler table within the regular descriptor limits took the update-after-bind path"); + + const auto tableContext = [] { + auto context = mockContext(); + context.sampledImageUpdateAfterBind = true; + context.limits.maxPerStageDescriptorSampledImages = 1; + context.limits.maxDescriptorSetSampledImages = 1; + context.limits.maxPerStageDescriptorSamplers = 1; + context.limits.maxDescriptorSetSamplers = 1; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSampledImages = 2; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSampledImages = 2; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSamplers = 2u * Abi::SamplerEntryElements; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers = 2u * Abi::SamplerEntryElements; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 16; + return context; + }; + const auto requireTableLayout = [](std::uint32_t tableBinding, VkDescriptorBindingFlags tableFlags, std::string_view what) { + Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, std::string(what) + " did not get an update-after-bind layout"); + Require(mock.layoutBindingFlags.size() == mock.layoutBindings.size(), std::string(what) + " lost its binding flags"); + for (std::size_t index = 0; index < mock.layoutBindings.size(); ++index) { + const auto expected = mock.layoutBindings[index].binding == tableBinding ? tableFlags | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u; + Require(mock.layoutBindingFlags[index] == expected, std::string(what) + " put update-after-bind on the wrong bindings"); + } + Require(mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, std::string(what) + " did not come from an update-after-bind pool"); + }; + for (const bool cached : {false, true}) { + mock = MockVulkan{}; + auto context = tableContext(); + context.imageTableCapacity = 2; + context.imageTableUpdateAfterBind = true; + { + DescriptorCache cache(context); + if (cached) context.descriptorCache = &cache; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 2u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 2u; + program.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 2u, std::vector(16u)); + images.kind = Kind::SampledImage; + images.imageShapes.resize(2u); + images.imageDepthCompare.resize(2u); + images.imageSamplers.resize(2u); + program.bindings.push_back(std::move(images)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, shader, {}, true); + requireTableLayout(ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, "an image table beyond the regular limits"); + if (cached) Require(DescribeSetLayouts(context, std::span(&shader, 1)).layouts.main == resources.Layout(), "the described image table layout differs from the built one"); + } + } + Require(mock.live == 0, "the update-after-bind image table leaked Vulkan objects"); + } + for (const bool cached : {false, true}) { + mock = MockVulkan{}; + auto context = tableContext(); + constexpr auto count = 2u * Abi::SamplerEntryElements; + context.samplerTableCapacity = count; + context.samplerTableUpdateAfterBind = true; + context.samplerAnisotropy = true; + context.samplerFilterMinmax = true; + context.limits.maxSamplerAnisotropy = 2.0f; + context.limits.maxSamplerLodBias = 0.5f; + { + DescriptorCache cache(context); + if (cached) context.descriptorCache = &cache; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderSamplerCount] = count; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + program.bindings.push_back(map); + const auto samplerBinding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable); + auto samplers = makeBinding(Role::SamplerTable, samplerBinding, count, {}); + samplers.kind = Kind::Sampler; + const std::array valid{0x92u, (4u * 256u) << 12u, 0u, 0u}; + for (std::uint32_t element = 0; element < count; ++element) { + samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), valid.begin(), valid.end()); + samplers.samplerDepthCompare.push_back(false); + } + program.bindings.push_back(std::move(samplers)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, shader, {}, true); + resources.Complete(); + requireTableLayout(samplerBinding, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, "a sampler table beyond the regular limits"); + Require(findWrite(samplerBinding).count == count, "the update-after-bind sampler table did not write its samplers"); + if (cached) Require(DescribeSetLayouts(context, std::span(&shader, 1)).layouts.main == resources.Layout(), "the described sampler table layout differs from the built one"); + } + } + Require(mock.live == 0 && mock.samplerFlags.empty(), "the update-after-bind sampler table leaked Vulkan objects"); + } +} + void textureCacheBudgetTests() { using AgcDriver::Graphics::TextureCacheBudget; constexpr std::uint64_t GiB = 1ull << 30u; @@ -9814,6 +9942,7 @@ int main(int argc, char** argv) { imageTableSnapshotAllocationTests(); samplerTableNonSeamlessCubeTests(); samplerTableDeviceLimitTests(); + tableUpdateAfterBindTests(); misalignedShaderDataTests(); debugBranchTests(); hostImportBudgetTests();