mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-04-19 11:10:44 -04:00
Merge pull request 'fix(core): Properly release and reclaim memory when stopping emulation' (#90) from fix/memory-allocation into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/90
This commit is contained in:
@@ -672,7 +672,11 @@ HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HostMemory::~HostMemory() = default;
|
HostMemory::~HostMemory() {
|
||||||
|
// We leave this empty.
|
||||||
|
// The "impl" unique_ptr handles the cleanup automatically and correctly.
|
||||||
|
// Manually calling munmap here causes a "double-free" crash.
|
||||||
|
}
|
||||||
|
|
||||||
HostMemory::HostMemory(HostMemory&&) noexcept = default;
|
HostMemory::HostMemory(HostMemory&&) noexcept = default;
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,9 @@ public:
|
|||||||
return address >= virtual_base && address < virtual_base + virtual_size;
|
return address >= virtual_base && address < virtual_base + virtual_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
size_t backing_size{};
|
size_t backing_size{};
|
||||||
|
|
||||||
|
private:
|
||||||
size_t virtual_size{};
|
size_t virtual_size{};
|
||||||
|
|
||||||
// Low level handler for the platform dependent memory routines
|
// Low level handler for the platform dependent memory routines
|
||||||
|
|||||||
@@ -4,9 +4,34 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <fstream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#ifndef __ANDROID__
|
||||||
|
#include <malloc.h>
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#include <psapi.h>
|
||||||
|
|
||||||
|
#undef GetCurrentTime
|
||||||
|
#undef ERROR
|
||||||
|
#undef GetMessage
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "audio_core/audio_core.h"
|
#include "audio_core/audio_core.h"
|
||||||
#include "common/fs/fs.h"
|
#include "common/fs/fs.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
@@ -65,6 +90,31 @@
|
|||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
|
static u64 GetCurrentRSS() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
PROCESS_MEMORY_COUNTERS_EX pmc;
|
||||||
|
GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc));
|
||||||
|
return static_cast<u64>(pmc.WorkingSetSize) / 1024 / 1024;
|
||||||
|
#elif defined(__linux__)
|
||||||
|
u64 rss = 0;
|
||||||
|
std::ifstream stat_file("/proc/self/status");
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(stat_file, line)) {
|
||||||
|
if (line.compare(0, 8, "RssAnon:") == 0) {
|
||||||
|
size_t start = line.find_first_of("0123456789");
|
||||||
|
size_t end = line.find_last_of("0123456789");
|
||||||
|
if (start != std::string::npos && end != std::string::npos) {
|
||||||
|
rss = std::stoull(line.substr(start, end - start + 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rss / 1024;
|
||||||
|
#else
|
||||||
|
return 0; // macOS/Other implementation
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
MICROPROFILE_DEFINE(ARM_CPU0, "ARM", "CPU 0", MP_RGB(255, 64, 64));
|
MICROPROFILE_DEFINE(ARM_CPU0, "ARM", "CPU 0", MP_RGB(255, 64, 64));
|
||||||
MICROPROFILE_DEFINE(ARM_CPU1, "ARM", "CPU 1", MP_RGB(255, 64, 64));
|
MICROPROFILE_DEFINE(ARM_CPU1, "ARM", "CPU 1", MP_RGB(255, 64, 64));
|
||||||
MICROPROFILE_DEFINE(ARM_CPU2, "ARM", "CPU 2", MP_RGB(255, 64, 64));
|
MICROPROFILE_DEFINE(ARM_CPU2, "ARM", "CPU 2", MP_RGB(255, 64, 64));
|
||||||
@@ -117,7 +167,10 @@ struct System::Impl {
|
|||||||
reporter{system}, applet_manager{system}, frontend_applets{system}, profile_manager{} {}
|
reporter{system}, applet_manager{system}, frontend_applets{system}, profile_manager{} {}
|
||||||
|
|
||||||
void Initialize(System& system) {
|
void Initialize(System& system) {
|
||||||
device_memory = std::make_unique<Core::DeviceMemory>();
|
// Only create the memory bucket if it literally does not exist (First launch)
|
||||||
|
if (!device_memory) {
|
||||||
|
device_memory = std::make_unique<Core::DeviceMemory>();
|
||||||
|
}
|
||||||
|
|
||||||
is_multicore = Settings::values.use_multi_core.GetValue();
|
is_multicore = Settings::values.use_multi_core.GetValue();
|
||||||
extended_memory_layout =
|
extended_memory_layout =
|
||||||
@@ -126,7 +179,7 @@ struct System::Impl {
|
|||||||
core_timing.SetMulticore(is_multicore);
|
core_timing.SetMulticore(is_multicore);
|
||||||
core_timing.Initialize([&system]() { system.RegisterHostThread(); });
|
core_timing.Initialize([&system]() { system.RegisterHostThread(); });
|
||||||
|
|
||||||
// Create a default fs if one doesn't already exist.
|
// LEAVE THESE ALONE if they exist. No more make_shared/unique here.
|
||||||
if (virtual_filesystem == nullptr) {
|
if (virtual_filesystem == nullptr) {
|
||||||
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||||
}
|
}
|
||||||
@@ -134,9 +187,7 @@ struct System::Impl {
|
|||||||
content_provider = std::make_unique<FileSys::ContentProviderUnion>();
|
content_provider = std::make_unique<FileSys::ContentProviderUnion>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create default implementations of applets if one is not provided.
|
|
||||||
frontend_applets.SetDefaultAppletsIfMissing();
|
frontend_applets.SetDefaultAppletsIfMissing();
|
||||||
|
|
||||||
is_async_gpu = Settings::values.use_asynchronous_gpu_emulation.GetValue();
|
is_async_gpu = Settings::values.use_asynchronous_gpu_emulation.GetValue();
|
||||||
|
|
||||||
kernel.SetMulticore(is_multicore);
|
kernel.SetMulticore(is_multicore);
|
||||||
@@ -145,20 +196,22 @@ struct System::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ReinitializeIfNecessary(System& system) {
|
void ReinitializeIfNecessary(System& system) {
|
||||||
const bool must_reinitialize =
|
const bool layout_changed = extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb);
|
||||||
is_multicore != Settings::values.use_multi_core.GetValue() ||
|
const bool must_reinitialize = !device_memory || is_multicore != Settings::values.use_multi_core.GetValue() || layout_changed;
|
||||||
extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() !=
|
|
||||||
Settings::MemoryLayout::Memory_4Gb);
|
|
||||||
|
|
||||||
if (!must_reinitialize) {
|
if (!must_reinitialize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layout_changed) {
|
||||||
|
device_memory.reset();
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DEBUG(Kernel, "Re-initializing");
|
LOG_DEBUG(Kernel, "Re-initializing");
|
||||||
|
|
||||||
|
// Update the tracked values before re-initializing
|
||||||
is_multicore = Settings::values.use_multi_core.GetValue();
|
is_multicore = Settings::values.use_multi_core.GetValue();
|
||||||
extended_memory_layout =
|
extended_memory_layout = (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb);
|
||||||
Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb;
|
|
||||||
|
|
||||||
Initialize(system);
|
Initialize(system);
|
||||||
}
|
}
|
||||||
@@ -400,21 +453,16 @@ struct System::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ShutdownMainProcess() {
|
void ShutdownMainProcess() {
|
||||||
|
const u64 mem_before = GetCurrentRSS();
|
||||||
SetShuttingDown(true);
|
SetShuttingDown(true);
|
||||||
|
|
||||||
// Log last frame performance stats if game was loaded
|
|
||||||
if (perf_stats) {
|
if (perf_stats) {
|
||||||
const auto perf_results = GetAndResetPerfStats();
|
const auto perf_results = GetAndResetPerfStats();
|
||||||
constexpr auto performance = Common::Telemetry::FieldType::Performance;
|
constexpr auto performance = Common::Telemetry::FieldType::Performance;
|
||||||
|
telemetry_session->AddField(performance, "Shutdown_EmulationSpeed", perf_results.emulation_speed * 100.0);
|
||||||
telemetry_session->AddField(performance, "Shutdown_EmulationSpeed",
|
telemetry_session->AddField(performance, "Shutdown_Framerate", perf_results.average_game_fps);
|
||||||
perf_results.emulation_speed * 100.0);
|
telemetry_session->AddField(performance, "Shutdown_Frametime", perf_results.frametime * 1000.0);
|
||||||
telemetry_session->AddField(performance, "Shutdown_Framerate",
|
telemetry_session->AddField(performance, "Mean_Frametime_MS", perf_stats->GetMeanFrametime());
|
||||||
perf_results.average_game_fps);
|
|
||||||
telemetry_session->AddField(performance, "Shutdown_Frametime",
|
|
||||||
perf_results.frametime * 1000.0);
|
|
||||||
telemetry_session->AddField(performance, "Mean_Frametime_MS",
|
|
||||||
perf_stats->GetMeanFrametime());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is_powered_on = false;
|
is_powered_on = false;
|
||||||
@@ -428,11 +476,11 @@ struct System::Impl {
|
|||||||
stop_event.request_stop();
|
stop_event.request_stop();
|
||||||
core_timing.SyncPause(false);
|
core_timing.SyncPause(false);
|
||||||
Network::CancelPendingSocketOperations();
|
Network::CancelPendingSocketOperations();
|
||||||
|
|
||||||
kernel.SuspendEmulation(true);
|
kernel.SuspendEmulation(true);
|
||||||
kernel.CloseServices();
|
kernel.CloseServices();
|
||||||
kernel.ShutdownCores();
|
kernel.ShutdownCores();
|
||||||
|
|
||||||
// FIX: Shut down all major systems BEFORE destroying the ServiceManager.
|
|
||||||
fs_controller.Reset();
|
fs_controller.Reset();
|
||||||
cheat_engine.reset();
|
cheat_engine.reset();
|
||||||
telemetry_session.reset();
|
telemetry_session.reset();
|
||||||
@@ -442,24 +490,42 @@ struct System::Impl {
|
|||||||
gpu_core.reset();
|
gpu_core.reset();
|
||||||
host1x_core.reset();
|
host1x_core.reset();
|
||||||
|
|
||||||
// Now it is safe to destroy the services and the ServiceManager.
|
|
||||||
services.reset();
|
services.reset();
|
||||||
service_manager.reset();
|
service_manager.reset();
|
||||||
|
|
||||||
perf_stats.reset();
|
perf_stats.reset();
|
||||||
cpu_manager.Shutdown();
|
cpu_manager.Shutdown();
|
||||||
debugger.reset();
|
debugger.reset();
|
||||||
|
|
||||||
|
// Kernel is the VERY last thing to go
|
||||||
kernel.Shutdown();
|
kernel.Shutdown();
|
||||||
|
|
||||||
stop_event = {};
|
stop_event = {};
|
||||||
Network::RestartSocketOperations();
|
Network::RestartSocketOperations();
|
||||||
|
arp_manager.ResetAll();
|
||||||
|
|
||||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
|
||||||
Network::GameInfo game_info{};
|
if (device_memory) {
|
||||||
room_member->SendGameInfo(game_info);
|
#ifdef __linux__
|
||||||
|
madvise(device_memory->buffer.BackingBasePointer(), device_memory->buffer.backing_size, MADV_DONTNEED);
|
||||||
|
|
||||||
|
// Only call malloc_trim on non-Android Linux (glibc)
|
||||||
|
#ifndef __ANDROID__
|
||||||
|
malloc_trim(0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Give the kernel time to update /proc/stats
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
VirtualAlloc(device_memory->buffer.BackingBasePointer(), device_memory->buffer.backing_size, MEM_RESET, PAGE_READWRITE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset all glue registrations
|
const u64 mem_after = GetCurrentRSS();
|
||||||
arp_manager.ResetAll();
|
const u64 shaved = (mem_before > mem_after) ? (mem_before - mem_after) : 0;
|
||||||
|
|
||||||
|
LOG_INFO(Core, "Shutdown Memory Audit: [Before: {}MB] -> [After: {}MB] | Total Shaved: {}MB",
|
||||||
|
mem_before, mem_after, shaved);
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Shutdown OK");
|
LOG_DEBUG(Core, "Shutdown OK");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,23 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
RasterizerVulkan::~RasterizerVulkan() = default;
|
RasterizerVulkan::~RasterizerVulkan() {
|
||||||
|
// 1. Tell the GPU to finish current work
|
||||||
|
scheduler.Finish();
|
||||||
|
|
||||||
|
// 2. Force runtimes to release internal references/handles FIRST
|
||||||
|
// This ensures VkBuffer/VkImage handles are gone before the memory they sit on is freed
|
||||||
|
buffer_cache_runtime.Finish();
|
||||||
|
texture_cache_runtime.Finish();
|
||||||
|
|
||||||
|
// 3. Clear the Staging Pool slabs
|
||||||
|
staging_pool.TriggerCacheRelease(MemoryUsage::Upload);
|
||||||
|
staging_pool.TriggerCacheRelease(MemoryUsage::Download);
|
||||||
|
staging_pool.TriggerCacheRelease(MemoryUsage::DeviceLocal);
|
||||||
|
|
||||||
|
// 4. Nuke the Vulkan slabs
|
||||||
|
memory_allocator.NukeAllAllocations();
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Func>
|
template <typename Func>
|
||||||
void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
||||||
|
|||||||
@@ -322,4 +322,17 @@ void StagingBufferPool::ReleaseLevel(StagingBuffersCache& cache, size_t log2) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StagingBufferPool::Nuke() {
|
||||||
|
auto nuke_cache = [](StagingBuffersCache& cache) {
|
||||||
|
for (auto& level : cache) {
|
||||||
|
level.entries.clear();
|
||||||
|
level.entries.shrink_to_fit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
nuke_cache(device_local_cache);
|
||||||
|
nuke_cache(upload_cache);
|
||||||
|
nuke_cache(download_cache);
|
||||||
|
stream_buffer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public:
|
|||||||
|
|
||||||
void TickFrame();
|
void TickFrame();
|
||||||
|
|
||||||
|
void Nuke();
|
||||||
|
|
||||||
u64 GetMemoryUsage() const;
|
u64 GetMemoryUsage() const;
|
||||||
|
|
||||||
void SetProgramId(u64 program_id_) {
|
void SetProgramId(u64 program_id_) {
|
||||||
|
|||||||
@@ -383,4 +383,11 @@ std::optional<u32> MemoryAllocator::FindType(VkMemoryPropertyFlags flags, u32 ty
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MemoryAllocator::NukeAllAllocations() {
|
||||||
|
// This calls the destructor for every MemoryAllocation slab.
|
||||||
|
// Each slab contains a Vulkan handle that will now call vkFreeMemory.
|
||||||
|
allocations.clear();
|
||||||
|
allocations.shrink_to_fit();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ public:
|
|||||||
/// Commits memory required by the buffer and binds it.
|
/// Commits memory required by the buffer and binds it.
|
||||||
MemoryCommit Commit(const vk::Buffer& buffer, MemoryUsage usage);
|
MemoryCommit Commit(const vk::Buffer& buffer, MemoryUsage usage);
|
||||||
|
|
||||||
|
void NukeAllAllocations();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Tries to allocate a chunk of memory.
|
/// Tries to allocate a chunk of memory.
|
||||||
bool TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size);
|
bool TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size);
|
||||||
|
|||||||
Reference in New Issue
Block a user