Merge pull request #5029 from fdfawef324/perf/agc-partially-resident-snapshot-hits

perf(agc): trust write stamps for snapshots of partially committed textures
This commit is contained in:
Dean 2026-10-14 15:21:38 +13:00 committed by GitHub
commit 71c71f50b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 527 additions and 4 deletions

View File

@ -1235,6 +1235,11 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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)

View File

@ -72,6 +72,8 @@ struct UnchangedQuery {
};
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> 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);

View File

@ -1270,6 +1270,30 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
return true;
}
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges) {
ranges.clear();
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address || !WriteWatched()) return 0;
auto committed = CommittedRanges(address, bytes);
auto generation = TrackerGeneration();
for (const auto& [begin, end] : committed) {
generation = CollectWrites(begin, static_cast<std::size_t>(end - begin));
if (generation == 0) return 0;
}
ranges = std::move(committed);
return generation;
}
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> 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<std::size_t>(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<std::pair<std::uint64_t, std::uint64_t>()>& store) {
if (bytes == 0) return 0;
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {

View File

@ -111,6 +111,7 @@ struct CachedTexture {
std::uint64_t sourceVersion = 0;
std::uint64_t accounted = 0;
std::uint64_t present = 0;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> 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<Texture> cachedTexture(const Context& context, std::span<const s
ReadTextureSurface(resource, *keys, snapshot);
return std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot, depthCompare);
}
auto snapshotGeneration = generation;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
if (source == nullptr && generation == 0) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> 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<Texture> cachedTexture(const Context& context, std::span<const s
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
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<Texture> cachedTexture(const Context& context, std::span<const s
eraseTexture(cache, it);
++cache.counts.stale;
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
CachedTexture entry{key, address, std::vector<std::byte>(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<Texture> 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()});

View File

@ -562,6 +562,142 @@ void CheckUnwatchedImport() {
#endif
}
class PartlyCommitted {
public:
static constexpr std::size_t Bytes = 4 * Block;
PartlyCommitted() {
#ifdef _WIN32
memory = static_cast<std::uint8_t*>(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<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast<void*>(aligned + Bytes), begin + Block - aligned);
memory = reinterpret_cast<std::uint8_t*>(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<std::uint64_t>(memory) + block * Block; }
private:
std::uint8_t* memory = nullptr;
};
using Runs = std::vector<std::pair<std::uint64_t, std::uint64_t>>;
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;

View File

@ -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 <SDL_loadso.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<PFN_vkGetInstanceProcAddr>(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<PFN_vkCreateInstance>("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance");
std::uint32_t count = 0;
const auto enumerate = function<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices");
Require(count != 0, "no Vulkan device");
std::vector<VkPhysicalDevice> devices(count);
Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices");
context.physical = devices.front();
const auto extensions = function<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties");
std::vector<VkExtensionProperties> available(count);
Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties");
auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), available);
auto address = AgcDriver::QueryBdaFeatures(context.physical, function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), available);
const auto queues = function<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
queues(context.physical, &count, nullptr);
std::vector<VkQueueFamilyProperties> 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<const char*, 2> 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<std::uint32_t>(extensionsEnabled.size());
device.ppEnabledExtensionNames = extensionsEnabled.data();
device.pEnabledFeatures = &enabled;
Check(function<PFN_vkCreateDevice>("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice");
context.deviceProc = function<PFN_vkGetDeviceProcAddr>("vkGetDeviceProcAddr");
function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory);
VkPhysicalDeviceProperties properties{};
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
context.limits = properties.limits;
context.bufferDeviceAddress = true;
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool.queueFamilyIndex = family;
Check(context.Function<PFN_vkCreateCommandPool>("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<typename TFunction>
TFunction function(const char* name) const {
const auto result = reinterpret_cast<TFunction>(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<PFN_vkDestroyCommandPool>("vkDestroyCommandPool")(context.device, context.pool, nullptr);
context.bufferPool.reset();
if (context.device != VK_NULL_HANDLE) function<PFN_vkDestroyDevice>("vkDestroyDevice")(context.device, nullptr);
if (instance != VK_NULL_HANDLE) function<PFN_vkDestroyInstance>("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<std::uint8_t*>(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<std::uintptr_t>(block + offset), bytes);
for (std::size_t page = 0; page < bytes; page += 4096) static_cast<void>(static_cast<volatile const std::uint8_t*>(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<std::uintptr_t>(block + offset), bytes);
}
std::uint8_t* Data() const { return block; }
std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block)); }
private:
std::uint8_t* block = nullptr;
};
std::atomic<std::uint64_t> surfaceBegin{0};
std::atomic<std::uint64_t> surfaceEnd{0};
std::atomic<std::uint32_t> 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<std::uint32_t, 8> Descriptor(std::uint64_t address) {
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((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<Texture> 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<bool>(false);
const auto value = submitted ? std::byte{0x99} : std::byte{0x77};
std::vector<std::byte> original(reinterpret_cast<const std::byte*>(surface.Data()), reinterpret_cast<const std::byte*>(surface.Data()) + 0x1000);
recorder.NotePendingWrite(address, 0x1000);
recorder.OnComplete([address, landed, value, original = std::move(original)] {
const std::vector<std::byte> 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<std::uint8_t>(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<bool>(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> 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;
}
}