Merge pull request #6647 from binarycleric/fix/agc-table-update-after-bind

fix(agc): bind image and sampler tables through update-after-bind limits
This commit is contained in:
Adrià authored and GitHub committed 2026-10-19 12:20:50 +02:00
commit c82ffb0f20
5 files changed
+222 -29

No files matched your search

@@ -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");
}
}
@@ -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<VkDescriptorSetLayoutBinding> bindings;
std::vector<VkDescriptorBindingFlags> 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<PFN_vkGetPhysicalDeviceFeatures2>("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<PFN_vkGetPhysicalDeviceProperties2>("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;
@@ -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);
}
@@ -1720,7 +1720,13 @@ std::string DescriptorLimitError(std::string_view description, std::uint64_t req
}
void ValidateDescriptorLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> 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<VkDescriptorType, std::uint64_t> heapTotals;
std::map<std::pair<VkShaderStageFlags, VkDescriptorType>, std::uint64_t> stageTotals;
std::map<VkShaderStageFlags, std::uint64_t> stageResources;
@@ -1732,13 +1738,13 @@ void ValidateDescriptorLayout(const Context& context, std::span<const VkDescript
const auto count = stageTotals[{stage, layout.descriptorType}] += layout.descriptorCount;
const auto all = stageResources[stage] += layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER ? 0u : layout.descriptorCount;
Require(all <= maxStageResources, "typed heaps exceed per-stage resource capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE) Require(count <= context.limits.maxPerStageDescriptorSampledImages, "typed heaps exceed per-stage sampled image capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE) Require(count <= maxStageSampledImages, "typed heaps exceed per-stage sampled image capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) Require(count <= context.limits.maxPerStageDescriptorStorageImages, "typed heaps exceed per-stage storage image capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER && count > 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<const std::uint32_t> words) {
@@ -1931,7 +1937,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> 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<std::int32_t>(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<std::int32_t>(set)});
tableBindings[set] = bindings.size() - 1u;
}
if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) {
@@ -1947,13 +1954,16 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
timing.uploadMs = phase(BuildPhase::Upload);
splitSets = SplitsSets(context, shaders);
std::array<std::vector<VkDescriptorSetLayoutBinding>, 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<std::uint32_t>(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<const C
std::array<VkShaderStageFlags, 2> tableFlags{};
std::array<VkShaderStageFlags, 2> 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<const C
}
if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) {
descriptions[set].push_back({binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr});
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT);
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | (context.samplerTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0));
tablesUpdateAfterBind |= context.samplerTableUpdateAfterBind;
continue;
}
const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers;
@@ -2121,7 +2133,8 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const C
for (std::size_t set = 0; set < tableFlags.size(); ++set) {
if (tableFlags[set] == 0) continue;
descriptions[set].push_back({ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr});
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT);
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT | (context.imageTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0));
tablesUpdateAfterBind |= context.imageTableUpdateAfterBind;
}
if (storageBuffers > 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<const C
if (updateAfterBind && descriptions[set][index].descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) bindingFlags[set][index] |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
}
}
updateAfterBind = updateAfterBind || tablesUpdateAfterBind;
for (std::size_t set = 0; set < descriptions.size(); ++set) {
auto& key = set == 0 ? result.mainKey : result.fragmentKey;
for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast<std::uint32_t>(binding.descriptorType), binding.descriptorCount, binding.stageFlags});
@@ -3819,12 +3833,15 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> 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<VkDescriptorPoolSize, 4> ChainPoolCreateSizes{{{VK_DESCRIPT
DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes, bool updateAfterBind, std::optional<std::uint32_t> 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<std::uint32_t>({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<std::uint64_t>(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity;
constexpr std::uint64_t storageHeaps = static_cast<std::uint64_t>(static_cast<std::uint32_t>(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<std::uint64_t>(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<std::uint32_t>(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<std::uint64_t>(capacity, room(context.limits.maxPerStageDescriptorSamplers, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity));
capacity = std::min<std::uint64_t>(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<std::uint32_t>(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<std::size_t>(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));
}
@@ -6189,6 +6189,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<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(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<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable);
auto samplers = makeBinding(Role::SamplerTable, samplerBinding, count, {});
samplers.kind = Kind::Sampler;
const std::array<std::uint32_t, 4> 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;
@@ -10116,6 +10244,7 @@ int main(int argc, char** argv) {
imageTableSnapshotAllocationTests();
samplerTableNonSeamlessCubeTests();
samplerTableDeviceLimitTests();
tableUpdateAfterBindTests();
misalignedShaderDataTests();
debugBranchTests();
hostImportBudgetTests();