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/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 059e3ed6e..3143d0b3e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1270,6 +1270,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 6c3f56e75..df5f105e5 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 @@ -637,6 +638,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); @@ -665,8 +675,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() ? !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; + it->committed = std::move(committed); touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -677,7 +692,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(); @@ -3277,7 +3293,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..4bdf081ef --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -0,0 +1,339 @@ +#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" +#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 +#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); +} + +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 argc, char** argv) { + 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()); + if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + else 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; + } +}