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:
commit
ca2b341852
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue