fix(agc): recycle descriptor pools after their last set is released
Allocate cached sets without individual-free pools. Reset a pool after its last owner releases it. Keep stable pool indices, skip full pools, and terminate if reset fails. No GPU wait is added. This avoids RADV 26.2.2's affected individual-free allocation path. Validation: Fedora 44 Release graphics tests pass. The reset-failure regression fails on the previous behavior and passes on this change. llvmpipe and RX 6600 pass the 3,301-set lifecycle test with core and synchronization validation. Windows was not tested. AI-assisted: yes.
This commit is contained in:
parent
1c75f4d228
commit
4e7ca3c760
|
|
@ -1037,8 +1037,9 @@ if(BUILD_TESTING)
|
|||
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)
|
||||
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
|
|
|
|||
|
|
@ -51,15 +51,6 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres
|
|||
// a storage image other than `except` has results pending in [address, address + bytes).
|
||||
bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except);
|
||||
|
||||
// Per-device descriptor objects shared by ShaderResources builds: set layouts by their binding list
|
||||
// (immutable; kept until the device is torn down, which Vulkan allows even for pipeline layouts made
|
||||
// from them) and a chain of descriptor pools that sets are freed back to, so a build creates and
|
||||
// destroys no layout or pool of its own. A set lives exactly as long as its ShaderResources, which
|
||||
// its batch keeps until the GPU completed. The cache locks itself: stage A of a dispatch build
|
||||
// (ShaderResources::buildPrepare, from VulkanDevice::PrepareDispatch) takes layouts and sets from it
|
||||
// without GuestMemory::GpuMutex, while other threads free and update sets of the same pools under
|
||||
// that lock (Vulkan synchronizes the pool for allocate/free and the set for update, so that is
|
||||
// legal). APS5_NO_LAYOUT_CACHE=1 / APS5_NO_POOL_CACHE=1 restore the per-build objects.
|
||||
class DescriptorCache {
|
||||
public:
|
||||
explicit DescriptorCache(const Context& context);
|
||||
|
|
@ -73,6 +64,8 @@ public:
|
|||
struct SetAllocation {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
std::size_t poolIndex = 0;
|
||||
bool updateAfterBind = false;
|
||||
};
|
||||
SetAllocation Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes, bool updateAfterBind = false);
|
||||
void Free(const SetAllocation& allocation) noexcept;
|
||||
|
|
@ -90,11 +83,16 @@ private:
|
|||
Context context;
|
||||
PFN_vkDestroyDescriptorSetLayout destroyLayout;
|
||||
PFN_vkDestroyDescriptorPool destroyPool;
|
||||
PFN_vkFreeDescriptorSets freeSets;
|
||||
PFN_vkResetDescriptorPool resetPool;
|
||||
mutable std::mutex mutex;
|
||||
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
|
||||
std::vector<VkDescriptorPool> pools;
|
||||
std::vector<VkDescriptorPool> updateAfterBindPools;
|
||||
struct Pool {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
std::size_t liveSets = 0;
|
||||
bool exhausted = false;
|
||||
};
|
||||
std::vector<Pool> pools;
|
||||
std::vector<Pool> updateAfterBindPools;
|
||||
std::vector<VkDescriptorPool> dedicated;
|
||||
Stats stats;
|
||||
};
|
||||
|
|
@ -424,7 +422,7 @@ private:
|
|||
VkDescriptorSet _set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
// The cache pool the set was allocated from, freed back to it on release.
|
||||
VkDescriptorPool cachePool = VK_NULL_HANDLE;
|
||||
DescriptorCache::SetAllocation cacheAllocation;
|
||||
std::vector<Allocation> allocations;
|
||||
std::vector<VkDescriptorBufferInfo> descriptorBuffers;
|
||||
std::vector<VkDescriptorImageInfo> descriptorImages;
|
||||
|
|
|
|||
|
|
@ -1420,7 +1420,7 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
if (context.descriptorCache != nullptr && !noPoolCache) {
|
||||
const auto allocated = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind);
|
||||
_set = allocated.set;
|
||||
cachePool = allocated.pool;
|
||||
cacheAllocation = allocated;
|
||||
}
|
||||
if (_set == VK_NULL_HANDLE) {
|
||||
pool = CreateSetPool(context, sizes, updateAfterBind);
|
||||
|
|
@ -2587,11 +2587,11 @@ ResourceCache& SharedResourceCache() {
|
|||
return *cache;
|
||||
}
|
||||
|
||||
DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")), destroyPool(context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")), freeSets(context.Function<PFN_vkFreeDescriptorSets>("vkFreeDescriptorSets")) {}
|
||||
DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")), destroyPool(context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")), resetPool(context.Function<PFN_vkResetDescriptorPool>("vkResetDescriptorPool")) {}
|
||||
|
||||
DescriptorCache::~DescriptorCache() {
|
||||
for (const auto pool : pools) destroyPool(context.device, pool, nullptr);
|
||||
for (const auto pool : updateAfterBindPools) destroyPool(context.device, pool, nullptr);
|
||||
for (const auto& pool : pools) destroyPool(context.device, pool.handle, nullptr);
|
||||
for (const auto& pool : updateAfterBindPools) destroyPool(context.device, pool.handle, nullptr);
|
||||
for (const auto pool : dedicated) destroyPool(context.device, pool, nullptr);
|
||||
for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr);
|
||||
}
|
||||
|
|
@ -2655,33 +2655,36 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l
|
|||
++stats.sets;
|
||||
return {set, pool};
|
||||
}
|
||||
// Newest pool first: it has the most room; a full or fragmented pool is left for its sets to
|
||||
// drain and tried again later.
|
||||
auto& chain = updateAfterBind ? updateAfterBindPools : pools;
|
||||
for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
|
||||
allocation.descriptorPool = *it;
|
||||
for (std::size_t index = chain.size(); index != 0; --index) {
|
||||
auto& pool = chain[index - 1];
|
||||
if (pool.exhausted) continue;
|
||||
allocation.descriptorPool = pool.handle;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
const auto result = allocate(context.device, &allocation, &set);
|
||||
if (result == VK_SUCCESS) {
|
||||
++stats.sets;
|
||||
return {set, *it};
|
||||
++pool.liveSets;
|
||||
return {set, pool.handle, index - 1, updateAfterBind};
|
||||
}
|
||||
if (result != VK_ERROR_OUT_OF_POOL_MEMORY && result != VK_ERROR_FRAGMENTED_POOL) Check(result, "vkAllocateDescriptorSets");
|
||||
pool.exhausted = true;
|
||||
}
|
||||
VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT | (updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0);
|
||||
poolInfo.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0;
|
||||
poolInfo.maxSets = ChainPoolSets;
|
||||
poolInfo.poolSizeCount = static_cast<std::uint32_t>(ChainPoolCreateSizes.size());
|
||||
poolInfo.pPoolSizes = ChainPoolCreateSizes.data();
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
|
||||
chain.push_back(pool);
|
||||
chain.push_back({pool});
|
||||
++stats.pools;
|
||||
allocation.descriptorPool = pool;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
Check(allocate(context.device, &allocation, &set), "vkAllocateDescriptorSets");
|
||||
chain.back().liveSets = 1;
|
||||
++stats.sets;
|
||||
return {set, pool};
|
||||
return {set, pool, chain.size() - 1, updateAfterBind};
|
||||
}
|
||||
|
||||
void DescriptorCache::Free(const SetAllocation& allocation) noexcept {
|
||||
|
|
@ -2692,7 +2695,13 @@ void DescriptorCache::Free(const SetAllocation& allocation) noexcept {
|
|||
destroyPool(context.device, allocation.pool, nullptr);
|
||||
return;
|
||||
}
|
||||
static_cast<void>(freeSets(context.device, allocation.pool, 1, &allocation.set));
|
||||
auto& chain = allocation.updateAfterBind ? updateAfterBindPools : pools;
|
||||
if (allocation.poolIndex >= chain.size()) std::terminate();
|
||||
auto& pool = chain[allocation.poolIndex];
|
||||
if (pool.handle != allocation.pool || pool.liveSets == 0) std::terminate();
|
||||
if (--pool.liveSets != 0) return;
|
||||
if (resetPool(context.device, pool.handle, 0) != VK_SUCCESS) std::terminate();
|
||||
pool.exhausted = false;
|
||||
}
|
||||
|
||||
DescriptorCache::Stats DescriptorCache::Counters() const {
|
||||
|
|
@ -3274,10 +3283,10 @@ ShaderResources::~ShaderResources() {
|
|||
|
||||
void ShaderResources::release() noexcept {
|
||||
// A set from the cache's pool chain goes back to it; a dedicated pool dies with its set.
|
||||
if (cachePool && context.descriptorCache != nullptr) context.descriptorCache->Free({_set, cachePool});
|
||||
if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation);
|
||||
if (pool) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
|
||||
if (_layout && ownsLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr);
|
||||
cachePool = VK_NULL_HANDLE;
|
||||
cacheAllocation = {};
|
||||
pool = VK_NULL_HANDLE;
|
||||
_set = VK_NULL_HANDLE;
|
||||
_layout = VK_NULL_HANDLE;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
|
|
@ -1673,7 +1674,16 @@ struct MockVulkan {
|
|||
std::vector<VkDescriptorPoolSize> poolSizes;
|
||||
std::uint32_t poolMaxSets = 0;
|
||||
VkDescriptorPoolCreateFlags poolFlags = 0;
|
||||
std::uint32_t freedSets = 0;
|
||||
struct DescriptorPool {
|
||||
VkDescriptorPoolCreateFlags flags;
|
||||
std::uint32_t maxSets;
|
||||
std::uint32_t allocated = 0;
|
||||
std::uint32_t attempts = 0;
|
||||
std::uint32_t resets = 0;
|
||||
};
|
||||
std::map<VkDescriptorPool, DescriptorPool> descriptorPools;
|
||||
std::uint32_t descriptorPoolLimit = 1000000;
|
||||
VkResult descriptorResetResult = VK_SUCCESS;
|
||||
std::vector<MockDescriptorWrite> writes;
|
||||
std::vector<VkCopyDescriptorSet> copies;
|
||||
bool allowCopies = false;
|
||||
|
|
@ -1777,22 +1787,33 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorPool(VkDevice, const VkDescri
|
|||
mock.poolSizes.assign(info->pPoolSizes, info->pPoolSizes + info->poolSizeCount);
|
||||
mock.poolMaxSets = info->maxSets;
|
||||
mock.poolFlags = info->flags;
|
||||
mock.descriptorPools.emplace(*pool, MockVulkan::DescriptorPool{info->flags, std::min(info->maxSets, mock.descriptorPoolLimit)});
|
||||
++mock.live;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) {
|
||||
VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool pool, const VkAllocationCallbacks*) {
|
||||
mock.descriptorPools.erase(pool);
|
||||
--mock.live;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateDescriptorSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* sets) {
|
||||
Require(info->descriptorSetCount == 1, "exactly one descriptor set must be allocated");
|
||||
auto& pool = mock.descriptorPools.at(info->descriptorPool);
|
||||
++pool.attempts;
|
||||
if (pool.allocated == pool.maxSets) return VK_ERROR_OUT_OF_POOL_MEMORY;
|
||||
++pool.allocated;
|
||||
sets[0] = makeHandle<VkDescriptorSet>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool, std::uint32_t count, const VkDescriptorSet*) {
|
||||
mock.freedSets += count;
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPool handle, VkDescriptorPoolResetFlags) {
|
||||
auto& pool = mock.descriptorPools.at(handle);
|
||||
const auto result = mock.descriptorResetResult;
|
||||
mock.descriptorResetResult = VK_SUCCESS;
|
||||
if (result != VK_SUCCESS) return result;
|
||||
pool.allocated = 0;
|
||||
++pool.resets;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
|
|
@ -1938,7 +1959,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
|
|||
{"vkCreateDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorPool)},
|
||||
{"vkDestroyDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorPool)},
|
||||
{"vkAllocateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateDescriptorSets)},
|
||||
{"vkFreeDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockFreeDescriptorSets)},
|
||||
{"vkResetDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockResetDescriptorPool)},
|
||||
{"vkUpdateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockUpdateDescriptorSets)},
|
||||
{"vkCmdBindDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindDescriptorSets)},
|
||||
{"vkCreatePipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(mockCreatePipelineLayout)},
|
||||
|
|
@ -2054,6 +2075,60 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st
|
|||
Require(mock.live == 0, std::string(what) + " leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void descriptorPoolTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
mock.descriptorPoolLimit = 2;
|
||||
{
|
||||
DescriptorCache cache(mockContext());
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags};
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1));
|
||||
const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount};
|
||||
const auto allocate = [&] { return cache.Allocate(layout, std::span(&size, 1)); };
|
||||
const auto first = allocate();
|
||||
const auto second = allocate();
|
||||
Require(first.pool == second.pool, "descriptor allocations did not share a pool");
|
||||
Require(mock.descriptorPools.at(first.pool).flags == 0, "descriptor pools use the individually freed allocation path");
|
||||
const auto third = allocate();
|
||||
const auto fourth = allocate();
|
||||
const auto fifth = allocate();
|
||||
Require(third.pool == fourth.pool && third.pool != first.pool && fifth.pool != third.pool, "exhausted descriptor pools did not advance the chain");
|
||||
Require(mock.descriptorPools.at(first.pool).attempts == 3, "an exhausted descriptor pool was retried before reset");
|
||||
cache.Free(first);
|
||||
Require(mock.descriptorPools.at(first.pool).resets == 0, "a descriptor pool reset while another set was live");
|
||||
cache.Free(second);
|
||||
Require(mock.descriptorPools.at(first.pool).resets == 1, "an empty descriptor pool was not reset");
|
||||
const auto sixth = allocate();
|
||||
const auto seventh = allocate();
|
||||
Require(sixth.pool == fifth.pool && seventh.pool == first.pool && cache.Counters().pools == 3, "an empty descriptor pool was not recycled");
|
||||
cache.Free(third);
|
||||
cache.Free(fourth);
|
||||
cache.Free(fifth);
|
||||
cache.Free(sixth);
|
||||
cache.Free(seventh);
|
||||
Require(mock.descriptorPools.at(first.pool).resets == 2 && mock.descriptorPools.at(third.pool).resets == 1 && mock.descriptorPools.at(fifth.pool).resets == 1, "descriptor pool ownership was lost after recycling");
|
||||
}
|
||||
Require(mock.live == 0 && mock.descriptorPools.empty(), "descriptor pool cache leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void descriptorPoolResetFailureTest() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
DescriptorCache cache(mockContext());
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags};
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1));
|
||||
const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount};
|
||||
const auto allocation = cache.Allocate(layout, std::span(&size, 1));
|
||||
mock.descriptorResetResult = VK_ERROR_DEVICE_LOST;
|
||||
const auto previous = std::set_terminate([] {
|
||||
std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1);
|
||||
});
|
||||
cache.Free(allocation);
|
||||
std::set_terminate(previous);
|
||||
}
|
||||
|
||||
void pushConstantTests() {
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
ShaderRecompiler::RecompileResult fragment;
|
||||
|
|
@ -2372,12 +2447,23 @@ void descriptorCacheTests() {
|
|||
Require(mock.poolSizes.size() == 2 && mock.poolSizes[0].type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && mock.poolSizes[0].descriptorCount == 4097 && mock.poolSizes[1].type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE && mock.poolSizes[1].descriptorCount == 3, "the dedicated pool is not sized to its set");
|
||||
Require(cache.Counters().pools == 0 && cache.Counters().sets == 1, "an oversized set was counted in the chain pools");
|
||||
cache.Free(dedicated);
|
||||
Require(mock.live == live && mock.freedSets == 0, "freeing an oversized set did not destroy its pool");
|
||||
Require(mock.live == live, "freeing an oversized set did not destroy its pool");
|
||||
const std::array<VkDescriptorPoolSize, 1> fitting{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}};
|
||||
const auto chained = cache.Allocate(layout(4096), fitting);
|
||||
Require(chained.set != VK_NULL_HANDLE && mock.poolMaxSets == 1024 && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT && cache.Counters().pools == 1, "a set the chain pool holds left the chain");
|
||||
Require(chained.set != VK_NULL_HANDLE && mock.poolMaxSets == 1024 && mock.poolFlags == 0 && cache.Counters().pools == 1, "a set the chain pool holds left the chain");
|
||||
cache.Free(chained);
|
||||
Require(mock.freedSets == 1 && mock.live == live + 2, "a chain set was not freed back to its pool");
|
||||
Require(mock.descriptorPools.at(chained.pool).resets == 1 && mock.live == live + 2, "a chain pool was not reset after its last set");
|
||||
const std::array<VkDescriptorPoolSize, 1> regular{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}};
|
||||
const auto first = cache.Allocate(layout(4096), regular);
|
||||
const auto oversizedMixed = cache.Allocate(large, oversized);
|
||||
const auto second = cache.Allocate(layout(4096), regular);
|
||||
Require(first.pool == chained.pool && second.pool == chained.pool && oversizedMixed.pool != chained.pool && mock.descriptorPools.at(chained.pool).resets == 1, "an oversized set disturbed the chain pool");
|
||||
cache.Free(oversizedMixed);
|
||||
Require(mock.live == live + 2 && mock.descriptorPools.count(oversizedMixed.pool) == 0 && mock.descriptorPools.at(chained.pool).resets == 1, "freeing an oversized set between chain sets touched the chain pool");
|
||||
cache.Free(first);
|
||||
Require(mock.descriptorPools.at(chained.pool).resets == 1, "a chain pool reset while a set was live");
|
||||
cache.Free(second);
|
||||
Require(mock.descriptorPools.at(chained.pool).resets == 2, "a chain pool was not reset after the oversized set was freed");
|
||||
const std::array<VkDescriptorPoolSize, 1> beyondDevice{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 8193}}};
|
||||
expectFailure([&] { cache.Allocate(large, beyondDevice); }, "descriptor set exceeds the device's per-set descriptor limit");
|
||||
const std::array<VkDescriptorPoolSize, 1> storageImages{{{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}}};
|
||||
|
|
@ -4077,7 +4163,7 @@ void parallelCompareTests() {
|
|||
CompareSpansFrom({}, none, 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
int main(int argc, char** argv) {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_PIN_WAIT_MS", "200");
|
||||
_putenv_s("APS5_HEAP_MIRROR_MIB", "4");
|
||||
|
|
@ -4086,6 +4172,11 @@ int main() {
|
|||
setenv("APS5_HEAP_MIRROR_MIB", "4", 1);
|
||||
#endif
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") {
|
||||
descriptorPoolResetFailureTest();
|
||||
std::cerr << "descriptor pool reset failure did not terminate\n";
|
||||
return 1;
|
||||
}
|
||||
{
|
||||
const AgcDriver::Graphics::Context context{};
|
||||
const AgcDriver::Graphics::State state{};
|
||||
|
|
@ -4152,6 +4243,7 @@ int main() {
|
|||
shaderUserDataTailPaddingTests();
|
||||
InitialContextTests();
|
||||
AlphaToCoverageTests();
|
||||
descriptorPoolTests();
|
||||
pushConstantTests();
|
||||
resourceTests();
|
||||
descriptorSnapshotTests();
|
||||
|
|
|
|||
|
|
@ -4591,6 +4591,33 @@ void pipelineCacheTests(const Device& device, bool benchmark) {
|
|||
std::cout << "Pipeline cache key and retained lifetime tests passed\n";
|
||||
}
|
||||
|
||||
void descriptorPoolTests(const Device& device) {
|
||||
DescriptorCache cache(device.GetContext());
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 32, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags};
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1));
|
||||
const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount};
|
||||
const auto held = cache.Allocate(layout, std::span(&size, 1));
|
||||
std::uint64_t initialPools = 0;
|
||||
for (unsigned round = 0; round < 3; ++round) {
|
||||
std::vector<DescriptorCache::SetAllocation> sets;
|
||||
for (unsigned i = 0; i < 1100; ++i) {
|
||||
const auto allocation = cache.Allocate(layout, std::span(&size, 1));
|
||||
Require(allocation.set != VK_NULL_HANDLE, "descriptor pool rollover refused a valid layout");
|
||||
sets.push_back(allocation);
|
||||
}
|
||||
const auto pools = cache.Counters().pools;
|
||||
if (round == 0) {
|
||||
initialPools = pools;
|
||||
} else {
|
||||
Require(pools <= initialPools + 1, "descriptor pool recycling grows behind one retained set");
|
||||
}
|
||||
for (const auto& allocation : sets) cache.Free(allocation);
|
||||
}
|
||||
cache.Free(held);
|
||||
std::cout << "Descriptor pool recycling passed across 3,301 sets (" << initialPools << " initial pools, " << cache.Counters().pools << " final pools)\n";
|
||||
}
|
||||
|
||||
void keysFillTests(const Device& device, Recorder& recorder) {
|
||||
const auto& base = device.GetContext();
|
||||
if (base.hostImportAlignment == 0) {
|
||||
|
|
@ -4891,6 +4918,10 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-only") {
|
||||
descriptorPoolTests(device);
|
||||
return 0;
|
||||
}
|
||||
std::unique_lock gpu(GpuMutex());
|
||||
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
|
||||
Recorder recorder(device.GetContext());
|
||||
|
|
|
|||
|
|
@ -562,6 +562,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix.
|
||||
- [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained.
|
||||
- [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware.
|
||||
- [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured.
|
||||
- [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields
|
||||
- [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert
|
||||
- [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not.
|
||||
|
|
|
|||
Loading…
Reference in New Issue