feat(shader): resolve bindless resources through runtime heap metadata

This commit is contained in:
boykopovar 2026-10-05 19:20:10 +03:00
parent 1174069239
commit ab0f9985fa
12 changed files with 202 additions and 44 deletions

View File

@ -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:$<TARGET_FILE_DIR:libc>")
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()

View File

@ -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<Binding> bindings;
std::vector<std::uint32_t> refreshResourceKey;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;
std::size_t index;

View File

@ -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<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
const auto validate = [&](const ShaderRecompiler::RuntimeAbi::ResourceMetadata& metadata, ShaderRecompiler::DescriptorRole role) {
Require(metadata.binding < static_cast<std::uint32_t>(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<const CompiledShader> 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<const CompiledShader> 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<std::uint32_t> 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;

View File

@ -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<void>(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();

View File

@ -242,7 +242,8 @@ void verifyBindlessTable() {
const std::vector<std::uint32_t> wholeCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
const auto srtAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(srt.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(srtAddress), static_cast<std::uint32_t>(srtAddress >> 32u)};
const std::array<std::uint32_t, 1> capabilities{29u};
const std::array<std::uint32_t, 4> capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
const std::array<std::string_view, 2> 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<std::uint32_t>& 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<void>(Recompile(split, *splitCapture)); }, "not uniform over the workgroup", "bindless: a split wave indexed the image array as uniform");
const std::array<std::uint32_t, 3> indexingCapabilities{29u, 5301u, 5307u};
const std::array<std::string_view, 1> indexingExtensions{"SPV_EXT_descriptor_indexing"};
const std::array<std::uint32_t, 6> indexingCapabilities{29u, 5301u, 5307u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
const std::array<std::string_view, 3> 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<void>(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<void>(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<void>(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();

View File

@ -0,0 +1,100 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <iostream>
namespace {
using AgcDriver::Graphics::Require;
using namespace ShaderRecompiler;
struct alignas(4096) GuestData {
std::array<std::array<std::uint32_t, 8>, 4> heap{};
std::array<std::uint32_t, 12> material{};
std::array<std::uint32_t, 16> srt{};
std::array<std::uint32_t, 64> output{};
};
alignas(256) std::array<std::array<std::uint32_t, 64>, 2> textures{};
std::array<GuestData, 12> guest;
constexpr std::array<std::uint32_t, 23> 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<std::uint32_t, 4> Buffer(const void* pointer, std::uint32_t stride, std::uint32_t records) {
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(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<std::uintptr_t>(data.srt.data());
const std::array<std::uint32_t, 3> userData{static_cast<std::uint32_t>(srtAddress), static_cast<std::uint32_t>(srtAddress >> 32u), iteration % 6u == 0u ? 0u : 1u};
const std::array regions{MemoryRegion{reinterpret_cast<std::uintptr_t>(&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<std::uintptr_t>(textures[texture].data());
data.heap[texture] = {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(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<std::uintptr_t>(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;
}
}

View File

@ -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);
};

View File

@ -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<unsigned long long>(rejected[0]), static_cast<unsigned long long>(rejected[1]), static_cast<unsigned long long>(rejected[2]), static_cast<unsigned long long>(rejected[3]), static_cast<unsigned long long>(rejected[4]), static_cast<unsigned long long>(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<DescriptorValue> slots;
std::vector<std::pair<std::uint32_t, std::uint32_t>> 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<std::uint64_t>::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::uint32_t>(std::min<std::uint64_t>((heapSize - table.entryOffset) / TableEntryBytes, std::numeric_limits<std::uint32_t>::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::uint32_t>(std::min<std::uint64_t>(materialSize / table.selectorStride, std::numeric_limits<std::uint32_t>::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<std::uint64_t>(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<DescriptorValue> candidates(keys.size());
std::vector<std::uint8_t> 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<std::size_t>(tableSlots - 1u) * activeTables > ShaderInfo::MaxImages) {
rejectTable(BindlessRejection::ImageSlots, "bindless image tables need " + std::to_string(plan.info.images.size() + static_cast<std::size_t>(tableSlots - 1u) * activeTables) + " image slots, limit " + std::to_string(ShaderInfo::MaxImages));
if (tableCount != 0u && plan.info.images.size() + static_cast<std::size_t>(tableSlots - 1u) * tableCount > ShaderInfo::MaxImages) {
rejectTable(BindlessRejection::ImageSlots, "bindless image tables need " + std::to_string(plan.info.images.size() + static_cast<std::size_t>(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<std::uint32_t>(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<std::uint32_t>(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::uint32_t>(std::clamp<unsigned long>(value, 1ul, 48ul));
}();
return slots;
return RuntimeAbi::SampledHeapCapacity;
}
void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) {

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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();