diff --git a/Common/CMakeLists.txt b/Common/CMakeLists.txt
index 7c146831c6..59a473be3a 100644
--- a/Common/CMakeLists.txt
+++ b/Common/CMakeLists.txt
@@ -107,6 +107,7 @@ add_library(Common STATIC
Data/Text/Parsers.cpp
Data/Text/Parsers.h
Data/Text/StringWriter.h
+ Data/Text/StringWriter.cpp
Data/Text/WrapText.cpp
Data/Text/WrapText.h
Data/Random/Rng.h
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index 03421d52f1..7b33f35879 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -900,6 +900,7 @@
+
@@ -1081,4 +1082,4 @@
-
+
\ No newline at end of file
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index 45235ce676..72b9de58e3 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -1472,6 +1472,9 @@
UI
+
+ Data\Text
+
diff --git a/Common/Data/Text/Parsers.cpp b/Common/Data/Text/Parsers.cpp
index b5b5c0992c..55b626b0b4 100644
--- a/Common/Data/Text/Parsers.cpp
+++ b/Common/Data/Text/Parsers.cpp
@@ -160,16 +160,3 @@ bool TryParse(const std::string &str, bool *const output) {
return true;
}
-
-StringWriter &StringWriter::F(const char *format, ...) {
- const size_t remainder = bufSize_ - (p_ - start_);
- if (remainder < 3) {
- return *this;
- }
- va_list args;
- va_start(args, format);
- int wouldHaveBeenWritten = vsnprintf(p_, remainder, format, args);
- p_ += std::min((int)remainder, wouldHaveBeenWritten);
- va_end(args);
- return *this;
-}
diff --git a/Common/Data/Text/StringWriter.cpp b/Common/Data/Text/StringWriter.cpp
index 8d8ba6f53a..7e554f386f 100644
--- a/Common/Data/Text/StringWriter.cpp
+++ b/Common/Data/Text/StringWriter.cpp
@@ -1,2 +1,16 @@
+#include
#include "Common/Data/Text/StringWriter.h"
+
+StringWriter &StringWriter::F(const char *format, ...) {
+ const size_t remainder = bufSize_ - (p_ - start_);
+ if (remainder < 3) {
+ return *this;
+ }
+ va_list args;
+ va_start(args, format);
+ int wouldHaveBeenWritten = vsnprintf(p_, remainder, format, args);
+ p_ += std::min((int)remainder, wouldHaveBeenWritten);
+ va_end(args);
+ return *this;
+}
diff --git a/Core/Debugger/SymbolMap.cpp b/Core/Debugger/SymbolMap.cpp
index 577dfd187e..8136e320a5 100644
--- a/Core/Debugger/SymbolMap.cpp
+++ b/Core/Debugger/SymbolMap.cpp
@@ -477,11 +477,14 @@ u32 SymbolMap::GetModuleCrc(int moduleIndex) const {
// per-game rather than per-module, hence GetGameSymbolsPath instead of GetModuleSymbolsPath.
bool SymbolMap::SaveModuleSymbols(int moduleIndex, const Path &filename, const std::string &gameID, const std::string &gameTitle) const {
u32 crc = 0;
+ // Only for logging - module 0 isn't a module, it's the symbols that aren't in one.
+ const char *moduleName = "(no module)";
if (moduleIndex != 0) {
bool found = false;
for (const auto &module : modules) {
if (module.index == moduleIndex) {
crc = module.crc;
+ moduleName = module.name;
found = true;
break;
}
@@ -529,8 +532,10 @@ bool SymbolMap::SaveModuleSymbols(int moduleIndex, const Path &filename, const s
if (count == 0) {
// Nothing worth keeping. Remove any previous file rather than leaving one behind that
// would restore symbols the user has since deleted.
- if (File::Exists(filename))
+ if (File::Exists(filename)) {
+ INFO_LOG(Log::Loader, "SaveModuleSymbols: '%s' has no named symbols left, deleting %s", moduleName, filename.c_str());
File::Delete(filename);
+ }
return true;
}
@@ -552,6 +557,7 @@ bool SymbolMap::SaveModuleSymbols(int moduleIndex, const Path &filename, const s
fwrite(text.data(), 1, text.size(), f);
fclose(f);
+ INFO_LOG(Log::Loader, "SaveModuleSymbols: wrote %d symbol(s) for '%s' to %s", count, moduleName, filename.c_str());
return true;
}
diff --git a/UI/ImDebugger/ImDisasmView.cpp b/UI/ImDebugger/ImDisasmView.cpp
index 108b07b89e..4afe874ecc 100644
--- a/UI/ImDebugger/ImDisasmView.cpp
+++ b/UI/ImDebugger/ImDisasmView.cpp
@@ -1323,6 +1323,38 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
avail.y -= ImGui::GetTextLineHeightWithSpacing();
if (ImGui::BeginChild("left", ImVec2(150.0f, avail.y), ImGuiChildFlags_ResizeX)) {
+ // Modules load and unload as the game runs and nothing tells us about it, so poll. The
+ // list is a couple of dozen entries at most, and this only runs while the panel is open.
+ std::vector modules = g_symbolMap->getAllModules();
+ modules.erase(std::remove_if(modules.begin(), modules.end(), [](const LoadedModuleInfo &m) {
+ return !m.active;
+ }), modules.end());
+ if (modules.size() != symModules_.size() || !std::equal(modules.begin(), modules.end(), symModules_.begin(),
+ [](const LoadedModuleInfo &a, const LoadedModuleInfo &b) {
+ return a.address == b.address && a.size == b.size && a.name == b.name;
+ })) {
+ symModules_ = std::move(modules);
+ // symCache_ holds active symbols only, so it's stale now too.
+ symsDirty_ = true;
+
+ // Re-resolve the module filter against the new list. A module that reloaded lands at
+ // a different address, and one that's gone should drop back to All rather than leave
+ // the list mysteriously empty.
+ if (!symModuleFilter_.empty()) {
+ auto found = std::find_if(symModules_.begin(), symModules_.end(), [this](const LoadedModuleInfo &m) {
+ return m.name == symModuleFilter_;
+ });
+ if (found == symModules_.end()) {
+ symModuleFilter_.clear();
+ symModuleFilterStart_ = 0;
+ symModuleFilterSize_ = 0;
+ } else {
+ symModuleFilterStart_ = found->address;
+ symModuleFilterSize_ = found->size;
+ }
+ }
+ }
+
if (symCache_.empty() || symsDirty_) {
symCache_ = g_symbolMap->GetAllActiveSymbols(SymbolType::ST_FUNCTION);
symsDirty_ = false;
@@ -1346,6 +1378,30 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
symMatchesDirty_ = true;
}
+ ImGui::SetNextItemWidth(-1.0f);
+ if (ImGui::BeginCombo("##symmodule", symModuleFilter_.empty() ? "All modules" : symModuleFilter_.c_str())) {
+ if (ImGui::Selectable("All modules", symModuleFilter_.empty())) {
+ symModuleFilter_.clear();
+ symModuleFilterStart_ = 0;
+ symModuleFilterSize_ = 0;
+ symMatchesDirty_ = true;
+ }
+ for (int i = 0; i < (int)symModules_.size(); i++) {
+ const LoadedModuleInfo &module = symModules_[i];
+ // Module names are not unique - the same one can be loaded more than once.
+ ImGui::PushID(i);
+ const bool selected = symModuleFilter_ == module.name && symModuleFilterStart_ == module.address;
+ if (ImGui::Selectable(module.name.c_str(), selected)) {
+ symModuleFilter_ = module.name;
+ symModuleFilterStart_ = module.address;
+ symModuleFilterSize_ = module.size;
+ symMatchesDirty_ = true;
+ }
+ ImGui::PopID();
+ }
+ ImGui::EndCombo();
+ }
+
if (symMatchesDirty_) {
symMatchesDirty_ = false;
symMatches_.clear();
@@ -1353,6 +1409,11 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
// path to get wrong. These lists run to a few thousand entries, and this only reruns
// when the filter or the symbol map changes, not per frame.
for (int i = 0; i < (int)symCache_.size(); i++) {
+ if (!symModuleFilter_.empty()) {
+ const u32 addr = symCache_[i].address;
+ if (addr < symModuleFilterStart_ || addr >= symModuleFilterStart_ + symModuleFilterSize_)
+ continue;
+ }
if (symFilter_[0] == '\0' || containsNoCase(symCache_[i].name, symFilter_))
symMatches_.push_back(i);
}
diff --git a/UI/ImDebugger/ImDisasmView.h b/UI/ImDebugger/ImDisasmView.h
index 6aa850b94a..fa4723d5c1 100644
--- a/UI/ImDebugger/ImDisasmView.h
+++ b/UI/ImDebugger/ImDisasmView.h
@@ -230,6 +230,16 @@ private:
std::vector symMatches_;
bool symMatchesDirty_ = true;
+ // Currently loaded modules, for the module filter dropdown. Refreshed every frame the
+ // symbol list is visible: modules come and go while a game runs, and nothing notifies us.
+ // A change here also invalidates symCache_, since it's built from active symbols only.
+ std::vector symModules_;
+ // The module filter is remembered as a name plus an address range rather than an index into
+ // symModules_, so a selection survives that list being rebuilt. Empty name means "All".
+ std::string symModuleFilter_;
+ u32 symModuleFilterStart_ = 0;
+ u32 symModuleFilterSize_ = 0;
+
ImDisasmView disasmView_;
char searchTerm_[64]{};
};
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj
index a114502d9f..c7684f4dcb 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj
+++ b/UWP/CommonUWP/CommonUWP.vcxproj
@@ -93,6 +93,7 @@
+
@@ -305,6 +306,7 @@
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters
index 0489ff52a5..834c991ec0 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj.filters
+++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters
@@ -591,6 +591,9 @@
UI
+
+ Data\Text
+
@@ -1145,6 +1148,9 @@
UI
+
+ Data\Text
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index be75a70f5f..ff7d5723a9 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -334,6 +334,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Common/Data/Hash/Hash.cpp \
$(SRC)/Common/Data/Text/I18n.cpp \
$(SRC)/Common/Data/Text/Parsers.cpp \
+ $(SRC)/Common/Data/Text/StringWriter.cpp \
$(SRC)/Common/Data/Text/WrapText.cpp \
$(SRC)/Common/File/AndroidStorage.cpp \
$(SRC)/Common/File/AndroidContentURI.cpp \
diff --git a/build.gradle.kts b/build.gradle.kts
index 94f1c9630e..db41e13ec9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,4 @@
plugins {
- id("com.android.application") version "9.3.1" apply false
+ id("com.android.application") version "9.3.2" apply false
id("com.google.protobuf") version "0.10.0" apply false
}
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index fb201a2080..dff19e1e61 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -451,6 +451,7 @@ SOURCES_CXX += \
$(COMMONDIR)/Data/Hash/Hash.cpp \
$(COMMONDIR)/Data/Text/I18n.cpp \
$(COMMONDIR)/Data/Text/Parsers.cpp \
+ $(COMMONDIR)/Data/Text/StringWriter.cpp \
$(COMMONDIR)/Data/Text/WrapText.cpp \
$(COMMONDIR)/File/VFS/VFS.cpp \
$(COMMONDIR)/File/VFS/DirectoryReader.cpp \