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:
commit
71c71f50b7
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue