Compare commits

...

7 Commits

Author SHA1 Message Date
Megamouse 7a90d09cfe Qt: move guest memory dump code to own class 2026-07-23 10:58:18 +02:00
caner 5ecfc95c13 Qt: add guest memory dump utility (#19068)
This adds a tool that writes a snapshot of the full PS3 guest address
space to disk for offline analysis.

This tool produces a flat image that any hex editor or script can index
directly by guest address. It dumps allocated PS3 guest memory only, not
RPCS3 host-process memory, host registers, or GPU-side state.

What it does:

1. Pauses emulation and waits up to five seconds for every PPU, SPU, and
RSX thread to settle. If they do not settle, nothing is written.
2. Copies every allocated guest page into an intermediate buffer on a
named worker thread, then writes it to a sparse `guest_memory.bin` whose
file offset equals the PS3 effective address. Reads go through
`vm::g_sudo_addr` so guest protected pages are captured too.
3. Saves each live SPU thread's 256 KB local store to its own file, with
its ID, LV2 ID, PC, and type recorded.
4. Writes `manifest.json`, listing every allocated region with its
addresses and permissions, the storage mode, and the SPU local-store
table.
5. Resumes emulation.

The menu entry is under Utilities next to Memory Viewer and is enabled
while a game is loaded. Free space is checked before writing. If sparse
file setup fails, the incomplete attempt is removed and the user can
choose another destination or cancel. Failed or cancelled dumps remove
their output folder instead of leaving a partial image.

Tested on Windows using an NTFS destination with a build from this
branch: dumped a running game with 479,535,104 allocated guest bytes
across 76 regions and five SPU local stores. The image was sparse, its
offsets matched guest addresses, and emulation resumed cleanly. The
sparse file failure path was also exercised with a temporary local test:
choosing another location reopened the destination picker, while
cancelling aborted the attempt without leaving partial output. Runtime
behavior on Linux and macOS is untested.

The first draft of this implementation was AI assisted. I reviewed and
adjusted the code and tested the Windows/NTFS path myself. I am happy to
rework anything or answer any questions.
2026-07-23 00:08:05 +00:00
Megamouse 8604f1c5d8 cli: allow to boot last savestate of a game 2026-07-22 09:13:41 +02:00
RipleyTom 8b05c8cc1f bugfixes 2026-07-21 21:49:00 +03:00
Elad d75543a5b1 LLVM: Add short fallback for get_known_bits
Supports: when value is the immediate result of either a bitwise OR operation or a bitwise AND when either operands is a constant.

Prevents some false positives when the value has PHI nodes in its ancestors.
2026-07-21 21:49:00 +03:00
RipleyTom 2aeb08f929 Fix: get_known_bits fix 2026-07-21 21:49:00 +03:00
Elad 85c59207f7 LLVM: Improve v128 constant extraction 2026-07-21 20:35:56 +03:00
13 changed files with 834 additions and 51 deletions
+12
View File
@@ -26,6 +26,7 @@ std::string g_android_cache_dir;
#include <cwchar> #include <cwchar>
#include <Windows.h> #include <Windows.h>
#include <winioctl.h>
static std::unique_ptr<wchar_t[]> to_wchar(std::string_view source) static std::unique_ptr<wchar_t[]> to_wchar(std::string_view source)
{ {
@@ -1963,6 +1964,17 @@ fs::native_handle fs::file::get_handle() const
#endif #endif
} }
bool fs::set_sparse([[maybe_unused]] const fs::file& file)
{
#ifdef _WIN32
FILE_SET_SPARSE_BUFFER sparse{TRUE};
DWORD returned = 0;
return DeviceIoControl(file.get_handle(), FSCTL_SET_SPARSE, &sparse, sizeof(sparse), nullptr, 0, &returned, nullptr) != FALSE;
#else
return true;
#endif
}
fs::file_id fs::file::get_id() const fs::file_id fs::file::get_id() const
{ {
if (m_file) if (m_file)
+3
View File
@@ -496,6 +496,9 @@ namespace fs
} }
}; };
// Enable sparse-file semantics when required by the host platform.
bool set_sparse(const file& file);
class dir final class dir final
{ {
std::unique_ptr<dir_base> m_dir{}; std::unique_ptr<dir_base> m_dir{};
+133
View File
@@ -317,6 +317,9 @@ llvm::Value* cpu_translator::bitcast(llvm::Value* val, llvm::Type* type, std::so
template <> template <>
std::pair<bool, v128> cpu_translator::get_const_vector<v128>(llvm::Value* c, u32 _pos, u32 _line) std::pair<bool, v128> cpu_translator::get_const_vector<v128>(llvm::Value* c, u32 _pos, u32 _line)
{ {
// Bitcasts do not matter
c = peek_through_bitcasts(c);
v128 result{}; v128 result{};
if (!llvm::isa<llvm::Constant>(c)) if (!llvm::isa<llvm::Constant>(c))
@@ -610,4 +613,134 @@ void cpu_translator::erase_stores(llvm::ArrayRef<llvm::Value*> args)
} }
} }
llvm::KnownBits cpu_translator::get_known_bits_fallback(llvm::Value* value)
{
// TODO: Improve it - add support for integer addition/subtraction and more stuff
const auto type = value->getType();
if (!type->isVectorTy())
{
if (llvm::isa<llvm::IntegerType>(type))
{
if (auto bin_inst = llvm::dyn_cast<llvm::BinaryOperator>(value))
{
llvm::Value* lhs = ensure(bin_inst->getOperand(0));
llvm::Value* rhs = ensure(bin_inst->getOperand(1));
llvm::ConstantInt* constant_value = llvm::dyn_cast<llvm::ConstantInt>(rhs) ? llvm::dyn_cast<llvm::ConstantInt>(rhs) : llvm::dyn_cast<llvm::ConstantInt>(lhs);
if (!constant_value)
{
return llvm::KnownBits(type->getScalarSizeInBits());
}
if (bin_inst->getOpcode() == llvm::Instruction::Or)
{
llvm::KnownBits ret(type->getScalarSizeInBits());
ret.One = constant_value->getValue();
return ret;
}
if (bin_inst->getOpcode() == llvm::Instruction::And)
{
llvm::KnownBits ret(type->getScalarSizeInBits());
ret.Zero = constant_value->getValue();
ret.Zero.flipAllBits();
return ret;
}
return llvm::KnownBits(type->getScalarSizeInBits());
}
}
fmt::throw_exception("Bad KnownBits type: i%ux", type->getScalarSizeInBits());
}
if (auto v = llvm::cast<llvm::FixedVectorType>(type); v->getScalarSizeInBits() * v->getNumElements() != 128)
{
// Unsupported
return llvm::KnownBits(type->getScalarSizeInBits());
}
const auto original_value = peek_through_bitcasts(value);
auto bin_inst = llvm::dyn_cast<llvm::BinaryOperator>(original_value);
if (!bin_inst)
{
return llvm::KnownBits(type->getScalarSizeInBits());
}
llvm::Value* lhs = ensure(bin_inst->getOperand(0));
llvm::Value* rhs = ensure(bin_inst->getOperand(1));
llvm::Value* constant_value = llvm::dyn_cast<llvm::ConstantDataVector>(rhs) ? llvm::dyn_cast<llvm::ConstantDataVector>(rhs) : llvm::dyn_cast<llvm::ConstantDataVector>(lhs);
if (!constant_value)
{
return llvm::KnownBits(value->getType()->getScalarSizeInBits());
}
const auto [ok, v128_const] = get_const_vector(constant_value, -1);
ensure(ok);
llvm::APInt all_lanes{};
llvm::APInt any_lanes{};
auto combine_bits = [&](const auto& array, u32 size)
{
auto all = +array[0];
auto any = +array[0];
for (u32 i = 1; i < size; i++)
{
all &= +array[i];
any |= +array[i];
}
return std::make_pair(all, any);
};
if (type->getScalarType()->isIntegerTy(8))
{
const auto [all, any] = combine_bits(v128_const._u8, 16);
all_lanes = llvm::APInt(8, all);
any_lanes = llvm::APInt(8, any);
}
else if (type->getScalarType()->isIntegerTy(16))
{
const auto [all, any] = combine_bits(v128_const._u16, 8);
all_lanes = llvm::APInt(16, all);
any_lanes = llvm::APInt(16, any);
}
else if (type->getScalarType()->isIntegerTy(32))
{
const auto [all, any] = combine_bits(v128_const._u32, 4);
all_lanes = llvm::APInt(32, all);
any_lanes = llvm::APInt(32, any);
}
else // if (type->getScalarType()->isIntegerTy(64))
{
return llvm::KnownBits(type->getScalarSizeInBits());
}
if (bin_inst->getOpcode() == llvm::Instruction::Or)
{
llvm::KnownBits ret(type->getScalarSizeInBits());
ret.One = all_lanes;
return ret;
}
if (bin_inst->getOpcode() == llvm::Instruction::And)
{
llvm::KnownBits ret(type->getScalarSizeInBits());
ret.Zero = any_lanes;
ret.Zero.flipAllBits();
return ret;
}
return llvm::KnownBits(type->getScalarSizeInBits());
}
#endif #endif
+46 -1
View File
@@ -4271,10 +4271,55 @@ template <typename T1, typename T2, typename T3>
template <typename T = v128> template <typename T = v128>
llvm::Constant* make_const_vector(T, llvm::Type*, u32 = __builtin_LINE()); llvm::Constant* make_const_vector(T, llvm::Type*, u32 = __builtin_LINE());
// IR is emitted in a single pass: phi nodes may still be missing their back-edge incoming
// values, so any known bits computeKnownBits derives through a phi are unsound for the
// final IR. Whether a phi is complete cannot be queried (the CFG edges from not-yet-emitted
// predecessors don't exist either), so reject every value whose bits may derive from a phi.
static bool is_known_bits_safe(llvm::Value* value)
{
llvm::SmallPtrSet<const llvm::Value*, 32> visited;
llvm::SmallVector<const llvm::Value*, 32> worklist{value};
while (!worklist.empty())
{
const llvm::Value* v = worklist.pop_back_val();
if (!visited.insert(v).second)
{
continue;
}
if (llvm::isa<llvm::PHINode>(v) || visited.size() > 256)
{
return false;
}
// Loads don't propagate operand bits; constants and arguments are leaves
if (auto i = llvm::dyn_cast<llvm::Instruction>(v); i && !llvm::isa<llvm::LoadInst>(i))
{
for (const llvm::Use& op : i->operands())
{
worklist.push_back(op.get());
}
}
}
return true;
}
llvm::KnownBits get_known_bits_fallback(llvm::Value* value);
template <typename T> template <typename T>
llvm::KnownBits get_known_bits(T a) llvm::KnownBits get_known_bits(T a)
{ {
return llvm::computeKnownBits(a.eval(m_ir), m_module->getDataLayout()); llvm::Value* value = a.eval(m_ir);
if (!is_known_bits_safe(value))
{
return get_known_bits_fallback(value);
}
return llvm::computeKnownBits(value, m_module->getDataLayout());
} }
template <typename T> template <typename T>
+49 -31
View File
@@ -70,9 +70,12 @@ DYNAMIC_IMPORT("ntdll.dll", NtSetTimerResolution, NTSTATUS(ULONG DesiredResoluti
#include "Emu/System.h" #include "Emu/System.h"
#include "Emu/system_config.h" #include "Emu/system_config.h"
#include "Emu/system_utils.hpp" #include "Emu/system_utils.hpp"
#include "Emu/savestate_utils.hpp"
#include "Emu/RSX/Overlays/overlay_message.h" #include "Emu/RSX/Overlays/overlay_message.h"
#include <thread> #include <thread>
#include <charconv> #include <charconv>
#include <regex>
#include "util/sysinfo.hpp" #include "util/sysinfo.hpp"
@@ -384,36 +387,37 @@ private:
}; };
// Arguments that force a headless application (need to be checked in create_application) // Arguments that force a headless application (need to be checked in create_application)
constexpr auto arg_headless = "headless"; constexpr auto arg_headless = "headless";
constexpr auto arg_decrypt = "decrypt"; constexpr auto arg_decrypt = "decrypt";
// Arguments that can be used with a gui application // Arguments that can be used with a gui application
constexpr auto arg_no_gui = "no-gui"; constexpr auto arg_no_gui = "no-gui";
constexpr auto arg_fullscreen = "fullscreen"; // only useful with no-gui constexpr auto arg_fullscreen = "fullscreen"; // only useful with no-gui
constexpr auto arg_gs_screen = "game-screen"; constexpr auto arg_gs_screen = "game-screen";
constexpr auto arg_high_dpi = "hidpi"; constexpr auto arg_high_dpi = "hidpi";
constexpr auto arg_rounding = "dpi-rounding"; constexpr auto arg_rounding = "dpi-rounding";
constexpr auto arg_styles = "styles"; constexpr auto arg_styles = "styles";
constexpr auto arg_style = "style"; constexpr auto arg_style = "style";
constexpr auto arg_stylesheet = "stylesheet"; constexpr auto arg_stylesheet = "stylesheet";
constexpr auto arg_config = "config"; constexpr auto arg_config = "config";
constexpr auto arg_input_config = "input-config"; // only useful with no-gui constexpr auto arg_input_config = "input-config"; // only useful with no-gui
constexpr auto arg_q_debug = "qDebug"; constexpr auto arg_q_debug = "qDebug";
constexpr auto arg_error = "error"; constexpr auto arg_error = "error";
constexpr auto arg_updating = "updating"; constexpr auto arg_updating = "updating";
constexpr auto arg_user_id = "user-id"; constexpr auto arg_user_id = "user-id";
constexpr auto arg_installfw = "installfw"; constexpr auto arg_installfw = "installfw";
constexpr auto arg_installpkg = "installpkg"; constexpr auto arg_installpkg = "installpkg";
constexpr auto arg_savestate = "savestate"; constexpr auto arg_savestate = "savestate";
constexpr auto arg_rsx_capture = "rsx-capture"; constexpr auto arg_last_savestate = "last-savestate";
constexpr auto arg_timer = "high-res-timer"; constexpr auto arg_rsx_capture = "rsx-capture";
constexpr auto arg_verbose_curl = "verbose-curl"; constexpr auto arg_timer = "high-res-timer";
constexpr auto arg_any_location = "allow-any-location"; constexpr auto arg_verbose_curl = "verbose-curl";
constexpr auto arg_codecs = "codecs"; constexpr auto arg_any_location = "allow-any-location";
constexpr auto arg_codecs = "codecs";
#ifdef _WIN32 #ifdef _WIN32
constexpr auto arg_stdout = "stdout"; constexpr auto arg_stdout = "stdout";
constexpr auto arg_stderr = "stderr"; constexpr auto arg_stderr = "stderr";
#endif #endif
constexpr auto arg_emulation_barrier = ""; constexpr auto arg_emulation_barrier = "";
@@ -839,6 +843,8 @@ int run_rpcs3(int argc, char** argv)
parser.addOption(user_id_option); parser.addOption(user_id_option);
const QCommandLineOption savestate_option(arg_savestate, "Path for directly loading a savestate.", "path", ""); const QCommandLineOption savestate_option(arg_savestate, "Path for directly loading a savestate.", "path", "");
parser.addOption(savestate_option); parser.addOption(savestate_option);
const QCommandLineOption last_savestate_option(arg_last_savestate, "Loading the last savestate of a game.", "path", "Title-ID or path");
parser.addOption(last_savestate_option);
const QCommandLineOption rsx_capture_option(arg_rsx_capture, "Path for directly loading an rsx capture.", "path", ""); const QCommandLineOption rsx_capture_option(arg_rsx_capture, "Path for directly loading an rsx capture.", "path", "");
parser.addOption(rsx_capture_option); parser.addOption(rsx_capture_option);
parser.addOption(QCommandLineOption(arg_q_debug, "Log qDebug to RPCS3.log.")); parser.addOption(QCommandLineOption(arg_q_debug, "Log qDebug to RPCS3.log."));
@@ -1196,14 +1202,26 @@ int run_rpcs3(int argc, char** argv)
} }
} }
if (parser.isSet(arg_savestate)) if (parser.isSet(arg_savestate) || parser.isSet(arg_last_savestate))
{ {
const std::string savestate_path = parser.value(savestate_option).toStdString(); std::string savestate_path;
sys_log.notice("Booting savestate from command line: %s", savestate_path);
if (!fs::is_file(savestate_path)) if (parser.isSet(arg_savestate))
{ {
report_fatal_error(fmt::format("No savestate file found: %s", savestate_path)); savestate_path = parser.value(savestate_option).toStdString();
sys_log.notice("Booting savestate from command line: path='%s'", savestate_path);
}
else
{
const std::string serial_or_path = parser.value(last_savestate_option).toStdString();
const bool is_serial = std::regex_match(serial_or_path, std::regex(R"(^[A-Z]{4}\d{5}$)"));
savestate_path = get_savestate_file(is_serial ? serial_or_path : "", is_serial ? "" : serial_or_path, 1);
sys_log.notice("Booting last savestate from command line: game='%s', path='%s'", serial_or_path, savestate_path);
}
if (!is_savestate_compatible(savestate_path))
{
report_fatal_error(fmt::format("No savestate file found or savestate not compatible: path='%s'", savestate_path));
} }
Emu.CallFromMainThread([path = savestate_path]() Emu.CallFromMainThread([path = savestate_path]()
+17
View File
@@ -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>
+19 -9
View File
@@ -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>
+1
View File
@@ -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
+479
View File
@@ -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();
}
}
+45
View File
@@ -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);
};
+8
View File
@@ -48,6 +48,7 @@
#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 <unordered_set> #include <unordered_set>
@@ -2192,6 +2193,7 @@ void main_window::EnableMenus(bool enabled) const
// Tools // Tools
ui->toolskernel_explorerAct->setEnabled(enabled); ui->toolskernel_explorerAct->setEnabled(enabled);
ui->toolsmemory_viewerAct->setEnabled(enabled); ui->toolsmemory_viewerAct->setEnabled(enabled);
ui->toolsDumpGuestMemoryAct->setEnabled(enabled);
ui->toolsRsxDebuggerAct->setEnabled(enabled); ui->toolsRsxDebuggerAct->setEnabled(enabled);
ui->toolsSystemCommandsAct->setEnabled(enabled); ui->toolsSystemCommandsAct->setEnabled(enabled);
ui->actionCreate_RSX_Capture->setEnabled(enabled); ui->actionCreate_RSX_Capture->setEnabled(enabled);
@@ -3387,6 +3389,12 @@ 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, [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]
{ {
rsx_debugger* rsx = new rsx_debugger(m_gui_settings); rsx_debugger* rsx = new rsx_debugger(m_gui_settings);
+12
View File
@@ -365,6 +365,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="toolsSystemCommandsAct"/> <addaction name="toolsSystemCommandsAct"/>
<addaction name="toolsmemory_viewerAct"/> <addaction name="toolsmemory_viewerAct"/>
<addaction name="toolsDumpGuestMemoryAct"/>
<addaction name="toolskernel_explorerAct"/> <addaction name="toolskernel_explorerAct"/>
<addaction name="toolsRsxDebuggerAct"/> <addaction name="toolsRsxDebuggerAct"/>
<addaction name="separator"/> <addaction name="separator"/>
@@ -759,6 +760,17 @@
<string>Memory Viewer</string> <string>Memory Viewer</string>
</property> </property>
</action> </action>
<action name="toolsDumpGuestMemoryAct">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Dump Guest Memory</string>
</property>
<property name="toolTip">
<string>Dump all allocated PS3 guest memory and live SPU local stores</string>
</property>
</action>
<action name="toolsRsxDebuggerAct"> <action name="toolsRsxDebuggerAct">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
+10 -10
View File
@@ -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)
{ {