mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 08:33:12 -05:00
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:
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();
|
||||
|
||||
Reference in new issue
Block a user