diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a670574094..fd47b92142 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2478,12 +2478,14 @@ if(BUILD_TESTING) 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_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) set_tests_properties(agc_shader_table_snapshot_reuse PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_table_snapshot_reuse_unmasked COMMAND agc_shader_memory_tests --table-snapshot-reuse) + set_tests_properties(agc_shader_table_snapshot_reuse_unmasked PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_image_table_vouched_lookups COMMAND agc_driver_image_table_cache_bounds_tests --vouched-lookups) set_tests_properties(agc_driver_image_table_vouched_lookups PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_WRITE_WATCH_IMPORTS=watch" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) add_test(NAME agc_driver_image_table_vouch_queued_draws COMMAND agc_driver_image_table_cache_bounds_tests --queued-draws) set_tests_properties(agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) if(WIN32) - set_tests_properties(agc_shader_table_snapshot_reuse agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_table_snapshot_reuse agc_shader_table_snapshot_reuse_unmasked agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) set_tests_properties(agc_shader_type_zero_image_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4d7e4dc551..91adec15aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2339,7 +2339,10 @@ void verifyTableSnapshotReuse() { const auto otherBuffer = CompileTable(request); std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); const auto resolvedShared = ResourceMaterializer::TableReuse(); - require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == 1u && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const bool resolves = std::getenv("APS5_NO_MASKED_RESULT_MEMO") != nullptr || std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == (resolves ? 1u : 0u) && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const auto* otherBuffers = TableBinding(*otherBuffer.result, DescriptorRole::GuestBuffers); + require(otherBuffers != nullptr && std::search(otherBuffers->guestDescriptor.begin(), otherBuffers->guestDescriptor.end(), otherOutputV.begin(), otherOutputV.end()) != otherBuffers->guestDescriptor.end(), "table snapshot reuse: a result for another buffer does not hold that buffer's descriptor"); const auto storedWord = heap[5u * 8u]; heap[5u * 8u] = heap[6u * 8u]; const auto restoredMap = map; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index fad7606ef9..e7ddf63820 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -353,11 +353,22 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat // A materialized result of one variant over one snapshot (Recompile(request, capture)): the // shared immutable object every later capture that reproduces the snapshot receives, so Populate // and the per-request copy run once per distinct snapshot. +struct MemoPatch { + static constexpr std::uint32_t NoBinding = ~0u; + std::vector>> buffers; + std::vector userWords; + std::uint32_t shaderData = NoBinding; + std::uint32_t flattenedSrt = NoBinding; + bool pushData = false; + bool valid = false; +}; + struct ResultMemoEntry { std::uint64_t variantId = 0; std::uint64_t hash = 0; std::shared_ptr result; std::uint64_t used = 0; + std::shared_ptr patch; }; struct EmissionFailure { @@ -387,6 +398,7 @@ struct SourceEntry : std::enable_shared_from_this { std::vector> variants; std::vector emissionFailures; std::unique_ptr memo; + std::unique_ptr maskedMemo; std::uint64_t memoClock = 0; }; @@ -1008,7 +1020,28 @@ const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const R return merged; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { +void recordMemoPatch(MemoPatch& patch, const DescriptorBindingPlan& plan, const CompiledVariant& variant) { + const auto& layout = variant.bindings.layout; + patch = {}; + for (std::size_t index = 0; index < plan.bindings.size(); ++index) { + const auto& binding = plan.bindings[index]; + const auto position = static_cast(index); + if (binding.descriptor.role == DescriptorRole::GuestBuffers) { + patch.buffers.push_back({position, binding.resources}); + } else if (binding.descriptor.role == DescriptorRole::FlattenedSrt) { + patch.flattenedSrt = position; + } else if (binding.descriptor.role == DescriptorRole::ShaderData) { + patch.shaderData = position; + } + } + patch.pushData = layout.UsesPushData(); + if (patch.pushData || plan.storageData) { + for (const auto reg : layout.userDataRegisters) patch.userWords.push_back(reg - variant.info.userDataBase); + } + patch.valid = layout.userDataRegisters.size() <= layout.DispatchThreadLimitDword() && (patch.userWords.empty() || patch.pushData || patch.shaderData != MemoPatch::NoBinding); +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false, MemoPatch* patch = nullptr) { ResourceSnapshot merged; const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; @@ -1162,6 +1195,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto& tables = *resolved; BindingAllocationResult bindings; DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables); + if (patch != nullptr) recordMemoPatch(*patch, *bindingPlan, variant); result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); if (std::ranges::none_of(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::BdaPagetable || binding.role == DescriptorRole::FaultBuffer; })) result.bdaAbiVersion = 0u; @@ -1357,21 +1391,78 @@ bool ResultMemo() { return resultMemo; } -ResultMemoEntry* memoSet(SourceEntry& source, std::uint64_t index) { - if (source.memo == nullptr) return nullptr; - return source.memo->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; +bool MaskedResultMemo() { + static const bool masked = std::getenv("APS5_NO_MASKED_RESULT_MEMO") == nullptr; + return masked; +} + +bool VerifyMaskedResultMemo() { + static const bool verify = std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + return verify; +} + +ResultMemoEntry* memoSet(const std::unique_ptr& table, std::uint64_t index) { + if (table == nullptr) return nullptr; + return table->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; } struct ResultMemoCounters { - std::atomic hits{0}, misses{0}, evictions{0}, populateNanoseconds{0}; + std::atomic hits{0}, patched{0}, misses{0}, evictions{0}, populateNanoseconds{0}, patchNanoseconds{0}, mismatches{0}; std::atomic lastReport{0}; }; +void applyMemoPatch(RecompileResult& result, const MemoPatch& patch, const ResourceSnapshot& snapshot) { + for (const auto& [binding, resources] : patch.buffers) { + auto& words = result.bindings.at(binding).guestDescriptor; + if (words.size() != resources.size() * 4u) throw std::runtime_error("a patched guest buffer binding changed its width"); + for (std::size_t element = 0; element < resources.size(); ++element) std::copy_n(snapshot.buffers.at(resources[element]).dwords.begin(), 4u, words.begin() + element * 4u); + } + if (!patch.userWords.empty()) { + if (patch.pushData) { + if (patch.userWords.size() * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("patched user data lies outside the push data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) std::memcpy(result.pushConstants.data() + word * sizeof(std::uint32_t), &snapshot.userData.at(patch.userWords[word]), sizeof(std::uint32_t)); + } else { + auto& words = result.bindings.at(patch.shaderData).guestDescriptor; + if (patch.userWords.size() > words.size()) throw std::runtime_error("patched user data lies outside the shader data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) words[word] = snapshot.userData.at(patch.userWords[word]); + } + } + if (patch.flattenedSrt != MemoPatch::NoBinding) result.bindings.at(patch.flattenedSrt).guestDescriptor = snapshot.flattenedSrt; + result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; +} + +const char* patchedResultDifference(const RecompileResult& patched, const RecompileResult& full) { + if (patched.bindings.size() != full.bindings.size()) return "binding count"; + if (patched.pushConstants != full.pushConstants) return "push constants"; + if (patched.specializationId != full.specializationId || patched.spirv.ContentHash() != full.spirv.ContentHash()) return "module"; + if (patched.poisonedSrtReads != full.poisonedSrtReads) return "poisoned SRT reads"; + if (patched.shaderDataUserWords != full.shaderDataUserWords) return "shader data user words"; + if (patched.specialization.size() != full.specialization.size()) return "specialization"; + for (std::size_t index = 0; index < patched.specialization.size(); ++index) { + if (patched.specialization[index].id != full.specialization[index].id || patched.specialization[index].value != full.specialization[index].value) return "specialization"; + } + for (std::size_t index = 0; index < patched.bindings.size(); ++index) { + const auto& left = patched.bindings[index]; + const auto& right = full.bindings[index]; + if (left.role != right.role || left.binding != right.binding || left.count != right.count) return "binding shape"; + if (left.guestDescriptor != right.guestDescriptor) return "binding words"; + } + return nullptr; +} + ResultMemoCounters& resultMemoCounters() { static ResultMemoCounters counters; return counters; } +void verifyPatchedResult(const RecompileResult& patched, const RecompileResult& full, std::uint64_t variantId) { + const auto* difference = patchedResultDifference(patched, full); + if (difference == nullptr) return; + resultMemoCounters().mismatches.fetch_add(1, std::memory_order_relaxed); + static std::atomic printed{0}; + if (const auto index = printed.fetch_add(1, std::memory_order_relaxed); index < 20u) std::fprintf(stderr, "[recompile] masked result memo mismatch %u: variant %016llx differs in its %s\n", index + 1u, static_cast(variantId), difference); +} + void reportResultMemo() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -1384,16 +1475,20 @@ void reportResultMemo() { } if (now - last < 10'000'000'000ll || !counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed)) return; const auto hits = counters.hits.exchange(0, std::memory_order_relaxed); + const auto patched = counters.patched.exchange(0, std::memory_order_relaxed); const auto misses = counters.misses.exchange(0, std::memory_order_relaxed); const auto evictions = counters.evictions.exchange(0, std::memory_order_relaxed); const auto populate = counters.populateNanoseconds.exchange(0, std::memory_order_relaxed); - std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu misses (%.1f%% hits), Populate %.1f us per miss / %.1f ms in total, %llu evictions\n", static_cast(hits), static_cast(misses), hits + misses != 0 ? 100.0 * static_cast(hits) / static_cast(hits + misses) : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, static_cast(evictions)); + const auto patching = counters.patchNanoseconds.exchange(0, std::memory_order_relaxed); + const auto mismatches = counters.mismatches.exchange(0, std::memory_order_relaxed); + const auto total = static_cast(hits + patched + misses); + std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu patched, %llu misses (%.1f%% hits, %.1f%% patched), Populate %.1f us per miss / %.1f ms in total, patch %.1f us each / %.1f ms in total, %llu evictions, %llu patch mismatches\n", static_cast(hits), static_cast(patched), static_cast(misses), total != 0.0 ? 100.0 * static_cast(hits) / total : 0.0, total != 0.0 ? 100.0 * static_cast(patched) / total : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, patched != 0 ? static_cast(patching) / 1000.0 / static_cast(patched) : 0.0, static_cast(patching) / 1e6, static_cast(evictions), static_cast(mismatches)); } // Everything materializeResult reads besides the variant: the snapshot (the descriptor words, the // flattened SRT, the user data, the uniform fill) and, for the vertex family, the V# table the // attributes are resolved from. -std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot) { +std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot, bool masked) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](std::uint64_t value) { hash ^= value; @@ -1411,11 +1506,28 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (std::uint32_t i = 0; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); } }; - mixDescriptors(snapshot.buffers); + if (masked) { + mix(snapshot.buffers.size()); + for (const auto& value : snapshot.buffers) { + mix(value.dwordCount); + mix(value.availableDwordMask); + const bool present = value.dwords[2] != 0u && (value.dwords[0] != 0u || (value.dwords[1] & 0xffffu) != 0u); + mix(present ? 4u | (value.dwords[0] & 3u) : 0u); + mix(value.dwords[1] & 0xffff0000u); + for (std::uint32_t i = 3; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); + } + } else { + mixDescriptors(snapshot.buffers); + } mixDescriptors(snapshot.images); mixDescriptors(snapshot.samplers); - mixWords(snapshot.flattenedSrt); - mixWords(snapshot.userData); + if (masked) { + mix(snapshot.flattenedSrt.size()); + mix(snapshot.userData.size()); + } else { + mixWords(snapshot.flattenedSrt); + mixWords(snapshot.userData); + } mix(static_cast(snapshot.uniformFill.kind)); mix(snapshot.uniformFill.resource); for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); @@ -1474,16 +1586,16 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh // as good). `memoHit` reports the hit. std::shared_ptr materializePreparedMemoized(SourceEntry& source, const std::shared_ptr& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool cacheHit, bool* memoHit) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const auto hash = snapshotHash(request, snapshot); - std::uint64_t index = 0; + const auto variantId = variant->artifact.variantId; + const auto hash = snapshotHash(request, snapshot, false); + const auto index = (variantId * 0x9e3779b97f4a7c15ull) ^ hash; auto& counters = resultMemoCounters(); { std::lock_guard lock(source.mutex); - index = (variant->artifact.variantId * 0x9e3779b97f4a7c15ull) ^ hash; - if (auto* set = memoSet(source, index)) { + if (auto* set = memoSet(source.memo, index)) { for (std::size_t way = 0; way < ResultMemoWays; ++way) { auto& entry = set[way]; - if (entry.result == nullptr || entry.variantId != variant->artifact.variantId || entry.hash != hash) continue; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != hash) continue; entry.used = ++source.memoClock; counters.hits.fetch_add(1, std::memory_order_relaxed); if (memoHit != nullptr) *memoHit = true; @@ -1492,32 +1604,61 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& } } } - const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto result = std::make_shared(materializeResult(*variant, request, snapshot)); - result->cacheHit = cacheHit; - if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - counters.misses.fetch_add(1, std::memory_order_relaxed); - std::shared_ptr shared = std::move(result); - { + const bool masking = MaskedResultMemo(); + const auto maskedHash = masking ? snapshotHash(request, snapshot, true) : 0u; + const auto maskedIndex = (variantId * 0x9e3779b97f4a7c15ull) ^ maskedHash; + std::shared_ptr donor; + std::shared_ptr donorPatch; + if (masking) { std::lock_guard lock(source.mutex); - if (source.memo == nullptr) source.memo = std::make_unique(); - auto* set = memoSet(source, index); - auto* victim = set; - bool present = false; - for (std::size_t way = 0; way < ResultMemoWays && !present; ++way) { - auto& entry = set[way]; - if (entry.result != nullptr && entry.variantId == variant->artifact.variantId && entry.hash == hash) { + if (auto* set = memoSet(source.maskedMemo, maskedIndex)) { + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != maskedHash) continue; entry.used = ++source.memoClock; - shared = entry.result; - present = true; - } else if (entry.used < victim->used) { - victim = &entry; + donor = entry.result; + donorPatch = entry.patch; + break; } } - if (!present) { - if (victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); - *victim = {variant->artifact.variantId, hash, shared, ++source.memoClock}; + } + const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + std::shared_ptr result; + std::shared_ptr patch; + if (donor != nullptr) { + result = std::make_shared(*donor); + applyMemoPatch(*result, *donorPatch, snapshot); + if (VerifyMaskedResultMemo()) verifyPatchedResult(*result, materializeResult(*variant, request, snapshot), variantId); + if (profile) counters.patchNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.patched.fetch_add(1, std::memory_order_relaxed); + } else { + if (masking) patch = std::make_shared(); + result = std::make_shared(materializeResult(*variant, request, snapshot, false, patch.get())); + if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.misses.fetch_add(1, std::memory_order_relaxed); + } + result->cacheHit = cacheHit; + std::shared_ptr shared = std::move(result); + const auto insert = [&](std::unique_ptr& table, std::uint64_t at, std::uint64_t key, std::shared_ptr recipe, bool count) { + if (table == nullptr) table = std::make_unique(); + auto* set = memoSet(table, at); + auto* victim = set; + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result != nullptr && entry.variantId == variantId && entry.hash == key) { + entry.used = ++source.memoClock; + return entry.result; + } + if (entry.used < victim->used) victim = &entry; } + if (count && victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); + *victim = {variantId, key, shared, ++source.memoClock, std::move(recipe)}; + return shared; + }; + { + std::lock_guard lock(source.mutex); + shared = insert(source.memo, index, hash, nullptr, true); + if (patch != nullptr && patch->valid) static_cast(insert(source.maskedMemo, maskedIndex, maskedHash, patch, false)); } reportResultMemo(); return shared; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d47a1af753..1d8ecab03a 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -39,6 +39,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | | `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | | `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | +| `APS5_NO_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes every result memo miss instead of patching the buffer descriptors, user data and flattened SRT of a cached result whose snapshot differs only in those words | | `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | | `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | | `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | @@ -55,6 +56,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | +| `APS5_VERIFY_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | also materializes every patched result memo hit, prints the first twenty whose patched result differs and counts them in the result memo report of `APS5_PROFILE_DRAW` | | `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | | `APS5_VERIFY_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | walks a plan's runtime sources a second time through the evaluator, adopts its answers where the two differ and prints the first twenty differences | | `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope |