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)
{