diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e4c0..5de7c6f8f234 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -329,4 +329,13 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + + add_test_executable(MemoryPoolTest + tests/MemoryPoolTest.cpp + prx/libkernel/DirectMemory/MemoryPool.cpp + ) + target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + configure_windows_unwind(MemoryPoolTest) + add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) endif() diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index d7c6c65d7624..0a6ed6e7490d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,10 +1,75 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include +#include +#include #include #include +#include +#include +#include -static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; +#ifndef SCE_KERNEL_ERROR_EINVAL +#define SCE_KERNEL_ERROR_EINVAL static_cast(0x80020016u) +#endif + +#ifndef SCE_KERNEL_ERROR_EFAULT +#define SCE_KERNEL_ERROR_EFAULT static_cast(0x8002000Eu) +#endif + +#ifndef SCE_KERNEL_ERROR_EAGAIN +#define SCE_KERNEL_ERROR_EAGAIN static_cast(0x80020023u) +#endif + +#ifndef SCE_KERNEL_ERROR_ENOMEM +#define SCE_KERNEL_ERROR_ENOMEM static_cast(0x8002000Cu) +#endif + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPoolBytes = static_cast(DIRECT_MEMORY_SIZE); +constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); + +static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); +static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); + +bool AlignUp(uint64_t v, uint64_t a, uint64_t& out) { + const uint64_t m = a - 1; + if (v > UINT64_MAX - m) return false; + out = (v + m) & ~m; + return true; +} + +bool NormalizeAlignment(size_t alignment, uint64_t& out) { + const uint64_t a = (alignment == 0) ? kPage : static_cast(alignment); + if ((a & (a - 1)) != 0) return false; + out = std::max(a, kPage); + return true; +} + +bool NormalizeLength(size_t len, uint64_t& out) { + if (len == 0) return false; + uint64_t rounded; + if (!AlignUp(static_cast(len), kPage, rounded)) return false; + if (rounded > kPoolBytes) return false; + out = rounded; + return true; +} + +bool NormalizeSearchRange(int64_t searchStart, int64_t searchEnd, uint64_t& start, uint64_t& end) { + if (searchStart < 0) return false; + start = static_cast(searchStart); + end = (searchEnd < 0) ? kPoolBytes : std::min(static_cast(searchEnd), kPoolBytes); + return start < end; +} + +bool NormalizeRegion(uint64_t start, size_t len, uint64_t& alignedLen) { + if (start >= kPoolBytes || (start & (kPage - 1)) != 0) return false; + if (!NormalizeLength(len, alignedLen)) return false; + return alignedLen <= kPoolBytes - start; +} struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { @@ -13,87 +78,100 @@ struct PhysicalMemoryPool { } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + + uint64_t start, end, align, alignedLen; + if (!NormalizeSearchRange(searchStart, searchEnd, start, end)) return SCE_KERNEL_ERROR_EINVAL; + if (!NormalizeAlignment(alignment, align)) return SCE_KERNEL_ERROR_EINVAL; + if (!NormalizeLength(len, alignedLen)) return SCE_KERNEL_ERROR_EINVAL; + + uint64_t cur; + if (!AlignUp(start, align, cur)) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(_mutex); - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - uint64_t start = static_cast(searchStart); - uint64_t end = static_cast(searchEnd); - uint64_t cur = (start + align - 1) & ~(align - 1); - while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { - if (_isFree(cur, len)) { - CreateDirectMemoryBacking(static_cast(cur), len, memoryType); - _mark(cur, len, true); - _blocks[cur] = {cur + len, memoryType}; + const size_t pageCount = static_cast(alignedLen / kPage); + + while (cur <= end && alignedLen <= end - cur) { + size_t lastUsed; + if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { + _commit(cur, alignedLen, memoryType); *physOut = static_cast(cur); return 0; } - cur += align; + if (!AlignUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } return SCE_KERNEL_ERROR_EAGAIN; } - void Free(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - _free(start, len); - } + bool Free(uint64_t start, size_t len) { return _release(start, len, false); } - bool CheckedFree(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; - const size_t first = start / PS5_PAGE_SIZE; - for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) - if (!_used[first + i]) return false; - _free(start, len); - return true; - } + bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { + if (!start || !end || !memoryType || offset >= kPoolBytes) return false; std::lock_guard lock(_mutex); - auto it = _blocks.upper_bound(offset); + BlockMap::const_iterator it = _blocks.upper_bound(offset); if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); *end = static_cast(it->second.end); *memoryType = it->second.type; return true; } - void Retype(uint64_t start, size_t len, int memoryType) { + bool Retype(uint64_t start, size_t len, int memoryType) { + uint64_t alignedLen; + if (!NormalizeRegion(start, len, alignedLen)) return false; + const uint64_t end = start + alignedLen; + std::lock_guard lock(_mutex); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + if (!_anyUsed(static_cast(start / kPage), static_cast(alignedLen / kPage))) return false; + + const bool splitStart = _splitAt(start); + try { + _splitAt(end); + } catch (...) { + if (splitStart) _unsplitAt(start); + throw; + } + for (BlockMap::iterator it = _blocks.lower_bound(start); it != _blocks.end() && it->first < end; ++it) { + it->second.type = memoryType; } + return true; } bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return false; + uint64_t start, end, align; + if (!NormalizeSearchRange(searchStart, searchEnd, start, end)) return false; + if (!NormalizeAlignment(alignment, align)) return false; + + uint64_t cur; + if (!AlignUp(start, kPage, cur)) return false; + const uint64_t limit = end & ~(kPage - 1); + std::lock_guard lock(_mutex); - if (searchStart < 0 || searchEnd <= searchStart) return false; - const uint64_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - const uint64_t limit = std::min(static_cast(searchEnd), DIRECT_MEMORY_SIZE); uint64_t bestStart = 0; uint64_t bestSize = 0; - uint64_t cur = (static_cast(searchStart) + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - while (cur + PS5_PAGE_SIZE <= limit) { - if (!_isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; + + while (cur < limit) { + if (_used[cur / kPage]) { + cur += kPage; continue; } uint64_t runEnd = cur; - while (runEnd + PS5_PAGE_SIZE <= limit && _isFree(runEnd, PS5_PAGE_SIZE)) runEnd += PS5_PAGE_SIZE; - const uint64_t alignedStart = (cur + align - 1) & ~(align - 1); + while (runEnd < limit && !_used[runEnd / kPage]) runEnd += kPage; + + uint64_t alignedStart; + if (!AlignUp(cur, align, alignedStart)) break; if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { bestStart = alignedStart; bestSize = runEnd - alignedStart; } cur = runEnd; } + if (bestSize == 0) return false; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); @@ -101,82 +179,155 @@ struct PhysicalMemoryPool { } size_t FreeRun(uint64_t offset, uint64_t limit) { + const uint64_t lim = std::min(limit, kPoolBytes); + if (offset >= lim) return 0; + std::lock_guard lock(_mutex); - uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); + uint64_t cur = offset & ~(kPage - 1); size_t run = 0; - while (cur + PS5_PAGE_SIZE <= limit && cur + PS5_PAGE_SIZE <= DIRECT_MEMORY_SIZE - && _isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; + while (cur <= lim && kPage <= lim - cur && !_used[cur / kPage]) { + cur += kPage; + run += static_cast(kPage); } return run; } private: - void _free(uint64_t start, size_t len) { - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + struct Block { + uint64_t end; + int type; + }; + typedef std::map BlockMap; + + bool _lastUsedPage(size_t first, size_t count, size_t& out) const { + for (size_t i = count; i-- > 0;) { + if (_used[first + i]) { + out = first + i; + return true; + } + } + return false; + } + + bool _anyUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (_used[first + i]) return true; } + return false; } - bool _isFree(uint64_t offset, size_t len) const { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - if (_used[first + i]) return false; + bool _allUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (!_used[first + i]) return false; + } return true; } - void _mark(uint64_t offset, size_t len, bool used) { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - _used[first + i] = used; + void _mark(size_t first, size_t count, bool used) { + for (size_t i = 0; i < count; ++i) _used[first + i] = used; } - struct Block { - uint64_t end; - int type; - }; + void _commit(uint64_t start, uint64_t len, int type) { + std::pair ins = _blocks.emplace(start, Block{start + len, type}); + if (!ins.second) throw std::logic_error("DirectMemory pool: block map and page flags disagree"); + + try { + CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); + } catch (...) { + _blocks.erase(ins.first); + throw; + } + + _mark(static_cast(start / kPage), static_cast(len / kPage), true); + } + + bool _release(uint64_t start, size_t len, bool requireAllocated) { + uint64_t alignedLen; + if (!NormalizeRegion(start, len, alignedLen)) return false; + const uint64_t end = start + alignedLen; + const size_t first = static_cast(start / kPage); + const size_t count = static_cast(alignedLen / kPage); + + std::lock_guard lock(_mutex); + if (requireAllocated ? !_allUsed(first, count) : !_anyUsed(first, count)) return false; + + const bool splitStart = _splitAt(start); + bool splitEnd = false; + try { + splitEnd = _splitAt(end); + ForgetDirectMemory(static_cast(start), static_cast(alignedLen)); + } catch (...) { + if (splitEnd) _unsplitAt(end); + if (splitStart) _unsplitAt(start); + throw; + } + + _blocks.erase(_blocks.lower_bound(start), _blocks.lower_bound(end)); + _mark(first, count, false); + return true; + } + + bool _splitAt(uint64_t at) { + BlockMap::iterator it = _blocks.upper_bound(at); + if (it == _blocks.begin()) return false; + --it; + if (it->first == at || it->second.end <= at) return false; + + const Block tail = {it->second.end, it->second.type}; + _blocks.emplace(at, tail); + it->second.end = at; + return true; + } + + void _unsplitAt(uint64_t at) noexcept { + BlockMap::iterator right = _blocks.find(at); + BlockMap::iterator left = std::prev(right); + left->second.end = right->second.end; + _blocks.erase(right); + } std::mutex _mutex; - bool _used[NUM_PAGES] = {}; - std::map _blocks; + bool _used[kNumPages] = {}; + BlockMap _blocks; }; +} + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + try { + return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } } void DirectMemoryFree(int64_t start, size_t len) { - PhysicalMemoryPool::Instance().Free(static_cast(start), len); + if (start < 0 || len == 0) return; + (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { + if (start < 0 || len == 0) return false; return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); } bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { + if (offset < 0 || !start || !end || !memoryType) return false; return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { - PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); + if (start < 0 || len == 0) return; + (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return false; return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { + if (offset >= kPoolBytes) return 0; return PhysicalMemoryPool::Instance().FreeRun(offset, limit); } diff --git a/core/libs/tests/MemoryPoolTest.cpp b/core/libs/tests/MemoryPoolTest.cpp new file mode 100644 index 000000000000..fe0e117bb329 --- /dev/null +++ b/core/libs/tests/MemoryPoolTest.cpp @@ -0,0 +1,336 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include +#include +#include +#include +#include +#include + +bool DirectMemoryCheckedFree(int64_t start, size_t len); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); +size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPool = static_cast(DIRECT_MEMORY_SIZE); +constexpr int kEinval = static_cast(0x80020016u); +constexpr int kEfault = static_cast(0x8002000Eu); +constexpr int kEagain = static_cast(0x80020023u); +constexpr int kEnomem = static_cast(0x8002000Cu); + +enum class Fault { None, BackingBadAlloc, BackingRuntimeError, ForgetRuntimeError }; +Fault g_fault = Fault::None; +long g_newBudget = -1; + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf(" FAILED line %d: %s\n", __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +struct NewBudget { + explicit NewBudget(long n) { g_newBudget = n; } + ~NewBudget() { g_newBudget = -1; } +}; + +bool BlockAt(uint64_t offset, int64_t& s, int64_t& e, int& t) { + return DirectMemoryFind(static_cast(offset), false, &s, &e, &t); +} + +bool BlockIs(uint64_t offset, uint64_t start, uint64_t end, int type) { + int64_t s = 0, e = 0; + int t = 0; + return BlockAt(offset, s, e, t) && s == static_cast(start) && e == static_cast(end) && t == type; +} + +bool PoolEmpty() { + int64_t s, e; + int t; + return !DirectMemoryFind(0, true, &s, &e, &t) && DirectMemoryFreeRun(0, kPool) == kPool; +} + +void alloc_invalid_args() { + int64_t out = -1; + CHECK(DirectMemoryAlloc(0, kPool, 0, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(-1, kPool, kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, SIZE_MAX, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, kPool + kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 3 * kPage, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(kPage, kPage, kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(kPool, kPool + kPage, kPage, 0, 1, &out) == kEinval); + CHECK(out == -1); + CHECK(PoolEmpty()); +} + +void alloc_null_out() { + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, nullptr) == kEfault); + CHECK(PoolEmpty()); +} + +void alloc_rounding_alignment_and_clamping() { + int64_t a = -1, b = -1; + CHECK(DirectMemoryAlloc(0, kPool, 1, 1, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, -1, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + CHECK(DirectMemoryAlloc(0, INT64_MAX, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + + CHECK(DirectMemoryAlloc(1, kPool, kPage, 0, 1, &a) == 0 && a == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &b) == kEagain); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_exhaustion_and_gap_reuse() { + int64_t a, b, c; + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(2 * kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == kEagain); + DirectMemoryFree(static_cast(kPage), kPage); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(kPage)); + DirectMemoryFree(0, kPool); +} + +void free_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryFree(0, SIZE_MAX); + DirectMemoryFree(static_cast(kPool), kPage); + DirectMemoryFree(static_cast(kPool - kPage), 2 * kPage); + DirectMemoryFree(1, kPage); + DirectMemoryFree(INT64_MAX, kPage); + CHECK(!DirectMemoryCheckedFree(0, SIZE_MAX)); + CHECK(!DirectMemoryCheckedFree(static_cast(kPool), kPage)); + CHECK(!DirectMemoryCheckedFree(1, kPage)); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryRetype(0, SIZE_MAX, 9); + DirectMemoryRetype(static_cast(kPool), kPage, 9); + DirectMemoryRetype(static_cast(kPool - kPage), 2 * kPage, 9); + DirectMemoryRetype(1, kPage, 9); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); +} + +void free_partial_and_spanning() { + int64_t a, b; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 2, &b) == 0 && b == static_cast(4 * kPage)); + DirectMemoryFree(static_cast(2 * kPage), 4 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 1)); + CHECK(BlockIs(6 * kPage, 6 * kPage, 8 * kPage, 2)); + CHECK(DirectMemoryFreeRun(2 * kPage, kPool) == 4 * kPage); + CHECK(!DirectMemoryCheckedFree(0, 4 * kPage)); + CHECK(DirectMemoryCheckedFree(0, 2 * kPage)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_splits_and_skips_gaps() { + int64_t a; + CHECK(DirectMemoryAlloc(2 * kPage, kPool, 4 * kPage, 0, 1, &a) == 0 && a == static_cast(2 * kPage)); + DirectMemoryRetype(static_cast(3 * kPage), kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 1)); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 9)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 6 * kPage, 1)); + DirectMemoryRetype(0, 8 * kPage, 5); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 5)); + CHECK(BlockIs(5 * kPage, 4 * kPage, 6 * kPage, 5)); + int64_t s, e; + int t; + CHECK(!BlockAt(0, s, e, t) && !BlockAt(6 * kPage, s, e, t)); + DirectMemoryFree(0, kPool); +} + +void alloc_backing_bad_alloc_rolls_back() { + int64_t out = -1; + g_fault = Fault::BackingBadAlloc; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == kEnomem); + g_fault = Fault::None; + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_node_bad_alloc_rolls_back() { + int64_t out = -1; + { + NewBudget budget(0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == kEnomem); + } + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_unexpected_exception_propagates() { + int64_t out = -1; + g_fault = Fault::BackingRuntimeError; + bool threw = false; + try { + (void)DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out); + } catch (const std::runtime_error&) { + threw = true; + } + g_fault = Fault::None; + CHECK(threw); + CHECK(PoolEmpty()); +} + +void free_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 0); + } + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 3)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 2 * kPage); + DirectMemoryFree(0, kPool); +} + +void retype_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + } + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 4 * kPage, 9)); + DirectMemoryFree(0, kPool); +} + +void forget_exception_propagates_and_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + g_fault = Fault::ForgetRuntimeError; + bool threwFree = false, threwChecked = false; + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwFree = true; + } + try { + (void)DirectMemoryCheckedFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwChecked = true; + } + g_fault = Fault::None; + CHECK(threwFree); + CHECK(threwChecked); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +struct Case { + const char* name; + void (*fn)(); +}; + +const Case kCases[] = { + {"alloc_invalid_args", alloc_invalid_args}, + {"alloc_null_out", alloc_null_out}, + {"alloc_rounding_alignment_and_clamping", alloc_rounding_alignment_and_clamping}, + {"alloc_exhaustion_and_gap_reuse", alloc_exhaustion_and_gap_reuse}, + {"free_out_of_range_is_ignored", free_out_of_range_is_ignored}, + {"retype_out_of_range_is_ignored", retype_out_of_range_is_ignored}, + {"free_partial_and_spanning", free_partial_and_spanning}, + {"retype_splits_and_skips_gaps", retype_splits_and_skips_gaps}, + {"alloc_backing_bad_alloc_rolls_back", alloc_backing_bad_alloc_rolls_back}, + {"alloc_node_bad_alloc_rolls_back", alloc_node_bad_alloc_rolls_back}, + {"alloc_unexpected_exception_propagates", alloc_unexpected_exception_propagates}, + {"free_node_bad_alloc_rolls_back", free_node_bad_alloc_rolls_back}, + {"retype_node_bad_alloc_rolls_back", retype_node_bad_alloc_rolls_back}, + {"forget_exception_propagates_and_rolls_back", forget_exception_propagates_and_rolls_back}, +}; + +} // namespace + +void CreateDirectMemoryBacking(int64_t, size_t, int) { + if (g_fault == Fault::BackingBadAlloc) throw std::bad_alloc(); + if (g_fault == Fault::BackingRuntimeError) throw std::runtime_error("backing"); +} + +void ForgetDirectMemory(int64_t, size_t) { + if (g_fault == Fault::ForgetRuntimeError) throw std::runtime_error("forget"); +} + +void* operator new(size_t n) { + if (g_newBudget == 0) throw std::bad_alloc(); + if (g_newBudget > 0) --g_newBudget; + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--list") == 0) { + for (const Case& c : kCases) std::printf("%s\n", c.name); + return 0; + } + int ran = 0; + for (const Case& c : kCases) { + if (argc == 2 && std::strcmp(argv[1], c.name) != 0) continue; + std::printf("[ RUN ] %s\n", c.name); + const int before = g_failures; + try { + c.fn(); + } catch (...) { + std::printf(" FAILED: unexpected exception escaped\n"); + ++g_failures; + } + std::printf("[ %s ] %s\n", g_failures == before ? " OK " : "FAIL", c.name); + ++ran; + } + if (ran == 0) { + std::printf("no such case\n"); + return 2; + } + return g_failures == 0 ? 0 : 1; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7c82..c7cc685e88ef 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,6 +97,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels +- [DirectMemoryFree, DirectMemoryRetype, LargestFree](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Invalid ranges are rejected with `EINVAL` or `EFAULT` in the current implementation. A negative `searchEnd` is treated as unbounded in `LargestFree`, but this behavior has not been confirmed against a console or a reference implementation. - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified @@ -345,6 +346,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [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. @@ -591,9 +594,11 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [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. +