fix(shader): reset nested table budgets on snapshot retries

This commit is contained in:
Daniele Giovane committed 2026-10-30 12:44:20 +01:00
1 parent 11366202db
commit 7f36e41c45
4 files changed
+114 -2

No files matched your search

@@ -2803,6 +2803,8 @@ if(BUILD_TESTING)
set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching)
set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_nested_snapshot_retry COMMAND agc_shader_memory_tests --nested-snapshot-retry)
set_tests_properties(agc_shader_nested_snapshot_retry PROPERTIES TIMEOUT 20)
add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse)
add_test(NAME agc_shader_region_merge COMMAND agc_shader_memory_tests --region-merge)
set_tests_properties(agc_shader_region_merge PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -2656,6 +2656,106 @@ void verifyRegionMerge(bool strided = false, bool benchmark = false) {
}
void verifyNestedSnapshotRetry() {
using namespace ShaderRecompiler;
constexpr std::uint32_t Parents = ImageTableAbi::MaxParentKeys / 2u + 1u;
constexpr std::uint32_t Children = ImageTableAbi::MaxNestedKeys / 2u + 1u;
std::vector<std::array<std::uint32_t, 8>> leaves(Children, TableT(reinterpret_cast<const void*>(0x100000000ull), TableFormat8888UNorm, TableType2D, 1u));
const auto child = TableV(leaves.data(), 32u, Children);
std::vector<std::uint64_t> parents(Parents, reinterpret_cast<std::uintptr_t>(child.data()));
const auto root = TableV(parents.data(), 8u, Parents);
std::vector<std::unique_ptr<IrValue>> values;
IrResourcePlan plan;
plan.srtPlanComplete = true;
plan.resourceTrackingComplete = true;
plan.userDataCount = 0u;
plan.materializationSources = {2u};
plan.descriptorSources.resize(3u);
for (std::uint32_t word = 0; word < 4u; ++word) {
auto value = std::make_unique<IrValue>(IrOpcode::Void, IrType::U32, word);
value->SetImmediateU32(root[word]);
plan.descriptorSources[0].dwords[word] = value.get();
values.push_back(std::move(value));
}
plan.descriptorSources[0].dwordCount = 4u;
TableColumn parent;
parent.heapSource = 0u;
parent.stride = 8u;
parent.dwordCount = 4u;
parent.indirect = true;
parent.buffer = true;
plan.descriptorSources[1].tableColumn = parent;
TableColumn nested;
nested.heapSource = 1u;
nested.stride = 32u;
nested.dwordCount = 8u;
plan.descriptorSources[2].tableColumn = nested;
ImageResource image;
image.source = 2u;
image.table = 0u;
image.resourceClass = ImageResourceClass::Sampled;
image.dimension = RdnaImageDimension::Dim2D;
image.read = true;
plan.info.images.push_back(image);
plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image));
struct Memory {
std::array<std::pair<std::uint64_t, std::size_t>, 3> ranges;
std::uint64_t epoch = 0;
bool invalidate = false;
std::uint32_t firstReads = 0;
std::uint32_t epochQueries = 0;
std::vector<std::array<std::size_t, 2>> coverage;
};
const auto capture = [&](bool invalidate) {
Memory memory{{{{reinterpret_cast<std::uintptr_t>(parents.data()), parents.size() * sizeof(parents[0])},
{reinterpret_cast<std::uintptr_t>(child.data()), sizeof(child)},
{reinterpret_cast<std::uintptr_t>(leaves.data()), leaves.size() * sizeof(leaves[0])}}}, 0u, invalidate};
SrtRuntime runtime{};
runtime.userContext = &memory;
runtime.readMemory = +[](void* context, std::uint64_t address, std::uint32_t* value) {
auto& memory = *static_cast<Memory*>(context);
for (const auto& [base, size] : memory.ranges) {
if (address >= base && address - base <= size - sizeof(*value)) {
if (address == memory.ranges[0].first) {
++memory.firstReads;
memory.coverage.push_back({});
}
if (memory.invalidate && address == memory.ranges[2].first + memory.ranges[2].second - sizeof(*value)) {
++memory.epoch;
memory.invalidate = false;
}
if (!memory.coverage.empty()) {
if (base == memory.ranges[0].first) memory.coverage.back()[0] = std::max(memory.coverage.back()[0], static_cast<std::size_t>(address - base + sizeof(*value)));
if (base == memory.ranges[2].first) memory.coverage.back()[1] = std::max(memory.coverage.back()[1], static_cast<std::size_t>(address - base + sizeof(*value)));
}
std::memcpy(value, reinterpret_cast<const void*>(address), sizeof(*value));
return true;
}
}
return false;
};
runtime.descriptorScope = +[](void* context, std::size_t bytes) -> std::uint64_t {
auto& memory = *static_cast<Memory*>(context);
if (bytes == 0u) ++memory.epochQueries;
return memory.epoch;
};
ResourceSnapshot snapshot;
ResourceMaterializer{}.Materialize(plan, runtime, snapshot);
require(memory.epochQueries != 0u, "nested retry: a single nested table did not check the descriptor epoch");
require(memory.firstReads == (invalidate ? 3u : 1u), "nested retry: expected inner retry and outer revalidation");
for (const auto& covered : memory.coverage)
require(covered[0] == memory.ranges[0].second && covered[1] == memory.ranges[2].second,
"nested retry: a discarded attempt consumed the next attempt key budgets");
require(snapshot.tables.tables.at(0).keys == Parents && snapshot.tables.nested.at(0).keys == Children,
"nested retry: parent or child key coverage was truncated");
return snapshot.tables;
};
const auto stable = capture(false);
const auto retried = capture(true);
require(stable.tables == retried.tables && stable.nested == retried.nested,
"nested retry: an invalidation changed the captured keys, pointers, words or codes");
}
void verifyTableSnapshotReuse() {
using namespace ShaderRecompiler;
namespace Abi = ImageTableAbi;
@@ -9651,6 +9751,11 @@ int main(int argc, char** argv) {
std::cout << "Image table keys, maps, records and registration passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--nested-snapshot-retry") {
verifyNestedSnapshotRetry();
std::cout << "Nested snapshot retry preserves parent and child coverage\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--table-snapshot-reuse") {
verifyTableSnapshotReuse();
verifyTableStampRace();
@@ -707,7 +707,10 @@ public:
for (const auto& image : plan.info.images) {
if (image.table != NoTable) {
count = std::max(count, image.table + 1u);
if (image.source < plan.descriptorSources.size() && plan.descriptorSources[image.source].tableColumn.has_value() && plan.descriptorSources[image.source].tableColumn->indirect) hasIndirect = true;
if (image.source < plan.descriptorSources.size() && plan.descriptorSources[image.source].tableColumn.has_value()) {
const auto& column = *plan.descriptorSources[image.source].tableColumn;
hasIndirect |= column.indirect || plan.descriptorSources.at(column.heapSource).tableColumn.has_value();
}
}
}
for (const auto& sampler : plan.info.samplers) {
@@ -726,6 +729,8 @@ public:
const bool checkEpoch = (count > 1u || hasIndirect) && runtime.descriptorScope != nullptr;
bool completed = false;
for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) {
parentKeysRemaining = ImageTableAbi::MaxParentKeys;
nestedKeysRemaining = ImageTableAbi::MaxNestedKeys;
if (attempt != 0u) {
columns.clear();
pages.clear();
+1 -1
View File
@@ -13,7 +13,7 @@
namespace ShaderRecompiler::ShaderDiskCache {
inline constexpr std::uint32_t FormatVersion = 50;
inline constexpr std::uint32_t FormatVersion = 51;
enum class LoadStatus {
Loaded,