From 955b56a2b5841829444bca2c56f0b619613db6e0 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:50:40 -0400 Subject: [PATCH 1/2] perf(agc): trust write stamps for snapshots of partially committed textures A sampled texture over a partially resident surface (a texture array whose heap the guest commits on demand) never got a write generation: CollectWrites fails on a range with reserved pages, so every cache hit compared the whole committed part with its snapshot. CollectCommittedWrites collects the committed runs one by one and returns the runs with the generation; the cache entry records them, and a hit skips the compare only when the committed runs are the same and UnchangedCommittedSince holds over them. A commit, decommit, view mapping or unmapping inside the surface changes the runs or stamps the new pages, so it is compared as before. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/GuestMemory.hpp | 2 + .../Execution/src/GuestMemory.cpp | 24 ++ .../Graphics/src/ShaderResources.cpp | 24 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 137 ++++++++ .../execution/PartiallyResidentSnapshot.cpp | 293 ++++++++++++++++++ 5 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 3932b5dc5..6e174a34c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -72,6 +72,8 @@ struct UnchangedQuery { }; bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); bool UnchangedSinceAll(std::span queries); +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges); +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation); // Returns the generation the blocks were stamped with (0 when the arena is not write-watched): // UnchangedSince(range, it) holds until the next store over the range. std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b28..9456db6c6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1263,6 +1263,30 @@ bool UnchangedSinceAll(std::span queries) { return true; } +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges) { + ranges.clear(); + if (bytes == 0 || bytes > std::numeric_limits::max() - address || !WriteWatched()) return 0; + auto committed = CommittedRanges(address, bytes); + auto generation = TrackerGeneration(); + for (const auto& [begin, end] : committed) { + generation = CollectWrites(begin, static_cast(end - begin)); + if (generation == 0) return 0; + } + ranges = std::move(committed); + return generation; +} + +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || generation == 0) return false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || !tracker.covers(begin, static_cast(end - begin)) || tracker.newestStamp(begin, end) > generation) return false; + } + return true; +} + std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function()>& store) { if (bytes == 0) return 0; const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 552145a60..a0bfa681b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -111,6 +111,7 @@ struct CachedTexture { std::uint64_t sourceVersion = 0; std::uint64_t accounted = 0; std::uint64_t present = 0; + std::optional>> committed; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -631,6 +632,15 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, snapshot, depthCompare); } + auto snapshotGeneration = generation; + std::optional>> committed; + if (source == nullptr && generation == 0) { + std::vector> ranges; + if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) { + snapshotGeneration = partial; + committed = std::move(ranges); + } + } auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); @@ -659,8 +669,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes); }; - if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { - it->generation = generation; + const auto unchanged = [&] { + if (it->committed != committed) return false; + return committed.has_value() ? GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + }; + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { + it->generation = snapshotGeneration; + it->committed = std::move(committed); touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -671,7 +686,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, generation}; + CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -3268,7 +3284,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) auto& cache = Textures(); std::lock_guard lock(cache.mutex); const auto it = findSampledEntry(cache, context.device, record.words, record.components); - if (it == cache.entries.end() || it->texture != record.texture) return nullptr; + if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr; if (it->source == nullptr) it->generation = record.generation; touchTexture(cache, it); logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()}); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index f839edd79..d3cf8a962 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -562,6 +562,142 @@ void CheckUnwatchedImport() { #endif } +class PartlyCommitted { +public: + static constexpr std::size_t Bytes = 4 * Block; + + PartlyCommitted() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block)); +#else + void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the partly committed range"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast(aligned + Bytes), begin + Block - aligned); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve the partly committed range"); + } + + ~PartlyCommitted() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes); + munmap(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); +#endif + } + + PartlyCommitted(const PartlyCommitted&) = delete; + PartlyCommitted& operator=(const PartlyCommitted&) = delete; + + void Commit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block); +#else + Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + void Decommit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block); + Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast(memory) + block * Block; } + +private: + std::uint8_t* memory = nullptr; +}; + +using Runs = std::vector>; + +std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) { + BumpCollectEpoch(); + return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs); +} + +void CheckCommittedRanges() { + PartlyCommitted range; + const auto base = range.Address(); + range.Commit(0); + std::memset(range.Data(), 0x11, Block); + Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole"); + Runs runs; + const Runs first{{base, base + Block}}; + const auto synced = CollectCommitted(range, runs); + Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected"); + Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed"); + + range.Data()[8] = 0x22; + const auto written = CollectCommitted(range, runs); + Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed"); + Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed"); + Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed"); + + const auto beforeCommit = CollectCommitted(range, runs); + range.Commit(2); + const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}}; + const auto grown = CollectCommitted(range, runs); + Require(grown != 0 && runs == both, "a block committed inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit"); + Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect"); + + range.Decommit(2); + range.Commit(2); + const auto recommitted = CollectCommitted(range, runs); + Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged"); + + range.Decommit(2); + Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed"); + +#ifdef _WIN32 + HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + struct Sections { + HANDLE first; + HANDLE second; + ~Sections() { + if (first != nullptr) CloseHandle(first); + if (second != nullptr) CloseHandle(second); + } + } sections{firstSection, secondSection}; + Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range"); + const auto beforeMap = CollectCommitted(range, runs); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}}; + const auto mapped = CollectCommitted(range, runs); + Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping"); + Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed"); +#endif + + range.Decommit(0); + Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty"); + Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts"); +} + int main() { try { if (!WriteWatched()) { @@ -587,6 +723,7 @@ int main() { CheckCollectBoundaries(); CheckImportWindow(); CheckUnwatchedImport(); + CheckCommittedRanges(); } catch (const std::exception& error) { std::cerr << "write tracking test failed: " << error.what() << "\n"; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp new file mode 100644 index 000000000..f3270ac38 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -0,0 +1,293 @@ +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr int Skipped = 77; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderInt64 = VK_TRUE; + address.pNext = &bytes; + const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.bufferDeviceAddress = true; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearCachedTextures(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class ReservedSurface { +public: + ReservedSurface() { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~ReservedSurface() { + GuestAllocations::Mutation().Remove(block); + Decommit(0, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + } + + ReservedSurface(const ReservedSurface&) = delete; + ReservedSurface& operator=(const ReservedSurface&) = delete; + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(block + offset, bytes, PAGE_READWRITE, bytes); +#else + Require(mmap(block + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot commit a page"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + for (std::size_t page = 0; page < bytes; page += 4096) static_cast(static_cast(block)[offset + page]); + } + + void Decommit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block + offset, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block + offset, bytes); + Require(mmap(block + offset, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot decommit a page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + } + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +std::atomic surfaceBegin{0}; +std::atomic surfaceEnd{0}; +std::atomic surfaceReads{0}; + +void CountReads(std::uint64_t address, std::size_t bytes) { + if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); +} + +std::array Descriptor(std::uint64_t address) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == SurfaceBytes, "the test descriptor does not describe the reserved surface"); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the partially resident surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the partially resident surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the partially resident surface was compared with guest memory " + when + " although nothing changed (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the partially resident surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + sample("again", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write to its committed part", true, false); + sample("after the re-read of the written part", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store into its committed part", true, true); + sample("after the compare that followed the driver store", false, true); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was committed", true, true); + sample("after the compare that followed the commit", false, true); + std::memset(surface.Data() + 2 * BlockBytes, 0x33, BlockBytes); + sample("after a CPU write to the newly committed page", true, false); + sample("after the re-read of the newly committed page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted and committed again", true, false); + sample("after the re-read of the recommitted page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted", true, true); + sample("after the compare that followed the decommit", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +} + +int main() { + try { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory has no write watch"); + return Skipped; + } + std::optional device; + try { + device.emplace(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return Skipped; + } + { + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + ReservedSurface surface; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Run(context, surface); + ClearCachedTextures(context.device); + } + device.reset(); + std::puts("partially resident snapshot tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 8d8fe39eede50d0bce27a1d8488ca80e1bec7863 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:01:20 -0400 Subject: [PATCH 2/2] fix(agc): flush pending stores before a partial snapshot hit A partially resident snapshot cannot be reused from committed write stamps while a recorder batch still owns an overlapping completion store. Make that case compare through the guest-memory flush hook before deciding whether the snapshot is unchanged. Cover copied completion stores in both open and submitted batches, then verify that unchanged snapshots are reused and an unrelated pending write does not synchronize. The focused regression fails on the published PR head and passes with the guard. The existing fully committed snapshot path is unchanged. Co-Authored-By: Codex --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../Graphics/src/ShaderResources.cpp | 2 +- .../execution/PartiallyResidentSnapshot.cpp | 50 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4fc24a5f2..3fdab7d9a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1235,6 +1235,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) + set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index a0bfa681b..a1f9a6661 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -671,7 +671,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spancommitted != committed) return false; - return committed.has_value() ? GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); }; if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { it->generation = snapshotGeneration; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index f3270ac38..4bdf081ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -19,6 +20,7 @@ #endif #include #include +#include #include #include #include @@ -29,6 +31,8 @@ #include #include #include +#include +#include #include namespace { @@ -258,9 +262,50 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void PendingStores(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + Recorder recorder(context); + recorder.Activate(); + const auto lookup = [&] { + AgcDriver::GuestMemory::BumpCollectEpoch(); + return CachedSampledTexture(context, words); + }; + auto previous = lookup(); + Require(previous != nullptr && !previous->ViewsStorageImage(), "the pending-store surface did not produce a snapshot"); + Require(lookup() == previous, "the unchanged pending-store surface did not reuse its snapshot"); + for (const bool submitted : {false, true}) { + const auto landed = std::make_shared(false); + const auto value = submitted ? std::byte{0x99} : std::byte{0x77}; + std::vector original(reinterpret_cast(surface.Data()), reinterpret_cast(surface.Data()) + 0x1000); + recorder.NotePendingWrite(address, 0x1000); + recorder.OnComplete([address, landed, value, original = std::move(original)] { + const std::vector current(0x1000, value); + AgcDriver::GuestMemory::WriteChanged(address, current, original); + *landed = true; + }); + if (submitted) recorder.Submit(); + const auto next = lookup(); + Require(*landed, "the partially resident lookup returned before its pending copied store completed"); + Require(next != previous, "the partially resident lookup reused the snapshot from before its pending copied store"); + Require(surface.Data()[0] == std::to_integer(value), "the pending copied store did not update guest memory"); + Require(lookup() == next, "the partially resident lookup did not reuse the snapshot after its pending copied store"); + previous = next; + } + const auto unrelated = std::make_shared(false); + recorder.NotePendingWrite(address + SurfaceBytes, BlockBytes); + recorder.OnComplete([unrelated] { *unrelated = true; }); + Require(lookup() == previous, "an unrelated pending write replaced the partially resident snapshot"); + Require(!*unrelated, "a partially resident lookup waited for an unrelated pending write"); + recorder.Sync(); + Require(*unrelated, "the unrelated pending completion did not run when synchronized"); } -int main() { +} + +int main(int argc, char** argv) { try { if (!AgcDriver::GuestMemory::WriteWatched()) { std::puts("skipped, guest memory has no write watch"); @@ -280,7 +325,8 @@ int main() { context.detiler = &detiler; ReservedSurface surface; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - Run(context, surface); + if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + else Run(context, surface); ClearCachedTextures(context.device); } device.reset();