From 5c4323035f7e89c5aa40858c85b92223cc9c4f01 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Thu, 29 Oct 2026 03:52:40 +0530 Subject: [PATCH 1/2] perf(agc): leave a capture's branch over a pending GPU write unpruned instead of waiting Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/ShaderMemory.cpp | 12 ++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 94 +++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../include/Optimization/SrtWalker.hpp | 10 ++ .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 7 +- .../Optimization/src/SrtWalker/SrtTape.cpp | 6 +- .../Optimization/src/SrtWalker/SrtWalker.cpp | 5 + docs/dev/ENVIRONMENT.md | 1 + 8 files changed, 133 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index c91decd2d2..f0bdf794fe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -6,6 +6,7 @@ #include "Optimization/RequestMemoryView.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/SrtWalker.hpp" #include "ThreadOwned.hpp" #include #include @@ -35,6 +36,7 @@ struct CaptureProfile { // were read raw on the driver's evidence, or were read both ways (verify) and differed. std::atomic pagesWordwise{0}; std::atomic wordWaits{0}; + std::atomic branchesUnpruned{0}; std::atomic wordWaitNanoseconds{0}; std::atomic wordsRaw{0}; std::atomic wordsVerified{0}; @@ -94,6 +96,11 @@ CaptureProfile& CaptureTotals() { return profile; } +bool BranchesSkipPendingWords() { + static const bool skip = std::getenv("APS5_WAIT_PENDING_BRANCHES") == nullptr; + return skip; +} + std::atomic waitedMsProvider{nullptr}; double WaitedMs() { @@ -372,6 +379,9 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); word = verifiedWord; } + } else if (page.wordwise && policy == PendingWrite::Sync && !forcedRaw && BranchesSkipPendingWords() && ShaderRecompiler::SrtEvaluatingBranch()) { + ++CaptureTotals().branchesUnpruned; + return false; } else { // On a word-wise page the bytes before the hook's wait are kept for the observer. const bool observed = page.wordwise && policy == PendingWrite::Sync && self.observe != nullptr && GuestMemory::Accessible(reinterpret_cast(address), sizeof(word)); @@ -772,7 +782,7 @@ std::shared_ptr ShaderMemory::capture(c const auto materializeNs = ShaderRecompiler::ResourceMaterializer::MaterializeNanoseconds(); const auto evaluateNs = ShaderRecompiler::ResourceMaterializer::EvaluateNanoseconds(); const auto walkNs = captureNs > resolveNs + pageNs + wordNs + specializationNs ? captureNs - resolveNs - pageNs - wordNs - specializationNs : 0; - AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); + AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait, %llu branch conditions left unpruned instead of waiting), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), static_cast(totals.branchesUnpruned.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); } } return capture; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3efcc4e97a..0c8831276b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -865,6 +865,99 @@ void verifyPlanRetention() { require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "plan retention: the plan's walk differs from the program's"); } +std::uint32_t branchReadsRefused = 0; + +bool readFiveUnlessBranch(void*, std::uint64_t, std::uint32_t* value) { + if (ShaderRecompiler::SrtEvaluatingBranch()) { + ++branchReadsRefused; + return false; + } + *value = 5u; + return true; +} + +void verifyPendingBranchLeftUnpruned() { + using namespace ShaderRecompiler; + std::vector> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + constexpr std::uint32_t Chain = 64; + IrValue* address = &userData(4); + for (std::uint32_t step = 0; step < Chain; ++step) address = &binary(IrOpcode::IAdd32, IrType::U32, *address, constant(1)); + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& low = binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, userData(1), load), constant(0)); + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(6)); + choice.AddArgument(&load); + auto& offset = binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask); + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, userData(2), userData(3)), offset); + read.SetFlags(MemoryFlags{0, 0}); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + for (std::uint32_t source = 0; source < 2u; ++source) { + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + source * 4u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + buffer.source = source; + resources.info.buffers.push_back(buffer); + } + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, low, constant(7)), {1, 2}, {0}, {}}); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {1}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + std::array userWords{}; + userWords[1] = 7u; + userWords[2] = 0x1000u; + userWords[6] = 8u; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + SrtRuntime runtime{userWords}; + runtime.readMemory = readFiveUnlessBranch; + runtime.readSpecializationMemory = readFiveUnlessBranch; + branchReadsRefused = 0; + const auto copiedWalk = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + require(branchReadsRefused != 0u && !SrtEvaluatingBranch(), "pending branch: the plan's walk read its branch condition outside a branch scope or left the scope open"); + branchReadsRefused = 0; + const std::array sources{0u, 1u}; + const auto wholeWalk = walkPlan(program.Resources(), sources, {}, runtime); + require(branchReadsRefused != 0u && !SrtEvaluatingBranch(), "pending branch: the program's walk read its branch condition outside a branch scope or left the scope open"); + require(wholeWalk.active == std::vector{1u, 1u}, "pending branch: a condition whose word could not be read pruned a successor"); + require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "pending branch: the plan's walk differs from the program's"); +} + + void verifyPlanRetentionChains() { using namespace ShaderRecompiler; std::vector kept; @@ -9438,6 +9531,7 @@ int main(int argc, char** argv) { verifyPureFlatSlots(); verifyPlanConditionPruning(); verifyPlanRetention(); + verifyPendingBranchLeftUnpruned(); verifyPlanRetentionChains(); verifyPlanCopyAnswers(); verifyGuardedDescriptorSources(); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 031d897b2b..d7989c87f4 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -290,6 +290,7 @@ function(add_shader_recompiler target runtimeTarget) RuntimeAbi::HeapCapacity SampledTextureNumericClass SetDebugProbeActive + SrtEvaluatingBranch ShaderCacheDirectory ShaderDiskCache::Shutdown ShaderStage diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index c44b2faf44..590e604c17 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -112,6 +112,16 @@ enum class RuntimeValueType { Integer }; +bool& SrtEvaluatingBranch(); + +struct SrtBranchScope { + bool saved; + SrtBranchScope() : saved(SrtEvaluatingBranch()) { SrtEvaluatingBranch() = true; } + ~SrtBranchScope() { SrtEvaluatingBranch() = saved; } + SrtBranchScope(const SrtBranchScope&) = delete; + SrtBranchScope& operator=(const SrtBranchScope&) = delete; +}; + class SrtWalker { public: void BuildPlan(IrProgram& program) const; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 74739dfae6..9b7db1eadd 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -104,7 +104,12 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::span Date: Thu, 29 Oct 2026 04:19:33 +0530 Subject: [PATCH 2/2] perf(agc): redo a capture waiting when it fails behind a branch left unpruned Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/ShaderMemory.hpp | 3 + .../Execution/src/ShaderMemory.cpp | 32 +++++-- .../libSceAgcDriver/tests/ShaderMemory.cpp | 85 ++++++++++++++++++- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp index 269104b0bc..b35ff1652e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp @@ -90,6 +90,7 @@ public: private: std::shared_ptr capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation); + std::shared_ptr captureWaitingOnFailure(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation); static constexpr std::size_t PageBytes = 4096; static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t); @@ -169,6 +170,8 @@ private: std::size_t descriptorSpanBytes = 0; std::uint64_t rawInvalidations = 0; std::uint32_t forceSync = 0; + std::uint64_t branchReadsRefused = 0; + bool waitBranches = false; }; // The positions, among a dispatch-cache variant's stored words, of the pure flat-SRT leaves a diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index f0bdf794fe..2dae15cba2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -36,7 +36,8 @@ struct CaptureProfile { // were read raw on the driver's evidence, or were read both ways (verify) and differed. std::atomic pagesWordwise{0}; std::atomic wordWaits{0}; - std::atomic branchesUnpruned{0}; + std::atomic branchReadsRefused{0}; + std::atomic branchCapturesRedone{0}; std::atomic wordWaitNanoseconds{0}; std::atomic wordsRaw{0}; std::atomic wordsVerified{0}; @@ -379,8 +380,9 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); word = verifiedWord; } - } else if (page.wordwise && policy == PendingWrite::Sync && !forcedRaw && BranchesSkipPendingWords() && ShaderRecompiler::SrtEvaluatingBranch()) { - ++CaptureTotals().branchesUnpruned; + } else if (page.wordwise && policy == PendingWrite::Sync && !forcedRaw && !self.waitBranches && BranchesSkipPendingWords() && ShaderRecompiler::SrtEvaluatingBranch()) { + ++self.branchReadsRefused; + if (CaptureProfiled()) ++CaptureTotals().branchReadsRefused; return false; } else { // On a word-wise page the bytes before the hook's wait are kept for the observer. @@ -732,11 +734,29 @@ std::vector> ShaderMemory } std::shared_ptr ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle) { - return capture(request, handle, nullptr); + return captureWaitingOnFailure(request, handle, nullptr); } std::shared_ptr ShaderMemory::Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation) { - return capture(invocation.Request(), nullptr, &invocation); + return captureWaitingOnFailure(invocation.Request(), nullptr, &invocation); +} + +std::shared_ptr ShaderMemory::captureWaitingOnFailure(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) { + branchReadsRefused = 0; + try { + return capture(request, handle, invocation); + } catch (const std::exception&) { + if (branchReadsRefused == 0u) throw; + } + if (CaptureProfiled()) ++CaptureTotals().branchCapturesRedone; + struct WaitBranches { + bool& wait; + explicit WaitBranches(bool& flag) : wait(flag) { wait = true; } + ~WaitBranches() { wait = false; } + WaitBranches(const WaitBranches&) = delete; + WaitBranches& operator=(const WaitBranches&) = delete; + } waiting(waitBranches); + return capture(request, handle, invocation); } std::shared_ptr ShaderMemory::capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) { @@ -782,7 +802,7 @@ std::shared_ptr ShaderMemory::capture(c const auto materializeNs = ShaderRecompiler::ResourceMaterializer::MaterializeNanoseconds(); const auto evaluateNs = ShaderRecompiler::ResourceMaterializer::EvaluateNanoseconds(); const auto walkNs = captureNs > resolveNs + pageNs + wordNs + specializationNs ? captureNs - resolveNs - pageNs - wordNs - specializationNs : 0; - AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait, %llu branch conditions left unpruned instead of waiting), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), static_cast(totals.branchesUnpruned.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); + AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait, %llu branch condition reads over pending GPU writes left the branch unpruned instead of waiting, %llu captures redone waiting after one failed), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), static_cast(totals.branchReadsRefused.load()), static_cast(totals.branchCapturesRedone.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); } } return capture; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0c8831276b..2f7a528d39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -957,7 +957,6 @@ void verifyPendingBranchLeftUnpruned() { require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "pending branch: the plan's walk differs from the program's"); } - void verifyPlanRetentionChains() { using namespace ShaderRecompiler; std::vector kept; @@ -2069,6 +2068,89 @@ void verifyExecGuardedDescriptorSources() { } } +void verifyPendingBranchCapture() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 2u << 28u, 0u, 0u, 0u, 0u}; + const std::array sampler{0u, 0u, 0u, 0u}; + static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + alignas(4096) static std::array srt{}; + alignas(4096) static std::array flags{}; + const auto flagsAddress = static_cast(reinterpret_cast(flags.data())); + const std::array flagsV{static_cast(flagsAddress), static_cast((flagsAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + static std::uint64_t conditionAddress = 0; + static std::uint64_t waits = 0; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + srt.fill(0u); + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + std::copy(flagsV.begin(), flagsV.end(), srt.begin() + 24); + flags[0] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + conditionAddress = flagsAddress; + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + const std::vector code{0xf4080700u, 0xfa000060u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf420040eu, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x2a000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + 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; + + struct FlushReset { + ~FlushReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } flushReset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + if (address < conditionAddress + sizeof(std::uint32_t) && conditionAddress < address + bytes) ++waits; + }); + const auto pending = +[](std::uint64_t address, std::size_t bytes, std::span) -> AgcDriver::ShaderMemory::PendingWrite { + using Pending = AgcDriver::ShaderMemory::PendingWrite; + return address < conditionAddress + sizeof(std::uint32_t) && conditionAddress < address + bytes ? Pending::Sync : Pending::None; + }; + const auto captured = [&](const std::array& image, std::uint32_t mode) { + fill(image, mode); + waits = 0; + AgcDriver::ShaderMemory memory({}, pending); + return memory.Capture(request); + }; + const auto holds = [](const std::shared_ptr& capture, const std::array& image) { + return capture->snapshot.images.size() == 1u && std::equal(image.begin(), image.end(), capture->snapshot.images[0].dwords.begin()); + }; + const std::array null{}; + + const auto unpruned = captured(valid, 0u); + require(waits == 0u, ("pending branch capture: the branch condition over a pending GPU write waited " + std::to_string(waits) + " times").c_str()); + require(holds(unpruned, valid), "pending branch capture: the branch over a pending GPU write was pruned instead of keeping both successors"); + const auto redone = captured(invalid, 0u); + require(waits != 0u, "pending branch capture: a capture that failed behind an unpruned branch was not redone waiting for the condition"); + require(holds(redone, null), "pending branch capture: the capture redone waiting did not prune the branch its condition skips"); + fill(invalid, 1u); + { + AgcDriver::ShaderMemory memory({}, pending); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "pending branch capture: an invalid T# behind a taken branch was accepted after the capture was redone waiting"); + } + std::cout << "pending branch capture passed\n"; +} + void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -9535,6 +9617,7 @@ int main(int argc, char** argv) { verifyPlanRetentionChains(); verifyPlanCopyAnswers(); verifyGuardedDescriptorSources(); + verifyPendingBranchCapture(); verifyLaneMaskGuardedDescriptorSources(); verifyExecGuardedDescriptorSources(); verifySrtTape();