From 015bd226782ea86a6cb17205cc4e9a4d4a479b40 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Wed, 14 Oct 2026 19:59:51 -0500 Subject: [PATCH 1/9] perf(agc): an untracked comparison memo holds until memory it covers is forgotten Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 28 ++++++++++++++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 5 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 238868499733..265723efaebd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -199,6 +199,7 @@ void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // value before consulting the page states and reading directly, and the same value after, knows // the states it consulted were current and no page it read changed its mapping meanwhile. std::uint64_t ForgetSerial(); +std::uint64_t ForgetStamp(std::uint64_t address, std::size_t bytes); // Attribution of guest memory accesses (the [hooksync] line in Recorder.cpp and the read-site // counts of the [guestmem] line, APS5_PROFILE_DRAW): a sync the flush hook makes for an access has diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab06105..447c38508028 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -83,6 +83,15 @@ namespace { std::atomic forgetCalls{0}; // See ForgetSerial: odd while a ForgetPages call stores its page states. std::atomic forgetSerial{0}; +constexpr std::size_t ForgetSlotCount = 65536; +constexpr std::uint64_t ForgetBlockBytes = 65536; +std::array, ForgetSlotCount> forgetSlots{}; +std::atomic wideForget{0}; + +void raiseForgetSlot(std::atomic& slot, std::uint64_t serial) { + auto current = slot.load(std::memory_order_relaxed); + while (current < serial && !slot.compare_exchange_weak(current, serial, std::memory_order_release, std::memory_order_relaxed)) {} +} std::atomic forgetBytes{0}; std::atomic collectMemoHits{0}; std::atomic collectEpochBumps{0}; @@ -470,8 +479,15 @@ std::size_t ImagePagesSize() { void ForgetPages(std::uintptr_t address, std::size_t bytes) { forgetCalls.fetch_add(1, std::memory_order_relaxed); forgetBytes.fetch_add(bytes, std::memory_order_relaxed); - forgetSerial.fetch_add(1, std::memory_order_release); + const auto started = forgetSerial.fetch_add(1, std::memory_order_release) + 1; Pages().forget(address, bytes); + const auto done = started + 1; + if (bytes != 0) { + const auto first = address / ForgetBlockBytes; + const auto last = (address + bytes - 1) / ForgetBlockBytes; + if (last - first >= ForgetSlotCount) raiseForgetSlot(wideForget, done); + else for (auto block = first; block <= last; ++block) raiseForgetSlot(forgetSlots[block % ForgetSlotCount], done); + } forgetSerial.fetch_add(1, std::memory_order_release); } @@ -707,6 +723,16 @@ std::uint64_t ForgetSerial() { return forgetSerial.load(std::memory_order_acquire); } +std::uint64_t ForgetStamp(std::uint64_t address, std::size_t bytes) { + if (bytes == 0) return 0; + const auto first = address / ForgetBlockBytes; + const auto last = (address + bytes - 1) / ForgetBlockBytes; + if (last - first >= ForgetSlotCount) return forgetSerial.load(std::memory_order_acquire); + auto newest = wideForget.load(std::memory_order_acquire); + for (auto block = first; block <= last; ++block) newest = std::max(newest, forgetSlots[block % ForgetSlotCount].load(std::memory_order_acquire)); + return newest; +} + Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable) { require(bytes <= std::numeric_limits::max() - address, "address range overflow"); const TimedAccess timed(CounterVerify, bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e71366c10b81..b587b0d88763 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2124,8 +2124,9 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto epoch = GuestMemory::CollectEpoch(); const bool whole = address == descriptor.baseAddress && bytes == guestBytes; const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; - const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; + const bool forgetting = (GuestMemory::ForgetSerial() & 1u) != 0; + const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const bool cacheable = memoize && epoch != 0 && !forgetting && whole; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; From a049317f3e4d538805b9e81a90d5552d791ab3bf Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Wed, 14 Oct 2026 19:59:51 -0500 Subject: [PATCH 2/9] test(agc): forgetting other memory keeps an untracked image's comparison memo Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 89 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5c8..3442e688b79d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1132,6 +1132,8 @@ if(BUILD_TESTING) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_untracked_forget COMMAND agc_driver_recorder_tests --untracked-forget-only) + set_tests_properties(agc_untracked_forget PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) @@ -1154,7 +1156,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd19544..73e36667d2e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3374,6 +3374,91 @@ void unitShadowTests(const Device& device, Recorder& recorder) { Require(words[127 * unit] == 0x66 && words[128 * unit] == 0x77, "the retire did not publish the second slab's units covered by the old registration"); } +void untrackedForgetTests(const Device& device) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: untracked comparison memo not tested\n"; + return; + } + constexpr std::uint32_t side = 256; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::size_t otherBytes = 65536; + const auto allocate = [](std::size_t bytes) -> void* { +#ifdef _WIN32 + return VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + return std::aligned_alloc(65536, bytes); +#endif + }; + void* block = allocate(surfaceBytes); + void* other = allocate(otherBytes); + Require(block != nullptr && other != nullptr, "cannot allocate the untracked comparison blocks"); + std::memset(block, 0x55, surfaceBytes); + std::memset(other, 0x11, otherBytes); + const auto address = reinterpret_cast(block); + const auto otherAddress = reinterpret_cast(other); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, surfaceBytes, true, true, true); + mutation.Add(other, otherBytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + void* other; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + mutation.Remove(other); + } + HostImportFor(context, reinterpret_cast(block), surfaceBytes); + } + } unregister{base, block, other}; + if (HostImportFor(base, address, surfaceBytes) == nullptr) { + std::cout << "host import of the surface refused: untracked comparison memo not tested\n"; + return; + } + if (AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the surface is write-watched: untracked comparison memo not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the test surface has an unexpected size"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + AgcDriver::GuestMemory::BumpCollectEpoch(); + image->Refresh(); + const auto compares = StorageTexture::UntrackedCompares(); + image->Refresh(); + Require(StorageTexture::UntrackedCompares() == compares, "a second refresh in the same collect epoch compared the surface again"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(otherAddress, otherBytes); + image->Refresh(); + Require(StorageTexture::UntrackedCompares() == compares, "forgetting memory the surface does not cover made its refresh compare again"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(address, 65536); + image->Refresh(); + Require(StorageTexture::UntrackedCompares() == compares + 1, "forgetting the surface's own memory left its comparison memo standing"); + } +#ifdef _WIN32 + VirtualFree(other, 0, MEM_RELEASE); +#endif + std::cout << "untracked comparison memo: ok\n"; +} + void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -8208,6 +8293,10 @@ int main(int argc, char** argv) { std::cout << "Address space pending write tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--untracked-forget-only") { + untrackedForgetTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); unwrittenSnapshotTests(device); From d7b2ad3c57d35d434b63e92f1a069e7f6d1e0684 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Wed, 14 Oct 2026 20:26:44 -0500 Subject: [PATCH 3/9] perf(agc): fast revalidation over untracked memory proves a surface by its stamp until the next ordering point APS5_VERIFY_UNTRACKED_PROOFS=1 runs the full walk beside every such proof and counts disagreements. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/ShaderResources.hpp | 2 + .../Graphics/src/ShaderResources.cpp | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f18..5c7edc453d8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -445,6 +445,7 @@ class ShaderResources { const StorageTexture* source = nullptr; bool depth = false; bool valid = false; + std::array untracked{}; }; void captureValidation(); // A surface of this object the fast proof found a foreign image pending over: the element @@ -550,6 +551,7 @@ class ShaderResources { bool reusable = false; std::vector directRegions; std::vector validatedTextures; + std::vector> storageUntracked; // The pending registry's serial at the last Revalidate that proved this object, taken before // its checks and kept only when unchanged after them (0: none): while it is still the serial, // no image other than the object's own sources has results pending over its surfaces and diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 15a0f3c2d2e2..d0e9d818aaa4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2448,6 +2448,17 @@ bool EpochRevalidate() { // Turns the lookups' records into this object's per-texture validation records (see // ValidatedSurface); the last record of an object is the freshest. +namespace { + +std::array untrackedRangeStamp(std::uint64_t address, std::size_t bytes) { + const auto epoch = GuestMemory::CollectEpoch(); + if (epoch == 0 || bytes == 0 || (GuestMemory::ForgetSerial() & 1u) != 0) return {}; + const auto* recorder = Recorder::Active(); + return {epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; +} + +} + void ShaderResources::captureValidation() { const auto record = [](const void* object) -> const LookupRecord* { const auto& log = lookupLog(); @@ -2459,6 +2470,16 @@ void ShaderResources::captureValidation() { validatedTextures.assign(textures.size(), {}); for (std::size_t i = 0; i < textures.size(); ++i) { if (const auto* found = record(textures[i].get())) validatedTextures[i] = {found->resource, found->bytes, found->keys, found->generation, 0, found->source, found->depth, true}; + auto& surface = validatedTextures[i]; + if (surface.valid && !surface.depth && GuestMemory::CollectWrites(surface.resource.baseAddress, static_cast(surface.bytes)) == 0) surface.untracked = untrackedRangeStamp(surface.resource.baseAddress, static_cast(surface.bytes)); + } + storageUntracked.assign(storageTextures.size(), {}); + for (std::size_t i = 0; i < storageTextures.size(); ++i) { + const auto* image = storageTextures[i].get(); + if (image == nullptr) continue; + const auto address = image->Descriptor().baseAddress; + const auto bytes = static_cast(image->GuestBytes()); + if (GuestMemory::CollectWrites(address, bytes) == 0) storageUntracked[i] = untrackedRangeStamp(address, bytes); } lookupLog().clear(); } @@ -2521,7 +2542,11 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span(surface.bytes); surface.collected = GuestMemory::CollectWrites(address, bytes); - if (surface.collected == 0) return fail(FastFail::Collect); + bool untracked = false; + if (surface.collected == 0) { + if (surface.untracked[0] == 0 || untrackedRangeStamp(address, bytes) != surface.untracked) return fail(FastFail::Collect); + untracked = true; + } const auto* source = surface.source; if (!keyProofs) { // Without proofs the keys are compared with the record's (a cleared view's identity @@ -2559,9 +2584,9 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, source->Generation()}); + if (!untracked) queries.push_back({address, bytes, source->Generation()}); images.push_back(source); - } else { + } else if (!untracked) { queries.push_back({address, bytes, surface.generation}); } } @@ -2573,7 +2598,11 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanDescriptor(); const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); - if (GuestMemory::CollectWrites(address, bytes) == 0) return fail(FastFail::Collect); + bool untracked = false; + if (GuestMemory::CollectWrites(address, bytes) == 0) { + if (i >= storageUntracked.size() || storageUntracked[i][0] == 0 || untrackedRangeStamp(address, bytes) != storageUntracked[i]) return fail(FastFail::Collect); + untracked = true; + } if (own.dccAddress != 0) { // Refresh's unchanged branch (its key compare against the keys the content was // uploaded under, through the image's proof; the memory query below), minus its @@ -2583,7 +2612,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, image->Generation()}); + if (!untracked) queries.push_back({address, bytes, image->Generation()}); images.push_back(image); } if (!pending.empty()) { @@ -2913,6 +2942,19 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR FastFail reason = FastFail::Count; bool accepted = false; bool fast = !noFast && fastRevalidate(serialBefore, refreshed, reason, overlapping, accepted); + { + static const bool verifyUntracked = std::getenv("APS5_VERIFY_UNTRACKED_PROOFS") != nullptr; + const bool anyUntracked = std::any_of(validatedTextures.begin(), validatedTextures.end(), [](const ValidatedSurface& surface) { return surface.untracked[0] != 0; }) || std::any_of(storageUntracked.begin(), storageUntracked.end(), [](const auto& stamp) { return stamp[0] != 0; }); + if (verifyUntracked && fast && anyUntracked) { + static std::atomic checked{0}, disagreed{0}; + const auto saved = validatedTextures; + const auto savedStorage = storageUntracked; + if (!fullWalk()) disagreed.fetch_add(1, std::memory_order_relaxed); + validatedTextures = saved; + storageUntracked = savedStorage; + if (checked.fetch_add(1, std::memory_order_relaxed) % 100000 == 99999) std::fprintf(stderr, "[verify-untracked] %llu fast proofs over untracked memory checked by the full walk, %llu disagreed%c", static_cast(checked.load()), static_cast(disagreed.load()), 10); + } + } // T1 (design_cpu_final M3, rule RT1): a Pending failure whose overlapping images are foreign // to the surfaces is resolved by the own objects' refresh (what the walk's lookups would do to // them) and the fast proof run again, which is then authoritative (it accepts what stays From 2d030407eb8f3ac7fb8078e1fd0d48d95acf450f Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Wed, 14 Oct 2026 20:26:44 -0500 Subject: [PATCH 4/9] test(agc): a sampled template over untracked memory proves fast within a collect epoch Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3442e688b79d..e58341d9be92 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1134,6 +1134,8 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_untracked_forget COMMAND agc_driver_recorder_tests --untracked-forget-only) set_tests_properties(agc_untracked_forget PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_untracked_revalidate COMMAND agc_driver_recorder_tests --untracked-revalidate-only) + set_tests_properties(agc_untracked_revalidate PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) @@ -1156,7 +1158,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_untracked_revalidate agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 73e36667d2e2..18a31ce905ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3459,6 +3459,82 @@ void untrackedForgetTests(const Device& device) { std::cout << "untracked comparison memo: ok\n"; } +void untrackedRevalidateTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + context.nullDescriptors = device.NullDescriptors(); + if (context.hostImportAlignment == 0 || !context.nullDescriptors) { + std::cout << "host imports or null descriptors unavailable: fast revalidation over untracked memory not tested\n"; + return; + } + constexpr std::size_t bytes = 2 * 65536; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the untracked revalidation block"); + std::memset(block, 0x40, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the block refused: fast revalidation over untracked memory not tested\n"; + return; + } + if (AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "the block is write-watched: fast revalidation over untracked memory not tested\n"; + return; + } + TextureDetiler detiler(context); + auto imageContext = context; + imageContext.detiler = &detiler; + TextureCache textureCache(imageContext); + imageContext.textureCache = &textureCache; + struct Clear { + const Context& context; + Recorder& recorder; + ~Clear() { recorder.Sync(); ClearCachedTextures(context.device); } + } clear{context, recorder}; + ShaderRecompiler::RecompileResult imageProgram; + ShaderRecompiler::DescriptorBinding imageBinding; + imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages; + imageBinding.descriptorSet = 0; + imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + imageBinding.count = 1; + imageBinding.imageSamplers = {0}; + imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + imageBinding.guestDescriptor = {static_cast(address >> 8), static_cast(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0}; + imageProgram.bindings.push_back(imageBinding); + const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0}; + AgcDriver::GuestMemory::BumpCollectEpoch(); + auto sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + ShaderResources::ProofReport report; + Require(sampled->Revalidate(std::span(&imageCompute, 1), &report), "a sampled template over untracked memory did not revalidate"); + Require(report.path == ShaderResources::ProofPath::Fast, "a sampled template over untracked memory took the full walk in the same collect epoch"); + Require(sampled->Revalidate(std::span(&imageCompute, 1), &report) && report.path == ShaderResources::ProofPath::Fast, "a second proof in the same collect epoch was not fast"); + std::memset(block, 0x41, 4096); + AgcDriver::GuestMemory::BumpCollectEpoch(); + static_cast(sampled->Revalidate(std::span(&imageCompute, 1), &report)); + Require(report.path != ShaderResources::ProofPath::Fast, "a sampled template over untracked memory kept its fast proof across a store and a new collect epoch"); + sampled.reset(); + std::cout << "fast revalidation over untracked memory: ok\n"; +} + void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -8293,6 +8369,10 @@ int main(int argc, char** argv) { std::cout << "Address space pending write tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--untracked-revalidate-only") { + untrackedRevalidateTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--untracked-forget-only") { untrackedForgetTests(device); return 0; From d89ed0749a0347ef954118701944a2d353452673 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:48:42 -0700 Subject: [PATCH 5/9] docs(agc): list APS5_VERIFY_UNTRACKED_PROOFS in the environment reference --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 05678c738d62..f0158adda814 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -479,6 +479,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_VERIFY_REGISTER_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | checks the incremental register fingerprint against the full state | | `APS5_VERIFY_SET_LAYOUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | checks the draw's set layouts against those described from the shaders | | `APS5_VERIFY_SHAPE_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | looks up the resident targets and pipeline of every shape memo hit anew as well, throws when they differ and prints `[shape-memo]` counts of the checked hits every 10 s | +| `APS5_VERIFY_UNTRACKED_PROOFS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs the full walk after every fast proof that accepted a surface over untracked memory by its stamp, and prints a `[verify-untracked]` line with the counts of checked and disagreeing proofs every 100,000 checks | | `APS5_WAIT_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | always takes the GPU mutex on a wait (no unlocked label lookup) | | `APS5_WAIT_LOCK_ENTRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | takes the GPU mutex blocking on the entry instead of trying it | | `APS5_WATCH_MEMORY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | watches named guest ranges (`:[,...]`) across dispatches; needs `APS5_SYNC_DISPATCH=1` | From e440b742c3d930015200aa89dae2b4dd960c7f38 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:48:44 -0700 Subject: [PATCH 6/9] test(agc): finish recorded work before the untracked forget test frees its detiler The test's last refresh records a detile dispatch that is still pending when the function returns. The detiler, declared after the Unregister guard, was destroyed first, freeing the pipeline and descriptor pools that dispatch uses; Unregister then removed the allocations, which syncs the recorder, and lavapipe executed the dispatch with the freed pipeline and crashed on its queue thread. Sync the recorder before the detiler goes away, as the neighbouring untracked tests already do. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 53d0de0b2695..886c7819e376 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5576,7 +5576,7 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) { std::cout << "idle publish wait verified\n"; } -void untrackedForgetTests(const Device& device) { +void untrackedForgetTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { std::cout << "host imports unavailable: untracked comparison memo not tested\n"; @@ -5626,6 +5626,10 @@ void untrackedForgetTests(const Device& device) { return; } TextureDetiler detiler(base); + struct Finish { + Recorder& recorder; + ~Finish() { recorder.Sync(); } + } finish{recorder}; auto context = base; context.detiler = &detiler; GuestTextureResource resource{}; @@ -12420,7 +12424,7 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--untracked-forget-only") { - untrackedForgetTests(device); + untrackedForgetTests(device, recorder); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { From fdc70046f8fca66d37bcb9ce0410db88f283b816 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:48:45 -0700 Subject: [PATCH 7/9] perf(agc): keep an untracked fast proof across collect epochs while its write-watch hint has not moved --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/include/ShaderResources.hpp | 4 +- .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 37 ++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 7 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 161 ++++++++++++++++++ 6 files changed, 201 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b931d6827371..eb3729061a2e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1869,6 +1869,8 @@ if(BUILD_TESTING) set_tests_properties(agc_untracked_forget PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_untracked_revalidate COMMAND agc_driver_recorder_tests --untracked-revalidate-only) set_tests_properties(agc_untracked_revalidate PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_untracked_hint_proof COMMAND agc_driver_recorder_tests --untracked-hint-proof-only) + set_tests_properties(agc_untracked_hint_proof PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) @@ -1895,7 +1897,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_untracked_revalidate agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_untracked_revalidate agc_untracked_hint_proof agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 9f98e2363114..cfd11e554c33 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -522,7 +522,7 @@ class ShaderResources { const StorageTexture* source = nullptr; bool depth = false; bool valid = false; - std::array untracked{}; + std::array untracked{}; }; void captureValidation(); // A surface of this object the fast proof found a foreign image pending over: the element @@ -635,7 +635,7 @@ class ShaderResources { bool reusable = false; std::vector directRegions; std::vector validatedTextures; - std::vector> storageUntracked; + std::vector> storageUntracked; // The pending registry's serial at the last Revalidate that proved this object, taken before // its checks and kept only when unchanged after them (0: none): while it is still the serial, // no image other than the object's own sources has results pending over its surfaces and diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 952b359a3218..4b8e9e8fda8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -387,6 +387,7 @@ class StorageTexture : public std::enable_shared_from_this { static std::uint64_t HandedOverSuperseded(); static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); + std::array HintedComparison(std::uint64_t address, std::size_t bytes) const; std::uint64_t GuestBytes() const; std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4a2ad44ad0b2..6d7cf9d2b3b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -451,6 +451,7 @@ struct LookupRecord { std::uint64_t generation; const StorageTexture* source; bool depth = false; + std::array untracked{}; }; thread_local std::vector* lookupLogSlot = nullptr; @@ -913,7 +914,10 @@ std::shared_ptr cachedTexture(const Context& context, std::span{}; + const bool keyed = source == nullptr && generation == 0 && !committed.has_value() && *keys == DccKeys::Uncompressed; + const auto forgetStamp = keyed ? GuestMemory::ForgetStamp(address, bytes) : 0; + const auto untracked = keyed ? untrackedKey(address, bytes) : std::array{}; + const auto proofKey = untracked[0] != 0 ? std::array{untracked[0], untracked[1], forgetStamp, untracked[3]} : std::array{}; auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); @@ -960,7 +964,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spancommitted = std::move(committed); it->untracked = untracked; touchTexture(cache, it); - logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); + logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr, false, proofKey}); reportTextureCounters(); if (profile) LookupOutcomes::Add(LookupOutcomes::SampledHitSnapshot, start); return it->texture; @@ -1018,11 +1022,12 @@ std::shared_ptr cachedTexture(const Context& context, std::span SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } + const auto madeKey = entry.untracked[0] != 0 ? proofKey : std::array{}; const auto budget = sampledBudget(context, cache); auto texture = entry.texture; if (!admitTexture(cache, key, entry.accounted, budget)) { noteReleased(texture); - logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()}); + logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get(), false, madeKey}); reportCacheCounts(cache, budget); reportTextureCounters(); if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start); @@ -1031,7 +1036,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span logged, std::span untrackedRangeStamp(std::uint64_t address, std::size_t bytes) { +std::array hintedProof(std::uint64_t address, std::size_t bytes) { + const auto hint = GuestMemory::UntrackedHint(address, bytes); + if (hint == 0) return {}; + const auto* recorder = Recorder::Active(); + return {hint, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; +} + +std::array untrackedRangeStamp(std::uint64_t address, std::size_t bytes, const std::array& hinted) { const auto epoch = GuestMemory::CollectEpoch(); if (epoch == 0 || bytes == 0 || (GuestMemory::ForgetSerial() & 1u) != 0) return {}; + if (hinted[0] != 0) return {hinted[0], hinted[1], hinted[2], hinted[3], 1}; const auto* recorder = Recorder::Active(); - return {epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + return {epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0, 0}; +} + +std::array currentUntrackedStamp(std::uint64_t address, std::size_t bytes, const std::array& recorded) { + return untrackedRangeStamp(address, bytes, recorded[4] != 0 ? hintedProof(address, bytes) : std::array{}); } } @@ -3074,7 +3091,7 @@ void ShaderResources::captureValidation() { const auto& found = log[indices[i]]; validatedTextures[i] = {found.resource, found.bytes, found.keys, found.generation, 0, found.source, found.depth, true}; auto& surface = validatedTextures[i]; - if (surface.valid && !surface.depth && GuestMemory::CollectWrites(surface.resource.baseAddress, static_cast(surface.bytes)) == 0) surface.untracked = untrackedRangeStamp(surface.resource.baseAddress, static_cast(surface.bytes)); + if (surface.valid && !surface.depth && GuestMemory::CollectWrites(surface.resource.baseAddress, static_cast(surface.bytes)) == 0) surface.untracked = untrackedRangeStamp(surface.resource.baseAddress, static_cast(surface.bytes), surface.source != nullptr ? surface.source->HintedComparison(surface.resource.baseAddress, static_cast(surface.bytes)) : found.untracked); } storageUntracked.assign(storageTextures.size(), {}); for (std::size_t i = 0; i < storageTextures.size(); ++i) { @@ -3082,7 +3099,7 @@ void ShaderResources::captureValidation() { if (image == nullptr) continue; const auto address = image->Descriptor().baseAddress; const auto bytes = static_cast(image->GuestBytes()); - if (GuestMemory::CollectWrites(address, bytes) == 0) storageUntracked[i] = untrackedRangeStamp(address, bytes); + if (GuestMemory::CollectWrites(address, bytes) == 0) storageUntracked[i] = untrackedRangeStamp(address, bytes, image->HintedComparison(address, bytes)); } lookupLog().clear(); const auto trim = [](auto& scratch) { @@ -3161,7 +3178,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span= storageUntracked.size() || storageUntracked[i][0] == 0 || untrackedRangeStamp(address, bytes) != storageUntracked[i]) return fail(FastFail::Collect); + if (i >= storageUntracked.size() || storageUntracked[i][0] == 0 || currentUntrackedStamp(address, bytes, storageUntracked[i]) != storageUntracked[i]) return fail(FastFail::Collect); untracked = true; } if (own.dccAddress != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 58bde0cd82c5..39d7ac929cb4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2532,6 +2532,13 @@ std::uint64_t StorageTexture::UntrackedCompares() { return untrackedCompares.load(std::memory_order_relaxed); } +std::array StorageTexture::HintedComparison(std::uint64_t address, std::size_t bytes) const { + for (const auto& entry : comparedRanges) { + if (entry.stamp[0] != 0 && entry.address <= address && address - entry.address <= entry.bytes && bytes <= entry.bytes - (address - entry.address)) return {entry.stamp[1], entry.stamp[2], entry.stamp[3], entry.stamp[4]}; + } + return {}; +} + bool StorageTexture::ServesKeysAt(std::uint64_t dccAddress) const { const bool locked = GuestMemory::GpuMutex().HeldByThisThread(); const auto readFollowed = [&] { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 886c7819e376..3a749923f71b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5741,6 +5741,163 @@ void untrackedRevalidateTests(const Device& device, Recorder& recorder) { std::cout << "fast revalidation over untracked memory: ok\n"; } +struct WalkStore { + std::uint64_t trigger = 0; + std::size_t triggerBytes = 0; + std::uint8_t* target = nullptr; + std::size_t storeBytes = 0; + std::size_t hintBytes = 0; + bool done = true; +}; +WalkStore walkStore; + +void storeDuringWalk(std::uint64_t address, std::size_t bytes) { + if (walkStore.done || address >= walkStore.trigger + walkStore.triggerBytes || walkStore.trigger >= address + bytes) return; + walkStore.done = true; + std::memset(walkStore.target, 0x45, walkStore.storeBytes); + static_cast(AgcDriver::GuestMemory::UntrackedHint(reinterpret_cast(walkStore.target), walkStore.hintBytes)); +} + +void untrackedHintProofTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + context.nullDescriptors = device.NullDescriptors(); + if (context.hostImportAlignment == 0 || !context.nullDescriptors) { + std::cout << "host imports or null descriptors unavailable: untracked proofs across collect epochs not tested\n"; + return; + } + constexpr std::size_t surface = 65536; + constexpr std::size_t bytes = 2 * surface; + auto* block = static_cast(AllocateWatched(bytes, surface)); + if (block == nullptr) { + std::cout << "guest memory is not write-watched: untracked proofs across collect epochs not tested\n"; + return; + } + std::memset(block, 0x40, bytes); + const auto first = reinterpret_cast(block); + const auto second = first + surface; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, first, bytes) == nullptr) { + std::cout << "host import of the block refused: untracked proofs across collect epochs not tested\n"; + return; + } + AgcDriver::GuestMemory::Unwatch(first, bytes); + if (AgcDriver::GuestMemory::Watched(first, bytes)) { + std::cout << "the block stayed write-watched: untracked proofs across collect epochs not tested\n"; + return; + } + const bool hinted = AgcDriver::GuestMemory::UntrackedHint(first, bytes) != 0; + std::cout << (hinted ? "the block has a write-watch hint: its proofs are expected to hold across collect epochs\n" : "the block has no write-watch hint: its proofs are expected to follow the collect epoch\n"); + TextureDetiler detiler(context); + auto imageContext = context; + imageContext.detiler = &detiler; + TextureCache textureCache(imageContext); + imageContext.textureCache = &textureCache; + struct Clear { + const Context& context; + Recorder& recorder; + ~Clear() { recorder.Sync(); ClearCachedTextures(context.device); } + } clear{context, recorder}; + const auto words = [](std::uint64_t address) { + return std::array{static_cast(address >> 8), static_cast(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0}; + }; + const auto firstWords = words(first); + const auto secondWords = words(second); + const auto imageBytes = DescribeSurface(DecodeTextureResource(firstWords)).guestBytes; + Require(imageBytes == surface, "the test surface does not fill its 64 KiB block"); + ShaderRecompiler::RecompileResult imageProgram; + ShaderRecompiler::DescriptorBinding imageBinding; + imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages; + imageBinding.descriptorSet = 0; + imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + imageBinding.count = 2; + imageBinding.imageSamplers = {0, 0}; + imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + imageBinding.guestDescriptor.assign(firstWords.begin(), firstWords.end()); + imageBinding.guestDescriptor.insert(imageBinding.guestDescriptor.end(), secondWords.begin(), secondWords.end()); + imageProgram.bindings.push_back(imageBinding); + const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0}; + AgcDriver::GuestMemory::BumpCollectEpoch(); + auto sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + const auto fast = [&] { + ShaderResources::ProofReport report; + Require(sampled->Revalidate(std::span(&imageCompute, 1), &report), "a sampled template over untracked memory did not revalidate"); + return report.path == ShaderResources::ProofPath::Fast; + }; + const auto version = [&](std::uint64_t address) { + const auto image = StorageTexture::FindLive(address, imageBytes); + Require(image != nullptr, "the view's storage image is not cached"); + return image->Version(); + }; + const auto settle = [&](const std::string& after) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + static_cast(fast()); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(fast() == hinted, after + (hinted ? ": a hinted proof walked across an unwritten collect epoch" : ": a proof without a hint stayed fast across a collect epoch")); + }; + Require(fast(), "a sampled template over untracked memory took the full walk in the same collect epoch"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + static_cast(fast()); + Require(fast(), "a sampled template over untracked memory took the full walk twice in one collect epoch"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(fast() == hinted, hinted ? "a proof over a hinted untracked range walked after a collect epoch with no write" : "a proof over untracked memory without a write-watch hint stayed fast across a collect epoch"); + + auto before = version(first); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::memset(block, 0x41, 4096); + Require(!fast(), "a CPU store after a collect epoch kept the fast proof"); + Require(version(first) != before, "the walk after a CPU store kept the old content"); + settle("after a CPU store"); + + before = version(first); + std::memset(block, 0x42, 4096); + AgcDriver::GuestMemory::MarkWritten(first, 4096); + Require(!fast(), "a driver store kept the fast proof"); + Require(version(first) != before, "the walk after a driver store kept the old content"); + settle("after a driver store"); + + before = version(first); + std::memset(block, 0x43, 4096); + recorder.NotePendingWrite(first, 4096); + Require(!fast(), "a recorded GPU write over the range kept the fast proof"); + recorder.Sync(); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!fast(), "a completed GPU write over the range kept the fast proof after its completion"); + Require(version(first) != before, "the walk after a completed GPU write kept the old content"); + settle("after a recorded GPU write"); + + before = version(first); + walkStore = {second, surface, block, 4096, bytes, false}; + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::MarkWritten(second, 4096); + AgcDriver::GuestMemory::SetFlushHook(&storeDuringWalk); + const bool walked = !fast(); + recorder.Activate(); + Require(walkStore.done, "the walk did not compare the second surface"); + Require(walked, "a driver store over the second surface kept the fast proof"); + Require(version(first) == before, "the first surface was read again after the store made while the second was compared"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!fast(), "a CPU store made after the first surface was compared and before its stamp was recorded kept the fast proof"); + Require(version(first) != before, "the walk after a store made during the previous walk kept the old content"); + sampled.reset(); + std::cout << "untracked proofs across collect epochs: ok\n"; +} + void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -12423,6 +12580,10 @@ int main(int argc, char** argv) { untrackedRevalidateTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint-proof-only") { + untrackedHintProofTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--untracked-forget-only") { untrackedForgetTests(device, recorder); return 0; From 2b74a6c5d5e71ca3c948b55e2574c8a5d99f27ae Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:18:41 -0700 Subject: [PATCH 8/9] test(agc): store into untracked hint ranges from other threads around the proof's compare and check --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 57 ++++++++++++++++++- .../libSceAgcDriver/tests/WriteTracking.cpp | 51 +++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2d6172ba6e58..da7cfcc28c40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5760,14 +5760,23 @@ struct WalkStore { std::size_t storeBytes = 0; std::size_t hintBytes = 0; bool done = true; + std::uint8_t value = 0x45; + bool threaded = false; }; WalkStore walkStore; void storeDuringWalk(std::uint64_t address, std::size_t bytes) { if (walkStore.done || address >= walkStore.trigger + walkStore.triggerBytes || walkStore.trigger >= address + bytes) return; walkStore.done = true; - std::memset(walkStore.target, 0x45, walkStore.storeBytes); - static_cast(AgcDriver::GuestMemory::UntrackedHint(reinterpret_cast(walkStore.target), walkStore.hintBytes)); + const auto store = [] { + std::memset(walkStore.target, walkStore.value, walkStore.storeBytes); + static_cast(AgcDriver::GuestMemory::UntrackedHint(reinterpret_cast(walkStore.target), walkStore.hintBytes)); + }; + if (walkStore.threaded) { + std::thread(store).join(); + } else { + store(); + } } void untrackedHintProofTests(const Device& device, Recorder& recorder) { @@ -5906,6 +5915,50 @@ void untrackedHintProofTests(const Device& device, Recorder& recorder) { AgcDriver::GuestMemory::BumpCollectEpoch(); Require(!fast(), "a CPU store made after the first surface was compared and before its stamp was recorded kept the fast proof"); Require(version(first) != before, "the walk after a store made during the previous walk kept the old content"); + + before = version(first); + walkStore = {second, surface, block, 4096, bytes, false, 0x46, true}; + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::MarkWritten(second, 4096); + AgcDriver::GuestMemory::SetFlushHook(&storeDuringWalk); + const bool threadWalked = !fast(); + recorder.Activate(); + Require(walkStore.done, "the walk did not compare the second surface before the store from another thread"); + Require(threadWalked, "a driver store over the second surface kept the fast proof before the store from another thread"); + Require(version(first) == before, "the first surface was read again after another thread stored into it while the second was compared"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!fast(), "a store from another thread right after the first surface was compared kept the fast proof"); + Require(version(first) != before, "the walk after a store from another thread during the previous walk kept the old content"); + + AgcDriver::GuestMemory::BumpCollectEpoch(); + static_cast(fast()); + Require(fast(), "a second proof in one collect epoch was not fast before the store from another thread"); + before = version(first); + std::thread([&] { std::memset(block, 0x47, 4096); }).join(); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!fast(), "a store from another thread after a fast proof kept the fast proof in the next collect epoch"); + Require(version(first) != before, "the walk after a store from another thread kept the old content"); + + AgcDriver::GuestMemory::BumpCollectEpoch(); + static_cast(fast()); + Require(fast(), "a second proof in one collect epoch was not fast before the check on another thread"); + before = version(first); + std::memset(block, 0x48, 4096); + bool checked = false; + ShaderResources::ProofReport threadReport; + std::exception_ptr threadError; + std::thread([&] { + try { + AgcDriver::GuestMemory::BumpCollectEpoch(); + checked = sampled->Revalidate(std::span(&imageCompute, 1), &threadReport); + } catch (...) { + threadError = std::current_exception(); + } + }).join(); + if (threadError) std::rethrow_exception(threadError); + Require(checked, "a sampled template over untracked memory did not revalidate on another thread"); + Require(threadReport.path != ShaderResources::ProofPath::Fast, "a check on another thread after a store on the proving thread kept the fast proof"); + Require(version(first) != before, "the walk on another thread after a store kept the old content"); sampled.reset(); std::cout << "untracked proofs across collect epochs: ok\n"; } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index c8931d43c0bc..b9ba3cc22494 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -117,6 +117,56 @@ void CheckSparseLeafBoundaries() { Require(UntrackedHint(base, Block) == changed && UntrackedHint(base + 3 * Block, Block) == changed, "a cross-leaf hint store was missed"); Require(UntrackedHint(boundary, 2 * Block) == hint, "hint stores changed untouched blocks in the other leaf"); } + +void CheckUntrackedHintAcrossThreads() { + constexpr std::size_t bytes = 2 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory), bytes, PAGE_READWRITE, bytes); + struct Cleanup { + void* memory; + ~Cleanup() { + const auto begin = reinterpret_cast(memory); + const std::array, 1> range{{{begin, begin + bytes}}}; + Rewatch(range); + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } cleanup{const_cast(memory)}; + const auto base = reinterpret_cast(memory); + for (std::size_t offset = 0; offset < bytes; offset += 4096) memory[offset] = 0x11; + BumpCollectEpoch(); + Require(CollectWrites(base, bytes) != 0, "the threaded hint range was not collected"); + Unwatch(base, bytes); + BumpCollectEpoch(); + const auto read = UntrackedHint(base, bytes); + Require(read != 0, "the threaded hint range has no untracked hint"); + std::thread([&] { memory[Block + 8] ^= 1; }).join(); + std::uint64_t other = 0; + std::thread([&] { + BumpCollectEpoch(); + other = UntrackedHint(base, bytes); + }).join(); + Require(other > read, "a thread in its own collect epoch did not see a store another thread made after the hint was read"); + Require(UntrackedHint(base, bytes) == other, "the reading thread did not see, in its old collect epoch, the hint another thread stamped"); + BumpCollectEpoch(); + Require(UntrackedHint(base, bytes) == other, "the reading thread did not read the newer hint in its next collect epoch"); + std::atomic stop{false}; + std::thread writer([&] { + while (!stop.load(std::memory_order_acquire)) memory[8] ^= 1; + memory[8] ^= 1; + }); + std::uint64_t newest = other; + for (unsigned epoch = 0; epoch < 50; ++epoch) { + BumpCollectEpoch(); + const auto hint = UntrackedHint(base, bytes); + Require(hint >= newest, "an untracked hint went back while another thread stored"); + newest = hint; + } + stop.store(true, std::memory_order_release); + writer.join(); + BumpCollectEpoch(); + Require(UntrackedHint(base, bytes) > newest, "the last store of a racing thread did not move the hint past every value read before it"); +} #endif void* AllocateWatched(std::size_t bytes, bool crossLeaf = false) { @@ -2181,6 +2231,7 @@ int main() { #endif CheckCoarseRuns(); #ifdef _WIN32 + CheckUntrackedHintAcrossThreads(); CheckSparseLeafBoundaries(); CheckDeferredSharedImports(); CheckPrivateMappingReuse(); From 793fc7f8ea83a023d6944a5122e3964d61bce6a2 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 1 Nov 2026 03:36:10 -0800 Subject: [PATCH 9/9] test(agc): report the untracked proof tests as skipped when they cannot run --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 33bd748cf5f4..b9541cb51371 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5707,11 +5707,11 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) { std::cout << "idle publish wait verified\n"; } -void untrackedForgetTests(const Device& device, Recorder& recorder) { +bool untrackedForgetTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { std::cout << "host imports unavailable: untracked comparison memo not tested\n"; - return; + return false; } constexpr std::uint32_t side = 256; constexpr std::size_t surfaceBytes = side * side * 4; @@ -5750,11 +5750,11 @@ void untrackedForgetTests(const Device& device, Recorder& recorder) { } unregister{base, block, other}; if (HostImportFor(base, address, surfaceBytes) == nullptr) { std::cout << "host import of the surface refused: untracked comparison memo not tested\n"; - return; + return false; } if (AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { std::cout << "the surface is write-watched: untracked comparison memo not tested\n"; - return; + return false; } TextureDetiler detiler(base); struct Finish { @@ -5794,14 +5794,15 @@ void untrackedForgetTests(const Device& device, Recorder& recorder) { VirtualFree(other, 0, MEM_RELEASE); #endif std::cout << "untracked comparison memo: ok\n"; + return true; } -void untrackedRevalidateTests(const Device& device, Recorder& recorder) { +bool untrackedRevalidateTests(const Device& device, Recorder& recorder) { auto context = device.GetContext(); context.nullDescriptors = device.NullDescriptors(); if (context.hostImportAlignment == 0 || !context.nullDescriptors) { std::cout << "host imports or null descriptors unavailable: fast revalidation over untracked memory not tested\n"; - return; + return false; } constexpr std::size_t bytes = 2 * 65536; #ifdef _WIN32 @@ -5829,11 +5830,11 @@ void untrackedRevalidateTests(const Device& device, Recorder& recorder) { } unregister{context, block}; if (HostImportFor(context, address, bytes) == nullptr) { std::cout << "host import of the block refused: fast revalidation over untracked memory not tested\n"; - return; + return false; } if (AgcDriver::GuestMemory::Watched(address, bytes)) { std::cout << "the block is write-watched: fast revalidation over untracked memory not tested\n"; - return; + return false; } TextureDetiler detiler(context); auto imageContext = context; @@ -5870,6 +5871,7 @@ void untrackedRevalidateTests(const Device& device, Recorder& recorder) { Require(report.path != ShaderResources::ProofPath::Fast, "a sampled template over untracked memory kept its fast proof across a store and a new collect epoch"); sampled.reset(); std::cout << "fast revalidation over untracked memory: ok\n"; + return true; } struct WalkStore { @@ -5898,19 +5900,19 @@ void storeDuringWalk(std::uint64_t address, std::size_t bytes) { } } -void untrackedHintProofTests(const Device& device, Recorder& recorder) { +bool untrackedHintProofTests(const Device& device, Recorder& recorder) { auto context = device.GetContext(); context.nullDescriptors = device.NullDescriptors(); if (context.hostImportAlignment == 0 || !context.nullDescriptors) { std::cout << "host imports or null descriptors unavailable: untracked proofs across collect epochs not tested\n"; - return; + return false; } constexpr std::size_t surface = 65536; constexpr std::size_t bytes = 2 * surface; auto* block = static_cast(AllocateWatched(bytes, surface)); if (block == nullptr) { std::cout << "guest memory is not write-watched: untracked proofs across collect epochs not tested\n"; - return; + return false; } std::memset(block, 0x40, bytes); const auto first = reinterpret_cast(block); @@ -5932,12 +5934,12 @@ void untrackedHintProofTests(const Device& device, Recorder& recorder) { } unregister{context, block}; if (HostImportFor(context, first, bytes) == nullptr) { std::cout << "host import of the block refused: untracked proofs across collect epochs not tested\n"; - return; + return false; } AgcDriver::GuestMemory::Unwatch(first, bytes); if (AgcDriver::GuestMemory::Watched(first, bytes)) { std::cout << "the block stayed write-watched: untracked proofs across collect epochs not tested\n"; - return; + return false; } const bool hinted = AgcDriver::GuestMemory::UntrackedHint(first, bytes) != 0; std::cout << (hinted ? "the block has a write-watch hint: its proofs are expected to hold across collect epochs\n" : "the block has no write-watch hint: its proofs are expected to follow the collect epoch\n"); @@ -6080,6 +6082,7 @@ void untrackedHintProofTests(const Device& device, Recorder& recorder) { Require(version(first) != before, "the walk on another thread after a store kept the old content"); sampled.reset(); std::cout << "untracked proofs across collect epochs: ok\n"; + return true; } void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { @@ -13047,16 +13050,13 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--untracked-revalidate-only") { - untrackedRevalidateTests(device, recorder); - return 0; + return untrackedRevalidateTests(device, recorder) ? 0 : 77; } if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint-proof-only") { - untrackedHintProofTests(device, recorder); - return 0; + return untrackedHintProofTests(device, recorder) ? 0 : 77; } if (argc == 2 && std::string_view(argv[1]) == "--untracked-forget-only") { - untrackedForgetTests(device, recorder); - return 0; + return untrackedForgetTests(device, recorder) ? 0 : 77; } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder);