From b2d3ce6dfc5666e2564218323f41989833f7c434 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 18 Oct 2026 22:00:23 +0200 Subject: [PATCH] shader: size expanded sampler bindings independently of native roots --- .../host_gpu/renderer/pipeline/shaders.cpp | 20 ++++-- .../ir/passes/ResourceMaterialization.cpp | 64 +++++++++---------- tests/ResourceTrackingTests.cpp | 26 ++++++-- tests/ShaderRecompilerComputeTests.cpp | 39 +++++++++-- 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp index fc419dbed..dd667a8b4 100644 --- a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp @@ -196,21 +196,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe const auto& limits = graphics.GetPhysicalDeviceProperties().limits; uint64_t descriptor_count = 0; uint64_t storage_buffers = 0; - std::array stage_buffers {}, stage_resources {}; + uint64_t samplers = 0; + std::array stage_buffers {}, stage_resources {}, stage_samplers {}; for (const auto& binding: bindings) { descriptor_count += binding.descriptorCount; const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer; + const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler; if (storage) storage_buffers += binding.descriptorCount; + if (sampler) samplers += binding.descriptorCount; for (auto stages = static_cast(binding.stageFlags); stages != 0; stages &= stages - 1u) { const auto stage = std::countr_zero(stages); if (storage) stage_buffers[stage] += binding.descriptorCount; - if (binding.descriptorType != vk::DescriptorType::eSampler) + if (sampler) stage_samplers[stage] += binding.descriptorCount; + else stage_resources[stage] += binding.descriptorCount; if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers || - stage_resources[stage] > limits.maxPerStageResources) { + stage_resources[stage] > limits.maxPerStageResources || + stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) { EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64 - "/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage], - limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources); + "/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage], + limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources, + stage_samplers[stage], limits.maxPerStageDescriptorSamplers); } } } @@ -218,6 +224,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n", storage_buffers, limits.maxDescriptorSetStorageBuffers); } + if (samplers > limits.maxDescriptorSetSamplers) { + EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n", + samplers, limits.maxDescriptorSetSamplers); + } pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors; vk::DescriptorSetLayoutCreateInfo create {}; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 77be9d668..c07eca966 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -545,7 +545,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p ResourceSnapshot& snapshot, std::vector& descriptors, std::vector& specializations, - uint32_t maximum_resources, Normalize&& normalize) { + Normalize&& normalize) { const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor; const auto descriptor_bytes = read_dwords * sizeof(uint32_t); const auto& sources = indirect.sources; @@ -557,19 +557,17 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p auto& byte_offsets = program.material_keys; const auto children_begin = descriptors.size(); const auto root_resource = specializations[resource_index]; - const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) { - ordinal = 0u; - if (candidate == descriptors[resource_index]) return true; + const auto intern_candidate = [&](const DescriptorValue& candidate) { + if (candidate == descriptors[resource_index]) return 0u; const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate); - ordinal = static_cast(found - descriptors.begin() - children_begin + 1u); + const auto ordinal = static_cast(found - descriptors.begin() - children_begin + 1u); if (found == descriptors.end()) { - if (descriptors.size() >= maximum_resources) return false; descriptors.push_back(candidate); auto child = root_resource; child.indirect_root = resource_index; specializations.push_back(child); } - return true; + return ordinal; }; uint64_t table_base = 0; uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds. @@ -581,7 +579,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p "indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} " "base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} " "maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} " - "child_offset={} indexed_pointer={} candidates={} capacity={} {}", + "child_offset={} indexed_pointer={} candidates={} {}", reason, resource_index, source_index, indirect.table_source, table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3], table_base, table_size, table.Type(), table.Stride(), table.NumRecords(), @@ -590,7 +588,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes, pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u, pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed, - descriptors.size(), maximum_resources, details)); + descriptors.size(), details)); }; const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child, uint64_t base, uint64_t offset, size_t bytes) { @@ -725,8 +723,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p if (!normalize(candidate)) return normalization_failed(table_offset, word, step, indirect.table_maximum ? "key" : "probe"); - uint32_t ordinal; - if (!intern_candidate(candidate, ordinal)) return false; + const auto ordinal = intern_candidate(candidate); if (ordinal == 0u) continue; const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child; snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u); @@ -813,8 +810,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p uint32_t ordinal = 0; if (entry == 0 && !byte_keys) { descriptors[resource_index] = candidate; - } else if (!intern_candidate(candidate, ordinal)) { - return false; + } else { + ordinal = intern_candidate(candidate); } snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key; snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal; @@ -837,9 +834,8 @@ struct SamplerPlan { uint32_t source; SamplerClass type; }; - std::array, ShaderInfo::MaxSamplers> mapping; - std::array bindings; - uint32_t sampler_count = 0; + std::vector> mapping; + std::vector bindings; }; template @@ -1076,11 +1072,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna } bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) { - if (base.samplers.size() > plan.mapping.size()) { - return false; - } - std::array usage {}; - plan.sampler_count = static_cast(base.samplers.size()); + plan.mapping.resize(base.samplers.size()); + plan.bindings.resize(base.samplers.size()); + std::vector usage(base.samplers.size()); for (const auto& pair: base.sampled_pairs) { if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) { return false; @@ -1102,8 +1096,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) { bool first = true; for (uint32_t type = 0; type < mapping.size(); type++) { if ((classes & (1u << type)) == 0u) continue; - const auto target = first ? index : plan.sampler_count++; - if (target >= ShaderInfo::MaxSamplers) return false; + const auto target = first ? index : static_cast(plan.bindings.size()); + if (!first) plan.bindings.emplace_back(); mapping[type] = target; plan.bindings[target] = {index, static_cast(type)}; first = false; @@ -1609,9 +1603,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime return true; }; const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source, - size_t candidates, uint32_t capacity) { - return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}", - kind, resource, source, candidates, capacity)); + size_t candidates) { + return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={}", + kind, resource, source, candidates)); }; snapshot.buffers.resize(program.info.buffers.size()); specialization.buffers.assign(program.info.buffers.size(), {}); @@ -1628,12 +1622,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime if (active.empty() || active[base.source]) { if (!MaterializeIndirectDescriptor( program, base.source, i, 4u, 4u, observed, clean, snapshot, - snapshot.buffers, specialization.buffers, UINT32_MAX, + snapshot.buffers, specialization.buffers, [&](DescriptorValue& value) { return NormalizeIndirectBuffer( value, source->indirect_descriptor->indexed, base); })) - return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX); + return resource_failed("buffer", i, base.source, snapshot.buffers.size()); } } else if (!evaluate(base.source, snapshot.buffers[i], base.written)) { return false; @@ -1692,14 +1686,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime const bool bounded_table = IsBoundedDescriptorTable(program, indirect); if (!MaterializeIndirectDescriptor( program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images, - specialization.images, UINT32_MAX, [&](DescriptorValue& value) { + specialization.images, [&](DescriptorValue& value) { // A broad heap also contains resources for other typed image operations. if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) || (bounded_table && DescriptorDimension(value, image.dimension) != image.dimension)) value.dwords.fill(0u); return true; })) { - return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX); + return resource_failed("image", i, image.source, snapshot.images.size()); } } else { if (!evaluate(image.source, snapshot.images[i], image.written)) { @@ -1725,9 +1719,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime snapshot.samplers[i] = {.dword_count = 4u}; if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor( program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers, - specialization.samplers, ShaderInfo::MaxSamplers, + specialization.samplers, [](DescriptorValue&) { return true; })) - return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers); + return resource_failed("sampler", i, source, snapshot.samplers.size()); } else { if (indirect != nullptr) source = indirect->sources[0]; if (!evaluate(source, snapshot.samplers[i])) return false; @@ -1835,8 +1829,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& SamplerPlan sampler_plan; EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan)); const auto original_sampler_count = samplers.size(); - samplers.reserve(sampler_plan.sampler_count); - for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) { + samplers.reserve(sampler_plan.bindings.size()); + for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) { const auto& binding = sampler_plan.bindings[index]; if (index >= original_sampler_count) { samplers.push_back(samplers[binding.source]); @@ -1845,7 +1839,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& samplers[index].force_point_filtering = binding.type == SamplerClass::Integer; samplers[index].integer_border = binding.type != SamplerClass::Float; } - for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) { + for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) { const auto& binding = sampler_plan.bindings[index]; for (auto& candidate: samplers[index].indirect_resources) { candidate = sampler_plan.mapping[candidate][static_cast(binding.type)]; diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index a7ddaf65e..8ed34222d 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -890,7 +890,7 @@ void TestNestedPointerImageTable() { "nested pointer descriptors lost their row geometry or captured the GPU selector payload"); } - constexpr uint32_t table = 0x2000u, objects = 0x4000u, stride = 592u, count = 6u; + constexpr uint32_t table = 0x2000u, objects = 0x10000u, stride = 592u, count = 59u; LinearTestMemory memory; memory.words.resize((objects + count * 0x430u - memory.base) / 4u); memory.fail_address = 0x1800u; // The GPU selector's contents are unavailable to the CPU. @@ -899,7 +899,8 @@ void TestNestedPointerImageTable() { const auto object = objects + row * 0x430u; *at(table + row * stride) = object; const std::array image{10u + row, - uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u, + uint32_t(row == count - 1u ? Libs::Graphics::Prospero::BufferFormat::k32UInt + : Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u, Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u), 0u, 0u, 0u, 0u}; std::copy(image.begin(), image.end(), at(object + 0x3d0u)); const std::array sampler{row, 0x00fff000u, 0u, 0u}; @@ -918,6 +919,21 @@ void TestNestedPointerImageTable() { Check(snapshot.images[row + 1u].dwords[0] == 10u + row && snapshot.samplers[row + 1u].dwords[0] == row, "nested pointer table mixed rows or pointee members"); + auto typed = make(); + typed->PlanAndTrack(); + ApplyResourceSpecialization(typed->program, specialization); + const auto& samplers = typed->program.info.samplers; + Check(samplers.size() == 2u * (count + 1u) && samplers[0].indirect_resources.size() == count + 1u && + samplers[count + 1u].indirect_resources.size() == count + 1u && + snapshot.samplers[0].dwords[1] == 0u, + "expanded sampler table lost its null root or numeric-class variants"); + for (uint32_t index = 0; index <= count; ++index) { + const auto& integer = samplers[index + count + 1u]; + Check(samplers[index].snapshot_index == index && !samplers[index].integer_border && + integer.snapshot_index == index && integer.integer_border && integer.force_point_filtering && + samplers[count + 1u].indirect_resources[index] == index + count + 1u, + "large sampler class mapping changed descriptor identity or integer filtering"); + } const auto populated = specialization; auto equivalent = make(0u, Address::Equivalent); equivalent->PlanAndTrack(); @@ -940,11 +956,11 @@ void TestNestedPointerImageTable() { specialization == populated, "empty-to-positive pointer table refresh did not restore its specialization"); *at(table) = objects + 0x430u; - *at(objects + 0x430u + 0x3d0u) = 42u; - *at(objects + 0x430u + 0x3f8u) = 7u; + *at(objects + 0x430u + 0x3d0u) = 10u + count; + *at(objects + 0x430u + 0x3f8u) = count; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == count && snapshot.samplers.size() == count && - snapshot.images[1].dwords[0] == 42u && snapshot.samplers[1].dwords[0] == 7u, + snapshot.images[1].dwords[0] == 10u + count && snapshot.samplers[1].dwords[0] == count, "nested descriptor refresh retained stale pointers or failed to deduplicate new pointees"); *at(table) = objects; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index a56a63285..a151fa9d8 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -39675,24 +39675,32 @@ void CheckPointerImageTable(VulkanHarness &vulkan) { void CheckNestedPointerImageTable(VulkanHarness &vulkan) { using O = ShaderOpcode; - constexpr u32 list_address = 160, objects = 4096; - constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u}; + constexpr u32 list_address = 160; + constexpr std::array small_keys{5u, 0u, 3u, 1u, 4u, 2u}; + constexpr std::array large_keys{58u, 0u, 33u, 31u, 34u, 32u, 59u, 65535u}; constexpr std::array list{2u, 1u, 4u, 0u, 5u, 3u}; - for (const auto [member, counted] : {std::pair{0u, false}, std::pair{0x98u, false}, - std::pair{0x16cu, false}, std::pair{64u, false}, std::pair{64u, true}}) + for (const auto [member_mode, counted] : {std::pair{0u, false}, std::pair{0x98u, false}, + std::pair{0x16cu, false}, std::pair{64u, false}, std::pair{64u, true}, + std::pair{UINT32_MAX, false}}) for (const auto [wave, upper_only] : {std::pair{32u, false}, std::pair{64u, false}, std::pair{64u, true}}) { + const bool large_samplers = member_mode == UINT32_MAX; + const u32 member = large_samplers ? 0u : member_mode; const bool child_table = member == 64u, compact = member != 0u && !child_table; + const std::span keys = large_samplers ? std::span(large_keys) + : std::span(small_keys); + const u32 objects = large_samplers ? 65536u : 4096u; if (upper_only && !child_table) continue; const u32 keys_address = child_table ? 1024u : 128u, table = child_table ? 2048u : 256u; - const u32 rows = child_table ? 4u : keys.size(), children = child_table ? 3u : 1u; + const u32 rows = large_samplers ? 59u : child_table ? 4u : keys.size(), children = child_table ? 3u : 1u; const u32 records_count = counted ? 2u : rows, count_address = 768u; const u32 stride = child_table ? 136u : compact ? 0x330u : 592u; const u32 object_stride = child_table ? 1536u : 0x430u; const u32 table_bytes = (rows - 1u) * stride + member + 16u; const u32 outputs = child_table ? wave * 2u : keys.size(); TestCase test; - test.name = counted ? (upper_only ? "CountedPointerChildTableUpper64" : "CountedPointerChildTable") + test.name = large_samplers ? "LargeIndirectSamplerTable" + : counted ? (upper_only ? "CountedPointerChildTableUpper64" : "CountedPointerChildTable") : child_table ? (upper_only ? "IndexedPointerChildTableUpper64" : "IndexedPointerChildTable") : member == 0x16cu ? "CompactPairedImageSamplerTable" : compact ? "CompactRecordImageSamplerTable" : "NestedPointerImageSamplerTable"; @@ -39718,7 +39726,8 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { test.initial.begin() + (object + (child_table ? child * 368u + member : compact ? member : 0x3d0u)) / 4u); } - const std::array sampler{0u, 0x00fff000u, (row & 1u) ? 0x0a500000u : 0u, 0u}; + const std::array sampler{large_samplers ? row : 0u, 0x00fff000u, + (row & 1u) ? 0x0a500000u : 0u, 0u}; std::copy(sampler.begin(), sampler.end(), test.initial.begin() + (child_table ? table + row * stride + 80u : object + (compact ? 0x88u : 0x3f8u)) / 4u); } @@ -39846,6 +39855,18 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { } else if (!child_table) test.opcodes.insert(test.opcodes.end(), {O::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4}); const auto compiled = CompileCase(test, vulkan.SubgroupSize()); + if (large_samplers) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + std::string text; + const auto halves = wave > vulkan.SubgroupSize() ? 2u : 1u; + Require(test.name, "expanded sampler bindings and emitted size", + compiled.resources.samplers.size() == rows + 1u && + compiled.program.info.samplers.size() == rows + 1u && + tools.Disassemble(compiled.spirv, &text) && + CountText(text, "OpImageSampleExplicitLod") == halves && + compiled.spirv.size() <= 8192u * halves, + "59 sampler records hit the SGPR root limit or duplicated image sampling bodies"); + } if (child_table) { std::vector candidates; for (const auto &image : compiled.resources.images) @@ -39923,6 +39944,10 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { test.expected[wave + group] = 0x76543210u; if (outer >= records_count || child >= children) continue; } + if (!child_table && outer >= rows) { + test.expected[group] = 0u; + continue; + } const u32 row = compact ? list[outer] : outer; test.expected[group] = std::bit_cast(float(10u + row * children + child) * ((row & 1u) ? 0.5f : 1.0f));