diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c320caf623..0f7a55c8b2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2393,9 +2393,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) - set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced) + add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 9999703dc6..382338e94f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -307,7 +307,7 @@ public: bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. - enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count }; + enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; // Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see // refreshOwnObjects); Count: it was not. enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count }; @@ -592,6 +592,7 @@ private: // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; + std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now(); std::vector> writeOnlyRanges; bool drawBuild = false; std::uint64_t dispatchThreads = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6dae4405cb..f348916998 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2720,7 +2720,7 @@ namespace { // of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated. // Why the fast path left an object to the full walk (the "fast-fail by reason" counts). using FastFail = ShaderResources::FastFail; -constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"}; +constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"}; using OwnRefreshFallback = ShaderResources::OwnRefreshFallback; constexpr std::array(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"}; @@ -2858,6 +2858,19 @@ bool EpochRevalidate() { return enabled; } +std::chrono::steady_clock::duration TemplateAuditInterval() { + static const auto interval = [] { + const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS"); + return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull); + }(); + return interval; +} + +bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { + const auto interval = TemplateAuditInterval(); + return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval; +} + } // Turns the lookups' records into this object's per-texture validation records (see @@ -2936,6 +2949,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanReleased()) return fail(FastFail::Replaced); const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); surface.collected = GuestMemory::CollectWrites(address, bytes); @@ -3062,6 +3076,7 @@ bool ShaderResources::fastRevalidateEach() { if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false; continue; } + if (textures[i]->Released()) return false; const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); surface.collected = GuestMemory::CollectWrites(address, bytes); @@ -3262,6 +3277,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR // they must hand back the very objects the set's views belong to. The build appended them stage // by stage, binding by binding, so the walk repeats that order. const auto fullWalk = [&] { + lastFullWalk = std::chrono::steady_clock::now(); lookupLog().clear(); BuildLookups lookups; std::size_t textureIndex = 0; @@ -3367,6 +3383,11 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback); } + if (fast && TemplateAuditDue(lastFullWalk)) { + fast = false; + ownRefreshed = false; + reason = FastFail::Audit; + } if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast; if (!fast) { countFullWalk(reason); @@ -3374,7 +3395,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto failure = [&] { switch (reason) { case FastFail::Pending: return ProofFailure::Pending; - case FastFail::Evicted: return ProofFailure::Evicted; + case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted; case FastFail::Changed: return ProofFailure::Changed; case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys; default: return ProofFailure::Other; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index be30cdaff8..46e3184ea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -9,6 +9,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" #include #ifdef _WIN32 @@ -16,6 +18,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -275,6 +279,53 @@ private: void* memory = nullptr; }; +class WatchedSurface { +public: + explicit WatchedSurface(const Context& context) : context(context) { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + } +#endif + Require(memory != nullptr, "cannot allocate the watched surface"); + Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, SurfaceBytes); + GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + } + + ~WatchedSurface() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); +#else + munmap(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); +#endif + } + + WatchedSurface(const WatchedSurface&) = delete; + WatchedSurface& operator=(const WatchedSurface&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + const Context& context; + std::uint8_t* memory = nullptr; +}; + std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), @@ -480,6 +531,103 @@ void viewTests(const Context& context) { Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } +ShaderRecompiler::RecompileResult SampledProgram(std::uint64_t address) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(1, false); + binding.imageSamplers.assign(1, 0u); + const auto words = Descriptor(address); + binding.guestDescriptor.assign(words.begin(), words.end()); + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + return program; +} + +void replacedEntryTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable(), "the sampled snapshot's descriptor set is not eligible for resource caching"); + Require(first->SampledTextures().size() == 1, "the sampled descriptor set binds no texture"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the replaced-entry fixture is not a snapshot"); + Require(Sampled(context, surface.Address()) == bound, "the descriptor set does not bind the cache's texture"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "a template over an unchanged, still cached texture did not prove current"); + Require(first->Revalidate(shader, &report), "a template over an unchanged, still cached texture did not prove current twice"); + Require(report.path == ShaderResources::ProofPath::Fast, "the fixture's template did not take the fast proof"); + Require(first->SampledTextures().front() == bound, "the proof changed the bound texture"); + ClearCachedTextures(context.device); + Require(TextureCacheUsage().sampledEntries == 0, "clearing the sampled cache left entries"); + Require(!first->Revalidate(shader, &report), "a template binding a texture whose cache entry left still proved current"); + const auto replacement = Sampled(context, surface.Address()); + Require(replacement != bound, "the lookup after the clear handed back the old texture"); + Require(!first->Revalidate(shader), "a template binding the replaced texture proved current against the new entry"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == replacement, "the rebuilt descriptor set does not bind the cache's new texture"); + Require(rebuilt->Revalidate(shader), "the rebuilt template did not prove current over its cached texture"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a replaced sampled cache entry passed"); +} + +bool lookupChangedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + if (context.hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); + std::puts("skipped, host imports are unavailable"); + return false; + } + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable() && first->SampledTextures().size() == 1, "the lookup-changed template is not reusable"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the lookup-changed fixture is not a snapshot before the import"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "the lookup-changed template did not prove current"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the lookup-changed template did not take the fast proof"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(first->Revalidate(shader, &report), "the audit refused an unchanged template"); + Require(report.path == ShaderResources::ProofPath::Full, "a proof past the audit interval did not take the full walk"); + Require(first->SampledTextures().front() == bound, "the audit changed the bound texture"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); + if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); + std::puts("skipped, host imports are unavailable"); + return false; + } + Require(!bound->Released(), "the snapshot left the cache without a lookup"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(!first->Revalidate(shader, &report), "a template binding a snapshot of a surface its lookup now views through a storage image still proved current past the audit interval"); + const auto view = Sampled(context, surface.Address()); + Require(view != bound && view->ViewsStorageImage(), "the lookup of the imported surface does not view its storage image"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == view, "the rebuilt template does not bind the storage view"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a changed sampled lookup passed"); + return true; +} + void reportedBudgetTests(const Context& context) { if (context.memoryProperties2 == nullptr) { std::puts("VK_EXT_memory_budget unavailable: the sampled texture cache keeps a quarter of the device-local heap"); @@ -872,8 +1020,10 @@ int main(int argc, char** argv) { const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; try { @@ -912,6 +1062,18 @@ int main(int argc, char** argv) { std::puts("resource cache tests passed"); return 0; } + if (mode == "resource-replaced") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + replacedEntryTests(context); + std::puts("resource cache replaced entry tests passed"); + return 0; + } + if (mode == "resource-lookup-changed") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!lookupChangedTests(context)) return VulkanTestSkipped; + std::puts("resource cache lookup-changed tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 600164daa1..f68a5d5bf3 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -377,6 +377,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | | `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` |