Merge pull request #1059 from Celegans12/perf/agc-storage-image-cache-size

perf(agc): size the storage and sampled texture caches to the device
This commit is contained in:
Dean 2026-10-09 22:40:39 +13:00 committed by GitHub
commit ca2b341852
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 837 additions and 8 deletions

View File

@ -640,6 +640,12 @@ if(BUILD_TESTING)
endif()
endforeach()
set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE)
add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
if(WIN32)

View File

@ -214,6 +214,7 @@ struct VulkanDevice::State {
bool occlusionQueryPrecise = false;
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
bool memoryBudget = false;
bool depthRangeUnrestricted = false;
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
@ -856,6 +857,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
// without it resolves such draws on the CPU.
state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
if (state->drawIndirectCount) deviceExtensions.push_back(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
state->memoryBudget = hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
// Guest memory is host memory: importing it lets address-based shaders use it in place instead of
// copying every registered allocation per draw.
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && std::getenv("APS5_NO_HOST_IMPORT") == nullptr) {
@ -2539,6 +2541,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
context.srgbDecodeFormats = state->srgbDecodeFormats;
context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
return context;
}

View File

@ -107,6 +107,7 @@ struct Context {
// Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment.
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;

View File

@ -26,6 +26,17 @@ class Recorder;
void FlushCachedTextures(VkDevice device);
void ClearCachedTextures(VkDevice device);
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes);
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
struct TextureCacheUse {
std::size_t sampledEntries = 0;
std::uint64_t sampledBytes = 0;
std::uint64_t storageBytes = 0;
};
TextureCacheUse TextureCacheUsage();
std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.

View File

@ -34,6 +34,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat);
// Whether a storage image of the guest format takes DCC clear `keys` as a GPU clear (see
// StorageTexture::upload); false for integer formats and non-clear keys.
bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys);
std::uint64_t SampledTextureMemory();
// A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload
// still writes it (see the snapshot constructor), so the texture may go before the batch completes.
@ -101,6 +102,7 @@ private:
ViewRange firstLayerRange{};
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
VkDeviceSize countedBytes = 0;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
std::shared_ptr<ResidentColor> source;
std::shared_ptr<StorageTexture> storageSource;
@ -290,6 +292,7 @@ public:
// Keeps the image current with guest memory (see GuestMemory::CollectWrites).
bool Refresh();
std::uint64_t GuestBytes() const;
VkDeviceSize AllocationBytes() const { return memoryBytes; }
private:
// The regions of every array layer, or of the tracked layers `layers` selects.
@ -457,6 +460,7 @@ private:
bool lent = false;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize memoryBytes = 0;
VkImageView view = VK_NULL_HANDLE;
std::uint32_t defaultMip = 0;
std::map<std::uint32_t, VkImageView> extraViews;

View File

