Files
PS5_RetroArch/tests/memory_ps5_test.cpp
T
MihawkandClaude Opus 5.5 64864a02ba Large frontend buffers take direct memory first; Dolphin profiles
RetroArch reads a save state whole, and loading Resident Evil 4's 260 MB
entry state while Dolphin booted left the JIT 45 MB of the 56 MB of
flexible memory it maps; the core then wrote its code through a null
pointer. Large frontend allocations now take direct memory first and
anonymous (flexible) mappings only when it refuses, which keeps flexible
memory for executable code and thread stacks. The Dolphin allocator
reports the flexible memory free when an executable mapping fails.

tooling/dolphin-profiles holds the acceptance profiles as core-option
files. PHASE_LOG records Profile 0 on the three games.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 02:09:00 -04:00

162 lines
5.6 KiB
C++

#include <cassert>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
extern "C"
{
void *__wrap_malloc(size_t);
void *__wrap_calloc(size_t, size_t);
void *__wrap_realloc(void *, size_t);
void __wrap_free(void *);
// Host build binds these to libc, preserving genuinely foreign pointers.
void *__real_malloc(size_t n)
{
return std::malloc(n);
}
void *__real_calloc(size_t n, size_t s)
{
return std::calloc(n, s);
}
// A libc heap that can be told to refuse growth, for the overflow move.
bool refuse_native_realloc = false;
void *__real_realloc(void *p, size_t n)
{
return refuse_native_realloc ? nullptr : std::realloc(p, n);
}
// A stand-in overflow heap: libc-backed, tagged so ownership is testable,
// and locked as the real one is (the worker threads below reach it).
std::vector<void *> overflow_blocks;
std::recursive_mutex overflow_lock;
int ps5_overflow_owns(const void *p)
{
std::lock_guard<std::recursive_mutex> hold(overflow_lock);
for (void *block : overflow_blocks)
if (block == p)
return 1;
return 0;
}
// Direct memory that can be told to refuse, for the flexible fallback.
bool refuse_overflow = false;
void *ps5_overflow_malloc(size_t n)
{
std::lock_guard<std::recursive_mutex> hold(overflow_lock);
if (refuse_overflow)
return nullptr;
void *p = std::malloc(n);
if (p)
overflow_blocks.push_back(p);
return p;
}
void *ps5_overflow_calloc(size_t c, size_t n)
{
std::lock_guard<std::recursive_mutex> hold(overflow_lock);
void *p = std::calloc(c, n);
if (p)
overflow_blocks.push_back(p);
return p;
}
void *ps5_overflow_realloc(void *p, size_t n)
{
std::lock_guard<std::recursive_mutex> hold(overflow_lock);
void *grown = n == SIZE_MAX ? nullptr : std::realloc(p, n);
if (grown)
for (auto &block : overflow_blocks)
if (block == p)
block = grown;
return grown;
}
void *ps5_overflow_memalign(size_t, size_t)
{
return nullptr;
}
void ps5_overflow_free(void *p)
{
std::lock_guard<std::recursive_mutex> hold(overflow_lock);
for (auto &block : overflow_blocks)
if (block == p)
block = nullptr;
std::free(p);
}
void __real_free(void *p)
{
std::free(p);
}
}
int main()
{
constexpr size_t large = 16 * 1024 * 1024;
auto *p = static_cast<unsigned char *>(__wrap_malloc(large));
assert(p && uintptr_t(p) % alignof(std::max_align_t) == 0);
// A large buffer takes direct memory first: flexible memory is kept for
// what only it can hold (a core's JIT code, thread stacks).
assert(ps5_overflow_owns(p));
std::memset(p, 0xA5, large);
assert(!__wrap_realloc(p, SIZE_MAX));
assert(p[0] == 0xA5 && p[large - 1] == 0xA5);
p = static_cast<unsigned char *>(__wrap_realloc(p, large * 2));
assert(p && p[0] == 0xA5 && p[large - 1] == 0xA5);
p = static_cast<unsigned char *>(__wrap_realloc(p, 13));
assert(p && p[12] == 0xA5);
__wrap_free(p);
p = static_cast<unsigned char *>(__wrap_calloc(2, large));
assert(p);
for (size_t i = 0; i < 2 * large; ++i)
assert(p[i] == 0);
__wrap_free(p);
assert(!__wrap_calloc(SIZE_MAX, 2) && errno == ENOMEM);
assert(!__wrap_malloc(SIZE_MAX) && errno == ENOMEM);
// strdup/native library buffers must still be freed by libc.
char *foreign = strdup("native allocation");
foreign = static_cast<char *>(__wrap_realloc(foreign, 128));
assert(foreign && !strcmp(foreign, "native allocation"));
__wrap_free(foreign);
__wrap_free(nullptr);
// A native block whose heap refuses to grow moves to the overflow heap with
// its contents, and is freed there.
// (A libc-owned block, as strdup or a system library hands the title.)
auto *native = static_cast<unsigned char *>(std::malloc(100));
std::memset(native, 0x5A, 100);
refuse_native_realloc = true;
auto *moved = static_cast<unsigned char *>(__wrap_realloc(native, 4096));
refuse_native_realloc = false;
assert(moved && ps5_overflow_owns(moved));
for (size_t i = 0; i < 100; ++i)
assert(moved[i] == 0x5A);
__wrap_free(moved);
assert(!ps5_overflow_owns(moved));
// With direct memory refusing, a large buffer is still served, from an
// anonymous mapping (flexible memory), and grows and frees there.
refuse_overflow = true;
auto *flexible = static_cast<unsigned char *>(__wrap_malloc(large));
assert(flexible && !ps5_overflow_owns(flexible));
std::memset(flexible, 0x3C, large);
flexible = static_cast<unsigned char *>(__wrap_realloc(flexible, large * 2));
assert(flexible && flexible[large - 1] == 0x3C);
__wrap_free(flexible);
refuse_overflow = false;
void *empty = __wrap_calloc(0, SIZE_MAX);
assert(empty);
__wrap_free(empty);
assert(!__wrap_realloc(__wrap_malloc(large), 0));
std::vector<std::thread> workers;
for (int t = 0; t < 4; ++t)
workers.emplace_back(
[]
{
for (int i = 0; i < 50; ++i)
{
void *buffer = __wrap_malloc(1024 * 1024);
assert(buffer);
memset(buffer, i, 1024 * 1024);
__wrap_free(buffer);
}
});
for (auto &worker : workers)
worker.join();
}