diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d54e6fac9..c25b94d75 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -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:$") endif() if(NOT WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 43cc45c10..e386d4a95 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -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 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, VkDescriptorSetLayout> layouts; - std::vector pools; - std::vector updateAfterBindPools; + struct Pool { + VkDescriptorPool handle = VK_NULL_HANDLE; + std::size_t liveSets = 0; + bool exhausted = false; + }; + std::vector pools; + std::vector updateAfterBindPools; std::vector 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 allocations; std::vector descriptorBuffers; std::vector descriptorImages; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 72b8204af..e465c1ce7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1420,7 +1420,7 @@ void ShaderResources::buildPrepare(std::span 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("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), freeSets(context.Function("vkFreeDescriptorSets")) {} +DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("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(ChainPoolCreateSizes.size()); poolInfo.pPoolSizes = ChainPoolCreateSizes.data(); VkDescriptorPool pool = VK_NULL_HANDLE; Check(context.Function("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(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("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); - cachePool = VK_NULL_HANDLE; + cacheAllocation = {}; pool = VK_NULL_HANDLE; _set = VK_NULL_HANDLE; _layout = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9cd184dfd..f1b60ff0d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -1673,7 +1674,16 @@ struct MockVulkan { std::vector 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 descriptorPools; + std::uint32_t descriptorPoolLimit = 1000000; + VkResult descriptorResetResult = VK_SUCCESS; std::vector writes; std::vector 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(); 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(mockCreateDescriptorPool)}, {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, - {"vkFreeDescriptorSets", reinterpret_cast(mockFreeDescriptorSets)}, + {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, {"vkCreatePipelineLayout", reinterpret_cast(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 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 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 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 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 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 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(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 9b894726c..7ef1394f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -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 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 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()); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ac8f34836..d76b7bb1f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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.