@ -109,7 +109,13 @@ struct TextureCache {
std::mutex mutex;
std::list<CachedTexture> entries;
std::unordered_map<TextureKey, std::list<CachedTexture>::iterator, TextureKeyHash> index;
std::unordered_multimap<const StorageTexture*, std::list<CachedTexture>::iterator> views;
std::uint64_t bytes = 0;
VkPhysicalDevice budgetDevice = VK_NULL_HANDLE;
std::chrono::steady_clock::time_point budgetRead{};
std::uint64_t budget = 0;
std::uint64_t reportedBudget = 0;
std::chrono::steady_clock::time_point reportedAt{};
};
TextureCache& Textures() {
@ -117,6 +123,24 @@ TextureCache& Textures() {
return cache;
}
struct UncachedImages {
std::mutex mutex;
std::vector<std::weak_ptr<StorageTexture>> images;
std::atomic<bool> any{false};
};
UncachedImages& Uncached() {
static UncachedImages uncached;
return uncached;
}
void noteUncached(const std::shared_ptr<StorageTexture>& image) {
auto& uncached = Uncached();
std::lock_guard lock(uncached.mutex);
uncached.images.push_back(image);
uncached.any.store(true, std::memory_order_release);
}
bool TextureHashEnabled() {
static const bool disabled = std::getenv("APS5_NO_TEXTURE_HASH") != nullptr;
return !disabled;
@ -135,10 +159,34 @@ std::list<CachedTexture>::iterator findTexture(TextureCache& cache, const Textur
void eraseTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
cache.bytes -= it->accounted;
if (it->source != nullptr) {
for (auto [view, last] = cache.views.equal_range(it->source.get()); view != last; ++view) {
if (view->second == it) {
cache.views.erase(view);
break;
}
}
}
cache.index.erase(it->key);
cache.entries.erase(it);
}
void dropUncachedViews(TextureCache& cache) {
auto& uncached = Uncached();
if (!uncached.any.load(std::memory_order_acquire)) return;
std::vector<std::weak_ptr<StorageTexture>> images;
{
std::lock_guard lock(uncached.mutex);
images.swap(uncached.images);
uncached.any.store(false, std::memory_order_release);
}
for (const auto& weak : images) {
const auto image = weak.lock();
if (image == nullptr) continue;
for (auto view = cache.views.find(image.get()); view != cache.views.end(); view = cache.views.find(image.get())) eraseTexture(cache, view->second);
}
}
// Moves an entry to the front (most recently used).
void touchTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
cache.entries.splice(cache.entries.begin(), cache.entries, it);
@ -212,6 +260,10 @@ void logLookup(const LookupRecord& record) {
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0);
std::uint64_t heldBytes(const StorageTexture& image) {
return std::max<std::uint64_t>(image.AllocationBytes(), image.GuestBytes());
}
bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) {
return resource.dccAddress != 0 && image.Descriptor().dccAddress != resource.dccAddress && !image.ServesKeysAt(resource.dccAddress);
}
@ -281,6 +333,55 @@ std::shared_ptr<StorageTexture> sampledStorageSource(const Context& context, con
}
}
std::uint64_t storageCacheBytes();
std::optional<std::uint32_t> largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) {
std::optional<std::uint32_t> heap;
for (std::uint32_t i = 0; i < std::min<std::uint32_t>(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) {
if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i;
}
return heap;
}
std::uint64_t forcedCacheBudget() {
static const std::uint64_t forced = [] {
const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB");
return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u;
}();
return forced;
}
std::uint64_t storageBudget(const Context& context) {
const auto forced = forcedCacheBudget();
return forced != 0 ? forced : TextureCacheBudget(context.memory);
}
std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
if (const auto forced = forcedCacheBudget(); forced != 0) return forced;
if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory);
const auto now = std::chrono::steady_clock::now();
if (cache.budgetDevice == context.physical && now - cache.budgetRead < std::chrono::seconds(1)) return cache.budget;
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
properties.pNext = &reported;
context.memoryProperties2(context.physical, &properties);
const auto sampled = SampledTextureMemory();
const auto storage = storageCacheBytes();
const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage);
const bool fresh = cache.budgetDevice != context.physical;
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) {
char was[48] = "";
if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast<unsigned long long>(cache.reportedBudget >> 20u));
std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast<unsigned long long>(budget >> 20u), was, *heap, static_cast<unsigned long long>(reported.heapBudget[*heap] >> 20u), static_cast<unsigned long long>(reported.heapUsage[*heap] >> 20u), static_cast<unsigned long long>(sampled >> 20u), static_cast<unsigned long long>(cache.bytes >> 20u), static_cast<unsigned long long>(storage >> 20u), static_cast<unsigned long long>(TextureCacheBudget(context.memory) >> 20u));
cache.reportedBudget = budget;
cache.reportedAt = now;
}
cache.budgetDevice = context.physical;
cache.budgetRead = now;
cache.budget = budget;
return budget;
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) {
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
@ -387,6 +488,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
auto& cache = Textures();
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
std::lock_guard lock(cache.mutex);
dropUncachedViews(cache);
if (auto it = findTexture(cache, key); it != cache.entries.end()) {
if (it->source != nullptr) {
// The view follows the storage image, whatever the GPU wrote to it since; guest memory
@ -422,11 +524,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
eraseTexture(cache, it);
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
entry.accounted = guestBytes;
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
entry.texture = std::make_shared<Texture>(context, source, resource, components);
entry.accounted = source->Cached() ? 0u : heldBytes(*source);
counters.fromStorage.fetch_add(1, std::memory_order_relaxed);
} else {
// Snapshot before the upload so a write racing with it is caught by the next comparison.
@ -439,15 +541,17 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
std::fprintf(stderr, "[texture] 0x%llx %ux%u format %u tile %d: %zu of %zu sampled bytes nonzero\n", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), nonzero, entry.bytes.size() / 64);
}
entry.texture = std::make_shared<Texture>(context, *context.detiler, resource, components, entry.bytes, depthCompare);
entry.accounted = entry.texture->AllocationBytes();
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
constexpr std::uint64_t budget = 2048ull << 20u;
const auto budget = sampledBudget(context, cache);
while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) eraseTexture(cache, std::prev(cache.entries.end()));
cache.bytes += entry.accounted;
auto texture = entry.texture;
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()});
cache.entries.push_front(std::move(entry));
cache.index[key] = cache.entries.begin();
if (source != nullptr) cache.views.emplace(source.get(), cache.entries.begin());
reportTextureCounters();
if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start);
return texture;
@ -471,6 +575,7 @@ struct CachedStorageTexture {
StorageKey key;
std::uint32_t mip;
std::shared_ptr<StorageTexture> texture;
std::uint64_t accounted = 0;
};
// As TextureCache: use order with a hash index by key, plus one by image for StorageImageCached.
@ -479,7 +584,7 @@ struct StorageTextureCache {
std::list<CachedStorageTexture> entries;
std::unordered_map<StorageKey, std::list<CachedStorageTexture>::iterator, StorageKeyHash> index;
std::unordered_map<const StorageTexture*, std::list<CachedStorageTexture>::iterator> byImage;
std::uint64_t bytes = 0;
std::atomic<std::uint64_t> bytes{0};
};
StorageTextureCache& StorageTextures() {
@ -487,6 +592,10 @@ StorageTextureCache& StorageTextures() {
return cache;
}
std::uint64_t storageCacheBytes() {
return StorageTextures().bytes.load(std::memory_order_relaxed);
}
std::list<CachedStorageTexture>::iterator findStorage(StorageTextureCache& cache, const StorageKey& key) {
if (TextureHashEnabled()) {
const auto found = cache.index.find(key);
@ -512,8 +621,9 @@ std::list<CachedStorageTexture>::iterator findStorageByImage(StorageTextureCache
// Evicts an entry: its pending results go to guest memory first (the image may die with the entry).
void evictStorage(StorageTextureCache& cache, std::list<CachedStorageTexture>::iterator it) {
it->texture->SetCached(false);
noteUncached(it->texture);
it->texture->Flush();
cache.bytes -= it->texture->GuestBytes();
cache.bytes -= it->accounted;
cache.index.erase(it->key);
cache.byImage.erase(it->texture.get());
cache.entries.erase(it);
@ -609,9 +719,10 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
// APS5_NO_KEEP_NEW_STORAGE=1 leaves the image to its cache entry alone, as before.
static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr;
if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture);
constexpr std::uint64_t budget = 2048ull << 20u;
while (!cache.entries.empty() && cache.bytes + entry.texture->GuestBytes() > budget) evictStorage(cache, std::prev(cache.entries.end()));
cache.bytes += entry.texture->GuestBytes();
entry.accounted = heldBytes(*entry.texture);
const auto budget = storageBudget(context);
while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evictStorage(cache, std::prev(cache.entries.end()));
cache.bytes += entry.accounted;
auto texture = entry.texture;
cache.entries.push_front(std::move(entry));
cache.index[key] = cache.entries.begin();
@ -624,6 +735,49 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
}
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) {
const auto heap = largestDeviceLocalHeap(memory);
return std::max<std::uint64_t>(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u);
}
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) {
const auto heap = largestDeviceLocalHeap(memory);
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
const auto budget = reported->heapBudget[*heap];
const auto usage = reported->heapUsage[*heap];
const auto reserved = usage - std::min<std::uint64_t>(usage, textureBytes) + TextureCacheBudget(memory) + budget / 8u;
return std::max<std::uint64_t>(2048ull << 20u, budget > reserved ? budget - reserved : 0u);
}
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport) {
const auto change = budget > reported ? budget - reported : reported - budget;
return change > reported / 10u && sinceReport >= std::chrono::seconds(10);
}
TextureCacheUse TextureCacheUsage() {
TextureCacheUse use;
{
auto& sampled = Textures();
std::lock_guard lock(sampled.mutex);
dropUncachedViews(sampled);
use.sampledEntries = sampled.entries.size();
use.sampledBytes = sampled.bytes;
}
use.storageBytes = storageCacheBytes();
return use;
}
std::uint64_t SampledTextureCacheBudget(const Context& context) {
auto& cache = Textures();
std::lock_guard lock(cache.mutex);
return sampledBudget(context, cache);
}
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words) {
const auto resource = DecodeTextureResource(words);
return cachedTexture(context, words, resource, ViewComponents(resource));
}
void FlushCachedTextures(VkDevice device) {
Require(device != VK_NULL_HANDLE, "cannot flush textures without a Vulkan device");
GuestMemory::AssertGpuLockHeld("FlushCachedTextures");
@ -652,7 +806,8 @@ void ClearCachedTextures(VkDevice device) {
continue;
}
it->texture->SetCached(false);
storage.bytes -= it->texture->GuestBytes();
noteUncached(it->texture);
storage.bytes -= it->accounted;
storage.index.erase(it->key);
storage.byImage.erase(it->texture.get());
it = storage.entries.erase(it);

View File

@ -223,6 +223,15 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor,
viewInfo.pNext = &minLod;
}
std::atomic<std::uint64_t>& SampledMemoryCounter() {
static std::atomic<std::uint64_t> bytes{0};
return bytes;
}
}
std::uint64_t SampledTextureMemory() {
return SampledMemoryCounter().load(std::memory_order_relaxed);
}
Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span<const std::byte> snapshot, bool depthCompare) : context(context) {
@ -438,6 +447,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
totals.view += timer.lap();
if (++totals.count % 200 == 0) std::fprintf(stderr, "[texture] %llu textures (%llu copied from storage images, %llu uploads recorded): allocate+image %.0f ms, guest read %.0f ms, detile+copy %.0f ms, view+buffer release %.0f ms\n", static_cast<unsigned long long>(totals.count), static_cast<unsigned long long>(totals.fromStorage), static_cast<unsigned long long>(totals.recordedUploads), totals.allocate, totals.read, totals.gpu, totals.view);
}
countedBytes = allocationBytes;
SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed);
} catch (...) {
release();
throw;
@ -525,6 +536,7 @@ void Texture::release() noexcept {
// The image and its memory go with the last holder: this texture, or the batch still uploading it.
owned.reset();
image = VK_NULL_HANDLE;
SampledMemoryCounter().fetch_sub(std::exchange(countedBytes, 0), std::memory_order_relaxed);
}
VkImageView Texture::View() const {
@ -730,6 +742,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
memoryBytes = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage");

View File

@ -1724,6 +1724,63 @@ void descriptorCacheTests() {
Require(mock.live == 0, "the descriptor cache leaked a pool or layout");
}
void textureCacheBudgetTests() {
using AgcDriver::Graphics::TextureCacheBudget;
constexpr std::uint64_t GiB = 1ull << 30u;
VkPhysicalDeviceMemoryProperties memory{};
Require(TextureCacheBudget(memory) == 2 * GiB, "a device without memory heaps does not keep 2 GiB of cached textures");
memory.memoryHeapCount = 3;
memory.memoryHeaps[0] = {256ull << 20u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
memory.memoryHeaps[1] = {32 * GiB, 0};
memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT | VK_MEMORY_HEAP_MULTI_INSTANCE_BIT};
Require(TextureCacheBudget(memory) == 4 * GiB, "a 16 GiB device-local heap does not give 4 GiB of cached textures");
memory.memoryHeaps[2].size = 6 * GiB;
Require(TextureCacheBudget(memory) == 2 * GiB, "a 6 GiB device-local heap does not keep the 2 GiB floor");
memory.memoryHeaps[2].size = 24 * GiB;
memory.memoryHeapCount = 2;
Require(TextureCacheBudget(memory) == 2 * GiB, "a heap past memoryHeapCount or a host heap counted toward the texture caches");
memory.memoryHeapCount = 3;
Require(TextureCacheBudget(memory) == 6 * GiB, "a 24 GiB device-local heap does not give 6 GiB of cached textures");
}
void sampledTextureBudgetTests() {
using AgcDriver::Graphics::SampledTextureBudget;
constexpr std::uint64_t GiB = 1ull << 30u;
constexpr std::uint64_t MiB = 1ull << 20u;
VkPhysicalDeviceMemoryProperties memory{};
memory.memoryHeapCount = 3;
memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
memory.memoryHeaps[1] = {32 * GiB, 0};
memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
Require(SampledTextureBudget(memory, nullptr, 0) == 4 * GiB, "without VK_EXT_memory_budget the sampled texture cache does not keep a quarter of the 16 GiB heap");
reported.heapBudget[0] = 100 * GiB;
reported.heapBudget[1] = 100 * GiB;
Require(SampledTextureBudget(memory, &reported, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap");
reported.heapBudget[2] = 15 * GiB;
reported.heapUsage[2] = 9 * GiB;
Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 5 * GiB + 128 * MiB, "a 15 GiB budget with 4 GiB used outside the texture caches does not leave 5 GiB 128 MiB after the 4 GiB storage cache and 1 GiB 896 MiB of headroom");
reported.heapUsage[2] = 3 * GiB;
Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 9 * GiB + 128 * MiB, "usage below the cached bytes is not read as no other device memory in use");
reported.heapUsage[2] = 12 * GiB;
Require(SampledTextureBudget(memory, &reported, 1 * GiB) == 2 * GiB, "a heap whose other users leave no room does not keep the 2 GiB floor");
memory.memoryHeaps[0].flags = 0;
memory.memoryHeaps[2].flags = 0;
Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures");
}
void sampledBudgetReportTests() {
using AgcDriver::Graphics::SampledBudgetReportDue;
using std::chrono::seconds;
constexpr std::uint64_t MiB = 1ull << 20u;
Require(SampledBudgetReportDue(0, 2048 * MiB, seconds(10)), "a budget never reported is not due");
Require(!SampledBudgetReportDue(4000 * MiB, 4400 * MiB, seconds(60)), "a budget 10% above the reported one is due, not only one more than 10% away");
Require(!SampledBudgetReportDue(4000 * MiB, 3600 * MiB, seconds(60)), "a budget 10% below the reported one is due, not only one more than 10% away");
Require(SampledBudgetReportDue(4000 * MiB, 4401 * MiB, seconds(10)), "a budget more than 10% above the reported one is not due after 10 s");
Require(SampledBudgetReportDue(4000 * MiB, 3599 * MiB, seconds(10)), "a budget more than 10% below the reported one is not due after 10 s");
Require(!SampledBudgetReportDue(4000 * MiB, 2048 * MiB, seconds(9)), "a budget change is reported again within 10 s of the last report");
}
void misalignedShaderDataTests() {
mock = MockVulkan{};
auto context = mockContext();
@ -2776,6 +2833,9 @@ int main() {
descriptorCacheTests();
misalignedShaderDataTests();
debugBranchTests();
textureCacheBudgetTests();
sampledTextureBudgetTests();
sampledBudgetReportTests();
meshArgumentTests();
meshIndexBufferTests();
validationTests();

View File

@ -0,0 +1,362 @@
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "VulkanTestDevice.hpp"
#include <SDL_loadso.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#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 SurfaceBytes = std::size_t{Side} * Side * 4u;
constexpr std::size_t Surfaces = 12;
constexpr std::size_t BlockBytes = SurfaceBytes * Surfaces;
constexpr std::uint64_t Budget = 1ull << 20u;
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");
const auto hasExtension = [&](const char* name) {
for (const auto& extension : available) {
if (std::strcmp(extension.extensionName, name) == 0) return true;
}
return false;
};
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;
std::vector<const char*> extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME};
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) {
extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties};
function<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(context.physical, &properties);
context.hostImportAlignment = hostProperties.minImportedHostPointerAlignment;
}
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");
if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2");
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(); }
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.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 Block {
public:
explicit Block(const Context& context) : context(context) {
#ifdef _WIN32
memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
memory = std::aligned_alloc(65536, BlockBytes);
#endif
Require(memory != nullptr, "cannot allocate the texture block");
std::memset(memory, 0x5a, BlockBytes);
GuestAllocations::Mutation().Add(memory, BlockBytes, true, true);
}
~Block() {
ClearCachedTextures(context.device);
GuestAllocations::Mutation().Remove(memory);
HostImportFor(context, Surface(0), BlockBytes);
#ifdef _WIN32
VirtualFree(memory, 0, MEM_RELEASE);
#else
std::free(memory);
#endif
}
std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast<std::uint64_t>(memory) + surface * SurfaceBytes; }
private:
const Context& context;
void* memory = nullptr;
};
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) {
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,
};
}
std::shared_ptr<Texture> Sampled(const Context& context, std::uint64_t address, std::uint32_t side = Side) {
const auto words = Descriptor(address, side);
return CachedSampledTexture(context, words);
}
GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) {
const auto words = Descriptor(address, side);
return DecodeTextureResource(words);
}
void viewTests(const Context& context) {
if (context.hostImportAlignment == 0) {
std::puts("host imports unavailable: views of storage images not tested");
return;
}
Block block(context);
if (HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) {
std::puts("host import of the texture block refused: views of storage images not tested");
return;
}
const auto before = TextureCacheUsage();
auto view = Sampled(context, block.Surface(0));
Require(view->ViewsStorageImage(), "a sampled texture in host-imported memory is not a view of its storage image");
const std::weak_ptr<Texture> viewed = view;
const auto* image = view->StorageSource();
const auto after = TextureCacheUsage();
Require(after.sampledEntries == before.sampledEntries + 1u, "the view did not enter the sampled texture cache");
Require(after.sampledBytes == before.sampledBytes, "a view of a cached storage image counted " + std::to_string(after.sampledBytes - before.sampledBytes) + " bytes against the sampled texture cache");
Require(after.storageBytes > before.storageBytes, "the storage image of the view is not counted by the storage image cache");
Require(Sampled(context, block.Surface(0)) == view, "the second lookup of the surface did not hit its view");
view.reset();
for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface)));
Require(!StorageImageCached(context, image), "the storage image cache never evicted the viewed image under a 1 MiB budget");
const auto evicted = TextureCacheUsage();
Require(viewed.expired(), "the sampled texture cache kept the view of an evicted storage image");
Require(evicted.sampledEntries == after.sampledEntries - 1u && evicted.sampledBytes == before.sampledBytes, "dropping the view of the evicted storage image changed other sampled entries");
auto again = Sampled(context, block.Surface(0));
Require(again->ViewsStorageImage() && again->StorageSource() != nullptr && StorageImageCached(context, again->StorageSource()), "the surface's next lookup does not view its new cached storage image");
}
void reportedBudgetTests(const Context& context) {
if (context.memoryProperties2 == nullptr) {
std::puts("VK_EXT_memory_budget unavailable: the sampled texture cache keeps a quarter of the device-local heap");
return;
}
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
properties.pNext = &reported;
context.memoryProperties2(context.physical, &properties);
const auto budget = SampledTextureBudget(context.memory, &reported, 0);
Require(budget >= (2048ull << 20u), "the sampled texture cache budget is below the 2 GiB floor");
for (std::uint32_t heap = 0; heap < context.memory.memoryHeapCount; ++heap) {
if ((context.memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) continue;
std::printf("device-local heap %u: %llu MiB, VK_EXT_memory_budget budget %llu MiB, usage %llu MiB\n", heap, static_cast<unsigned long long>(context.memory.memoryHeaps[heap].size >> 20u), static_cast<unsigned long long>(reported.heapBudget[heap] >> 20u), static_cast<unsigned long long>(reported.heapUsage[heap] >> 20u));
}
std::printf("sampled texture cache budget %llu MiB, storage image cache budget %llu MiB\n", static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(TextureCacheBudget(context.memory) >> 20u));
}
void snapshotTests(const Context& context) {
Block block(context);
auto first = Sampled(context, block.Surface(0));
Require(!first->ViewsStorageImage(), "a sampled texture outside host-imported memory is not a snapshot");
const auto held = TextureCacheUsage().sampledBytes;
Require(held == first->AllocationBytes(), "a snapshot counted " + std::to_string(held) + " bytes, not its image allocation of " + std::to_string(first->AllocationBytes()));
const std::weak_ptr<Texture> oldest = first;
first.reset();
const auto capacity = static_cast<std::size_t>(Budget / held);
Require(capacity >= 2u && capacity + 1u < Surfaces, "a snapshot of the 256 KiB surface counts " + std::to_string(held) + " bytes, which the test's 1 MiB budget does not fit as planned");
for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface));
Require(!oldest.expired() && TextureCacheUsage().sampledEntries == capacity, "the sampled texture cache did not keep " + std::to_string(capacity) + " snapshots within its budget");
Sampled(context, block.Surface(capacity));
Require(oldest.expired(), "the least recently used snapshot was not evicted past the budget");
Require(TextureCacheUsage().sampledBytes <= Budget, "the sampled texture cache holds more than its budget");
ClearCachedTextures(context.device);
constexpr std::uint32_t TinySide = 16;
const auto tiny = Sampled(context, block.Surface(0), TinySide);
const auto guestBytes = DescribeSurface(Resource(block.Surface(0), TinySide)).guestBytes;
if (tiny->AllocationBytes() >= guestBytes) {
std::printf("the 16x16 snapshot's image takes %llu bytes, not less than its %llu guest bytes: counting the allocation alone not tested\n", static_cast<unsigned long long>(tiny->AllocationBytes()), static_cast<unsigned long long>(guestBytes));
return;
}
Require(TextureCacheUsage().sampledBytes == tiny->AllocationBytes(), "a snapshot whose " + std::to_string(tiny->AllocationBytes()) + "-byte image is smaller than its " + std::to_string(guestBytes) + " guest bytes counted " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes, not its allocation");
}
constexpr std::uint64_t ReportedBudget = 64ull << 30u;
constexpr std::uint64_t ReportedUsage = 12ull << 30u;
void VKAPI_PTR ReportBudget(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) {
auto* reported = static_cast<VkPhysicalDeviceMemoryBudgetPropertiesEXT*>(properties->pNext);
for (std::uint32_t heap = 0; heap < VK_MAX_MEMORY_HEAPS; ++heap) {
reported->heapBudget[heap] = ReportedBudget;
reported->heapUsage[heap] = ReportedUsage;
}
}
std::uint64_t ExpectedBudget(const Context& context) {
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported};
ReportBudget(context.physical, &properties);
return SampledTextureBudget(context.memory, &reported, SampledTextureMemory() + TextureCacheUsage().storageBytes);
}
std::uint64_t ReadBudget(const Context& context) {
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
return SampledTextureCacheBudget(context);
}
void memoryTests(Context context) {
context.memoryProperties2 = &ReportBudget;
const auto before = SampledTextureMemory();
Block block(context);
auto held = Sampled(context, block.Surface(0));
Require(!held->ViewsStorageImage() && held->AllocationBytes() != 0, "the test texture is not a snapshot with an image of its own");
Require(SampledTextureMemory() == before + held->AllocationBytes(), "the sampled texture memory grew by " + std::to_string(SampledTextureMemory() - before) + " bytes, not by the snapshot's allocation of " + std::to_string(held->AllocationBytes()));
Require(ReadBudget(context) == ExpectedBudget(context), "the sampled texture cache budget does not count its cached texture as the cache's own memory");
ClearCachedTextures(context.device);
Require(TextureCacheUsage().sampledEntries == 0 && SampledTextureMemory() == before + held->AllocationBytes(), "a texture its caller still holds stopped counting as sampled texture memory when it left the cache");
const auto budget = ReadBudget(context);
Require(budget == ExpectedBudget(context), "the sampled texture cache budget counts a texture its caller still holds as memory in use outside the texture caches");
std::printf("reported heap budget %llu MiB with %llu MiB in use: sampled texture cache budget %llu MiB with a %llu KiB texture held outside the cache\n", static_cast<unsigned long long>(ReportedBudget >> 20u), static_cast<unsigned long long>(ReportedUsage >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(held->AllocationBytes() >> 10u));
held.reset();
Require(SampledTextureMemory() == before, "a released texture still counts as sampled texture memory");
}
}
int main(int argc, char** argv) {
try {
const bool memory = argc > 1 && std::string(argv[1]) == "memory";
#ifdef _WIN32
if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1");
#else
if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1);
#endif
std::unique_ptr<Device> device;
try {
device = std::make_unique<Device>();
} 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 VulkanTestSkipped;
}
auto context = device->GetContext();
TextureDetiler detiler(context);
context.detiler = &detiler;
if (memory) {
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
memoryTests(context);
ClearCachedTextures(context.device);
std::puts("sampled texture memory budget tests passed");
return 0;
}
{
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
reportedBudgetTests(context);
snapshotTests(context);
viewTests(context);
ClearCachedTextures(context.device);
}
std::puts("sampled texture cache budget tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,214 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <SDL_loadso.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Check;
using AgcDriver::Graphics::Require;
using AgcDriver::Graphics::StorageTexture;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Side = 4096;
constexpr std::uint32_t Format32UInt = 20;
constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2D = 9;
constexpr std::uint64_t SurfaceBytes = std::uint64_t{Side} * Side * 4u;
constexpr std::uint64_t Stride = 0x10000;
constexpr std::uint64_t FixedBudget = 2048ull << 20u;
constexpr std::uint64_t EvictionLimit = 4ull << 30u;
constexpr std::uint32_t MaxSurfaces = 80;
alignas(256) constexpr std::array<std::uint32_t, 10> LoadStoreCode{
0x7e040280, 0x7e060280, 0xf0001108, 0x00010402, 0xbf8c3f70, 0xf0200108, 0x00010402, 0xe0700000, 0x80000400, 0xbf810000,
};
alignas(256) std::array<std::uint32_t, 64> Output{};
std::uint32_t Marker(std::uint32_t surface) {
return 0x5a000000u | surface;
}
template<typename TFunction>
TFunction InstanceFunction(PFN_vkGetInstanceProcAddr resolve, VkInstance instance, const char* name) {
const auto result = reinterpret_cast<TFunction>(resolve(instance, name));
Require(result != nullptr, name);
return result;
}
struct SelectedDevice {
VkPhysicalDeviceMemoryProperties memory{};
VkPhysicalDeviceType type = VK_PHYSICAL_DEVICE_TYPE_OTHER;
};
SelectedDevice SelectDevice() {
#ifdef _WIN32
void* library = SDL_LoadObject("vulkan-1.dll");
#else
void* library = SDL_LoadObject("libvulkan.so.1");
#endif
Require(library != nullptr, "cannot load Vulkan");
const auto resolve = reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_LoadFunction(library, "vkGetInstanceProcAddr"));
VkInstance instance = VK_NULL_HANDLE;
SelectedDevice chosen;
try {
Require(resolve != 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(InstanceFunction<PFN_vkCreateInstance>(resolve, VK_NULL_HANDLE, "vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance");
const auto enumerate = InstanceFunction<PFN_vkEnumeratePhysicalDevices>(resolve, instance, "vkEnumeratePhysicalDevices");
std::uint32_t count = 0;
Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices");
std::vector<VkPhysicalDevice> devices(count);
Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices");
devices.resize(count);
const auto properties = InstanceFunction<PFN_vkGetPhysicalDeviceProperties>(resolve, instance, "vkGetPhysicalDeviceProperties");
const auto queues = InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(resolve, instance, "vkGetPhysicalDeviceQueueFamilyProperties");
constexpr auto graphicsCompute = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT;
VkPhysicalDevice selected = VK_NULL_HANDLE;
int selectedRank = -1;
for (const auto physical : devices) {
VkPhysicalDeviceProperties described{};
properties(physical, &described);
const int rank = described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 3 : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 2 : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU ? 1 : 0;
if (described.apiVersion < VK_API_VERSION_1_1 || rank <= selectedRank) continue;
std::uint32_t familyCount = 0;
queues(physical, &familyCount, nullptr);
std::vector<VkQueueFamilyProperties> families(familyCount);
queues(physical, &familyCount, families.data());
if (std::none_of(families.begin(), families.end(), [](const VkQueueFamilyProperties& family) { return family.queueCount != 0 && (family.queueFlags & graphicsCompute) == graphicsCompute; })) continue;
selected = physical;
selectedRank = rank;
chosen.type = described.deviceType;
}
Require(selected != VK_NULL_HANDLE, "no Vulkan 1.1 graphics and compute device");
InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties>(resolve, instance, "vkGetPhysicalDeviceMemoryProperties")(selected, &chosen.memory);
} catch (...) {
if (instance != VK_NULL_HANDLE) InstanceFunction<PFN_vkDestroyInstance>(resolve, instance, "vkDestroyInstance")(instance, nullptr);
SDL_UnloadObject(library);
throw;
}
InstanceFunction<PFN_vkDestroyInstance>(resolve, instance, "vkDestroyInstance")(instance, nullptr);
SDL_UnloadObject(library);
return chosen;
}
std::array<std::uint32_t, 8> TextureDescriptor(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,
};
}
std::shared_ptr<StorageTexture> Load(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface) {
const auto address = base + surface * Stride;
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(16, 0u);
const auto output = reinterpret_cast<std::uintptr_t>(Output.data());
const std::array<std::uint32_t, 4> buffer{static_cast<std::uint32_t>(output), static_cast<std::uint32_t>((output >> 32u) & 0xffffu), static_cast<std::uint32_t>(Output.size() * 4u), 0x31016facu};
const auto texture = TextureDescriptor(address);
std::copy(buffer.begin(), buffer.end(), userData.begin());
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(LoadStoreCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
StorageTexture::FlushPending(address, static_cast<std::size_t>(SurfaceBytes), nullptr, "test");
device.WaitIdle();
Require(Output[0] == Marker(surface), "surface " + std::to_string(surface) + ": the shader read " + std::to_string(Output[0]) + " instead of its first texel");
return StorageTexture::FindLive(address, SurfaceBytes);
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto selected = SelectDevice();
const auto budget = AgcDriver::Graphics::TextureCacheBudget(selected.memory);
std::vector<std::uint32_t> storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u);
auto* texels = reinterpret_cast<std::uint32_t*>((reinterpret_cast<std::uintptr_t>(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u});
for (std::uint32_t surface = 0; surface < MaxSurfaces; ++surface) texels[surface * Stride / 4u] = Marker(surface);
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
const auto blockBytes = static_cast<std::size_t>(SurfaceBytes + (MaxSurfaces - 1u) * Stride);
{
GuestAllocations::Mutation mutation;
mutation.Add(texels, blockBytes, true, true);
}
const auto release = [&] {
AgcDriver::Graphics::ClearCachedTextures(device->Device());
GuestAllocations::Mutation mutation;
mutation.Remove(texels);
};
try {
const auto first = Load(*device, base, 0);
Require(first != nullptr, "surface 0 has no cached storage image");
const auto held = std::max<std::uint64_t>(first->AllocationBytes(), first->GuestBytes());
const auto reused = static_cast<std::uint32_t>(FixedBudget / held) + 1u;
std::printf("texture cache budget %llu MiB; each 4096x4096 32_UINT surface holds %llu MiB; cyclic set of %u surfaces (%llu MiB)\n", static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(held >> 20u), reused, static_cast<unsigned long long>((reused * held) >> 20u));
if (budget < reused * held) {
std::printf("skipped, the device-local heap gives the texture caches less than the cyclic set\n");
release();
return VulkanTestSkipped;
}
std::vector<std::weak_ptr<StorageTexture>> images(reused);
images[0] = first;
for (std::uint32_t surface = 1; surface < reused; ++surface) images[surface] = Load(*device, base, surface);
std::uint32_t remade = 0;
for (std::uint32_t surface = 0; surface < reused; ++surface) {
const auto again = Load(*device, base, surface);
if (again == nullptr || again != images[surface].lock()) ++remade;
}
Require(remade == 0, std::to_string(remade) + " of " + std::to_string(reused) + " storage images were made again on the second pass over a " + std::to_string((reused * held) >> 20u) + " MiB cyclic set under a " + std::to_string(budget >> 20u) + " MiB budget");
const auto capacity = static_cast<std::uint32_t>(budget / held);
if (selected.type == VK_PHYSICAL_DEVICE_TYPE_CPU || budget > EvictionLimit || capacity + 2u > MaxSurfaces) {
std::printf("eviction past the budget not tested on a CPU device or over a 4 GiB budget\n");
} else {
for (std::uint32_t surface = reused; surface < capacity + 2u; ++surface) Load(*device, base, surface);
for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) {
const bool cached = StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) != nullptr;
Require(cached == (surface >= 2u), "surface " + std::to_string(surface) + (cached ? " is still cached" : " was evicted") + " after " + std::to_string(capacity + 2u) + " surfaces under a budget of " + std::to_string(capacity) + " of them");
}
}
} catch (...) {
release();
throw;
}
release();
std::puts("storage cache budget tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}