From 7a90d09cfe3c31bf95c3cb63c6301c5c0824c531 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 23 Jul 2026 08:05:59 +0200 Subject: [PATCH] Qt: move guest memory dump code to own class --- Utilities/File.cpp | 2 +- rpcs3/rpcs3.vcxproj | 17 + rpcs3/rpcs3.vcxproj.filters | 28 +- rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/guest_memory_dumper.cpp | 479 +++++++++++++++++++++++ rpcs3/rpcs3qt/guest_memory_dumper.h | 45 +++ rpcs3/rpcs3qt/main_window.cpp | 493 +----------------------- rpcs3/rpcs3qt/main_window.h | 1 - rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 20 +- 9 files changed, 578 insertions(+), 508 deletions(-) create mode 100644 rpcs3/rpcs3qt/guest_memory_dumper.cpp create mode 100644 rpcs3/rpcs3qt/guest_memory_dumper.h diff --git a/Utilities/File.cpp b/Utilities/File.cpp index 0bbb485129..bb635592d4 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -1964,7 +1964,7 @@ fs::native_handle fs::file::get_handle() const #endif } -bool fs::set_sparse(const fs::file& file) +bool fs::set_sparse([[maybe_unused]] const fs::file& file) { #ifdef _WIN32 FILE_SET_SPARSE_BUFFER sparse{TRUE}; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index c4338d2acb..e9a2925aa3 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -230,6 +230,9 @@ true + + true + true @@ -542,6 +545,9 @@ true + + true + true @@ -845,6 +851,7 @@ + @@ -1208,6 +1215,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index b8af6799f1..aa2c5bb569 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -585,6 +585,12 @@ Generated Files\Release + + Generated Files\Debug + + + Generated Files\Release + Generated Files\Debug @@ -1314,6 +1320,9 @@ Gui\settings + + Gui\utils + @@ -1565,7 +1574,7 @@ Gui\utils - + Gui\settings @@ -1783,9 +1792,6 @@ Gui\settings - - Gui\settings - Gui\patch manager @@ -1927,6 +1933,15 @@ Gui\game list + + Gui\game list + + + Gui\settings + + + Gui\utils + @@ -2082,9 +2097,4 @@ buildfiles\cmake - - - Gui\game list - - \ No newline at end of file diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 5dd092d074..d7ff5d565a 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(rpcs3_ui STATIC game_list_grid.cpp game_list_grid_item.cpp game_list_table.cpp + guest_memory_dumper.cpp gui_application.cpp gl_gs_frame.cpp gs_frame.cpp diff --git a/rpcs3/rpcs3qt/guest_memory_dumper.cpp b/rpcs3/rpcs3qt/guest_memory_dumper.cpp new file mode 100644 index 0000000000..c143239d92 --- /dev/null +++ b/rpcs3/rpcs3qt/guest_memory_dumper.cpp @@ -0,0 +1,479 @@ +#include "stdafx.h" +#include "guest_memory_dumper.h" +#include "progress_dialog.h" + +#include "Emu/Cell/PPUThread.h" +#include "Emu/Cell/SPUThread.h" +#include "Emu/Memory/vm.h" +#include "Emu/RSX/RSXThread.h" +#include "Emu/System.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_CHANNEL(gui_log, "GUI"); + +extern void qt_events_aware_op(int repeat_duration_ms, std::function wrapped_op); + +guest_memory_dumper::guest_memory_dumper(QWidget* parent, bool delete_later) + : QObject(parent), m_parent(parent), m_delete_later(delete_later) +{ +} + +// Emu.Pause() only requests the pause: every emulated thread drains into its +// wait or stop state asynchronously. The dump must not begin until each PPU, +// SPU and RSX thread has actually settled, or it could capture torn state. +bool guest_memory_dumper::emulated_processors_quiesced() +{ + bool quiesced = true; + const auto check_cpu = [&](u32, cpu_thread& cpu) + { + const auto state = +cpu.state; + if (!::is_stopped(state) && !(state & cpu_flag::wait)) + { + quiesced = false; + } + }; + + if (g_fxo->is_init>>()) + { + idm::select>(check_cpu); + } + + if (g_fxo->is_init>>()) + { + idm::select>(check_cpu); + } + + if (const auto rsx = g_fxo->try_get()) + { + check_cpu(0, *rsx); + } + + return quiesced; +} + +QString guest_memory_dumper::hex_u64(u64 value, int width) +{ + return QStringLiteral("0x%1").arg(value, width, 16, QLatin1Char('0')); +} + +void guest_memory_dumper::dump_guest_memory() +{ + if (Emu.IsStopped() || Emu.IsStarting()) + { + QMessageBox::warning(m_parent, tr("Dump Guest Memory"), tr("Start a game before creating a guest-memory dump.")); + return; + } + + const QString default_parent = QString::fromStdString(fs::get_config_dir() + "guest_memory_dumps"); + QDir().mkpath(default_parent); + + QString title_id = QString::fromStdString(Emu.GetTitleID()); + if (title_id.isEmpty()) + { + title_id = QStringLiteral("RPCS3"); + } + + // Remove the output folder again unless the dump ran to completion, so a + // failed or cancelled dump does not leave an empty folder or a partial + // image behind. This is declared before the output file so it destructs + // after the file is closed. + struct cleanup_guard + { + QDir dir; + bool enabled = false; + bool keep = false; + ~cleanup_guard() + { + if (enabled && !keep && !dir.removeRecursively()) + { + gui_log.error("Could not remove incomplete guest memory dump folder: %s", dir.absolutePath()); + } + } + } cleanup; + + QString picker_parent = default_parent; + QDir output; + fs::file guest_file; + bool sparse_image = false; + + while (!sparse_image) + { + const QString selected_parent = QFileDialog::getExistingDirectory(m_parent, tr("Select Guest Memory Dump Folder"), picker_parent, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (selected_parent.isEmpty()) + { + return; + } + + // The native folder dialog runs its own event loop, so the game may have + // stopped while the user was choosing a destination. + if (Emu.IsStopped() || Emu.IsStarting()) + { + QMessageBox::warning(m_parent, tr("Dump Guest Memory"), tr("The game stopped before the guest-memory dump could begin.")); + return; + } + + const QString timestamp = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz")); + const QString folder_stem = QStringLiteral("%1_guest_memory_%2").arg(title_id, timestamp); + QDir parent(selected_parent); + QString folder_name = folder_stem; + + for (u32 suffix = 2; parent.exists(folder_name); suffix++) + { + folder_name = QStringLiteral("%1_%2").arg(folder_stem).arg(suffix); + } + + if (!parent.mkpath(folder_name)) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not create the output folder:\n%0").arg(parent.filePath(folder_name))); + return; + } + + output.setPath(parent.filePath(folder_name)); + cleanup.dir = output; + cleanup.enabled = true; + + const QString guest_path = output.filePath(QStringLiteral("guest_memory.bin")); + if (!guest_file.open(guest_path.toStdString(), fs::rewrite)) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not create:\n%0").arg(guest_path)); + return; + } + + // POSIX file systems create holes for unwritten ranges by default; Windows + // needs the sparse attribute set explicitly. + sparse_image = fs::set_sparse(guest_file); + + if (!sparse_image) + { + guest_file.close(); + if (!output.removeRecursively()) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("The selected destination does not support sparse files, and the incomplete output folder could not be removed:\n%0").arg(output.absolutePath())); + return; + } + + cleanup.enabled = false; + QMessageBox message(QMessageBox::Critical, tr("Dump Guest Memory"), tr("The selected destination does not support sparse files. Choose a location on an NTFS volume."), QMessageBox::NoButton, m_parent); + QPushButton* choose_button = message.addButton(tr("Choose Another Location"), QMessageBox::AcceptRole); + message.addButton(QMessageBox::Cancel); + message.setDefaultButton(choose_button); + message.exec(); + + if (message.clickedButton() != choose_button) + { + return; + } + + picker_parent = selected_parent; + } + } + + const bool resume_after_dump = Emu.IsRunning(); + + if (resume_after_dump && !Emu.Pause(false, false)) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not pause emulation for a consistent memory snapshot.")); + return; + } + + struct resume_guard + { + bool enabled = false; + ~resume_guard() + { + if (enabled) + { + Emu.Resume(); + } + } + } resume{resume_after_dump}; + + bool quiesced = false; + QElapsedTimer pause_timer; + pause_timer.start(); + qt_events_aware_op(5, [&]() + { + quiesced = emulated_processors_quiesced(); + return quiesced || pause_timer.elapsed() >= 5000; + }); + + if (!quiesced) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("One or more emulated processors did not stop within five seconds. No dump was created.")); + return; + } + + std::vector regions; + guest_memory_region current{}; + + const auto finish_region = [&]() + { + if (current.size) + { + regions.emplace_back(current); + current = {}; + } + }; + + // Walk the guest page table and coalesce contiguous pages with identical + // flags into regions, so the manifest mirrors the guest memory map instead + // of listing a million single pages. + for (u32 page = 0; page < guest_address_space_size / guest_page_size; page++) + { + const u32 address = static_cast(static_cast(page) * guest_page_size); + const auto [allocated, flags] = vm::get_addr_flags(address); + + if (!allocated) + { + finish_region(); + continue; + } + + if (current.size && (current.flags != flags || static_cast(current.start) + current.size != address)) + { + finish_region(); + } + + if (!current.size) + { + current.start = address; + current.flags = flags; + } + + current.size += guest_page_size; + } + + finish_region(); + + // SPU local store is also part of the guest map (at each thread's + // vm_offset), but a per thread copy with id, pc and type recorded in the + // manifest is much easier to analyze than digging it out of the main image. + std::vector spu_dumps; + if (g_fxo->is_init>>()) + { + idm::select>([&](u32 id, spu_thread& spu) + { + spu_local_store_dump dump; + dump.id = id; + dump.lv2_id = spu.lv2_id; + dump.index = spu.index; + dump.pc = spu.pc; + dump.vm_offset = spu.vm_offset(); + dump.type = static_cast(spu.get_type()); + dump.name = spu.get_name(); + spu_dumps.emplace_back(std::move(dump)); + }); + } + + u64 guest_bytes = 0; + for (const auto& region : regions) + { + guest_bytes += region.size; + } + + const u64 total_bytes = guest_bytes + static_cast(spu_dumps.size()) * SPU_LS_SIZE; + + // Preflight the destination. A sparse image costs roughly the allocated + // bytes, so warn when even that plus some margin does not fit. + const QStorageInfo storage(output.absolutePath()); + const u64 available = storage.bytesAvailable() > 0 ? static_cast(storage.bytesAvailable()) : 0; + + if (const u64 needed = total_bytes + (64ull << 20); available < needed) + { + if (QMessageBox::question(m_parent, tr("Dump Guest Memory"), tr("The destination may not have enough free space (roughly %0 MiB needed, %1 MiB available).\n\nContinue anyway?").arg(needed >> 20).arg(available >> 20)) != QMessageBox::Yes) + { + return; + } + } + + atomic_t completed_bytes{0}; + progress_dialog progress(tr("Dump Guest Memory"), tr("Dumping PS3 guest memory..."), tr("Cancel"), 0, 1000, false, m_parent); + progress.setCancelButton(nullptr); + progress.setMinimumDuration(0); + + if (!guest_file.trunc(guest_address_space_size)) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not size the 4 GB address image.")); + return; + } + + // Read through the sudo mirror: it maps every allocated page as read/write, + // so pages the guest keeps protected (reservation notification, no access + // areas) are captured as well instead of faulting or being skipped. Copy + // into ordinary host memory before passing data to fs::file, and perform all + // guest-memory reads and file writes off the UI thread. + enum class dump_write_error + { + none, + guest_seek, + guest_write, + spu_write, + }; + + dump_write_error write_error = dump_write_error::none; + u64 error_address = 0; + QString error_path; + const QString output_path = output.absolutePath(); + + named_thread dump_thread("Guest Memory Dumper", [&]() + { + std::vector middle_buffer(dump_io_chunk_size); + + for (const auto& region : regions) + { + if (guest_file.seek(region.start) != region.start) + { + write_error = dump_write_error::guest_seek; + error_address = region.start; + return; + } + + for (u64 offset = 0; offset < region.size;) + { + const u64 chunk_size = std::min(dump_io_chunk_size, region.size - offset); + const u64 address = static_cast(region.start) + offset; + + std::memcpy(middle_buffer.data(), vm::g_sudo_addr + address, chunk_size); + if (guest_file.write(middle_buffer.data(), chunk_size) != chunk_size) + { + write_error = dump_write_error::guest_write; + error_address = address; + return; + } + + offset += chunk_size; + completed_bytes.fetch_add(chunk_size); + } + } + + guest_file.close(); + + QDir worker_output(output_path); + for (const auto& dump : spu_dumps) + { + const QString filename = QStringLiteral("spu_%1_ls.bin").arg(dump.id, 8, 16, QLatin1Char('0')); + const QString file_path = worker_output.filePath(filename); + std::memcpy(middle_buffer.data(), vm::g_sudo_addr + dump.vm_offset, SPU_LS_SIZE); + fs::file file(file_path.toStdString(), fs::rewrite); + + if (!file || file.write(middle_buffer.data(), SPU_LS_SIZE) != SPU_LS_SIZE) + { + write_error = dump_write_error::spu_write; + error_path = file_path; + return; + } + + completed_bytes.fetch_add(SPU_LS_SIZE); + } + }); + + qt_events_aware_op(10, [&]() + { + const u64 completed = completed_bytes.load(); + progress.SetValue(total_bytes ? static_cast(completed * 1000 / total_bytes) : 1000); + return static_cast(dump_thread) == thread_state::finished; + }); + dump_thread(); + + if (write_error == dump_write_error::guest_seek) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not seek to guest address %0 in the output file.").arg(hex_u64(error_address))); + return; + } + + if (write_error == dump_write_error::guest_write) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Writing failed at guest address %0. The dump is incomplete.").arg(hex_u64(error_address))); + return; + } + + if (write_error == dump_write_error::spu_write) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not write SPU local store:\n%0").arg(error_path)); + return; + } + + QJsonArray spu_json; + for (const auto& dump : spu_dumps) + { + const QString filename = QStringLiteral("spu_%1_ls.bin").arg(dump.id, 8, 16, QLatin1Char('0')); + QJsonObject item; + item.insert(QStringLiteral("file"), filename); + item.insert(QStringLiteral("id"), hex_u64(dump.id)); + item.insert(QStringLiteral("lv2_id"), hex_u64(dump.lv2_id)); + item.insert(QStringLiteral("index"), static_cast(dump.index)); + item.insert(QStringLiteral("pc"), hex_u64(dump.pc, 5)); + item.insert(QStringLiteral("vm_offset"), hex_u64(dump.vm_offset)); + item.insert(QStringLiteral("type"), static_cast(dump.type)); + item.insert(QStringLiteral("name"), QString::fromStdString(dump.name)); + item.insert(QStringLiteral("size"), static_cast(SPU_LS_SIZE)); + spu_json.append(item); + } + + QJsonArray region_json; + for (const auto& region : regions) + { + QJsonObject item; + item.insert(QStringLiteral("start"), hex_u64(region.start)); + item.insert(QStringLiteral("end_exclusive"), hex_u64(static_cast(region.start) + region.size)); + item.insert(QStringLiteral("size"), static_cast(region.size)); + item.insert(QStringLiteral("file_offset"), hex_u64(region.start)); + item.insert(QStringLiteral("raw_page_flags"), static_cast(region.flags)); + item.insert(QStringLiteral("readable"), !!(region.flags & vm::page_readable)); + item.insert(QStringLiteral("writable"), !!(region.flags & vm::page_writable)); + item.insert(QStringLiteral("executable"), !!(region.flags & vm::page_executable)); + region_json.append(item); + } + + QJsonObject manifest; + manifest.insert(QStringLiteral("format"), QStringLiteral("rpcs3-guest-memory-dump")); + manifest.insert(QStringLiteral("version"), 1); + manifest.insert(QStringLiteral("created_utc"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + manifest.insert(QStringLiteral("title"), QString::fromStdString(Emu.GetTitle())); + manifest.insert(QStringLiteral("title_id"), QString::fromStdString(Emu.GetTitleID())); + manifest.insert(QStringLiteral("address_space_file"), QStringLiteral("guest_memory.bin")); + manifest.insert(QStringLiteral("address_space_size"), hex_u64(guest_address_space_size, 9)); + manifest.insert(QStringLiteral("page_size"), static_cast(guest_page_size)); + manifest.insert(QStringLiteral("allocated_bytes"), static_cast(guest_bytes)); + manifest.insert(QStringLiteral("file_offset_equals_guest_address"), true); + manifest.insert(QStringLiteral("sparse_image"), sparse_image); + manifest.insert(QStringLiteral("regions"), region_json); + manifest.insert(QStringLiteral("spu_local_stores"), spu_json); + + QFile manifest_file(output.filePath(QStringLiteral("manifest.json"))); + if (!manifest_file.open(QIODevice::WriteOnly | QIODevice::Truncate) || manifest_file.write(QJsonDocument(manifest).toJson(QJsonDocument::Indented)) < 0) + { + QMessageBox::critical(m_parent, tr("Dump Guest Memory"), tr("Could not write the dump manifest.")); + return; + } + + manifest_file.close(); + cleanup.keep = true; + progress.SetValue(1000); + gui_log.success("Guest memory dump complete: %s (%u regions, %u SPU local stores, %u bytes allocated)", output.absolutePath(), regions.size(), spu_dumps.size(), guest_bytes); + + if (resume.enabled) + { + Emu.Resume(); + resume.enabled = false; + } + + QMessageBox::information(m_parent, tr("Dump Guest Memory"), tr("Guest memory dump completed.\n\n%0\n\nThe 4 GB guest_memory.bin file is sparse: its file offset equals the PS3 guest address, while only allocated pages consume disk space.").arg(output.absolutePath())); + + if (m_delete_later) + { + deleteLater(); + } +} diff --git a/rpcs3/rpcs3qt/guest_memory_dumper.h b/rpcs3/rpcs3qt/guest_memory_dumper.h new file mode 100644 index 0000000000..702a6f8bc6 --- /dev/null +++ b/rpcs3/rpcs3qt/guest_memory_dumper.h @@ -0,0 +1,45 @@ +#pragma once + +#include "util/types.hpp" + +#include + +class guest_memory_dumper : QObject +{ + Q_OBJECT + +public: + guest_memory_dumper(QWidget* parent, bool delete_later = false); + virtual ~guest_memory_dumper() {} + + void dump_guest_memory(); + +private: + QWidget* m_parent = nullptr; + bool m_delete_later = false; + + static constexpr u64 guest_address_space_size = 0x1'0000'0000ull; + static constexpr u32 guest_page_size = 0x1000; + static constexpr u64 dump_io_chunk_size = 4 * 1024 * 1024; + + struct guest_memory_region + { + u32 start = 0; + u64 size = 0; + u8 flags = 0; + }; + + struct spu_local_store_dump + { + u32 id = 0; + u32 lv2_id = 0; + u32 index = 0; + u32 pc = 0; + u32 vm_offset = 0; + u32 type = 0; + std::string name; + }; + + static bool emulated_processors_quiesced(); + static QString hex_u64(u64 value, int width = 8); +}; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 6714ac3541..979606f580 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -48,9 +48,9 @@ #include "sound_effect_manager_dialog.h" #include "recording_settings_dialog.h" #include "config_database.h" +#include "guest_memory_dumper.h" #include -#include #include #include @@ -60,17 +60,8 @@ #include #include #include -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include "rpcs3_version.h" #include "Emu/IdManager.h" @@ -81,10 +72,6 @@ #include "Emu/system_config.h" #include "Emu/savestate_utils.hpp" #include "Emu/Cell/timers.hpp" -#include "Emu/Cell/PPUThread.h" -#include "Emu/Cell/SPUThread.h" -#include "Emu/Memory/vm.h" -#include "Emu/RSX/RSXThread.h" #include "Crypto/unpkg.h" #include "Crypto/unself.h" @@ -124,69 +111,6 @@ extern atomic_t g_headless; class CPUDisAsm; std::shared_ptr make_basic_ppu_disasm(); -namespace -{ - constexpr u64 guest_address_space_size = 0x1'0000'0000ull; - constexpr u32 guest_page_size = 0x1000; - constexpr u64 dump_io_chunk_size = 4 * 1024 * 1024; - - struct guest_memory_region - { - u32 start = 0; - u64 size = 0; - u8 flags = 0; - }; - - struct spu_local_store_dump - { - u32 id = 0; - u32 lv2_id = 0; - u32 index = 0; - u32 pc = 0; - u32 vm_offset = 0; - u32 type = 0; - std::string name; - }; - - // Emu.Pause() only requests the pause: every emulated thread drains into its - // wait or stop state asynchronously. The dump must not begin until each PPU, - // SPU and RSX thread has actually settled, or it could capture torn state. - bool emulated_processors_quiesced() - { - bool quiesced = true; - const auto check_cpu = [&](u32, cpu_thread& cpu) - { - const auto state = +cpu.state; - if (!::is_stopped(state) && !(state & cpu_flag::wait)) - { - quiesced = false; - } - }; - - if (g_fxo->is_init>>()) - { - idm::select>(check_cpu); - } - - if (g_fxo->is_init>>()) - { - idm::select>(check_cpu); - } - - if (const auto rsx = g_fxo->try_get()) - { - check_cpu(0, *rsx); - } - - return quiesced; - } - - QString hex_u64(u64 value, int width = 8) - { - return QStringLiteral("0x%1").arg(value, width, 16, QLatin1Char('0')); - } -} - extern void qt_events_aware_op(int repeat_duration_ms, std::function wrapped_op) { ensure(wrapped_op); @@ -901,415 +825,6 @@ void main_window::BootRsxCapture(std::string path) } } -void main_window::DumpGuestMemory() -{ - if (Emu.IsStopped() || Emu.IsStarting()) - { - QMessageBox::warning(this, tr("Dump Guest Memory"), tr("Start a game before creating a guest-memory dump.")); - return; - } - - const QString default_parent = QString::fromStdString(fs::get_config_dir() + "guest_memory_dumps"); - QDir().mkpath(default_parent); - - QString title_id = QString::fromStdString(Emu.GetTitleID()); - if (title_id.isEmpty()) - { - title_id = QStringLiteral("RPCS3"); - } - - // Remove the output folder again unless the dump ran to completion, so a - // failed or cancelled dump does not leave an empty folder or a partial - // image behind. This is declared before the output file so it destructs - // after the file is closed. - struct cleanup_guard - { - QDir dir; - bool enabled = false; - bool keep = false; - ~cleanup_guard() - { - if (enabled && !keep && !dir.removeRecursively()) - { - gui_log.error("Could not remove incomplete guest memory dump folder: %s", dir.absolutePath()); - } - } - } cleanup; - - QString picker_parent = default_parent; - QDir output; - fs::file guest_file; - bool sparse_image = false; - - while (!sparse_image) - { - const QString selected_parent = QFileDialog::getExistingDirectory(this, tr("Select Guest Memory Dump Folder"), picker_parent, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if (selected_parent.isEmpty()) - { - return; - } - - // The native folder dialog runs its own event loop, so the game may have - // stopped while the user was choosing a destination. - if (Emu.IsStopped() || Emu.IsStarting()) - { - QMessageBox::warning(this, tr("Dump Guest Memory"), tr("The game stopped before the guest-memory dump could begin.")); - return; - } - - const QString timestamp = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz")); - const QString folder_stem = QStringLiteral("%1_guest_memory_%2").arg(title_id, timestamp); - QDir parent(selected_parent); - QString folder_name = folder_stem; - - for (u32 suffix = 2; parent.exists(folder_name); suffix++) - { - folder_name = QStringLiteral("%1_%2").arg(folder_stem).arg(suffix); - } - - if (!parent.mkpath(folder_name)) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not create the output folder:\n%0").arg(parent.filePath(folder_name))); - return; - } - - output.setPath(parent.filePath(folder_name)); - cleanup.dir = output; - cleanup.enabled = true; - - const QString guest_path = output.filePath(QStringLiteral("guest_memory.bin")); - if (!guest_file.open(guest_path.toStdString(), fs::rewrite)) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not create:\n%0").arg(guest_path)); - return; - } - - // POSIX file systems create holes for unwritten ranges by default; Windows - // needs the sparse attribute set explicitly. - sparse_image = fs::set_sparse(guest_file); - - if (!sparse_image) - { - guest_file.close(); - if (!output.removeRecursively()) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("The selected destination does not support sparse files, and the incomplete output folder could not be removed:\n%0").arg(output.absolutePath())); - return; - } - - cleanup.enabled = false; - QMessageBox message(QMessageBox::Critical, tr("Dump Guest Memory"), tr("The selected destination does not support sparse files. Choose a location on an NTFS volume."), QMessageBox::NoButton, this); - auto* choose_button = message.addButton(tr("Choose Another Location"), QMessageBox::AcceptRole); - message.addButton(QMessageBox::Cancel); - message.setDefaultButton(choose_button); - message.exec(); - - if (message.clickedButton() != choose_button) - { - return; - } - - picker_parent = selected_parent; - } - } - - const bool resume_after_dump = Emu.IsRunning(); - - if (resume_after_dump && !Emu.Pause(false, false)) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not pause emulation for a consistent memory snapshot.")); - return; - } - - struct resume_guard - { - bool enabled = false; - ~resume_guard() - { - if (enabled) - { - Emu.Resume(); - } - } - } resume{resume_after_dump}; - - bool quiesced = false; - QElapsedTimer pause_timer; - pause_timer.start(); - qt_events_aware_op(5, [&]() - { - quiesced = emulated_processors_quiesced(); - return quiesced || pause_timer.elapsed() >= 5000; - }); - - if (!quiesced) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("One or more emulated processors did not stop within five seconds. No dump was created.")); - return; - } - - std::vector regions; - guest_memory_region current{}; - - const auto finish_region = [&]() - { - if (current.size) - { - regions.emplace_back(current); - current = {}; - } - }; - - // Walk the guest page table and coalesce contiguous pages with identical - // flags into regions, so the manifest mirrors the guest memory map instead - // of listing a million single pages. - for (u32 page = 0; page < guest_address_space_size / guest_page_size; page++) - { - const u32 address = static_cast(static_cast(page) * guest_page_size); - const auto [allocated, flags] = vm::get_addr_flags(address); - - if (!allocated) - { - finish_region(); - continue; - } - - if (current.size && (current.flags != flags || static_cast(current.start) + current.size != address)) - { - finish_region(); - } - - if (!current.size) - { - current.start = address; - current.flags = flags; - } - - current.size += guest_page_size; - } - - finish_region(); - - // SPU local store is also part of the guest map (at each thread's - // vm_offset), but a per thread copy with id, pc and type recorded in the - // manifest is much easier to analyze than digging it out of the main image. - std::vector spu_dumps; - if (g_fxo->is_init>>()) - { - idm::select>([&](u32 id, spu_thread& spu) - { - spu_local_store_dump dump; - dump.id = id; - dump.lv2_id = spu.lv2_id; - dump.index = spu.index; - dump.pc = spu.pc; - dump.vm_offset = spu.vm_offset(); - dump.type = static_cast(spu.get_type()); - dump.name = spu.get_name(); - spu_dumps.emplace_back(std::move(dump)); - }); - } - - u64 guest_bytes = 0; - for (const auto& region : regions) - { - guest_bytes += region.size; - } - - const u64 total_bytes = guest_bytes + static_cast(spu_dumps.size()) * SPU_LS_SIZE; - - // Preflight the destination. A sparse image costs roughly the allocated - // bytes, so warn when even that plus some margin does not fit. - const QStorageInfo storage(output.absolutePath()); - const u64 available = storage.bytesAvailable() > 0 ? static_cast(storage.bytesAvailable()) : 0; - - if (const u64 needed = total_bytes + (64ull << 20); available < needed) - { - if (QMessageBox::question(this, tr("Dump Guest Memory"), tr("The destination may not have enough free space (roughly %0 MiB needed, %1 MiB available).\n\nContinue anyway?").arg(needed >> 20).arg(available >> 20)) != QMessageBox::Yes) - { - return; - } - } - - atomic_t completed_bytes{0}; - QProgressDialog progress(tr("Dumping PS3 guest memory..."), QString(), 0, 1000, this); - progress.setWindowTitle(tr("Dump Guest Memory")); - progress.setWindowModality(Qt::WindowModal); - progress.setCancelButton(nullptr); - progress.setMinimumDuration(0); - progress.setValue(0); - - if (!guest_file.trunc(guest_address_space_size)) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not size the 4 GB address image.")); - return; - } - - // Read through the sudo mirror: it maps every allocated page as read/write, - // so pages the guest keeps protected (reservation notification, no access - // areas) are captured as well instead of faulting or being skipped. Copy - // into ordinary host memory before passing data to fs::file, and perform all - // guest-memory reads and file writes off the UI thread. - enum class dump_write_error - { - none, - guest_seek, - guest_write, - spu_write, - }; - - dump_write_error write_error = dump_write_error::none; - u64 error_address = 0; - QString error_path; - const QString output_path = output.absolutePath(); - - named_thread dump_thread("Guest Memory Dumper", [&]() - { - std::vector middle_buffer(dump_io_chunk_size); - - for (const auto& region : regions) - { - if (guest_file.seek(region.start) != region.start) - { - write_error = dump_write_error::guest_seek; - error_address = region.start; - return; - } - - for (u64 offset = 0; offset < region.size;) - { - const u64 chunk_size = std::min(dump_io_chunk_size, region.size - offset); - const u64 address = static_cast(region.start) + offset; - - std::memcpy(middle_buffer.data(), vm::g_sudo_addr + address, chunk_size); - if (guest_file.write(middle_buffer.data(), chunk_size) != chunk_size) - { - write_error = dump_write_error::guest_write; - error_address = address; - return; - } - - offset += chunk_size; - completed_bytes.fetch_add(chunk_size); - } - } - - guest_file.close(); - - QDir worker_output(output_path); - for (const auto& dump : spu_dumps) - { - const QString filename = QStringLiteral("spu_%1_ls.bin").arg(dump.id, 8, 16, QLatin1Char('0')); - const QString file_path = worker_output.filePath(filename); - std::memcpy(middle_buffer.data(), vm::g_sudo_addr + dump.vm_offset, SPU_LS_SIZE); - fs::file file(file_path.toStdString(), fs::rewrite); - - if (!file || file.write(middle_buffer.data(), SPU_LS_SIZE) != SPU_LS_SIZE) - { - write_error = dump_write_error::spu_write; - error_path = file_path; - return; - } - - completed_bytes.fetch_add(SPU_LS_SIZE); - } - }); - - qt_events_aware_op(10, [&]() - { - const u64 completed = completed_bytes.load(); - progress.setValue(total_bytes ? static_cast(completed * 1000 / total_bytes) : 1000); - return static_cast(dump_thread) == thread_state::finished; - }); - dump_thread(); - - if (write_error == dump_write_error::guest_seek) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not seek to guest address %0 in the output file.").arg(hex_u64(error_address))); - return; - } - - if (write_error == dump_write_error::guest_write) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Writing failed at guest address %0. The dump is incomplete.").arg(hex_u64(error_address))); - return; - } - - if (write_error == dump_write_error::spu_write) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not write SPU local store:\n%0").arg(error_path)); - return; - } - - QJsonArray spu_json; - for (const auto& dump : spu_dumps) - { - const QString filename = QStringLiteral("spu_%1_ls.bin").arg(dump.id, 8, 16, QLatin1Char('0')); - QJsonObject item; - item.insert(QStringLiteral("file"), filename); - item.insert(QStringLiteral("id"), hex_u64(dump.id)); - item.insert(QStringLiteral("lv2_id"), hex_u64(dump.lv2_id)); - item.insert(QStringLiteral("index"), static_cast(dump.index)); - item.insert(QStringLiteral("pc"), hex_u64(dump.pc, 5)); - item.insert(QStringLiteral("vm_offset"), hex_u64(dump.vm_offset)); - item.insert(QStringLiteral("type"), static_cast(dump.type)); - item.insert(QStringLiteral("name"), QString::fromStdString(dump.name)); - item.insert(QStringLiteral("size"), static_cast(SPU_LS_SIZE)); - spu_json.append(item); - } - - QJsonArray region_json; - for (const auto& region : regions) - { - QJsonObject item; - item.insert(QStringLiteral("start"), hex_u64(region.start)); - item.insert(QStringLiteral("end_exclusive"), hex_u64(static_cast(region.start) + region.size)); - item.insert(QStringLiteral("size"), static_cast(region.size)); - item.insert(QStringLiteral("file_offset"), hex_u64(region.start)); - item.insert(QStringLiteral("raw_page_flags"), static_cast(region.flags)); - item.insert(QStringLiteral("readable"), !!(region.flags & vm::page_readable)); - item.insert(QStringLiteral("writable"), !!(region.flags & vm::page_writable)); - item.insert(QStringLiteral("executable"), !!(region.flags & vm::page_executable)); - region_json.append(item); - } - - QJsonObject manifest; - manifest.insert(QStringLiteral("format"), QStringLiteral("rpcs3-guest-memory-dump")); - manifest.insert(QStringLiteral("version"), 1); - manifest.insert(QStringLiteral("created_utc"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); - manifest.insert(QStringLiteral("title"), QString::fromStdString(Emu.GetTitle())); - manifest.insert(QStringLiteral("title_id"), QString::fromStdString(Emu.GetTitleID())); - manifest.insert(QStringLiteral("address_space_file"), QStringLiteral("guest_memory.bin")); - manifest.insert(QStringLiteral("address_space_size"), hex_u64(guest_address_space_size, 9)); - manifest.insert(QStringLiteral("page_size"), static_cast(guest_page_size)); - manifest.insert(QStringLiteral("allocated_bytes"), static_cast(guest_bytes)); - manifest.insert(QStringLiteral("file_offset_equals_guest_address"), true); - manifest.insert(QStringLiteral("sparse_image"), sparse_image); - manifest.insert(QStringLiteral("regions"), region_json); - manifest.insert(QStringLiteral("spu_local_stores"), spu_json); - - QFile manifest_file(output.filePath(QStringLiteral("manifest.json"))); - if (!manifest_file.open(QIODevice::WriteOnly | QIODevice::Truncate) || manifest_file.write(QJsonDocument(manifest).toJson(QJsonDocument::Indented)) < 0) - { - QMessageBox::critical(this, tr("Dump Guest Memory"), tr("Could not write the dump manifest.")); - return; - } - - manifest_file.close(); - cleanup.keep = true; - progress.setValue(1000); - gui_log.success("Guest memory dump complete: %s (%u regions, %u SPU local stores, %u bytes allocated)", output.absolutePath(), regions.size(), spu_dumps.size(), guest_bytes); - - if (resume.enabled) - { - Emu.Resume(); - resume.enabled = false; - } - - QMessageBox::information(this, tr("Dump Guest Memory"), tr("Guest memory dump completed.\n\n%0\n\nThe 4 GB guest_memory.bin file is sparse: its file offset equals the PS3 guest address, while only allocated pages consume disk space.").arg(output.absolutePath())); -} - bool main_window::InstallFileInExData(const std::string& extension, const QString& path, const std::string& filename) { if (path.isEmpty() || filename.empty() || extension.empty()) @@ -3874,7 +3389,11 @@ void main_window::CreateConnects() idm::make(this, make_basic_ppu_disasm()); }); - connect(ui->toolsDumpGuestMemoryAct, &QAction::triggered, this, &main_window::DumpGuestMemory); + connect(ui->toolsDumpGuestMemoryAct, &QAction::triggered, this, [this]() + { + guest_memory_dumper* dumper = new guest_memory_dumper(this, true); + dumper->dump_guest_memory(); + }); connect(ui->toolsRsxDebuggerAct, &QAction::triggered, this, [this] { diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index d5fd4c0d77..4f166728f7 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -109,7 +109,6 @@ private Q_SLOTS: void BootVSH(); void BootSavestate(); void BootRsxCapture(std::string path = ""); - void DumpGuestMemory(); void DecryptSPRXLibraries(); void show_boot_error(game_boot_result status); diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index 11046f2a70..b0f023e9b1 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -1079,21 +1079,21 @@ void trophy_manager_dialog::StartTrophyLoadThreads() for (int i = 0; i < count; ++i) indices.append(i); - QFutureWatcher futureWatcher; + QFutureWatcher future_watcher; - progress_dialog progressDialog(tr("Loading trophies"), tr("Loading trophy data, please wait..."), tr("Cancel"), 0, 1, false, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + progress_dialog progress_dlg(tr("Loading trophies"), tr("Loading trophy data, please wait..."), tr("Cancel"), 0, 1, false, this, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); - connect(&futureWatcher, &QFutureWatcher::progressRangeChanged, &progressDialog, &QProgressDialog::setRange); - connect(&futureWatcher, &QFutureWatcher::progressValueChanged, &progressDialog, &QProgressDialog::setValue); - connect(&futureWatcher, &QFutureWatcher::finished, this, [this]() { RepaintUI(true); }); - connect(&progressDialog, &QProgressDialog::canceled, this, [this, &futureWatcher]() + connect(&future_watcher, &QFutureWatcher::progressRangeChanged, &progress_dlg, &QProgressDialog::setRange); + connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress_dlg, &QProgressDialog::setValue); + connect(&future_watcher, &QFutureWatcher::finished, this, [this]() { RepaintUI(true); }); + connect(&progress_dlg, &QProgressDialog::canceled, this, [this, &future_watcher]() { - futureWatcher.cancel(); + future_watcher.cancel(); close(); // It's pointless to show an empty window }); atomic_t error_count{}; - futureWatcher.setFuture(QtConcurrent::map(indices, [this, &error_count, &folder_list](const int& i) + future_watcher.setFuture(QtConcurrent::map(indices, [this, &error_count, &folder_list](const int& i) { const std::string dir_name = folder_list.value(i).toStdString(); gui_log.trace("Loading trophy dir: %s", dir_name); @@ -1106,9 +1106,9 @@ void trophy_manager_dialog::StartTrophyLoadThreads() } })); - progressDialog.exec(); + progress_dlg.exec(); - futureWatcher.waitForFinished(); + future_watcher.waitForFinished(); if (error_count != 0) {