summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorAmber Brault <celestialamber1@gmail.com>2026-05-07 09:24:55 +0200
committerAmber Brault <celestialamber1@gmail.com>2026-05-07 09:25:28 +0200
commit3f5460a5ff318751b3effb85c4c57bbdd46e9ece (patch)
treea3d1de70144416637e4e1e0ccfa7bf9ee4d03ef0 /Source
parenteb44b64c9ecbca2f6e346a1d2bdcdf00609f9f31 (diff)
Core: Implement automatic symbol demangling
Diffstat (limited to 'Source')
-rw-r--r--Source/Core/Common/CMakeLists.txt2
-rw-r--r--Source/Core/Common/CWDemangler.cpp772
-rw-r--r--Source/Core/Common/CWDemangler.h67
-rw-r--r--Source/Core/Common/StringUtil.cpp31
-rw-r--r--Source/Core/Common/StringUtil.h2
-rw-r--r--Source/Core/Common/SymbolDB.cpp5
-rw-r--r--Source/Core/Common/SymbolDB.h15
-rw-r--r--Source/Core/DolphinLib.props2
-rw-r--r--Source/Core/DolphinQt/Debugger/CodeWidget.cpp35
-rw-r--r--Source/Core/DolphinQt/Debugger/CodeWidget.h3
-rw-r--r--Source/Core/DolphinQt/MenuBar.cpp8
-rw-r--r--Source/Core/DolphinQt/MenuBar.h1
-rw-r--r--Source/Core/DolphinQt/Settings.cpp14
-rw-r--r--Source/Core/DolphinQt/Settings.h3
-rw-r--r--Source/UnitTests/Common/CMakeLists.txt1
-rw-r--r--Source/UnitTests/Common/CWDemanglerTest.cpp431
-rw-r--r--Source/UnitTests/UnitTests.vcxproj1
17 files changed, 1386 insertions, 7 deletions
diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt
index 894fd38128..0759756629 100644
--- a/Source/Core/Common/CMakeLists.txt
+++ b/Source/Core/Common/CMakeLists.txt
@@ -47,6 +47,8 @@ add_library(common
Crypto/HMAC.h
Crypto/SHA1.cpp
Crypto/SHA1.h
+ CWDemangler.cpp
+ CWDemangler.h
Debug/MemoryPatches.cpp
Debug/MemoryPatches.h
Debug/Threads.h
diff --git a/Source/Core/Common/CWDemangler.cpp b/Source/Core/Common/CWDemangler.cpp
new file mode 100644
index 0000000000..8b4d700c2a
--- /dev/null
+++ b/Source/Core/Common/CWDemangler.cpp
@@ -0,0 +1,772 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Based on: https://github.com/encounter/cwdemangle
+// Copyright 2024 Luke Street <luke@street.dev>
+// SPDX-License-Identifier: CC0-1.0
+
+#include "Common/CWDemangler.h"
+
+#include <algorithm>
+#include <cctype>
+#include <map>
+
+#include <fmt/format.h>
+
+#include "Common/StringUtil.h"
+
+namespace CWDemangler
+{
+
+struct ParseQualifiersResult
+{
+ std::string pre;
+ std::string post;
+ std::string_view rest;
+};
+
+struct ParseDigitsResult
+{
+ std::size_t digits;
+ std::string_view rest;
+};
+
+inline static bool IsAscii(std::string_view s)
+{
+ return std::ranges::none_of(s, [](u8 c) { return c > 127; });
+}
+
+static const std::map<std::string_view, std::string_view> operators = {
+ {"nw", "operator new"}, {"nwa", "operator new[]"},
+ {"dl", "operator delete"}, {"dla", "operator delete[]"},
+ {"pl", "operator+"}, {"mi", "operator-"},
+ {"ml", "operator*"}, {"dv", "operator/"},
+ {"md", "operator%"}, {"er", "operator^"},
+ {"ad", "operator&"}, {"or", "operator|"},
+ {"co", "operator~"}, {"nt", "operator!"},
+ {"as", "operator="}, {"lt", "operator<"},
+ {"gt", "operator>"}, {"apl", "operator+="},
+ {"ami", "operator-="}, {"amu", "operator*="},
+ {"adv", "operator/="}, {"amd", "operator%="},
+ {"aer", "operator^="}, {"aad", "operator&="},
+ {"aor", "operator|="}, {"ls", "operator<<"},
+ {"rs", "operator>>"}, {"ars", "operator>>="},
+ {"als", "operator<<="}, {"eq", "operator=="},
+ {"ne", "operator!="}, {"le", "operator<="},
+ {"ge", "operator>="}, {"aa", "operator&&"},
+ {"oo", "operator||"}, {"pp", "operator++"},
+ {"mm", "operator--"}, {"cm", "operator,"},
+ {"rm", "operator->*"}, {"rf", "operator->"},
+ {"cl", "operator()"}, {"vc", "operator[]"},
+ {"vt", "__vtable"}};
+
+static const std::map<char, std::string_view> types = {
+ {'i', "int"}, {'b', "bool"}, {'c', "char"}, {'s', "short"},
+ {'l', "long"}, {'x', "long long"}, {'f', "float"}, {'d', "double"},
+ {'w', "wchar_t"}, {'v', "void"}, {'e', "..."}, {'r', "long double"},
+ {'D', "short double"},
+};
+
+// Finds the first double underscore in the string, excluding any that are part of a
+// template argument list or operator name.
+static std::optional<std::size_t> find_split(std::string_view s, bool special,
+ DemangleOptions options)
+{
+ std::size_t start = 0;
+
+ if (special && s.starts_with("op"))
+ {
+ const auto result = demangle_arg(s.substr(2), options);
+ if (!result)
+ return std::nullopt;
+
+ const std::string_view rest = result->rest;
+
+ start = s.length() - rest.length();
+ }
+
+ int depth = 0;
+ const std::size_t length = s.length();
+
+ for (std::size_t i = start; i < length; i++)
+ {
+ switch (s[i])
+ {
+ case '<':
+ depth++;
+ break;
+ case '>':
+ depth--;
+ break;
+ case '_':
+ if (i < length - 1 && s[i + 1] == '_' && depth == 0)
+ {
+ return i;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return std::nullopt;
+}
+
+ParseQualifiersResult parse_qualifiers(std::string_view str)
+{
+ std::string pre;
+ std::string post;
+ std::size_t index = 0;
+
+ for (char c : str)
+ {
+ bool found_non_qualifier = false;
+
+ switch (c)
+ {
+ case 'P':
+ if (pre.empty())
+ {
+ post.insert(0, "*");
+ }
+ else
+ {
+ post.insert(0, fmt::format("* {0}", StripTrailingWhitespace(pre)));
+ pre.clear();
+ }
+ break;
+ case 'R':
+ if (pre.empty())
+ {
+ post.insert(0, "&");
+ }
+ else
+ {
+ post.insert(0, fmt::format("& {0}", StripTrailingWhitespace(pre)));
+ pre.clear();
+ }
+ break;
+ case 'C':
+ pre += "const ";
+ break;
+ case 'V':
+ pre += "volatile ";
+ break;
+ case 'U':
+ pre += "unsigned ";
+ break;
+ case 'S':
+ pre += "signed ";
+ break;
+ default:
+ found_non_qualifier = true;
+ break;
+ }
+
+ if (found_non_qualifier)
+ break;
+ index++;
+ }
+ str.remove_prefix(index);
+ post = StripTrailingWhitespace(post);
+ return {pre, post, str};
+}
+
+std::optional<ParseDigitsResult> parse_digits(std::string_view str)
+{
+ if (str.empty())
+ return std::nullopt;
+
+ const char* str_start = str.data();
+ const char* str_end = str_start + str.size();
+ std::size_t val = 0;
+ std::string_view remainder;
+
+ const auto result = std::from_chars(str_start, str_end, val);
+
+ if (result.ec != std::errc{})
+ return std::nullopt;
+
+ const char* rest_start = result.ptr;
+ if (rest_start != str_end)
+ remainder = std::string_view(rest_start, str_end - rest_start);
+
+ return {{val, remainder}};
+}
+
+std::optional<DemangleTemplateArgsResult> demangle_template_args(std::string_view str,
+ DemangleOptions options)
+{
+ const std::size_t start_idx = str.find('<');
+
+ if (start_idx == std::string::npos)
+ return {{str, ""}};
+
+ const std::size_t end_idx = str.rfind('>');
+ if (end_idx == std::string::npos || end_idx < start_idx)
+ {
+ return std::nullopt;
+ }
+
+ std::string_view args(&str[start_idx + 1], &str[end_idx]);
+ const std::string_view template_name = str.substr(0, start_idx);
+ std::string tmpl_args = "<";
+
+ while (!args.empty())
+ {
+ const auto result = demangle_arg(args, options);
+ if (!result)
+ return std::nullopt;
+
+ const auto demangled_arg = result.value();
+ const std::string_view rest = demangled_arg.rest;
+ tmpl_args += demangled_arg.arg_pre;
+ tmpl_args += demangled_arg.arg_post;
+
+ if (rest.empty())
+ break;
+
+ tmpl_args += ", ";
+
+ args = rest.substr(1);
+ }
+
+ tmpl_args += ">";
+
+ return {{template_name, tmpl_args}};
+}
+
+std::optional<DemangleNameResult> demangle_name(std::string_view str, DemangleOptions options)
+{
+ const auto result = parse_digits(str);
+ if (!result)
+ return std::nullopt;
+
+ auto [size, rest] = result.value();
+
+ if (rest.length() < size)
+ {
+ return std::nullopt;
+ }
+
+ auto result1 = demangle_template_args(rest.substr(0, size), options);
+ if (!result1)
+ return std::nullopt;
+
+ auto [name, args] = result1.value();
+ const std::string result_name{name};
+ return {{result_name, fmt::format("{0}{1}", name, args), rest.substr(size)}};
+}
+
+std::optional<DemangleNameResult> demangle_qualified_name(std::string_view str,
+ DemangleOptions options)
+{
+ if (!str.starts_with('Q'))
+ return demangle_name(str, options);
+
+ if (str.length() < 3)
+ {
+ return std::nullopt;
+ }
+
+ // MWCC only preserves up to 9 layers of namespace/class depth in symbol names, so we
+ // can just check one digit
+ const int digit = static_cast<int>(str[1] - '0');
+ if (digit < 0 || digit > 9)
+ return std::nullopt;
+
+ const int count = digit;
+
+ str = str.substr(2);
+
+ std::string last_class;
+ std::string qualified;
+
+ for (int i = 0; i < count; i++)
+ {
+ const auto result = demangle_name(str, options);
+ if (!result)
+ return std::nullopt;
+
+ auto [class_name, full, rest] = *result;
+ qualified += full;
+ last_class = class_name;
+ str = rest;
+ if (i < count - 1)
+ {
+ qualified += "::";
+ }
+ }
+ return {{last_class, qualified, str}};
+}
+
+std::optional<DemangleArgResult> demangle_arg(std::string_view str, DemangleOptions options)
+{
+ // Negative constant
+ if (str.starts_with('-'))
+ {
+ const auto parse_result = parse_digits(str.substr(1));
+ if (!parse_result)
+ return std::nullopt;
+
+ const std::size_t size = parse_result->digits;
+ const std::string out_val = fmt::format("-{}", size);
+ return {{out_val, "", parse_result->rest}};
+ }
+
+ std::string result;
+
+ const auto parse_qual_result = parse_qualifiers(str);
+ std::string pre = parse_qual_result.pre;
+ std::string post = parse_qual_result.post;
+ std::string_view rest = parse_qual_result.rest;
+ result += pre;
+ str = rest;
+
+ // Disambiguate arguments starting with a number
+ if (str.length() > 0 && std::isdigit(static_cast<u8>(str[0])))
+ {
+ const auto parse_result = parse_digits(str);
+ if (!parse_result)
+ return std::nullopt;
+
+ auto& [num, rest_value] = parse_result.value();
+ rest = rest_value;
+
+ // If the number is followed by a comma or the end of the string, it's a template argument
+ if (rest.empty() || rest.starts_with(','))
+ {
+ // ...or a Metrowerks extension type
+ if (options.mw_extensions)
+ {
+ const std::string t = num == 1 ? "__int128" : num == 2 ? "__vec2x32float__" : "";
+
+ if (!t.empty())
+ {
+ result += t;
+ return {{result, post, rest}};
+ }
+ }
+ result += fmt::format("{}{}", num, post);
+ return {{result, "", rest}};
+ }
+ // Otherwise, it's (probably) the size of a type
+ const auto demangle_name_result = demangle_name(str, options);
+ if (!demangle_name_result)
+ return std::nullopt;
+
+ result += demangle_name_result->full;
+ result += post;
+ return {{result, "", demangle_name_result->rest}};
+ }
+
+ // Handle qualified names
+ if (str.starts_with('Q'))
+ {
+ const auto demangle_qual_result = demangle_qualified_name(str, options);
+ if (!demangle_qual_result)
+ return std::nullopt;
+
+ result += demangle_qual_result->full;
+ result += post;
+ return {{result, "", demangle_qual_result->rest}};
+ }
+
+ bool is_member = false;
+ bool const_member = false;
+ if (str.starts_with('M'))
+ {
+ is_member = true;
+ const auto demangle_qual_result = demangle_qualified_name(str.substr(1), options);
+ if (!demangle_qual_result)
+ return std::nullopt;
+
+ rest = demangle_qual_result->rest;
+ pre = fmt::format("{}::*{}", demangle_qual_result->full, pre);
+ if (!rest.starts_with('F'))
+ {
+ return std::nullopt;
+ }
+ str = rest;
+ }
+ if (is_member || str.starts_with('F'))
+ {
+ str.remove_prefix(1);
+ if (is_member)
+ {
+ // "const void*, const void*" or "const void*, void*"
+ if (str.starts_with("PCvPCv"))
+ {
+ const_member = true;
+ str.remove_prefix(6);
+ }
+ else if (str.starts_with("PCvPv"))
+ {
+ str.remove_prefix(5);
+ }
+ else
+ {
+ return std::nullopt;
+ }
+ }
+ else if (post.starts_with('*'))
+ {
+ post = StripLeadingWhitespace(post.substr(1));
+ pre = fmt::format("*{}", pre);
+ }
+ else
+ {
+ return std::nullopt;
+ }
+
+ const auto demangle_func_args_result = demangle_function_args(str, options);
+ if (!demangle_func_args_result)
+ return std::nullopt;
+
+ const auto demangled_func_args = demangle_func_args_result.value();
+
+ if (!demangled_func_args.rest.starts_with('_'))
+ {
+ return std::nullopt;
+ }
+
+ const auto demangle_arg_result = demangle_arg(demangled_func_args.rest.substr(1), options);
+ if (!demangle_arg_result)
+ return std::nullopt;
+
+ const std::string_view const_str = const_member ? " const" : "";
+ const std::string res_pre = fmt::format("{} ({}{}", demangle_arg_result->arg_pre, pre, post);
+ const std::string res_post = fmt::format(")({}){}{}", demangled_func_args.args, const_str,
+ demangle_arg_result->arg_post);
+ return {{res_pre, res_post, demangle_arg_result->rest}};
+ }
+
+ if (str.starts_with('A'))
+ {
+ const auto parse_result = parse_digits(str.substr(1));
+ if (!parse_result)
+ return std::nullopt;
+
+ auto& [count, rest_value] = parse_result.value();
+ rest = rest_value;
+
+ if (!rest.starts_with('_'))
+ {
+ return std::nullopt;
+ }
+
+ const auto demangle_arg_result = demangle_arg(rest.substr(1), options);
+ if (!demangle_arg_result)
+ return std::nullopt;
+
+ if (!post.empty())
+ {
+ post = fmt::format("({})", post);
+ }
+ result = fmt::format("{}{}{}", pre, demangle_arg_result->arg_pre, post);
+ const std::string ret_post = fmt::format("[{}]{}", count, demangle_arg_result->arg_post);
+ return {{result, ret_post, demangle_arg_result->rest}};
+ }
+
+ if (str.length() == 0)
+ return std::nullopt;
+
+ std::string_view type;
+
+ const char c = str[0];
+
+ if (types.contains(c))
+ {
+ type = types.at(c);
+ }
+ else
+ {
+ // Handle special cases
+ switch (c)
+ {
+ case '1':
+ if (options.mw_extensions)
+ type = "__int128";
+ break;
+ case '2':
+ if (options.mw_extensions)
+ type = "__vec2x32float__";
+ break;
+ case '_':
+ return {{result, "", rest}};
+ default:
+ return std::nullopt;
+ }
+ }
+
+ return {{fmt::format("{}{}{}", result, type, post), "", str.substr(1)}};
+}
+
+std::optional<DemangleFunctionArgsResult> demangle_function_args(std::string_view str,
+ DemangleOptions options)
+{
+ std::string result;
+
+ while (!str.empty())
+ {
+ if (!result.empty())
+ {
+ result += ", ";
+ }
+
+ const auto demangle_arg_result = demangle_arg(str, options);
+ if (!demangle_arg_result)
+ return std::nullopt;
+
+ result += demangle_arg_result->arg_pre;
+ result += demangle_arg_result->arg_post;
+ str = demangle_arg_result->rest;
+
+ if (str.starts_with('_') || str.starts_with(','))
+ {
+ break;
+ }
+ }
+
+ return {{result, str}};
+}
+
+std::optional<std::string> demangle_special_function(std::string_view str,
+ std::string_view class_name,
+ DemangleOptions options)
+{
+ if (str.starts_with("op"))
+ {
+ const std::string_view rest = str.substr(2);
+ const auto demangle_arg_result = demangle_arg(rest, options);
+ if (!demangle_arg_result)
+ return std::nullopt;
+
+ return fmt::format("operator {}{}", demangle_arg_result->arg_pre,
+ demangle_arg_result->arg_post);
+ }
+
+ const auto result = demangle_template_args(str, options);
+ if (!result)
+ return std::nullopt;
+
+ auto& [op, args] = result.value();
+
+ std::string_view func_name;
+
+ if (op == "dt")
+ {
+ return fmt::format("~{}{}", class_name, args);
+ }
+ else if (op == "ct")
+ {
+ func_name = class_name;
+ }
+ else if (operators.contains(op.data()))
+ {
+ func_name = operators.at(op.data());
+ }
+ else
+ {
+ return fmt::format("__{}{}", op, args);
+ }
+
+ return fmt::format("{0}{1}", func_name, args);
+}
+
+// Demangle a symbol name.
+//
+// Returns `std::nullopt` if the input is not a valid mangled name.
+std::optional<std::string> demangle(std::string_view str, DemangleOptions options)
+{
+ if (!IsAscii(str))
+ {
+ return std::nullopt;
+ }
+
+ bool special = false;
+ bool cnst = false;
+ std::string fn_name;
+ std::string static_var;
+
+ // Handle new static function variables (Wii CW)
+ const bool guard = str.starts_with("@GUARD@");
+ if (guard || str.starts_with("@LOCAL@"))
+ {
+ str = str.substr(7);
+ std::size_t idx = str.rfind('@');
+ if (idx == std::string::npos)
+ return std::nullopt;
+
+ const std::string_view rest = str.substr(0, idx);
+ const std::string_view var = str.substr(idx);
+
+ if (guard)
+ {
+ static_var = fmt::format("{0} guard", var.substr(1));
+ }
+ else
+ {
+ static_var = var.substr(1);
+ }
+ str = rest;
+ }
+
+ if (str.starts_with("__"))
+ {
+ special = true;
+ str = str.substr(2);
+ }
+
+ const auto idx_temp = find_split(str, special, options);
+ if (!idx_temp)
+ return std::nullopt;
+ std::size_t idx = idx_temp.value();
+ // Handle any trailing underscores in the function name
+ while (str[idx + 2] == '_')
+ {
+ idx++;
+ }
+
+ const std::string_view fn_name_out = str.substr(0, idx);
+ std::string_view rest = str.substr(idx);
+
+ if (special)
+ {
+ if (fn_name_out == "init")
+ {
+ // Special case for double __
+ const std::size_t rest_idx = rest.substr(2).find("__");
+ if (rest_idx == std::string::npos)
+ return std::nullopt;
+ fn_name = str.substr(0, rest_idx + 6);
+ rest.remove_prefix(rest_idx + 2);
+ }
+ else
+ {
+ fn_name = fn_name_out;
+ }
+ }
+ else
+ {
+ const auto result = demangle_template_args(fn_name_out, options);
+ if (!result)
+ return std::nullopt;
+
+ const auto template_args = result.value();
+ fn_name = fmt::format("{}{}", template_args.name, template_args.args);
+ }
+
+ // Handle old static function variables (GC CW)
+ const std::size_t first_idx = fn_name.find('$');
+ if (first_idx != std::string::npos)
+ {
+ const std::size_t second_idx = fn_name.substr(first_idx + 1).find('$');
+ if (second_idx == std::string::npos)
+ return std::nullopt;
+
+ const std::string var = fn_name.substr(0, first_idx);
+ std::string rest_temp = fn_name.substr(first_idx + 1);
+
+ const std::string var_type = rest_temp.substr(0, second_idx);
+ rest_temp = rest_temp.substr(second_idx);
+
+ if (!var_type.starts_with("localstatic"))
+ {
+ return std::nullopt;
+ }
+
+ if (var == "init")
+ {
+ // Sadly, $localstatic doesn't provide the variable name in guard/init
+ static_var = fmt::format("{} guard", var_type);
+ }
+ else
+ {
+ static_var = var;
+ }
+
+ fn_name = rest_temp.substr(1);
+ }
+
+ str = rest.substr(2);
+
+ std::string class_name;
+ std::string return_type_pre;
+ std::string return_type_post;
+ std::string qualified;
+
+ if (!str.starts_with('F'))
+ {
+ const auto result = demangle_qualified_name(str, options);
+ if (!result)
+ return std::nullopt;
+
+ class_name = result->class_name;
+ qualified = result->full;
+ str = result->rest;
+ }
+ if (special)
+ {
+ const auto result = demangle_special_function(fn_name, class_name, options);
+ if (!result)
+ return std::nullopt;
+ fn_name = result.value();
+ }
+ if (str.starts_with('C'))
+ {
+ str.remove_prefix(1);
+ cnst = true;
+ }
+ if (str.starts_with('F'))
+ {
+ str.remove_prefix(1);
+ const auto result = demangle_function_args(str, options);
+ if (!result)
+ return std::nullopt;
+
+ if (options.omit_empty_parameters && result->args == "void")
+ {
+ fn_name = fmt::format("{}()", fn_name);
+ }
+ else
+ {
+ fn_name = fmt::format("{}({})", fn_name, result->args);
+ }
+ str = result->rest;
+ }
+ if (str.starts_with('_'))
+ {
+ str.remove_prefix(1);
+ const auto result = demangle_arg(str, options);
+ if (!result)
+ return std::nullopt;
+
+ return_type_pre = result->arg_pre;
+ return_type_post = result->arg_post;
+ str = result->rest;
+ }
+ if (!str.empty())
+ {
+ return std::nullopt;
+ }
+ if (cnst)
+ {
+ fn_name = fmt::format("{} const", fn_name);
+ }
+ if (!qualified.empty())
+ {
+ fn_name = fmt::format("{}::{}", qualified, fn_name);
+ }
+ if (!return_type_pre.empty())
+ {
+ fn_name = fmt::format("{} {}{}", return_type_pre, fn_name, return_type_post);
+ }
+ if (!static_var.empty())
+ {
+ fn_name = fmt::format("{}::{}", fn_name, static_var);
+ }
+
+ return fn_name;
+}
+
+} // namespace CWDemangler
diff --git a/Source/Core/Common/CWDemangler.h b/Source/Core/Common/CWDemangler.h
new file mode 100644
index 0000000000..b15aa94926
--- /dev/null
+++ b/Source/Core/Common/CWDemangler.h
@@ -0,0 +1,67 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Based on: https://github.com/encounter/cwdemangle
+// Copyright 2024 Luke Street <luke@street.dev>
+// SPDX-License-Identifier: CC0-1.0
+
+#pragma once
+
+#include <optional>
+#include <string>
+
+#include "Common/CommonTypes.h"
+
+namespace CWDemangler
+{
+
+// Options for [demangle].
+struct DemangleOptions
+{
+ // Replace `(void)` function parameters with `()`
+ bool omit_empty_parameters = true;
+ // Enable Metrowerks extension types (`__int128`, `__vec2x32float__`, etc.)
+ //
+ // Disabled by default since they conflict with template argument literals
+ // and can't always be demangled correctly.
+ bool mw_extensions = false;
+};
+
+struct DemangleTemplateArgsResult
+{
+ std::string_view name;
+ std::string args;
+};
+
+struct DemangleNameResult
+{
+ std::string class_name;
+ std::string full;
+ std::string_view rest;
+};
+
+struct DemangleArgResult
+{
+ std::string arg_pre;
+ std::string arg_post;
+ std::string_view rest;
+};
+
+struct DemangleFunctionArgsResult
+{
+ std::string args;
+ std::string_view rest;
+};
+
+std::optional<DemangleTemplateArgsResult> demangle_template_args(std::string_view str,
+ DemangleOptions options);
+std::optional<DemangleNameResult> demangle_name(std::string_view str, DemangleOptions options);
+std::optional<DemangleNameResult> demangle_qualified_name(std::string_view str,
+ DemangleOptions options);
+std::optional<DemangleArgResult> demangle_arg(std::string_view str, DemangleOptions options);
+std::optional<DemangleFunctionArgsResult> demangle_function_args(std::string_view str,
+ DemangleOptions options);
+std::optional<std::string> demangle_special_function(std::string_view str,
+ std::string_view class_name,
+ DemangleOptions options);
+std::optional<std::string> demangle(std::string_view str, DemangleOptions options);
+} // namespace CWDemangler
diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp
index ed4837b112..e84fabcbb5 100644
--- a/Source/Core/Common/StringUtil.cpp
+++ b/Source/Core/Common/StringUtil.cpp
@@ -193,7 +193,7 @@ std::string ArrayToString(const u8* data, u32 size, int line_len, bool spaces)
template <typename T>
static std::string_view StripEnclosingChars(std::string_view str, T chars)
{
- const size_t s = str.find_first_not_of(chars);
+ const std::size_t s = str.find_first_not_of(chars);
if (str.npos != s)
return str.substr(s, str.find_last_not_of(chars) - s + 1);
@@ -201,12 +201,41 @@ static std::string_view StripEnclosingChars(std::string_view str, T chars)
return "";
}
+template <typename T>
+static std::string_view StripLeadingChars(std::string_view str, T chars)
+{
+ const std::size_t s = str.find_first_not_of(chars);
+
+ if (str.npos != s)
+ return str.substr(s);
+ else
+ return "";
+}
+
+template <typename T>
+static std::string_view StripTrailingChars(std::string_view str, T chars)
+{
+ return str.substr(0, str.find_last_not_of(chars) + 1);
+}
+
// Turns "\n\r\t hello " into "hello" (trims at the start and end but not inside).
std::string_view StripWhitespace(std::string_view str)
{
return StripEnclosingChars(str, " \t\r\n");
}
+// Turns "\n\r\t hello " into "hello " (trims at the start).
+std::string_view StripLeadingWhitespace(std::string_view str)
+{
+ return StripLeadingChars(str, " \t\r\n");
+}
+
+// Turns "\n\r\t hello " into "\n\r\t hello" (trims at the end).
+std::string_view StripTrailingWhitespace(std::string_view str)
+{
+ return StripTrailingChars(str, " \t\r\n");
+}
+
std::string_view StripSpaces(std::string_view str)
{
return StripEnclosingChars(str, ' ');
diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h
index 0cc4e6c15e..61404376d3 100644
--- a/Source/Core/Common/StringUtil.h
+++ b/Source/Core/Common/StringUtil.h
@@ -49,6 +49,8 @@ inline void CharArrayFromFormat(char (&out)[Count], const char* format, ...)
std::string ArrayToString(const u8* data, u32 size, int line_len = 20, bool spaces = true);
std::string_view StripWhitespace(std::string_view s);
+std::string_view StripLeadingWhitespace(std::string_view s);
+std::string_view StripTrailingWhitespace(std::string_view s);
std::string_view StripSpaces(std::string_view s);
std::string_view StripQuotes(std::string_view s);
diff --git a/Source/Core/Common/SymbolDB.cpp b/Source/Core/Common/SymbolDB.cpp
index 2fa514ba86..e1778d2ca5 100644
--- a/Source/Core/Common/SymbolDB.cpp
+++ b/Source/Core/Common/SymbolDB.cpp
@@ -9,6 +9,7 @@
#include <string>
#include <utility>
+#include "Common/CWDemangler.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
@@ -31,6 +32,10 @@ void Symbol::Rename(const std::string& symbol_name)
{
this->name = symbol_name;
this->function_name = GetStrippedFunctionName(symbol_name);
+
+ // Try demangling the symbol name, saving it in the symbol if valid.
+ auto demangle_result = CWDemangler::demangle(symbol_name, CWDemangler::DemangleOptions());
+ this->demangled_name = demangle_result.value_or("");
}
void SymbolDB::List()
diff --git a/Source/Core/Common/SymbolDB.h b/Source/Core/Common/SymbolDB.h
index 8a1767d696..5074b03ad5 100644
--- a/Source/Core/Common/SymbolDB.h
+++ b/Source/Core/Common/SymbolDB.h
@@ -54,7 +54,22 @@ struct Symbol
void Rename(const std::string& symbol_name);
+ bool IsDemangled() const { return !demangled_name.empty(); }
+
+ const std::string& GetDisplayName(bool use_demangled_name) const
+ {
+ if (use_demangled_name && IsDemangled())
+ {
+ return demangled_name;
+ }
+ else
+ {
+ return name;
+ }
+ }
+
std::string name;
+ std::string demangled_name; // Demangled symbol name
std::string function_name; // stripped function name
std::string object_name; // name of object/source file symbol belongs to
std::vector<SCall> callers; // addresses of functions that call this function
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 9959caf60c..5ecd90d543 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -48,6 +48,7 @@
<ClInclude Include="Common\Crypto\ec.h" />
<ClInclude Include="Common\Crypto\HMAC.h" />
<ClInclude Include="Common\Crypto\SHA1.h" />
+ <ClInclude Include="Common\CWDemangler.h" />
<ClInclude Include="Common\Debug\MemoryPatches.h" />
<ClInclude Include="Common\Debug\Threads.h" />
<ClInclude Include="Common\Debug\Watches.h" />
@@ -847,6 +848,7 @@
<ClCompile Include="Common\Crypto\ec.cpp" />
<ClCompile Include="Common\Crypto\HMAC.cpp" />
<ClCompile Include="Common\Crypto\SHA1.cpp" />
+ <ClCompile Include="Common\CWDemangler.cpp" />
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
<ClCompile Include="Common\Debug\Watches.cpp" />
<ClCompile Include="Common\DirectIOFile.cpp" />
diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp
index a04f761cee..edac7e52b9 100644
--- a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp
+++ b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp
@@ -73,6 +73,9 @@ CodeWidget::CodeWidget(QWidget* parent)
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &CodeWidget::Update);
+ connect(&Settings::Instance(), &Settings::ShowDemangledNamesChanged, this,
+ &CodeWidget::OnShowDemangledNamesChanged);
+
ConnectWidgets();
m_code_splitter->restoreState(
@@ -248,6 +251,16 @@ void CodeWidget::OnSetCodeAddress(u32 address)
SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
}
+void CodeWidget::OnShowDemangledNamesChanged()
+{
+ UpdateSymbols();
+ if (const Common::Symbol* symbol = m_ppc_symbol_db.GetSymbolFromAddr(m_code_view->GetAddress()))
+ {
+ UpdateFunctionCalls(symbol);
+ UpdateFunctionCallers(symbol);
+ }
+}
+
void CodeWidget::OnPPCSymbolsChanged()
{
UpdateSymbols();
@@ -424,7 +437,7 @@ void CodeWidget::UpdateSymbols()
m_symbols_list->clear();
m_ppc_symbol_db.ForEachSymbol([&](const Common::Symbol& symbol) {
- QString name = QString::fromStdString(symbol.name);
+ QString name = QString::fromStdString(GetSymbolDisplayName(&symbol));
// If the symbol has an object name, add it to the entry name.
if (!symbol.object_name.empty())
@@ -488,15 +501,16 @@ void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol)
if (call_symbol)
{
QString name;
+ const std::string& symbol_name = GetSymbolDisplayName(call_symbol);
if (!call_symbol->object_name.empty())
{
name = QString::fromStdString(
- fmt::format("< {} ({}, {:08x})", call_symbol->name, call_symbol->object_name, addr));
+ fmt::format("< {} ({}, {:08x})", symbol_name, call_symbol->object_name, addr));
}
else
{
- name = QString::fromStdString(fmt::format("< {} ({:08x})", call_symbol->name, addr));
+ name = QString::fromStdString(fmt::format("< {} ({:08x})", symbol_name, addr));
}
if (!name.contains(filter, Qt::CaseInsensitive))
@@ -525,15 +539,16 @@ void CodeWidget::UpdateFunctionCallers(const Common::Symbol* symbol)
if (caller_symbol)
{
QString name;
+ const std::string& symbol_name = GetSymbolDisplayName(caller_symbol);
if (!caller_symbol->object_name.empty())
{
- name = QString::fromStdString(fmt::format("< {} ({}, {:08x})", caller_symbol->name,
- caller_symbol->object_name, addr));
+ name = QString::fromStdString(
+ fmt::format("< {} ({}, {:08x})", symbol_name, caller_symbol->object_name, addr));
}
else
{
- name = QString::fromStdString(fmt::format("< {} ({:08x})", caller_symbol->name, addr));
+ name = QString::fromStdString(fmt::format("< {} ({:08x})", symbol_name, addr));
}
if (!name.contains(filter, Qt::CaseInsensitive))
@@ -546,6 +561,14 @@ void CodeWidget::UpdateFunctionCallers(const Common::Symbol* symbol)
}
}
+// Gets the name of this symbol based on the option for whether or not to show
+// demangled names.
+const std::string& CodeWidget::GetSymbolDisplayName(const Common::Symbol* symbol) const
+{
+ const bool show_demangled_names = Settings::Instance().IsShowDemangledNames();
+ return symbol->GetDisplayName(show_demangled_names);
+}
+
void CodeWidget::Step()
{
auto& cpu = m_system.GetCPU();
diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.h b/Source/Core/DolphinQt/Debugger/CodeWidget.h
index 76ada4f506..9a678e1c2c 100644
--- a/Source/Core/DolphinQt/Debugger/CodeWidget.h
+++ b/Source/Core/DolphinQt/Debugger/CodeWidget.h
@@ -64,6 +64,7 @@ private:
void UpdateFunctionCallers(const Common::Symbol* symbol);
void UpdateNotes();
+ void OnShowDemangledNamesChanged();
void OnPPCSymbolsChanged();
void OnSearchAddress();
void OnSearchSymbols();
@@ -76,6 +77,8 @@ private:
void closeEvent(QCloseEvent*) override;
void showEvent(QShowEvent* event) override;
+ const std::string& GetSymbolDisplayName(const Common::Symbol* symbol) const;
+
Core::System& m_system;
PPCSymbolDB& m_ppc_symbol_db;
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index f5d87e0b77..a29ba272ec 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -1073,6 +1073,7 @@ void MenuBar::AddSymbolsMenu()
generate->addAction(tr("Address"), this, &MenuBar::GenerateSymbolsFromAddress);
generate->addAction(tr("Signature Database"), this, &MenuBar::GenerateSymbolsFromSignatureDB);
generate->addAction(tr("RSO Modules"), this, &MenuBar::GenerateSymbolsFromRSO);
+ m_debugger_show_demangled_names = m_symbols->addAction(tr("Show &Demangled Names"));
m_symbols->addSeparator();
m_symbols->addAction(tr("&Load Symbol Map"), this, &MenuBar::LoadSymbolMap);
@@ -1096,6 +1097,13 @@ void MenuBar::AddSymbolsMenu()
m_symbols->addSeparator();
m_symbols->addAction(tr("&Patch HLE Functions"), this, &MenuBar::PatchHLEFunctions);
+
+ m_debugger_show_demangled_names->setCheckable(true);
+ m_debugger_show_demangled_names->setChecked(Settings::Instance().IsShowDemangledNames());
+ connect(m_debugger_show_demangled_names, &QAction::toggled, &Settings::Instance(),
+ &Settings::SetShowDemangledNames);
+ connect(&Settings::Instance(), &Settings::ShowDemangledNamesChanged,
+ m_debugger_show_demangled_names, &QAction::setChecked);
}
void MenuBar::UpdateToolsMenu(const Core::State state)
diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h
index a381353b0c..1535924510 100644
--- a/Source/Core/DolphinQt/MenuBar.h
+++ b/Source/Core/DolphinQt/MenuBar.h
@@ -310,6 +310,7 @@ private:
QAction* m_jit_systemregisters_off;
QAction* m_jit_branch_off;
QAction* m_jit_register_cache_off;
+ QAction* m_debugger_show_demangled_names;
bool m_game_selected = false;
diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp
index 595e9d8084..618a2b5ced 100644
--- a/Source/Core/DolphinQt/Settings.cpp
+++ b/Source/Core/DolphinQt/Settings.cpp
@@ -871,6 +871,20 @@ QFont Settings::GetDebugFont() const
return GetQSettings().value(QStringLiteral("debugger/font"), default_font).value<QFont>();
}
+void Settings::SetShowDemangledNames(bool enabled)
+{
+ if (IsShowDemangledNames() == enabled)
+ return;
+ QSettings().setValue(QStringLiteral("debugger/showdemanglednames"), enabled);
+
+ emit ShowDemangledNamesChanged(enabled);
+}
+
+bool Settings::IsShowDemangledNames() const
+{
+ return QSettings().value(QStringLiteral("debugger/showdemanglednames")).toBool();
+}
+
void Settings::SetAutoUpdateTrack(const QString& mode)
{
if (mode == GetAutoUpdateTrack())
diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h
index 2fe0d272e6..0c79020ff8 100644
--- a/Source/Core/DolphinQt/Settings.h
+++ b/Source/Core/DolphinQt/Settings.h
@@ -176,6 +176,8 @@ public:
bool IsAssemblerVisible() const;
QFont GetDebugFont() const;
void SetDebugFont(const QFont& font);
+ void SetShowDemangledNames(bool enabled);
+ bool IsShowDemangledNames() const;
// Auto-Update
QString GetAutoUpdateTrack() const;
@@ -223,6 +225,7 @@ signals:
void AssemblerVisibilityChanged(bool visible);
void DebugModeToggled(bool enabled);
void DebugFontChanged(const QFont& font);
+ void ShowDemangledNamesChanged(bool enabled);
void AutoUpdateTrackChanged(const QString& mode);
void FallbackRegionChanged(const DiscIO::Region& region);
void AnalyticsToggled(bool enabled);
diff --git a/Source/UnitTests/Common/CMakeLists.txt b/Source/UnitTests/Common/CMakeLists.txt
index a2e21d7a80..f7689b3ec4 100644
--- a/Source/UnitTests/Common/CMakeLists.txt
+++ b/Source/UnitTests/Common/CMakeLists.txt
@@ -7,6 +7,7 @@ add_dolphin_test(BusyLoopTest BusyLoopTest.cpp)
add_dolphin_test(CommonFuncsTest CommonFuncsTest.cpp)
add_dolphin_test(CryptoEcTest Crypto/EcTest.cpp)
add_dolphin_test(CryptoSHA1Test Crypto/SHA1Test.cpp)
+add_dolphin_test(CWDemanglerTest CWDemanglerTest.cpp)
add_dolphin_test(EnumFormatterTest EnumFormatterTest.cpp)
add_dolphin_test(EventTest EventTest.cpp)
add_dolphin_test(FileUtilTest FileUtilTest.cpp)
diff --git a/Source/UnitTests/Common/CWDemanglerTest.cpp b/Source/UnitTests/Common/CWDemanglerTest.cpp
new file mode 100644
index 0000000000..7320e48472
--- /dev/null
+++ b/Source/UnitTests/Common/CWDemanglerTest.cpp
@@ -0,0 +1,431 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Based on: https://github.com/encounter/cwdemangle
+// Copyright 2024 Luke Street <luke@street.dev>
+// SPDX-License-Identifier: CC0-1.0
+
+#include <gtest/gtest.h>
+#include <optional>
+#include <string>
+#include <tuple>
+
+#include "Common/CWDemangler.h"
+
+using namespace CWDemangler;
+
+void DoDemangleTemplateArgsTest(std::string mangled, std::string name, std::string template_args)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = demangle_template_args(mangled, options);
+ DemangleTemplateArgsResult expected = {name, template_args};
+
+ EXPECT_TRUE(result.has_value());
+ if (result.has_value())
+ {
+ EXPECT_EQ(result.value().args, expected.args);
+ EXPECT_EQ(result.value().name, expected.name);
+ }
+}
+
+void DoDemangleNameTest(std::string mangled, std::string name, std::string full_name)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = demangle_name(mangled, options);
+ DemangleNameResult expected = {name, full_name, ""};
+
+ EXPECT_TRUE(result.has_value());
+ if (result.has_value())
+ {
+ EXPECT_EQ(result.value().class_name, expected.class_name);
+ EXPECT_EQ(result.value().full, expected.full);
+ EXPECT_EQ(result.value().rest, expected.rest);
+ }
+}
+
+void DoDemangleQualifiedNameTest(std::string mangled, std::string base_name, std::string full_name)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = demangle_qualified_name(mangled, options);
+ DemangleNameResult expected = {base_name, full_name, ""};
+
+ EXPECT_TRUE(result.has_value());
+ if (result.has_value())
+ {
+ EXPECT_EQ(result.value().class_name, expected.class_name);
+ EXPECT_EQ(result.value().full, expected.full);
+ EXPECT_EQ(result.value().rest, expected.rest);
+ }
+}
+
+void DoDemangleArgTest(std::string mangled, std::string type_pre, std::string type_post,
+ std::string remainder)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = CWDemangler::demangle_arg(mangled, options);
+ DemangleArgResult expected = {type_pre, type_post, remainder};
+
+ EXPECT_TRUE(result.has_value());
+ if (result.has_value())
+ {
+ EXPECT_EQ(result.value().arg_post, expected.arg_post);
+ EXPECT_EQ(result.value().arg_pre, expected.arg_pre);
+ EXPECT_EQ(result.value().rest, expected.rest);
+ }
+}
+
+void DoDemangleFunctionArgsTest(std::string mangled, std::string args, std::string remainder)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = demangle_function_args(mangled, options);
+ DemangleFunctionArgsResult expected = {args, remainder};
+
+ EXPECT_TRUE(result.has_value());
+ if (result.has_value())
+ {
+ EXPECT_EQ(result.value().args, expected.args);
+ EXPECT_EQ(result.value().rest, expected.rest);
+ }
+}
+
+void DoDemangleTest(std::string mangled, std::string demangled)
+{
+ DemangleOptions options = DemangleOptions();
+
+ auto result = demangle(mangled, options);
+ std::optional<std::string> expected = {demangled};
+ if (demangled == "")
+ expected = std::nullopt;
+
+ EXPECT_EQ(result, expected);
+}
+
+void DoDemangleOptionsTest(bool omit_empty_params, bool mw_extensions, std::string mangled,
+ std::string demangled)
+{
+ DemangleOptions options = DemangleOptions(omit_empty_params, mw_extensions);
+
+ auto result = demangle(mangled, options);
+ std::optional<std::string> expected = {demangled};
+
+ EXPECT_EQ(result, expected);
+}
+
+TEST(CWDemangler, TestDemangleTemplateArgs)
+{
+ DoDemangleTemplateArgsTest("ShaderUid<24geometry_shader_uid_data>", "ShaderUid",
+ "<geometry_shader_uid_data>");
+ DoDemangleTemplateArgsTest("basic_string<w,Q23std14char_traits<w>,Q23std12allocator<w>>",
+ "basic_string",
+ "<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>");
+}
+
+TEST(CWDemangler, TestDemangleName)
+{
+ DoDemangleNameTest("37ShaderUid<24geometry_shader_uid_data>", "ShaderUid",
+ "ShaderUid<geometry_shader_uid_data>");
+ DoDemangleNameTest("59basic_string<w,Q23std14char_traits<w>,Q23std12allocator<w>>",
+ "basic_string",
+ "basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>");
+}
+
+TEST(CWDemangler, TestDemangleQualifiedName)
+{
+ DoDemangleQualifiedNameTest("7Wiimote", "Wiimote", "Wiimote");
+ DoDemangleQualifiedNameTest("Q23Gen8XEmitter", "XEmitter", "Gen::XEmitter");
+ DoDemangleQualifiedNameTest(
+ "Q23std59basic_string<w,Q23std14char_traits<w>,Q23std12allocator<w>>", "basic_string",
+ "std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>");
+}
+
+TEST(CWDemangler, TestDemangleArg)
+{
+ DoDemangleArgTest("v", "void", "", "");
+ DoDemangleArgTest("b", "bool", "", "");
+ DoDemangleArgTest("RC9CVector3fUc", "const CVector3f&", "", "Uc");
+ DoDemangleArgTest("Q23std14char_traits<w>,", "std::char_traits<wchar_t>", "", ",");
+ DoDemangleArgTest("PFPCcPCc_v", "void (*", ")(const char*, const char*)", "");
+ DoDemangleArgTest("RCPCVPCVUi", "const volatile unsigned int* const volatile* const&", "", "");
+}
+
+TEST(CWDemangler, TestDemangleFunctionArgs)
+{
+ DoDemangleFunctionArgsTest("v", "void", "");
+ DoDemangleFunctionArgsTest("b", "bool", "");
+ DoDemangleFunctionArgsTest("RC9CVector3fUc_x", "const CVector3f&, unsigned char", "_x");
+}
+
+TEST(CWDemangler, TestDemangle)
+{
+ DoDemangleTest("__ct__11AbstractGfxFv", "AbstractGfx::AbstractGfx()");
+
+ DoDemangleTest("__dt__11AbstractGfxFv", "AbstractGfx::~AbstractGfx()");
+
+ DoDemangleTest(
+ "OnPrimitiveCommand__Q216@unnamed@code_c@20FifoPlaybackAnalyzerFQ316@unnamed@code_c@"
+ "13OpcodeDecoder9PrimitiveUcUlUsPUc",
+ "@unnamed@code_c@::FifoPlaybackAnalyzer::OnPrimitiveCommand(@unnamed@code_c@::OpcodeDecoder::"
+ "Primitive, unsigned char, unsigned long, unsigned short, unsigned char*)");
+
+ DoDemangleTest("SetSIMDMode__Q26Common3FPUFQ36Common3FPU9RoundModeb",
+ "Common::FPU::SetSIMDMode(Common::FPU::RoundMode, bool)");
+
+ DoDemangleTest("SupportsUtilityDrawing__11AbstractGfxCFv",
+ "AbstractGfx::SupportsUtilityDrawing() const");
+
+ DoDemangleTest("SetViewport__11AbstractGfxFffffff",
+ "AbstractGfx::SetViewport(float, float, float, float, float, float)");
+
+ DoDemangleTest("CreateTexture__11AbstractGfxFR13TextureConfigQ23std43basic_string_view<c,"
+ "Q23std14char_traits<c>>",
+ "AbstractGfx::CreateTexture(TextureConfig&, std::basic_string_view<char, "
+ "std::char_traits<char>>)");
+
+ DoDemangleTest("SetViewportAndScissor__11AbstractGfxFRQ28MathUtil12Rectangle<i>ff",
+ "AbstractGfx::SetViewportAndScissor(MathUtil::Rectangle<int>&, float, float)");
+
+ DoDemangleTest("GetInstance__18AchievementManagerFv", "AchievementManager::GetInstance()");
+
+ DoDemangleTest("FilterApprovedPatches__18AchievementManagerCFRQ23std70vector<"
+ "Q211PatchEngine5Patch,Q23std32allocator<Q211PatchEngine5Patch>>RQ23std59basic_"
+ "string<c,Q23std14char_traits<c>,Q23std12allocator<c>>Us",
+ "AchievementManager::FilterApprovedPatches(std::vector<PatchEngine::Patch, "
+ "std::allocator<PatchEngine::Patch>>&, std::basic_string<char, "
+ "std::char_traits<char>, std::allocator<char>>&, unsigned short) const");
+
+ DoDemangleTest(
+ "__as__Q218AchievementManager15FilereaderStateFPQ218AchievementManager15FilereaderState",
+ "AchievementManager::FilereaderState::operator=(AchievementManager::FilereaderState*)");
+
+ DoDemangleTest("LeaderboardEntriesCallback__18AchievementManagerFiPcP34rc_client_leaderboard_"
+ "entry_list_tP11rc_client_tPv",
+ "AchievementManager::LeaderboardEntriesCallback(int, char*, "
+ "rc_client_leaderboard_entry_list_t*, rc_client_t*, void*)");
+
+ DoDemangleTest("Request__18AchievementManagerFP16rc_api_request_tPFP24rc_api_server_response_tPv_"
+ "vPvP11rc_client_t",
+ "AchievementManager::Request(rc_api_request_t*, void "
+ "(*)(rc_api_server_response_t*, void*), void*, rc_client_t*)");
+
+ DoDemangleTest("MemoryPeeker__18AchievementManagerFUlPUcUlP11rc_client_t",
+ "AchievementManager::MemoryPeeker(unsigned long, unsigned char*, unsigned long, "
+ "rc_client_t*)");
+
+ DoDemangleTest("Write__11BoundingBoxFUlQ23std11span<Ci,-1>",
+ "BoundingBox::Write(unsigned long, std::span<const int, -1>)");
+
+ DoDemangleTest(
+ "__ct__24CachedInterpreterEmitterFPUcPUc",
+ "CachedInterpreterEmitter::CachedInterpreterEmitter(unsigned char*, unsigned char*)");
+
+ DoDemangleTest("Write__24CachedInterpreterEmitterFPFRQ27PowerPC12PowerPCStatePv_iPvUx",
+ "CachedInterpreterEmitter::Write(int (*)(PowerPC::PowerPCState&, void*), void*, "
+ "unsigned long long)");
+
+ DoDemangleTest("Shear__Q26Common8Matrix44Fff", "Common::Matrix44::Shear(float, float)");
+
+ DoDemangleTest("__opQ310WiimoteEmu10Shinkansen10DataFormat__Q26Common61BitCastPtrType<"
+ "Q310WiimoteEmu10Shinkansen10DataFormat,A21_Uc>Fv",
+ "Common::BitCastPtrType<WiimoteEmu::Shinkansen::DataFormat, unsigned "
+ "char[21]>::operator WiimoteEmu::Shinkansen::DataFormat()");
+
+ DoDemangleTest(
+ "__ct__Q26Common61BitCastPtrType<Q310WiimoteEmu10Shinkansen10DataFormat,A21_Uc>FPA21_Uc",
+ "Common::BitCastPtrType<WiimoteEmu::Shinkansen::DataFormat, unsigned "
+ "char[21]>::BitCastPtrType(unsigned char(*)[21])");
+
+ DoDemangleTest("__pp__Q36Common10BitSet<Uc>8IteratorFi",
+ "Common::BitSet<unsigned char>::Iterator::operator++(int)");
+
+ DoDemangleTest("__ne__Q36Common10BitSet<Uc>8IteratorFQ36Common10BitSet<Uc>8Iterator",
+ "Common::BitSet<unsigned char>::Iterator::operator!=(Common::BitSet<unsigned "
+ "char>::Iterator)");
+
+ DoDemangleTest("__lt__Q26Common10BitSet<Uc>FQ26Common10BitSet<Uc>",
+ "Common::BitSet<unsigned char>::operator<(Common::BitSet<unsigned char>)");
+
+ DoDemangleTest("__opb__Q26Common10BitSet<Uc>Fv",
+ "Common::BitSet<unsigned char>::operator bool()");
+
+ DoDemangleTest("AddChildCodeSpace__Q26Common28CodeBlock<Q23Gen8XEmitter,1>FPQ26Common28CodeBlock<"
+ "Q23Gen8XEmitter,1>Ux",
+ "Common::CodeBlock<Gen::XEmitter, "
+ "1>::AddChildCodeSpace(Common::CodeBlock<Gen::XEmitter, 1>*, unsigned long long)");
+
+ DoDemangleTest(
+ "__ct__Q26Common505Lazy<Q23std490unordered_map<Q23std59basic_string<c,Q23std14char_traits<c>,"
+ "Q23std12allocator<c>>,Q23std59basic_string<c,Q23std14char_traits<c>,Q23std12allocator<c>>,"
+ "Q23std73hash<Q23std59basic_string<c,Q23std14char_traits<c>,Q23std12allocator<c>>>,"
+ "Q23std77equal_to<Q23std59basic_string<c,Q23std14char_traits<c>,Q23std12allocator<c>>>,"
+ "Q23std162allocator<Q23std142pair<CQ23std59basic_string<c,Q23std14char_traits<c>,"
+ "Q23std12allocator<c>>,Q23std59basic_string<c,Q23std14char_traits<c>,Q23std12allocator<c>>>>>"
+ ">Fv",
+ "Common::Lazy<std::unordered_map<std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>, std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>, std::hash<std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>>, std::equal_to<std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>>, std::allocator<std::pair<const std::basic_string<char, "
+ "std::char_traits<char>, std::allocator<char>>, std::basic_string<char, "
+ "std::char_traits<char>, std::allocator<char>>>>>>::Lazy()");
+
+ DoDemangleTest(
+ "Append__Q26Common59LinearDiskCache<37ShaderUid<24geometry_shader_uid_data>,Uc>"
+ "FRC37ShaderUid<24geometry_shader_uid_data>PCUcUl",
+ "Common::LinearDiskCache<ShaderUid<geometry_shader_uid_data>, unsigned char>::Append(const "
+ "ShaderUid<geometry_shader_uid_data>&, const unsigned char*, unsigned long)");
+
+ DoDemangleTest("SetIsWii__Q24Core6SystemFb", "Core::System::SetIsWii(bool)");
+
+ DoDemangleTest(
+ "SetDOLFromFile__Q26DiscIO22DirectoryBlobPartitionFRQ23std59basic_string<c,Q23std14char_"
+ "traits<c>,Q23std12allocator<c>>UxPQ23std32vector<Uc,Q23std13allocator<Uc>>",
+ "DiscIO::DirectoryBlobPartition::SetDOLFromFile(std::basic_string<char, "
+ "std::char_traits<char>, std::allocator<char>>&, unsigned long long, std::vector<unsigned "
+ "char, std::allocator<unsigned char>>*)");
+
+ DoDemangleTest(
+ "SetDOL__Q26DiscIO22DirectoryBlobPartitionFQ26DiscIO14FSTBuilderNodeUxPQ23std32vector<Uc,"
+ "Q23std13allocator<Uc>>",
+ "DiscIO::DirectoryBlobPartition::SetDOL(DiscIO::FSTBuilderNode, unsigned long long, "
+ "std::vector<unsigned char, std::allocator<unsigned char>>*)");
+
+ DoDemangleTest("ReadFromGroups__Q26DiscIO19WIARVZFileReader<1>FPUxPUxPPUcUxUlUxUxUlUlUl",
+ "DiscIO::WIARVZFileReader<1>::ReadFromGroups(unsigned long long*, unsigned long "
+ "long*, unsigned char**, unsigned long long, unsigned long, unsigned long long, "
+ "unsigned long long, unsigned long, unsigned long, unsigned long)");
+
+ DoDemangleTest("GetCompanyFromID__6DiscIOFRCQ23std59basic_string<c,Q23std14char_traits<c>,"
+ "Q23std12allocator<c>>",
+ "DiscIO::GetCompanyFromID(const std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>&)");
+
+ DoDemangleTest("DiscordJoinRequest__Q27Discord7HandlerFPcRQ23std59basic_string<c,Q23std14char_"
+ "traits<c>,Q23std12allocator<c>>Pc",
+ "Discord::Handler::DiscordJoinRequest(char*, std::basic_string<char, "
+ "std::char_traits<char>, std::allocator<char>>&, char*)");
+
+ DoDemangleTest("AddVoice__Q33DSP3HLE18ZeldaAudioRendererFUs",
+ "DSP::HLE::ZeldaAudioRenderer::AddVoice(unsigned short)");
+
+ DoDemangleTest("SetConstPatterns__Q33DSP3HLE18ZeldaAudioRendererFPQ23std12array<s,256>",
+ "DSP::HLE::ZeldaAudioRenderer::SetConstPatterns(std::array<short, 256>*)");
+
+ DoDemangleTest("ReJitConditional__Q33DSP3x6410DSPEmitterFUsMQ33DSP3x6410DSPEmitterFPCvPvUs_v",
+ "DSP::x64::DSPEmitter::ReJitConditional(unsigned short, void "
+ "(DSP::x64::DSPEmitter::*)(unsigned short))");
+
+ DoDemangleTest("decode_mb__9ERContextFPviiiPA4_A2_Piiiii",
+ "ERContext::decode_mb(void*, int, int, int, int*(*)[4][2], int, int, int, int)");
+
+ DoDemangleTest("Write__Q24File12DirectIOFileFQ23std11span<Uc,-1>",
+ "File::DirectIOFile::Write(std::span<unsigned char, -1>)");
+
+ DoDemangleTest("SetBoth__Q27PowerPC12PairedSingleFff",
+ "PowerPC::PairedSingle::SetBoth(float, float)");
+
+ DoDemangleTest("GetPaletteConversionPipeline__Q211VideoCommon11ShaderCacheF10TLUTFormat",
+ "VideoCommon::ShaderCache::GetPaletteConversionPipeline(TLUTFormat)");
+
+ DoDemangleTest(
+ "GetGXPipelineConfig__"
+ "Q211VideoCommon11ShaderCacheFP18NativeVertexFormatP14AbstractShaderP14AbstractShaderP14Abstr"
+ "actShaderR18RasterizationStateR10DepthStateR13BlendingState21AbstractPipelineUsage",
+ "VideoCommon::ShaderCache::GetGXPipelineConfig(NativeVertexFormat*, AbstractShader*, "
+ "AbstractShader*, AbstractShader*, RasterizationState&, DepthState&, BlendingState&, "
+ "AbstractPipelineUsage)");
+
+ DoDemangleTest("LoadCaches__Q211VideoCommon11ShaderCacheFv",
+ "VideoCommon::ShaderCache::LoadCaches()");
+
+ DoDemangleTest(
+ "LoadPipelineCache<Q211VideoCommon17GXUberPipelineUid,"
+ "Q211VideoCommon27SerializedGXUberPipelineUid,Q23std355map<"
+ "Q211VideoCommon17GXUberPipelineUid,Q23std89pair<Q23std73unique_ptr<16AbstractPipeline,"
+ "Q23std34default_delete<16AbstractPipeline>>,b>,Q23std40less<"
+ "Q211VideoCommon17GXUberPipelineUid>,Q23std159allocator<Q23std139pair<"
+ "CQ211VideoCommon17GXUberPipelineUid,Q23std89pair<Q23std73unique_ptr<16AbstractPipeline,"
+ "Q23std34default_delete<16AbstractPipeline>>,b>>>>>__"
+ "Q211VideoCommon11ShaderCacheFRQ23std355map<Q211VideoCommon17GXUberPipelineUid,Q23std89pair<"
+ "Q23std73unique_ptr<16AbstractPipeline,Q23std34default_delete<16AbstractPipeline>>,b>,"
+ "Q23std40less<Q211VideoCommon17GXUberPipelineUid>,Q23std159allocator<Q23std139pair<"
+ "CQ211VideoCommon17GXUberPipelineUid,Q23std89pair<Q23std73unique_ptr<16AbstractPipeline,"
+ "Q23std34default_delete<16AbstractPipeline>>,b>>>>RQ26Common64LinearDiskCache<"
+ "Q211VideoCommon27SerializedGXUberPipelineUid,Uc>7APITypePCcb_v",
+ "void VideoCommon::ShaderCache::LoadPipelineCache<VideoCommon::GXUberPipelineUid, "
+ "VideoCommon::SerializedGXUberPipelineUid, std::map<VideoCommon::GXUberPipelineUid, "
+ "std::pair<std::unique_ptr<AbstractPipeline, std::default_delete<AbstractPipeline>>, bool>, "
+ "std::less<VideoCommon::GXUberPipelineUid>, std::allocator<std::pair<const "
+ "VideoCommon::GXUberPipelineUid, std::pair<std::unique_ptr<AbstractPipeline, "
+ "std::default_delete<AbstractPipeline>>, bool>>>>>(std::map<VideoCommon::GXUberPipelineUid, "
+ "std::pair<std::unique_ptr<AbstractPipeline, std::default_delete<AbstractPipeline>>, bool>, "
+ "std::less<VideoCommon::GXUberPipelineUid>, std::allocator<std::pair<const "
+ "VideoCommon::GXUberPipelineUid, std::pair<std::unique_ptr<AbstractPipeline, "
+ "std::default_delete<AbstractPipeline>>, bool>>>>&, "
+ "Common::LinearDiskCache<VideoCommon::SerializedGXUberPipelineUid, unsigned char>&, APIType, "
+ "const char*, bool)");
+
+ DoDemangleTest(
+ "BuildDesiredExtensionState__Q210WiimoteEmu7TaTaConFPQ210WiimoteEmu21DesiredExtensionState",
+ "WiimoteEmu::TaTaCon::BuildDesiredExtensionState(WiimoteEmu::DesiredExtensionState*)");
+
+ DoDemangleTest("__ct__Q210WiimoteEmu7WiimoteFUl", "WiimoteEmu::Wiimote::Wiimote(unsigned long)");
+
+ DoDemangleTest("SetWiimoteDeviceIndex__Q210WiimoteEmu7WiimoteFUc",
+ "WiimoteEmu::Wiimote::SetWiimoteDeviceIndex(unsigned char)");
+
+ DoDemangleTest("GetAngularVelocity__Q210WiimoteEmu7WiimoteFQ26Common8TVec3<f>",
+ "WiimoteEmu::Wiimote::GetAngularVelocity(Common::TVec3<float>)");
+
+ DoDemangleTest("BareFn__FPFPCcPv_v_v", "void BareFn(void (*)(const char*, void*))");
+ DoDemangleTest("BareFn__FPFPCcPv_v_PFPCvPv_v",
+ "void (* BareFn(void (*)(const char*, void*)))(const void*, void*)");
+ DoDemangleTest(
+ "SomeFn__FRCPFPFPCvPv_v_RCPFPCvPv_v",
+ "SomeFn(void (*const& (*const&)(void (*)(const void*, void*)))(const void*, void*))");
+ DoDemangleTest(
+ "SomeFn__Q29Namespace5ClassCFRCMQ29Namespace5ClassFPCvPCvMQ29Namespace5ClassFPCvPCvPCvPv_"
+ "v_RCMQ29Namespace5ClassFPCvPCvPCvPv_v",
+ "Namespace::Class::SomeFn(void (Namespace::Class::*const & (Namespace::Class::*const "
+ "&)(void (Namespace::Class::*)(const void*, void*) const) const)(const void*, void*) "
+ "const) const");
+ DoDemangleTest("Matrix__FfPA2_A3_f", "Matrix(float, float(*)[2][3])");
+ DoDemangleTest("test__FRCPCPCi", "test(const int* const* const&)");
+
+ DoDemangleTest("@GUARD@GetCompanyFromID__6DiscIOFRCQ23std59basic_string<c,Q23std14char_traits<c>,"
+ "Q23std12allocator<c>>@EMPTY_STRING",
+ "DiscIO::GetCompanyFromID(const std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>&)::EMPTY_STRING guard");
+
+ DoDemangleTest("@LOCAL@GetCompanyFromID__6DiscIOFRCQ23std59basic_string<c,Q23std14char_traits<c>,"
+ "Q23std12allocator<c>>@EMPTY_STRING",
+ "DiscIO::GetCompanyFromID(const std::basic_string<char, std::char_traits<char>, "
+ "std::allocator<char>>&)::EMPTY_STRING");
+
+ DoDemangleTest("@LOCAL@SetSIMDMode__Q26Common3FPUFQ36Common3FPU9RoundModeb@EXCEPTION_MASK",
+ "Common::FPU::SetSIMDMode(Common::FPU::RoundMode, bool)::EXCEPTION_MASK");
+
+ DoDemangleTest("@LOCAL@GetInstance__18AchievementManagerFv@s_instance",
+ "AchievementManager::GetInstance()::s_instance");
+
+ DoDemangleTest("s_instance$localstatic3$GetInstance__18AchievementManagerFv",
+ "AchievementManager::GetInstance()::s_instance");
+
+ DoDemangleTest("init$localstatic4$GetInstance__18AchievementManagerFv",
+ "AchievementManager::GetInstance()::localstatic4 guard");
+}
+
+TEST(CWDemangler, TestDemangleOptions)
+{
+ DoDemangleOptionsTest(true, false, "__dt__Q210WiimoteEmu7WiimoteFv",
+ "WiimoteEmu::Wiimote::~Wiimote()");
+ DoDemangleOptionsTest(false, false, "__dt__Q210WiimoteEmu7WiimoteFv",
+ "WiimoteEmu::Wiimote::~Wiimote(void)");
+ DoDemangleOptionsTest(true, true, "example__10HahaOne<1>CFv",
+ "HahaOne<__int128>::example() const");
+ DoDemangleOptionsTest(true, true, "fn<3,PV2>__FPC2",
+ "fn<3, volatile __vec2x32float__*>(const __vec2x32float__*)");
+}
diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj
index 6b476dad94..0cda54ff88 100644
--- a/Source/UnitTests/UnitTests.vcxproj
+++ b/Source/UnitTests/UnitTests.vcxproj
@@ -47,6 +47,7 @@
<ClCompile Include="Common\CommonFuncsTest.cpp" />
<ClCompile Include="Common\Crypto\EcTest.cpp" />
<ClCompile Include="Common\Crypto\SHA1Test.cpp" />
+ <ClCompile Include="Common\CWDemanglerTest.cpp" />
<ClCompile Include="Common\EnumFormatterTest.cpp" />
<ClCompile Include="Common\EventTest.cpp" />
<ClCompile Include="Common\FileUtilTest.cpp" />