diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6752641fb..11af875da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "ShaderCacheDirectory.hpp" +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -442,6 +444,96 @@ void verifyAcrossProcesses(const char* self) { require(std::system(command.c_str()) == 0, "the loading process failed"); } +struct ClockRequest { + std::vector code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; + std::array capabilities{1u}; + RecompileRequest request{}; + + explicit ClockRequest(std::uint32_t stride = 0) { + userData[1] = stride << 16u; + ShaderPixelStageInfo pixel{}; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + } +}; + +std::string emissionFailure(const RecompileRequest& request) { + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + require(message.find("reads the shader clock, which needs the device's VK_KHR_shader_clock") != std::string::npos, "unexpected failure: " + message.substr(0, message.find("RecompileRequest:"))); + return message; + } + throw std::runtime_error("a shader clock read compiled without VK_KHR_shader_clock"); +} + +std::uint64_t diskMisses() { + return ShaderDiskCache::Totals().misses; +} + +void verifyEmissionFailureMemo() { + const auto before = ShaderDiskCache::Totals(); + ClockRequest clock; + const auto first = emissionFailure(clock.request); + require(diskMisses() == before.misses + 1, "the first compile was not looked up"); + require(emissionFailure(clock.request) == first, "the second compile failed with another message"); + require(diskMisses() == before.misses + 1, "the second compile looked up and emitted the program again"); + + ClockRequest layout; + layout.request.layout = {0, 0, 16, 112}; + static_cast(emissionFailure(layout.request)); + require(diskMisses() == before.misses + 2, "another binding layout reused the failure"); + ClockRequest stride(16); + static_cast(emissionFailure(stride.request)); + require(diskMisses() == before.misses + 3, "another buffer specialization reused the failure"); + ClockRequest relocated; + relocated.request.shader.codeAddress += 0x1000u; + static_cast(emissionFailure(relocated.request)); + require(diskMisses() == before.misses + 4, "a relocated copy reused the failure"); + require(emissionFailure(clock.request) == first && diskMisses() == before.misses + 4, "the other requests replaced the failure"); + + ClockRequest concurrent; + concurrent.request.layout.pushConstantSizeBytes = 64; + std::array, 4> failures; + for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); }); + std::vector messages; + for (auto& future : failures) messages.push_back(future.get()); + require(std::all_of(messages.begin(), messages.end(), [&](const std::string& message) { return message == messages.front(); }), "concurrent requests failed with different messages"); + require(diskMisses() == before.misses + 5, "concurrent requests emitted the program " + std::to_string(diskMisses() - before.misses - 4) + " times"); + + ShaderDiskCache::Flush(); + require(ShaderDiskCache::Totals().writes == before.writes, "an emission failure was stored in the disk cache"); +} + +int runWithoutFailureMemo() { + ClockRequest clock; + const auto first = emissionFailure(clock.request); + require(emissionFailure(clock.request) == first, "the second compile failed with another message"); + require(diskMisses() == 2, "APS5_NO_FAILURE_MEMO=1 did not emit the program again (" + std::to_string(diskMisses()) + " lookups)"); + std::cout << "APS5_NO_FAILURE_MEMO=1 emitted the failing program twice\n"; + return 0; +} + +void verifyFailureMemoSwitch(const char* self) { + setEnvironment("APS5_NO_FAILURE_MEMO", "1"); + const std::string command = "\"" + std::string(self) + "\" --no-failure-memo"; + require(std::system(command.c_str()) == 0, "the process with APS5_NO_FAILURE_MEMO=1 failed"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -458,6 +550,7 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); @@ -468,6 +561,8 @@ int main(int argc, char** argv) { verifyKeySensitivity(); verifyStore(); verifyAcrossProcesses(argv[0]); + verifyEmissionFailureMemo(); + verifyFailureMemoSwitch(argv[0]); std::error_code error; std::filesystem::remove_all(directory, error); std::cout << "shader disk cache tests passed\n"; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 132df6d77..9f817d143 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -9,6 +9,7 @@ #include "ShaderDiskCache.hpp" #include #include +#include #include #include #include "ControlFlow/include/ControlFlow/GraphBuilder.hpp" @@ -184,6 +185,13 @@ struct ResultMemoEntry { std::shared_ptr result; }; +struct EmissionFailure { + std::uint64_t codeAddress; + BindingLayout layout; + ResourceSpecialization specialization; + std::exception_ptr failure; +}; + struct SourceEntry { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is @@ -197,6 +205,7 @@ struct SourceEntry { std::exception_ptr planFailure; std::unique_ptr program; std::vector> variants; + std::vector emissionFailures; // The result memo, most recently used first, at most ResultMemoEntries (under mutex). std::list memo; std::unordered_map::iterator> memoIndex; @@ -226,6 +235,11 @@ struct SourceKeyHash { } }; +bool FailureMemo() { + static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; + return memo; +} + std::shared_ptr getSource(const RecompileRequest& request) { static std::shared_mutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. @@ -267,14 +281,13 @@ std::shared_ptr getSource(const RecompileRequest& request) { { std::lock_guard lock(source->mutex); if (source->plan == nullptr) { - static const bool memoFailures = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; - if (memoFailures && source->planFailure) std::rethrow_exception(source->planFailure); + if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); try { ResourceProgram resource(request); source->plan = std::move(resource.plan); source->program = std::move(resource.program); } catch (...) { - if (memoFailures) source->planFailure = std::current_exception(); + if (FailureMemo()) source->planFailure = std::current_exception(); throw; } } @@ -291,7 +304,7 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -321,11 +334,17 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra constexpr SpirvEmitter spirvEmitter; RecompileResult result; result.variantId = nextVariantId(); - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); - + try { + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); #if ANYPS5_ENABLE_SPIRV_TOOLS - result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); + result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); #endif + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -384,6 +403,9 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, return candidate; } } + for (const auto& failure : source.emissionFailures) { + if (failure.codeAddress == request.shader.codeAddress && sameLayout(failure.layout, request.layout) && failure.specialization == specialization) std::rethrow_exception(failure.failure); + } cacheHit = false; const bool disk = ShaderDiskCache::Enabled() && !DebugProbeActive(); std::vector diskKey; @@ -401,7 +423,13 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, if (variant == nullptr) { auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request); source.program.reset(); - variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization)); + std::exception_ptr emissionFailure; + try { + variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure)); + } catch (...) { + if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure}); + throw; + } if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); } source.program.reset();