Qt: move guest memory dump code to own class
This commit is contained in:
+1
-1
@@ -1964,7 +1964,7 @@ fs::native_handle fs::file::get_handle() const
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fs::set_sparse(const fs::file& file)
|
bool fs::set_sparse([[maybe_unused]] const fs::file& file)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
FILE_SET_SPARSE_BUFFER sparse{TRUE};
|
FILE_SET_SPARSE_BUFFER sparse{TRUE};
|
||||||
|
|||||||
@@ -230,6 +230,9 @@
|
|||||||
<ClCompile Include="QTGeneratedFiles\Debug\moc_anaglyph_settings_dialog.cpp">
|
<ClCompile Include="QTGeneratedFiles\Debug\moc_anaglyph_settings_dialog.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="QTGeneratedFiles\Debug\moc_guest_memory_dumper.cpp">
|
||||||
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="QTGeneratedFiles\Debug\moc_breakpoint_list.cpp">
|
<ClCompile Include="QTGeneratedFiles\Debug\moc_breakpoint_list.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -542,6 +545,9 @@
|
|||||||
<ClCompile Include="QTGeneratedFiles\Release\moc_anaglyph_settings_dialog.cpp">
|
<ClCompile Include="QTGeneratedFiles\Release\moc_anaglyph_settings_dialog.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="QTGeneratedFiles\Release\moc_guest_memory_dumper.cpp">
|
||||||
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="QTGeneratedFiles\Release\moc_breakpoint_list.cpp">
|
<ClCompile Include="QTGeneratedFiles\Release\moc_breakpoint_list.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -845,6 +851,7 @@
|
|||||||
<ClCompile Include="rpcs3qt\call_stack_list.cpp" />
|
<ClCompile Include="rpcs3qt\call_stack_list.cpp" />
|
||||||
<ClCompile Include="rpcs3qt\camera_settings_dialog.cpp" />
|
<ClCompile Include="rpcs3qt\camera_settings_dialog.cpp" />
|
||||||
<ClCompile Include="rpcs3qt\emu_settings_type.cpp" />
|
<ClCompile Include="rpcs3qt\emu_settings_type.cpp" />
|
||||||
|
<ClCompile Include="rpcs3qt\guest_memory_dumper.cpp" />
|
||||||
<ClCompile Include="rpcs3qt\gui_game_info.cpp" />
|
<ClCompile Include="rpcs3qt\gui_game_info.cpp" />
|
||||||
<ClCompile Include="rpcs3qt\log_level_dialog.cpp" />
|
<ClCompile Include="rpcs3qt\log_level_dialog.cpp" />
|
||||||
<ClCompile Include="rpcs3qt\permissions.cpp" />
|
<ClCompile Include="rpcs3qt\permissions.cpp" />
|
||||||
@@ -1208,6 +1215,16 @@
|
|||||||
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
|
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
|
||||||
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
|
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
|
||||||
</CustomBuild>
|
</CustomBuild>
|
||||||
|
<CustomBuild Include="rpcs3qt\guest_memory_dumper.h">
|
||||||
|
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
|
||||||
|
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
|
||||||
|
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
|
||||||
|
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(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"</Command>
|
||||||
|
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
|
||||||
|
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing %(Identity)...</Message>
|
||||||
|
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
|
||||||
|
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
|
||||||
|
</CustomBuild>
|
||||||
<ClInclude Include="rpcs3qt\breakpoint_handler.h" />
|
<ClInclude Include="rpcs3qt\breakpoint_handler.h" />
|
||||||
<CustomBuild Include="rpcs3qt\breakpoint_list.h">
|
<CustomBuild Include="rpcs3qt\breakpoint_list.h">
|
||||||
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
|
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
|
||||||
|
|||||||
@@ -585,6 +585,12 @@
|
|||||||
<ClCompile Include="QTGeneratedFiles\Release\moc_anaglyph_settings_dialog.cpp">
|
<ClCompile Include="QTGeneratedFiles\Release\moc_anaglyph_settings_dialog.cpp">
|
||||||
<Filter>Generated Files\Release</Filter>
|
<Filter>Generated Files\Release</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="QTGeneratedFiles\Debug\moc_guest_memory_dumper.cpp">
|
||||||
|
<Filter>Generated Files\Debug</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="QTGeneratedFiles\Release\moc_guest_memory_dumper.cpp">
|
||||||
|
<Filter>Generated Files\Release</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="QTGeneratedFiles\Debug\moc_breakpoint_list.cpp">
|
<ClCompile Include="QTGeneratedFiles\Debug\moc_breakpoint_list.cpp">
|
||||||
<Filter>Generated Files\Debug</Filter>
|
<Filter>Generated Files\Debug</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -1314,6 +1320,9 @@
|
|||||||
<ClCompile Include="rpcs3qt\anaglyph_settings_dialog.cpp">
|
<ClCompile Include="rpcs3qt\anaglyph_settings_dialog.cpp">
|
||||||
<Filter>Gui\settings</Filter>
|
<Filter>Gui\settings</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="rpcs3qt\guest_memory_dumper.cpp">
|
||||||
|
<Filter>Gui\utils</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="Input\ds4_pad_handler.h">
|
<ClInclude Include="Input\ds4_pad_handler.h">
|
||||||
@@ -1565,7 +1574,7 @@
|
|||||||
<ClInclude Include="rpcs3qt\steam_utils.h">
|
<ClInclude Include="rpcs3qt\steam_utils.h">
|
||||||
<Filter>Gui\utils</Filter>
|
<Filter>Gui\utils</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="rpcs3qt\anaglyph_settings_dialog.h">
|
<ClInclude Include="rpcs3qt\render_creator.h">
|
||||||
<Filter>Gui\settings</Filter>
|
<Filter>Gui\settings</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -1783,9 +1792,6 @@
|
|||||||
<CustomBuild Include="rpcs3qt\microphone_creator.h">
|
<CustomBuild Include="rpcs3qt\microphone_creator.h">
|
||||||
<Filter>Gui\settings</Filter>
|
<Filter>Gui\settings</Filter>
|
||||||
</CustomBuild>
|
</CustomBuild>
|
||||||
<CustomBuild Include="rpcs3qt\render_creator.h">
|
|
||||||
<Filter>Gui\settings</Filter>
|
|
||||||
</CustomBuild>
|
|
||||||
<CustomBuild Include="rpcs3qt\patch_manager_dialog.h">
|
<CustomBuild Include="rpcs3qt\patch_manager_dialog.h">
|
||||||
<Filter>Gui\patch manager</Filter>
|
<Filter>Gui\patch manager</Filter>
|
||||||
</CustomBuild>
|
</CustomBuild>
|
||||||
@@ -1927,6 +1933,15 @@
|
|||||||
<CustomBuild Include="rpcs3qt\game_list_actions.h">
|
<CustomBuild Include="rpcs3qt\game_list_actions.h">
|
||||||
<Filter>Gui\game list</Filter>
|
<Filter>Gui\game list</Filter>
|
||||||
</CustomBuild>
|
</CustomBuild>
|
||||||
|
<CustomBuild Include="rpcs3qt\content_integrity.h">
|
||||||
|
<Filter>Gui\game list</Filter>
|
||||||
|
</CustomBuild>
|
||||||
|
<CustomBuild Include="rpcs3qt\anaglyph_settings_dialog.h">
|
||||||
|
<Filter>Gui\settings</Filter>
|
||||||
|
</CustomBuild>
|
||||||
|
<CustomBuild Include="rpcs3qt\guest_memory_dumper.h">
|
||||||
|
<Filter>Gui\utils</Filter>
|
||||||
|
</CustomBuild>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="rpcs3.ico" />
|
<Image Include="rpcs3.ico" />
|
||||||
@@ -2082,9 +2097,4 @@
|
|||||||
<Filter>buildfiles\cmake</Filter>
|
<Filter>buildfiles\cmake</Filter>
|
||||||
</Text>
|
</Text>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<CustomBuild Include="rpcs3qt\content_integrity.h">
|
|
||||||
<Filter>Gui\game list</Filter>
|
|
||||||
</CustomBuild>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -43,6 +43,7 @@ add_library(rpcs3_ui STATIC
|
|||||||
game_list_grid.cpp
|
game_list_grid.cpp
|
||||||
game_list_grid_item.cpp
|
game_list_grid_item.cpp
|
||||||
game_list_table.cpp
|
game_list_table.cpp
|
||||||
|
guest_memory_dumper.cpp
|
||||||
gui_application.cpp
|
gui_application.cpp
|
||||||
gl_gs_frame.cpp
|
gl_gs_frame.cpp
|
||||||
gs_frame.cpp
|
gs_frame.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 <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStorageInfo>
|
||||||
|
|
||||||
|
LOG_CHANNEL(gui_log, "GUI");
|
||||||
|
|
||||||
|
extern void qt_events_aware_op(int repeat_duration_ms, std::function<bool()> 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<id_manager::id_map<named_thread<ppu_thread>>>())
|
||||||
|
{
|
||||||
|
idm::select<named_thread<ppu_thread>>(check_cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_fxo->is_init<id_manager::id_map<named_thread<spu_thread>>>())
|
||||||
|
{
|
||||||
|
idm::select<named_thread<spu_thread>>(check_cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto rsx = g_fxo->try_get<rsx::thread>())
|
||||||
|
{
|
||||||
|
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<guest_memory_region> 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<u32>(static_cast<u64>(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<u64>(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_local_store_dump> spu_dumps;
|
||||||
|
if (g_fxo->is_init<id_manager::id_map<named_thread<spu_thread>>>())
|
||||||
|
{
|
||||||
|
idm::select<named_thread<spu_thread>>([&](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<u32>(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<u64>(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<u64>(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<u64> 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<u8> 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<u64>(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<int>(completed * 1000 / total_bytes) : 1000);
|
||||||
|
return static_cast<thread_state>(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<int>(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<int>(dump.type));
|
||||||
|
item.insert(QStringLiteral("name"), QString::fromStdString(dump.name));
|
||||||
|
item.insert(QStringLiteral("size"), static_cast<int>(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<u64>(region.start) + region.size));
|
||||||
|
item.insert(QStringLiteral("size"), static_cast<double>(region.size));
|
||||||
|
item.insert(QStringLiteral("file_offset"), hex_u64(region.start));
|
||||||
|
item.insert(QStringLiteral("raw_page_flags"), static_cast<int>(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<int>(guest_page_size));
|
||||||
|
manifest.insert(QStringLiteral("allocated_bytes"), static_cast<double>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "util/types.hpp"
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
#include "sound_effect_manager_dialog.h"
|
#include "sound_effect_manager_dialog.h"
|
||||||
#include "recording_settings_dialog.h"
|
#include "recording_settings_dialog.h"
|
||||||
#include "config_database.h"
|
#include "config_database.h"
|
||||||
|
#include "guest_memory_dumper.h"
|
||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <cstring>
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
@@ -60,17 +60,8 @@
|
|||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFontDatabase>
|
#include <QFontDatabase>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDateTime>
|
|
||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDir>
|
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QProgressDialog>
|
|
||||||
#include <QStorageInfo>
|
|
||||||
|
|
||||||
#include "rpcs3_version.h"
|
#include "rpcs3_version.h"
|
||||||
#include "Emu/IdManager.h"
|
#include "Emu/IdManager.h"
|
||||||
@@ -81,10 +72,6 @@
|
|||||||
#include "Emu/system_config.h"
|
#include "Emu/system_config.h"
|
||||||
#include "Emu/savestate_utils.hpp"
|
#include "Emu/savestate_utils.hpp"
|
||||||
#include "Emu/Cell/timers.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/unpkg.h"
|
||||||
#include "Crypto/unself.h"
|
#include "Crypto/unself.h"
|
||||||
@@ -124,69 +111,6 @@ extern atomic_t<bool> g_headless;
|
|||||||
class CPUDisAsm;
|
class CPUDisAsm;
|
||||||
std::shared_ptr<CPUDisAsm> make_basic_ppu_disasm();
|
std::shared_ptr<CPUDisAsm> 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<id_manager::id_map<named_thread<ppu_thread>>>())
|
|
||||||
{
|
|
||||||
idm::select<named_thread<ppu_thread>>(check_cpu);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_fxo->is_init<id_manager::id_map<named_thread<spu_thread>>>())
|
|
||||||
{
|
|
||||||
idm::select<named_thread<spu_thread>>(check_cpu);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto rsx = g_fxo->try_get<rsx::thread>())
|
|
||||||
{
|
|
||||||
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<bool()> wrapped_op)
|
extern void qt_events_aware_op(int repeat_duration_ms, std::function<bool()> wrapped_op)
|
||||||
{
|
{
|
||||||
ensure(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<guest_memory_region> 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<u32>(static_cast<u64>(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<u64>(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_local_store_dump> spu_dumps;
|
|
||||||
if (g_fxo->is_init<id_manager::id_map<named_thread<spu_thread>>>())
|
|
||||||
{
|
|
||||||
idm::select<named_thread<spu_thread>>([&](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<u32>(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<u64>(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<u64>(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<u64> 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<u8> 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<u64>(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<int>(completed * 1000 / total_bytes) : 1000);
|
|
||||||
return static_cast<thread_state>(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<int>(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<int>(dump.type));
|
|
||||||
item.insert(QStringLiteral("name"), QString::fromStdString(dump.name));
|
|
||||||
item.insert(QStringLiteral("size"), static_cast<int>(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<u64>(region.start) + region.size));
|
|
||||||
item.insert(QStringLiteral("size"), static_cast<double>(region.size));
|
|
||||||
item.insert(QStringLiteral("file_offset"), hex_u64(region.start));
|
|
||||||
item.insert(QStringLiteral("raw_page_flags"), static_cast<int>(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<int>(guest_page_size));
|
|
||||||
manifest.insert(QStringLiteral("allocated_bytes"), static_cast<double>(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)
|
bool main_window::InstallFileInExData(const std::string& extension, const QString& path, const std::string& filename)
|
||||||
{
|
{
|
||||||
if (path.isEmpty() || filename.empty() || extension.empty())
|
if (path.isEmpty() || filename.empty() || extension.empty())
|
||||||
@@ -3874,7 +3389,11 @@ void main_window::CreateConnects()
|
|||||||
idm::make<memory_viewer_handle>(this, make_basic_ppu_disasm());
|
idm::make<memory_viewer_handle>(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]
|
connect(ui->toolsRsxDebuggerAct, &QAction::triggered, this, [this]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ private Q_SLOTS:
|
|||||||
void BootVSH();
|
void BootVSH();
|
||||||
void BootSavestate();
|
void BootSavestate();
|
||||||
void BootRsxCapture(std::string path = "");
|
void BootRsxCapture(std::string path = "");
|
||||||
void DumpGuestMemory();
|
|
||||||
void DecryptSPRXLibraries();
|
void DecryptSPRXLibraries();
|
||||||
void show_boot_error(game_boot_result status);
|
void show_boot_error(game_boot_result status);
|
||||||
|
|
||||||
|
|||||||
@@ -1079,21 +1079,21 @@ void trophy_manager_dialog::StartTrophyLoadThreads()
|
|||||||
for (int i = 0; i < count; ++i)
|
for (int i = 0; i < count; ++i)
|
||||||
indices.append(i);
|
indices.append(i);
|
||||||
|
|
||||||
QFutureWatcher<void> futureWatcher;
|
QFutureWatcher<void> 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<void>::progressRangeChanged, &progressDialog, &QProgressDialog::setRange);
|
connect(&future_watcher, &QFutureWatcher<void>::progressRangeChanged, &progress_dlg, &QProgressDialog::setRange);
|
||||||
connect(&futureWatcher, &QFutureWatcher<void>::progressValueChanged, &progressDialog, &QProgressDialog::setValue);
|
connect(&future_watcher, &QFutureWatcher<void>::progressValueChanged, &progress_dlg, &QProgressDialog::setValue);
|
||||||
connect(&futureWatcher, &QFutureWatcher<void>::finished, this, [this]() { RepaintUI(true); });
|
connect(&future_watcher, &QFutureWatcher<void>::finished, this, [this]() { RepaintUI(true); });
|
||||||
connect(&progressDialog, &QProgressDialog::canceled, this, [this, &futureWatcher]()
|
connect(&progress_dlg, &QProgressDialog::canceled, this, [this, &future_watcher]()
|
||||||
{
|
{
|
||||||
futureWatcher.cancel();
|
future_watcher.cancel();
|
||||||
close(); // It's pointless to show an empty window
|
close(); // It's pointless to show an empty window
|
||||||
});
|
});
|
||||||
|
|
||||||
atomic_t<usz> error_count{};
|
atomic_t<usz> 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();
|
const std::string dir_name = folder_list.value(i).toStdString();
|
||||||
gui_log.trace("Loading trophy dir: %s", dir_name);
|
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)
|
if (error_count != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user