From ab0f9985fa12e909b2aa8e552b7f806c26f4e30a Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 5 Oct 2026 19:20:10 +0300 Subject: [PATCH] feat(shader): resolve bindless resources through runtime heap metadata --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 30 ++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 11 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 34 +++++- .../tests/execution/BindlessImages.cpp | 100 ++++++++++++++++++ .../Optimization/ResourceMaterializer.hpp | 1 - .../Optimization/src/ResourceMaterializer.cpp | 38 +++---- core/shader/recompiler/RuntimeAbi.hpp | 2 +- .../SpirvMemory/SpirvDescriptors.hpp | 1 - .../SpirvBackend/src/SpirvImageEmitter.cpp | 8 +- .../src/SpirvMemory/SpirvDescriptors.cpp | 15 +-- 12 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0cdc88a27..7a8c2f21a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -607,6 +607,11 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) endforeach() if(BUILD_TESTING) + add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) + set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests ${agcExecutionTests}) if(agcTest STREQUAL "agc_driver_constant_store_tests") continue() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 1902efa11..f645cca16 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -433,6 +433,7 @@ private: // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; + std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; std::size_t index; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index a01286df9..acdb95eb3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -886,6 +886,32 @@ double ShaderResources::phase(BuildPhase which) { return ms; } +namespace { + +void ValidateRuntimeResources(const CompiledShader& shader) { + Require(shader.program != nullptr, "missing compiled shader"); + const auto& descriptors = shader.program->bindings; + for (const auto& dataBinding : descriptors) { + if (dataBinding.role != ShaderRecompiler::DescriptorRole::ShaderData) continue; + Require(dataBinding.guestDescriptor.size() == ShaderRecompiler::RuntimeAbi::ShaderDataDwords, "invalid runtime resource metadata size"); + ShaderRecompiler::RuntimeAbi::ShaderData data{}; + std::memcpy(&data, dataBinding.guestDescriptor.data(), sizeof(data)); + ShaderRecompiler::RuntimeAbi::RequireVersion(data.version); + Require(data.imageCount <= data.images.size() && data.samplerCount <= data.samplers.size(), "runtime resource metadata capacity exceeded"); + const auto stageBase = dataBinding.binding - dataBinding.binding % static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + const auto validate = [&](const ShaderRecompiler::RuntimeAbi::ResourceMetadata& metadata, ShaderRecompiler::DescriptorRole role) { + Require(metadata.binding < static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count), "invalid runtime heap binding"); + const auto found = std::ranges::find_if(descriptors, [&](const ShaderRecompiler::DescriptorBinding& binding) { return binding.binding == stageBase + metadata.binding && binding.role == role; }); + Require(found != descriptors.end(), "runtime metadata references an unbound heap"); + Require(metadata.elementCount != 0u && metadata.firstElement < found->count && metadata.elementCount <= found->count - metadata.firstElement, "runtime metadata exceeds its bound heap"); + }; + for (std::uint32_t i = 0u; i < data.imageCount; ++i) validate(data.images[i], ShaderRecompiler::DescriptorRole::GuestImages); + for (std::uint32_t i = 0u; i < data.samplerCount; ++i) validate(data.samplers[i], ShaderRecompiler::DescriptorRole::GuestSamplers); + } +} + +} + void ShaderResources::buildPrepare(std::span shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes) { const auto stageStart = std::chrono::steady_clock::now(); phaseStart = stageStart; @@ -895,8 +921,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons } try { Require(!shaders.empty() && context.limits.maxBoundDescriptorSets >= 1, "shader descriptor set exceeds device limits"); + if (shaders.size() == 1u) refreshResourceKey = ContentKey(shaders.front(), false, true); std::set occupied; for (const auto& shader : shaders) { + ValidateRuntimeResources(shader); Require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); const VkShaderStageFlags flags = VulkanStage(shader.stage); @@ -2397,6 +2425,8 @@ bool ShaderResources::DataWordsDiffer(const CompiledShader& shader) const { bool ShaderResources::RefreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder) { Require(shader.program != nullptr, "missing compiled shader"); + Require(!refreshResourceKey.empty() && refreshResourceKey == ContentKey(shader, false, true), "runtime data refresh cannot replace bound resources"); + ValidateRuntimeResources(shader); const auto& program = *shader.program; Require(program.bindings.size() == bindings.size(), "template bindings disagree with the shader"); bool recorded = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5d4782620..a8c50a234 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2472,6 +2472,17 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { Require(resources.RefreshData(recorder.Commands(), original, &recorder), "the refresh back recorded nothing"); Require(resources.DataWordsHash() == ShaderResources::DataWordsHash(original), "the refreshed template's hash is not the original words'"); Require(!resources.RefreshData(recorder.Commands(), original, &recorder), "a refresh with equal words recorded"); + auto replaced = patched; + replaced.bindings[0].role = ShaderRecompiler::DescriptorRole::GuestSamplers; + replaced.bindings[0].kind = ShaderRecompiler::DescriptorKind::Sampler; + const CompiledShader changedResources{ShaderRecompiler::ShaderStage::Compute, &replaced, 0}; + bool rejected = false; + try { + static_cast(resources.RefreshData(recorder.Commands(), changedResources, &recorder)); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find("cannot replace bound resources") != std::string_view::npos; + } + Require(rejected && resources.DataWordsHash() == ShaderResources::DataWordsHash(original), "a data refresh changed the bound resources"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 00f628296..b2608c0ed 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -242,7 +242,8 @@ void verifyBindlessTable() { const std::vector wholeCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; const auto srtAddress = static_cast(reinterpret_cast(srt.data())); const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; - const std::array capabilities{29u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; // A wave64 workgroup on a 32-wide host is held by one subgroup (two lanes per invocation). const auto makeRequest = [&](const std::vector& code) { RecompileRequest request{}; @@ -255,6 +256,8 @@ void verifyBindlessTable() { request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 32; request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; request.target.fragmentShaderBarycentricEnabled = false; request.layout.pushConstantSizeBytes = 128; return request; @@ -356,8 +359,8 @@ void verifyBindlessTable() { AgcDriver::ShaderMemory splitMemory({}); const auto splitCapture = splitMemory.Capture(split); expectFailure([&] { static_cast(Recompile(split, *splitCapture)); }, "not uniform over the workgroup", "bindless: a split wave indexed the image array as uniform"); - const std::array indexingCapabilities{29u, 5301u, 5307u}; - const std::array indexingExtensions{"SPV_EXT_descriptor_indexing"}; + const std::array indexingCapabilities{29u, 5301u, 5307u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array indexingExtensions{"SPV_EXT_descriptor_indexing", "SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; split.target.supportedCapabilities = indexingCapabilities; split.target.supportedExtensions = indexingExtensions; AgcDriver::ShaderMemory indexingMemory({}); @@ -404,6 +407,23 @@ void verifyBindlessTable() { AgcDriver::ShaderMemory wideMemory({}); expectFailure([&] { static_cast(wideMemory.Capture(whole)); }, "bindless image table has 100 entries", "bindless: a wide table was bound"); fillSrt(4u); + const auto savedHeap = heap; + heap = {}; + AgcDriver::ShaderMemory emptyMemory({}); + const auto emptyCapture = emptyMemory.Capture(request); + require(mappingOf(emptyCapture->snapshot).front() == 0u, "bindless: an empty table has mapped keys"); + request.context.memory = emptyMemory.Regions(); + require(Recompile(request, *emptyCapture)->variantId == compiled->variantId, "bindless: an empty table changed the artifact"); + heap = savedHeap; + heap[0][3] &= 0x0fffffffu; + AgcDriver::ShaderMemory invalidMemory({}); + expectFailure([&] { static_cast(invalidMemory.Capture(request)); }, "invalid descriptor", "bindless: an invalid T# was accepted"); + heap = savedHeap; + srt[1] &= 0xffffu; + srt[2] = 127u; + AgcDriver::ShaderMemory partialMemory({}); + expectFailure([&] { static_cast(partialMemory.Capture(request)); }, "partial descriptor", "bindless: a partial heap descriptor was accepted"); + fillSrt(4u); } @@ -1704,9 +1724,15 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } -int main() { +int main(int argc, char** argv) { try { using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--bindless") { + verifyBindlessTable(); + std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; + return 0; + } + require(argc == 1, "unknown shader memory test arguments"); verifyRegisterSources(); verifyEvaluatedValues(); verifyPureFlatSlots(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp new file mode 100644 index 000000000..d4884f4fc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using namespace ShaderRecompiler; + +struct alignas(4096) GuestData { + std::array, 4> heap{}; + std::array material{}; + std::array srt{}; + std::array output{}; +}; + +alignas(256) std::array, 2> textures{}; +std::array guest; +constexpr std::array code{0xbf068002u, 0xbf850013u, 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u, 0xbf800000u}; + +std::array Buffer(const void* pointer, std::uint32_t stride, std::uint32_t records) { + const auto address = reinterpret_cast(pointer); + return {static_cast(address), static_cast(address >> 32u) | (stride << 16u), records, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + textures[0].fill(0x40000000u); + textures[1].fill(0x40400000u); + CompiledShaderArtifact artifact; + for (std::uint32_t iteration = 0u; iteration < guest.size(); ++iteration) { + auto& data = guest[iteration]; + const auto table = Buffer(data.heap.data(), 32u, 4u); + const auto material = Buffer(data.material.data(), 16u, 3u); + std::copy(table.begin(), table.end(), data.srt.begin()); + std::copy(material.begin(), material.end(), data.srt.begin() + 8u); + const auto srtAddress = reinterpret_cast(data.srt.data()); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), iteration % 6u == 0u ? 0u : 1u}; + const std::array regions{MemoryRegion{reinterpret_cast(&data), std::as_bytes(std::span(&data, 1u))}}; + const auto key = iteration % 4u; + if (key < 2u) { + for (std::uint32_t texture = 0u; texture < textures.size(); ++texture) { + const auto address = reinterpret_cast(textures[texture].data()); + data.heap[texture] = {static_cast(address >> 8u), static_cast(address >> 40u) | ((texture == 0u ? 20u : 22u) << 20u) | (3u << 30u), 7u, 0x90000facu, 0u, 0u, 0u, 0u}; + } + } + data.material[1] = key == 3u ? 0xffffffffu : key; + data.material[5] = 0u; + data.material[9] = 1u; + data.output.fill(0xdeadbeefu); + const auto output = Buffer(data.output.data(), 0u, 256u); + std::copy(output.begin(), output.end(), data.srt.begin() + 12u); + const ShaderComputeStageInfo compute{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(code.data()), code, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, regions}, device.Target(), {0u, 0u, 0u, 128u}}; + const auto shader = Recompile(request); + if (artifact.variantId == 0u) artifact = shader; + else Require(shader.cacheHit && shader.variantId == artifact.variantId && shader.spirv.data() == artifact.spirv.Words().data(), "bindless table changed the compiled artifact"); + if (iteration == 1u) { + auto invalid = shader; + auto binding = std::ranges::find_if(invalid.bindings, [](const DescriptorBinding& value) { return value.role == DescriptorRole::ShaderData; }); + Require(binding != invalid.bindings.end(), "bindless shader has no runtime metadata"); + RuntimeAbi::ShaderData metadata{}; + std::memcpy(&metadata, binding->guestDescriptor.data(), sizeof(metadata)); + metadata.images[0].firstElement = RuntimeAbi::SampledHeapCapacity; + std::memcpy(binding->guestDescriptor.data(), &metadata, sizeof(metadata)); + bool rejected = false; + try { + device.Dispatch(invalid, 1u, 1u, 1u); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find("runtime metadata exceeds its bound heap") != std::string_view::npos; + } + Require(rejected, "driver accepted an out-of-range runtime heap index"); + } + device.Dispatch(shader, 1u, 1u, 1u); + device.SubmitRecorded(false); + } + device.WaitIdle(); + for (std::uint32_t iteration = 0u; iteration < guest.size(); ++iteration) { + const auto expected = iteration % 6u == 0u ? 0xdeadbeefu : iteration % 4u < 2u ? textures[iteration % 2u][0] : 0u; + Require(guest[iteration].output[0] == expected, "bindless resource selection or lifetime failed at " + std::to_string(iteration) + " value=" + std::to_string(guest[iteration].output[0]) + " expected=" + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + std::cout << "bindless image materialization tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index cfcaac4ad..09ea8330b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -49,7 +49,6 @@ public: void Materialize(const IrResourcePlan& program, const SrtRuntime& runtime, ResourceSnapshot& snapshot, ResourceSpecialization& specialization) const; // APS5_PROFILE_DRAW: the time Materialize spent building specializations, over every call. static std::uint64_t SpecializationNanoseconds(); - // The slots every bindless image table binds (APS5_BINDLESS_SLOTS, default 16, 1..48). static std::uint32_t BindlessSlots(); static void CountBindlessRejection(BindlessRejection reason); }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 6cf70e830..4aaa73988 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -202,8 +202,6 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag constexpr std::uint32_t TableEntryBytes = 32; -// Bindless image tables: the most material records mode M reads to enumerate a table's keys -// (APS5_BINDLESS_MATERIAL_SCAN, default 256); beyond it the whole table is bound instead. std::uint32_t MaterialScanLimit() { static const std::uint32_t limit = [] { const char* text = std::getenv("APS5_BINDLESS_MATERIAL_SCAN"); @@ -266,10 +264,6 @@ void reportBindless() { static_cast(rejected[0]), static_cast(rejected[1]), static_cast(rejected[2]), static_cast(rejected[3]), static_cast(rejected[4]), static_cast(rejected[5])); } -// A bindless image table's bound slots and its (key, slot) mapping, keys ascending. Slots the -// mapping does not name (past the keys, or an entry that is null, invalid or of another shape) -// hold a copy of the first usable entry: a key the mapping lacks samples zeros in the shader, as -// a null T# does on hardware. Empty when the image is no table root. struct TableResolution { std::vector slots; std::vector> mapping; @@ -312,6 +306,8 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con walker.EvaluateDescriptorSource(plan, table.heapSource, runtime, heapValue); const ShaderBufferResource heap = decodeBufferDescriptor(heapValue); const std::uint64_t heapSize = heap.GetSize(); + if (heap.Type() != 0u || heap.Base48() > std::numeric_limits::max() - heapSize) throw std::runtime_error("bindless heap buffer is invalid"); + if (heapSize != 0u && (heapSize < table.entryOffset || (heapSize - table.entryOffset) % TableEntryBytes != 0u)) throw std::runtime_error("bindless heap contains a partial descriptor"); const auto entries = heapSize > table.entryOffset ? static_cast(std::min((heapSize - table.entryOffset) / TableEntryBytes, std::numeric_limits::max())) : 0u; const auto readWord = [&](std::uint64_t address, std::uint32_t& word) { if (!runtime.readMemory(runtime.userContext, address, &word)) { @@ -325,18 +321,21 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con std::uint32_t materialEntries = 0; std::uint64_t materialBase = 0; std::uint32_t outOfRange = 0; - if (table.hasMaterial && table.selectorStride != 0u) { + if (table.hasMaterial) { + if (table.selectorStride < sizeof(std::uint32_t) || table.selectorOffset > table.selectorStride - sizeof(std::uint32_t)) throw std::runtime_error("bindless material selector is outside its record"); DescriptorValue materialValue; walker.EvaluateDescriptorSource(plan, table.materialSource, runtime, materialValue); const ShaderBufferResource material = decodeBufferDescriptor(materialValue); materialBase = material.Base48(); const std::uint64_t materialSize = material.GetSize(); + if (material.Type() != 0u || materialSize % table.selectorStride != 0u) throw std::runtime_error("bindless material buffer has an invalid layout"); materialEntries = static_cast(std::min(materialSize / table.selectorStride, std::numeric_limits::max())); + if (materialSize / table.selectorStride > MaterialScanLimit()) rejectTable(BindlessRejection::MaterialScan, "bindless material scan capacity exceeded"); if (materialEntries <= MaterialScanLimit()) { materialMode = true; for (std::uint32_t record = 0; record < materialEntries; record++) { const std::uint64_t offset = static_cast(record) * table.selectorStride + table.selectorOffset; - if (offset + sizeof(std::uint32_t) > materialSize) break; + if (offset + sizeof(std::uint32_t) > materialSize) throw std::runtime_error("bindless material selector exceeds its buffer"); std::uint32_t key = 0; readWord(materialBase + offset, key); if (key >= entries) { @@ -360,10 +359,6 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con for (std::uint32_t key = 0; key < entries; key++) keys[key] = key; } counters.outOfRange.fetch_add(outOfRange, std::memory_order_relaxed); - if (keys.empty()) { - rejectTable(BindlessRejection::NoEntry, "bindless image table selects no entry"); - } - const std::uint64_t heapBase = heap.Base48(); std::vector candidates(keys.size()); std::vector valid(keys.size(), 0u); @@ -434,18 +429,17 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, snapshot.images.resize(plan.info.images.size()); tables.assign(plan.info.images.size(), {}); - std::uint32_t activeTables = 0; + std::uint32_t tableCount = 0; for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { const auto& image = plan.info.images[i]; if (image.source >= plan.descriptorSources.size()) { throw std::runtime_error("image resource references an unknown descriptor source"); } - const bool active = image.source >= activeSources.size() || activeSources[image.source] != 0u; - if (plan.descriptorSources[image.source].indirectImage.has_value() && active) activeTables++; + if (plan.descriptorSources[image.source].indirectImage.has_value()) tableCount++; } const auto tableSlots = ResourceMaterializer::BindlessSlots(); - if (activeTables != 0u && plan.info.images.size() + static_cast(tableSlots - 1u) * activeTables > ShaderInfo::MaxImages) { - rejectTable(BindlessRejection::ImageSlots, "bindless image tables need " + std::to_string(plan.info.images.size() + static_cast(tableSlots - 1u) * activeTables) + " image slots, limit " + std::to_string(ShaderInfo::MaxImages)); + if (tableCount != 0u && plan.info.images.size() + static_cast(tableSlots - 1u) * tableCount > ShaderInfo::MaxImages) { + rejectTable(BindlessRejection::ImageSlots, "bindless image tables need " + std::to_string(plan.info.images.size() + static_cast(tableSlots - 1u) * tableCount) + " image slots, limit " + std::to_string(ShaderInfo::MaxImages)); } for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { const auto& image = plan.info.images[i]; @@ -453,6 +447,7 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (source.indirectImage.has_value()) { if (image.source < activeSources.size() && activeSources[image.source] == 0u) { snapshot.images[i].dwordCount = 8u; + tables[i].slots.assign(tableSlots, snapshot.images[i]); continue; } resolveTableImage(plan, i, *source.indirectImage, runtime, walker, snapshot.images[i], tables[i]); @@ -587,7 +582,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s snapshot.flattenedSrt.push_back(static_cast(table.mapping.size())); for (const auto& [key, slot] : table.mapping) { snapshot.flattenedSrt.push_back(key); - snapshot.flattenedSrt.push_back(slot); + snapshot.flattenedSrt.push_back(slot == 0u ? i : static_cast(snapshot.images.size()) + slot - 1u); } snapshot.flattenedSrt.resize(mappingOffset + 1u + 2u * table.slots.size(), 0u); auto root = result.images[i]; @@ -892,12 +887,7 @@ std::uint64_t ResourceMaterializer::SpecializationNanoseconds() { } std::uint32_t ResourceMaterializer::BindlessSlots() { - static const std::uint32_t slots = [] { - const char* text = std::getenv("APS5_BINDLESS_SLOTS"); - const auto value = text != nullptr ? std::strtoul(text, nullptr, 0) : 16ul; - return static_cast(std::clamp(value, 1ul, 48ul)); - }(); - return slots; + return RuntimeAbi::SampledHeapCapacity; } void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) { diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index ea8f09155..8fde18b52 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 3u; +inline constexpr std::uint32_t Version = 4u; inline constexpr std::uint32_t DescriptorSet = 0u; inline constexpr std::uint32_t StageCount = 4u; inline constexpr std::uint32_t PushConstantDwords = 32u; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 404ac85d1..5aa9f0bdd 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -13,7 +13,6 @@ std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass nu std::uint32_t ImageVectorType(SpirvEmitterState& state, IrTextureNumericClass numericClass, std::uint32_t components); std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image); std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dimension); -// `slotId` (a SPIR-V id, 0 for none) selects a bindless table's slot at runtime: element(resource) + slot. std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t slotId); std::uint32_t LoadSamplerDescriptor(SpirvEmitterState& state, std::uint32_t sampler); std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler, std::uint32_t slotId); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 63c579e11..31e563383 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1039,10 +1039,6 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, gathered), resultNumericClass, false, true))); } -// The bound slot of a bindless table for the runtime key: a binary search over the (key, slot) -// pairs the materializer appended to the flattened SRT. A key the mapping lacks (past the table, -// or a null or unusable entry) selects slot 0 and reports unmapped: its result is zeroed, as a -// null T# samples on hardware. TableSelection EmitIndirectImageSelector(SpirvValueEmitContext& ctx, const ImageResource& image, std::uint32_t key) { auto& state = ctx.state; const auto loadMapping = [&](std::uint32_t index) { @@ -1055,7 +1051,7 @@ TableSelection EmitIndirectImageSelector(SpirvValueEmitContext& ctx, const Image const auto mapping = ConstantU32(state, image.indirectMappingOffset); auto low = ConstantU32(state, 0u); auto high = loadMapping(mapping); - auto selected = ConstantU32(state, 0u); + auto selected = ConstantU32(state, image.indirectRoot); auto mapped = ConstantBool(state, false); for (std::uint32_t iteration = 0; iteration < image.indirectSearchIterations; iteration++) { const auto active = Binary(state, spv::OpULessThan, TypeBool(state), low, high); @@ -1371,7 +1367,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = ConstantU32(state, memory.resource); const auto table = TableSlot(ctx, inst, memory, base); if (table.slot != 0u) { - for (std::uint32_t slot = 0u; slot < base.indirectResources.size(); ++slot) state.runtimeImageMetadata = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), table.slot, ConstantU32(state, slot)), ConstantU32(state, base.indirectResources[slot]), state.runtimeImageMetadata); + state.runtimeImageMetadata = table.slot; } const auto selector = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index d15e981bd..34ec70108 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -52,11 +52,7 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r return pointer; } -// The element of a bindless table's runtime slot, `arrayIndex` (the root's element) + `slotId`. -// The slot is wave-uniform; where that is not uniform over the invocation group (see -// SpirvEmitterState::tableIndexNonUniform) the index carries NonUniform, which needs -// VK_EXT_descriptor_indexing on the device. -std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, std::uint32_t arrayIndex, std::uint32_t slotId) { +std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) { const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage; const auto supported = [&](std::uint32_t capability) { return std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), capability) != state.supportedCapabilities.end(); @@ -66,7 +62,12 @@ std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind ki ExitDescriptorBindingFailure(state, kind, resource, "bindless image table needs image array dynamic indexing, which the device lacks"); } state.module.EmitCapability(dynamic); - const auto index = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, arrayIndex), slotId); + if (state.runtimeImageMetadata == 0u) FailEmit("bindless image has no runtime metadata index"); + const auto offset = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, (offsetof(RuntimeAbi::ShaderData, images) + offsetof(RuntimeAbi::ResourceMetadata, firstElement)) / sizeof(std::uint32_t)), Binary(state, spv::OpIMul, TypeU32(state), state.runtimeImageMetadata, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); + const auto metadata = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), metadata, state.shaderDataStorageVariable, ConstantU32(state, 0u), offset); + const auto index = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), index, metadata); if (!state.tableIndexNonUniform) { return index; } @@ -155,7 +156,7 @@ std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t pointer = DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "sampled image descriptor array was not emitted"); } else { pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, variable, TableImageIndex(state, kind, resource, arrayIndex, slotId)); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, variable, TableImageIndex(state, kind, resource)); DecorateTableNonUniform(state, slotId, pointer); } const auto image = state.module.AllocateId();