From 80a2782de58938869cdb655d279c0443ad38a9d6 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Sat, 10 Oct 2026 18:06:11 +0200 Subject: [PATCH] fix(agc): reclaim idle buffers on allocation OOM --- .../Graphics/include/BufferPool.hpp | 6 +- .../Graphics/src/BufferPool.cpp | 78 +++++++- .../Graphics/src/Resources.cpp | 4 +- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 180 +++++++++++++++++- docs/dev/TechnicalDebt.md | 2 + 5 files changed, 257 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 454890253060..129abd58ad53 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -61,12 +61,13 @@ class BufferPool { // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); static VkDeviceSize SlabBlockBytes(std::size_t capacity); SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); - void PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; + VkDeviceSize PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; std::size_t SlabBlocks(); private: @@ -117,7 +118,8 @@ class BufferPool { static bool DeviceTierEnabled(); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); - void destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize trim(); // Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 2c0c3998773e..4ae8a5074ff1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -41,6 +41,30 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation) { + VkDeviceMemory memory = VK_NULL_HANDLE; + auto result = allocateMemory(device, &allocation, nullptr, &memory); + const auto initialResult = result; + unsigned attempts = 1; + VkDeviceSize reclaimed = 0; + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) { + reclaimed = trim(); + if (reclaimed != 0) { + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } + if (result == VK_SUCCESS) { + if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(initialResult), attempts); + return memory; + } + char details[512]; + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(initialResult), attempts); + Check(result, details); + return VK_NULL_HANDLE; +} + bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false; const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity); @@ -80,7 +104,7 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if (addressable) allocation.pNext = &flags; allocation.allocationSize = blockBytes; allocation.memoryTypeIndex = memoryType; - Check(allocateMemory(device, &allocation, nullptr, &block->memory), "vkAllocateMemory buffer slab"); + block->memory = AllocateMemory(allocation, "vkAllocateMemory buffer slab"); if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { void* mapping = nullptr; const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping); @@ -105,26 +129,27 @@ void BufferPool::freeBlock(SlabBlock& block) noexcept { freeMemory(device, block.memory, nullptr); } -void BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { +VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { std::unique_ptr released; { std::lock_guard lock(slabMutex); const auto found = slabBlocks.find(memory); - if (found == slabBlocks.end()) return; + if (found == slabBlocks.end()) return 0; auto& block = *found->second; auto& slab = slabs[block.slab]; if (block.free.empty()) slab.available.push_back(&block); block.free.push_back(static_cast(offset / block.slotBytes)); - if (--block.used != 0) return; + if (--block.used != 0) return 0; if (slab.emptyBlocks == 0) { ++slab.emptyBlocks; - return; + return 0; } slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); released = std::move(found->second); slabBlocks.erase(found); } freeBlock(*released); + return SlabBlockBytes(released->slotBytes); } std::size_t BufferPool::SlabBlocks() { @@ -150,16 +175,16 @@ VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& me return std::max(VkDeviceSize{512} << 20u, largest / 8u); } -void BufferPool::destroy(const BufferAllocation& allocation) noexcept { +VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { if (allocation.slab) { destroyBuffer(device, allocation.buffer, nullptr); - PutSlot(allocation.memory, allocation.offset); - return; + return PutSlot(allocation.memory, allocation.offset); } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); + return allocation.allocationBytes; } bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { @@ -240,6 +265,43 @@ std::size_t BufferPool::MaxSlots() { return slots; } +VkDeviceSize BufferPool::trim() { + Tier retiredSmall; + Tier retiredLarge; + Tier retiredDevice; + { + std::lock_guard lock(mutex); + for (const auto& [tier, retired] : {std::pair{&smallTier, &retiredSmall}, std::pair{&largeTier, &retiredLarge}, std::pair{&deviceTier, &retiredDevice}}) { + tier->free.swap(retired->free); + tier->evictions += tier->slots; + tier->retainedBytes = 0; + tier->slots = 0; + } + } + VkDeviceSize bytes = 0; + for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { + for (const auto& [key, slots] : tier->free) { + for (const auto& slot : slots) bytes += destroy(slot.allocation); + } + } + for (;;) { + std::unique_ptr released; + { + std::lock_guard lock(slabMutex); + const auto found = std::find_if(slabBlocks.begin(), slabBlocks.end(), [](const auto& entry) { return entry.second->used == 0; }); + if (found == slabBlocks.end()) break; + auto& slab = slabs.at(found->second->slab); + slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get())); + --slab.emptyBlocks; + released = std::move(found->second); + slabBlocks.erase(found); + } + freeBlock(*released); + bytes += SlabBlockBytes(released->slotBytes); + } + return bytes; +} + void BufferPool::Put(const BufferAllocation& allocation) noexcept { // Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector // may throw on growth; a Put that cannot retain simply destroys, as noexcept requires. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 361f9d540507..0c7e64d0711b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -61,7 +61,7 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag ready = true; return; } - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory"); initializeAddress(usage); if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); @@ -131,7 +131,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 8c8cb0ec7f66..f2fa6ad103e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,11 @@ struct MockDevice { VkDeviceSize liveBytes = 0; std::map memoryBytes; std::map> bound; + std::set liveBuffers; + std::deque results; + VkDeviceSize limit = 0; + VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; + std::uint64_t attempts = 0; }; MockDevice mock; @@ -45,6 +51,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn *buffer = reinterpret_cast(static_cast(mock.next++)); mock.sizes[*buffer] = info->size; mock.usages[*buffer] = info->usage; + mock.liveBuffers.insert(*buffer); return VK_SUCCESS; } @@ -53,6 +60,17 @@ VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer bu } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + ++mock.attempts; + auto result = VK_SUCCESS; + if (!mock.results.empty()) { + result = mock.results.front(); + mock.results.pop_front(); + } + if (result == VK_SUCCESS && mock.limit != 0 && mock.liveBytes + info->allocationSize > mock.limit) result = mock.pressureResult; + if (result != VK_SUCCESS) { + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return result; + } *memory = reinterpret_cast(static_cast(mock.next++)); if (info->memoryTypeIndex == 1) mock.hostMemory[*memory].resize(info->allocationSize); mock.memoryBytes[*memory] = info->allocationSize; @@ -74,11 +92,16 @@ VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, Vk VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {} -VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); ++mock.destroyedBuffers; } VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + if (!mock.memoryBytes.contains(memory)) { + Expect(false, "an undefined failed allocation output was freed"); + return; + } mock.liveBytes -= mock.memoryBytes.at(memory); mock.memoryBytes.erase(memory); mock.hostMemory.erase(memory); @@ -309,6 +332,149 @@ void Slabs() { Expect(mock.liveBytes == 0, "slab blocks outlived their pool"); } +void AllocationRecovery(VkResult pressureResult, bool host) { + mock = MockDevice{}; + { + auto context = mockContext(); + const auto make = [&](std::size_t bytes) -> std::shared_ptr { + if (host) return std::make_shared(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + return std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + }; + auto active = make(2 * MiB); + const auto activeBuffer = *mock.liveBuffers.begin(); + const auto activeMemory = mock.memoryBytes.begin()->first; + { auto idle = make(2 * MiB); } + mock.limit = 6 * MiB; + mock.pressureResult = pressureResult; + const auto before = mock.attempts; + try { + auto recovered = make(4 * MiB); + Expect(mock.attempts == before + 2, "pool pressure did not trigger exactly one allocation retry"); + Expect(mock.frees == 1 && mock.liveBytes == 6 * MiB, "recovery did not release the idle allocation"); + Expect(mock.liveBuffers.contains(activeBuffer) && mock.memoryBytes.contains(activeMemory), "recovery destroyed an active allocation"); + } catch (const std::exception& error) { + Expect(false, std::string("idle memory did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "recovery leaked a resource"); +} + +void SlabAllocationRecovery() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "a new slab block did not retry after reclaiming an idle allocation"); + Expect(context.bufferPool->SlabBlocks() == 2 && mock.memoryBytes.contains(activeMemory), "slab allocation recovery discarded its live block"); + } catch (const std::exception& error) { + Expect(false, std::string("slab allocation did not recover: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab recovery leaked a resource"); +} + +void ReclaimSlabs() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + std::memset(active.Bytes().data(), 0x5a, active.Bytes().size()); + { + Buffer sameBlock(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer otherBlock(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } + mock.limit = 6 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 4 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "reclaim did not free the unused slab block"); + Expect(context.bufferPool->SlabBlocks() == 1 && mock.memoryBytes.contains(activeMemory), "reclaim freed a slab with a live slot"); + Expect(std::all_of(active.Bytes().begin(), active.Bytes().end(), [](std::byte value) { return value == std::byte{0x5a}; }), "reclaim changed a live slab buffer"); + } catch (const std::exception& error) { + Expect(false, std::string("empty slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab reclaim leaked a resource"); + mock = MockDevice{}; + { + auto context = mockContext(); + BufferPool pool(context); + const auto idle = pool.TakeSlot(context, 1, 512 * 1024, false); + pool.PutSlot(idle.memory, idle.offset); + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = pool.TakeSlot(context, 1, 256 * 1024, false); + Expect(mock.attempts == before + 2 && mock.frees == 1 && pool.SlabBlocks() == 1, "a spare slab without cached buffers was not reclaimed"); + pool.PutSlot(recovered.memory, recovered.offset); + } catch (const std::exception& error) { + Expect(false, std::string("spare slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0, "spare slab recovery leaked device memory"); +} + +void AllocationFailure(VkResult initial, VkResult final, bool cached) { + mock = MockDevice{}; + { + auto context = mockContext(); + if (cached) { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.results = {initial, final}; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + const bool reclaimed = cached && (initial == VK_ERROR_OUT_OF_DEVICE_MEMORY || initial == VK_ERROR_OUT_OF_HOST_MEMORY); + Expect(mock.attempts == before + (reclaimed ? 2 : 1), "allocation retries were not bounded or a non-memory failure was retried"); + Expect(failure.find("Vulkan result " + std::to_string(reclaimed ? final : initial)) != std::string::npos, "allocation failure lost its final Vulkan result"); + Expect(mock.liveBuffers.size() == (cached && !reclaimed ? 1u : 0u), "a failed allocation retained its buffer"); + Expect(mock.liveBytes == (cached && !reclaimed ? 2 * MiB : 0), "a failed allocation mishandled cached memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource"); +} + +void SuccessfulAllocationKeepsPool() { + mock = MockDevice{}; + { + auto context = mockContext(); + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == 2 && mock.frees == 0 && mock.liveBytes == 6 * MiB, "a successful first allocation reclaimed its cache"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "normal allocation leaked a resource"); +} + +void PartialSlabDoesNotRetry() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + { Buffer idle(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + Buffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(failure.find("Vulkan result -2") != std::string::npos, "a live slab's memory was treated as reclaimable"); + Expect(mock.attempts == before + 1 && mock.frees == 0, "an allocation retried without freeing device memory"); + Expect(mock.liveBuffers.size() == 1 && mock.liveBuffers.contains(active.Handle()) && context.bufferPool->SlabBlocks() == 1, "reclaim changed a live slab allocation"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource"); +} + } int main() { @@ -320,6 +486,18 @@ int main() { AddressAndHostUnchanged(); SmallDeviceClasses(); Slabs(); + for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { + AllocationRecovery(result, false); + AllocationRecovery(result, true); + AllocationFailure(result, result, true); + AllocationFailure(result, result, false); + } + SlabAllocationRecovery(); + ReclaimSlabs(); + AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true); + AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); + SuccessfulAllocationKeepsPool(); + PartialSlabDoesNotRetry(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4656d0fbbc31..6cda874f80c4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -284,6 +284,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. + - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI.