diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bd5ad7b9d..e05678f461 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -481,6 +481,8 @@ endif() if(UNIX) message(STATUS "Using named pipes as controller inputs") add_definitions(-DUSE_PIPES=1) + message(STATUS "Watching game memory for changes") + add_definitions(-DUSE_MEMORYWATCHER=1) endif() if(ENABLE_ANALYTICS) diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 97213f6448..7137dab3f6 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -63,6 +63,7 @@ #define STYLES_DIR "Styles" #define ANAGLYPH_DIR "Anaglyph" #define PIPES_DIR "Pipes" +#define MEMORYWATCHER_DIR "MemoryWatcher" #define WFSROOT_DIR "WFS" #define BACKUP_DIR "Backup" #define RESOURCEPACK_DIR "ResourcePacks" @@ -95,6 +96,10 @@ #define ARAM_DUMP "aram.raw" #define FAKEVMEM_DUMP "fakevmem.raw" +// Files in the directory returned by GetUserPath(D_MEMORYWATCHER_IDX) +#define MEMORYWATCHER_LOCATIONS "Locations.txt" +#define MEMORYWATCHER_SOCKET "MemoryWatcher" + // Sys files #define TOTALDB "totaldb.dsy" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 72d441906e..7030c9dc25 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -801,6 +801,12 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[F_GCSRAM_IDX] = s_user_paths[D_GCUSER_IDX] + GC_SRAM; s_user_paths[F_WIISDCARD_IDX] = s_user_paths[D_WIIROOT_IDX] + DIR_SEP WII_SDCARD; + s_user_paths[D_MEMORYWATCHER_IDX] = s_user_paths[D_USER_IDX] + MEMORYWATCHER_DIR DIR_SEP; + s_user_paths[F_MEMORYWATCHERLOCATIONS_IDX] = + s_user_paths[D_MEMORYWATCHER_IDX] + MEMORYWATCHER_LOCATIONS; + s_user_paths[F_MEMORYWATCHERSOCKET_IDX] = + s_user_paths[D_MEMORYWATCHER_IDX] + MEMORYWATCHER_SOCKET; + // The shader cache has moved to the cache directory, so remove the old one. // TODO: remove that someday. File::DeleteDirRecursively(s_user_paths[D_USER_IDX] + SHADERCACHE_LEGACY_DIR DIR_SEP); diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index dadee2ae23..71e9709da3 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -48,6 +48,7 @@ enum D_THEMES_IDX, D_STYLES_IDX, D_PIPES_IDX, + D_MEMORYWATCHER_IDX, D_WFSROOT_IDX, D_BACKUP_IDX, D_RESOURCEPACK_IDX, @@ -64,6 +65,8 @@ enum F_ARAMDUMP_IDX, F_FAKEVMEMDUMP_IDX, F_GCSRAM_IDX, + F_MEMORYWATCHERLOCATIONS_IDX, + F_MEMORYWATCHERSOCKET_IDX, F_WIISDCARD_IDX, NUM_PATH_INDICES }; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 4a9a0e7b74..60c23769ef 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -383,3 +383,7 @@ endif() if(GDBSTUB) target_sources(core PRIVATE PowerPC/GDBStub.cpp) endif() + +if(UNIX) + target_sources(core PRIVATE MemoryWatcher.cpp) +endif() diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 9f2f6669f5..46de565a01 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -64,6 +64,10 @@ #include "Core/PowerPC/GDBStub.h" #endif +#ifdef USE_MEMORYWATCHER +#include "Core/MemoryWatcher.h" +#endif + #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/GCAdapter.h" @@ -94,6 +98,10 @@ static bool s_request_refresh_info = false; static bool s_is_throttler_temp_disabled = false; static bool s_frame_step = false; +#ifdef USE_MEMORYWATCHER +static std::unique_ptr s_memory_watcher; +#endif + struct HostJob { std::function job; @@ -122,6 +130,13 @@ void FrameUpdateOnCPUThread() NetPlay::NetPlayClient::SendTimeBase(); } +void OnFrameEnd() +{ +#ifdef USE_MEMORYWATCHER + s_memory_watcher->Step(); +#endif +} + // Display messages and return values // Formatted stop message @@ -266,6 +281,10 @@ void Stop() // - Hammertime! } ResetRumble(); + +#ifdef USE_MEMORYWATCHER + s_memory_watcher.reset(); +#endif } void DeclareAsCPUThread() @@ -308,6 +327,10 @@ static void CpuThread(const std::optional& savestate_path, bool del if (_CoreParameter.bFastmem) EMM::InstallExceptionHandler(); // Let's run under memory watch +#ifdef USE_MEMORYWATCHER + s_memory_watcher = std::make_unique(); +#endif + if (savestate_path) { ::State::LoadAs(*savestate_path); diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h index 056e5ed3a1..fdd30a539a 100644 --- a/Source/Core/Core/Core.h +++ b/Source/Core/Core/Core.h @@ -66,6 +66,7 @@ void Callback_WiimoteInterruptChannel(int number, u16 channel_id, const u8* data void DisplayMessage(const std::string& message, int time_in_ms); void FrameUpdateOnCPUThread(); +void OnFrameEnd(); void VideoThrottle(); void RequestRefreshInfo(); diff --git a/Source/Core/Core/HW/VideoInterface.cpp b/Source/Core/Core/HW/VideoInterface.cpp index 8dc871d88b..db235404d2 100644 --- a/Source/Core/Core/HW/VideoInterface.cpp +++ b/Source/Core/Core/HW/VideoInterface.cpp @@ -726,6 +726,7 @@ static void BeginField(FieldType field, u64 ticks) static void EndField() { Core::VideoThrottle(); + Core::OnFrameEnd(); } // Purpose: Send VI interrupt when triggered diff --git a/Source/Core/Core/MemoryWatcher.cpp b/Source/Core/Core/MemoryWatcher.cpp new file mode 100644 index 0000000000..3e6e61c227 --- /dev/null +++ b/Source/Core/Core/MemoryWatcher.cpp @@ -0,0 +1,107 @@ +// Copyright 2015 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include +#include + +#include "Common/FileUtil.h" +#include "Core/HW/Memmap.h" +#include "Core/HW/SystemTimers.h" +#include "Core/MemoryWatcher.h" + +MemoryWatcher::MemoryWatcher() +{ + m_running = false; + if (!LoadAddresses(File::GetUserPath(F_MEMORYWATCHERLOCATIONS_IDX))) + return; + if (!OpenSocket(File::GetUserPath(F_MEMORYWATCHERSOCKET_IDX))) + return; + m_running = true; +} + +MemoryWatcher::~MemoryWatcher() +{ + if (!m_running) + return; + + m_running = false; + close(m_fd); +} + +bool MemoryWatcher::LoadAddresses(const std::string& path) +{ + std::ifstream locations; + File::OpenFStream(locations, path, std::ios_base::in); + if (!locations) + return false; + + std::string line; + while (std::getline(locations, line)) + ParseLine(line); + + return !m_values.empty(); +} + +void MemoryWatcher::ParseLine(const std::string& line) +{ + m_values[line] = 0; + m_addresses[line] = std::vector(); + + std::stringstream offsets(line); + offsets >> std::hex; + u32 offset; + while (offsets >> offset) + m_addresses[line].push_back(offset); +} + +bool MemoryWatcher::OpenSocket(const std::string& path) +{ + m_addr.sun_family = AF_UNIX; + strncpy(m_addr.sun_path, path.c_str(), sizeof(m_addr.sun_path) - 1); + + m_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + return m_fd >= 0; +} + +u32 MemoryWatcher::ChasePointer(const std::string& line) +{ + u32 value = 0; + for (u32 offset : m_addresses[line]) + value = Memory::Read_U32(value + offset); + return value; +} + +std::string MemoryWatcher::ComposeMessages() +{ + std::stringstream message_stream; + message_stream << std::hex; + + for (auto& entry : m_values) + { + std::string address = entry.first; + u32& current_value = entry.second; + + u32 new_value = ChasePointer(address); + if (new_value != current_value) + { + // Update the value + current_value = new_value; + message_stream << address << '\n' << new_value << '\n'; + } + } + + return message_stream.str(); +} + +void MemoryWatcher::Step() +{ + if (!m_running) + return; + + std::string message = ComposeMessages(); + sendto(m_fd, message.c_str(), message.size() + 1, 0, reinterpret_cast(&m_addr), + sizeof(m_addr)); +} diff --git a/Source/Core/Core/MemoryWatcher.h b/Source/Core/Core/MemoryWatcher.h new file mode 100644 index 0000000000..c180e4ebb5 --- /dev/null +++ b/Source/Core/Core/MemoryWatcher.h @@ -0,0 +1,44 @@ +// Copyright 2015 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +// MemoryWatcher reads a file containing in-game memory addresses and outputs +// changes to those memory addresses to a unix domain socket as the game runs. +// +// The input file is a newline-separated list of hex memory addresses, without +// the "0x". To follow pointers, separate addresses with a space. For example, +// "ABCD EF" will watch the address at (*0xABCD) + 0xEF. +// The output to the socket is two lines. The first is the address from the +// input file, and the second is the new value in hex. +class MemoryWatcher final +{ +public: + MemoryWatcher(); + ~MemoryWatcher(); + void Step(); + +private: + bool LoadAddresses(const std::string& path); + bool OpenSocket(const std::string& path); + + void ParseLine(const std::string& line); + u32 ChasePointer(const std::string& line); + std::string ComposeMessages(); + + bool m_running = false; + + int m_fd; + sockaddr_un m_addr{}; + + // Address as stored in the file -> list of offsets to follow + std::map> m_addresses; + // Address as stored in the file -> current value + std::map m_values; +};