diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c22ed4a08..69b2bf0c29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1525,6 +1525,8 @@ list(APPEND NativeAppSource UI/ImDebugger/ImDebugger.h UI/ImDebugger/ImDisasmView.cpp UI/ImDebugger/ImDisasmView.h + UI/ImDebugger/ImStructViewer.cpp + UI/ImDebugger/ImStructViewer.h UI/DiscordIntegration.cpp UI/NativeApp.cpp UI/BackgroundAudio.h diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index aa15f8fec6..15e5c12bf0 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -377,6 +377,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) { ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen); ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen); ImGui::Checkbox("sceAtrac", &cfg_.atracOpen); + ImGui::Checkbox("Struct viewer", &cfg_.structViewerOpen); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); @@ -411,6 +412,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) { } DrawHLEModules(cfg_); + + if (&cfg_.structViewerOpen) { + structViewer_.Draw(mipsDebug, &cfg_.structViewerOpen); + } } void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) { diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h index 389088deb6..b084dcf492 100644 --- a/UI/ImDebugger/ImDebugger.h +++ b/UI/ImDebugger/ImDebugger.h @@ -14,6 +14,7 @@ #include "Core/Debugger/DebugInterface.h" #include "UI/ImDebugger/ImDisasmView.h" +#include "UI/ImDebugger/ImStructViewer.h" // This is the main state container of the whole Dear ImGUI-based in-game cross-platform debugger. // @@ -57,6 +58,7 @@ struct ImConfig { bool modulesOpen = true; bool hleModulesOpen = false; bool atracOpen = true; + bool structViewerOpen = false; // HLE explorer settings // bool filterByUsed = true; @@ -71,6 +73,7 @@ struct ImDebugger { ImDisasmWindow disasm_; ImLuaConsole luaConsole_; + ImStructViewer structViewer_; // Open variables. ImConfig cfg_; diff --git a/UI/ImDebugger/ImStructViewer.cpp b/UI/ImDebugger/ImStructViewer.cpp new file mode 100644 index 0000000000..8604a0817f --- /dev/null +++ b/UI/ImDebugger/ImStructViewer.cpp @@ -0,0 +1,783 @@ +#include +#include + +#include "ext/imgui/imgui.h" + +#include "Core/MemMap.h" +#include "Core/Debugger/Breakpoints.h" +#include "Core/MIPS/MIPSDebugInterface.h" + +#include "UI/ImDebugger/ImStructViewer.h" + +static auto COLOR_GRAY = ImVec4(0.45f, 0.45f, 0.45f, 1); +static auto COLOR_RED = ImVec4(1, 0, 0, 1); + +enum class BuiltInType { + Bool, + Char, + Int8, + Int16, + Int32, + Int64, + TerminatedString, + Float, + Void, +}; + +struct BuiltIn { + BuiltInType type; + ImGuiDataType imGuiType; + const char* hexFormat; +}; + +static const std::unordered_map knownBuiltIns = { + {"/bool", {BuiltInType::Bool, ImGuiDataType_U8, "%hhx"}}, + + {"/char", {BuiltInType::Char, ImGuiDataType_S8, "%hhx"}}, + {"/uchar", {BuiltInType::Char, ImGuiDataType_U8, "%hhx"}}, + + {"/byte", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}}, + {"/sbyte", {BuiltInType::Int8, ImGuiDataType_S8, "%hhx"}}, + {"/undefined1", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}}, + + {"/word", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}}, + {"/sword", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}}, + {"/ushort", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}}, + {"/short", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}}, + {"/undefined2", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}}, + + {"/dword", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}}, + {"/sdword", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}}, + {"/uint", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}}, + {"/int", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}}, + {"/ulong", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}}, + {"/long", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}}, + {"/undefined4", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}}, + + {"/qword", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}}, + {"/sqword", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}}, + {"/ulonglong", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}}, + {"/longlong", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}}, + {"/undefined8", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}}, + + {"/TerminatedCString", {BuiltInType::TerminatedString, -1, nullptr}}, + + {"/float", {BuiltInType::Float, ImGuiDataType_Float, nullptr}}, + {"/float4", {BuiltInType::Float, ImGuiDataType_Float, nullptr}}, + + {"/void", {BuiltInType::Void, -1, nullptr}}, +}; + +static void DrawBuiltInEditPopup(const BuiltIn& builtIn, const u32 address) { + if (builtIn.imGuiType == -1) { + return; + } + ImGui::OpenPopupOnItemClick("edit", ImGuiPopupFlags_MouseButtonRight); + if (ImGui::BeginPopup("edit")) { + if (ImGui::Selectable("Set to zero")) { + switch (builtIn.type) { + case BuiltInType::Bool: + case BuiltInType::Char: + case BuiltInType::Int8: + Memory::Write_U8(0, address); + break; + case BuiltInType::Int16: + Memory::Write_U16(0, address); + break; + case BuiltInType::Int32: + Memory::Write_U32(0, address); + break; + case BuiltInType::Int64: + Memory::Write_U64(0, address); + break; + case BuiltInType::Float: + Memory::Write_Float(0, address); + break; + default: + break; + } + } + void* data = Memory::GetPointerWriteUnchecked(address); + if (builtIn.hexFormat) { + ImGui::DragScalar("Value (hex)", builtIn.imGuiType, data, 0.2f, nullptr, nullptr, builtIn.hexFormat); + } + ImGui::DragScalar("Value", builtIn.imGuiType, data, 0.2f); + ImGui::EndPopup(); + } +} + +static void DrawIntBuiltInEditPopup(const u32 address, const u32 length) { + switch (length) { + case 1: + DrawBuiltInEditPopup(knownBuiltIns.at("/byte"), address); + break; + case 2: + DrawBuiltInEditPopup(knownBuiltIns.at("/word"), address); + break; + case 4: + DrawBuiltInEditPopup(knownBuiltIns.at("/dword"), address); + break; + case 8: + DrawBuiltInEditPopup(knownBuiltIns.at("/qword"), address); + break; + default: + break; + } +} + +static void DrawBuiltInContent(const BuiltIn& builtIn, const u32 address) { + switch (builtIn.type) { + case BuiltInType::Bool: + ImGui::Text("= %s", Memory::Read_U8(address) ? "true" : "false"); + break; + case BuiltInType::Char: { + const u8 value = Memory::Read_U8(address); + if (std::isprint(value)) { + ImGui::Text("= %x '%c'", value, value); + } else { + ImGui::Text("= %x", value); + } + break; + } + case BuiltInType::Int8: + ImGui::Text("= %x", Memory::Read_U8(address)); + break; + case BuiltInType::Int16: + ImGui::Text("= %x", Memory::Read_U16(address)); + break; + case BuiltInType::Int32: + ImGui::Text("= %x", Memory::Read_U32(address)); + break; + case BuiltInType::Int64: + ImGui::Text("= %llx", Memory::Read_U64(address)); + break; + case BuiltInType::TerminatedString: + if (Memory::IsValidNullTerminatedString(address)) { + ImGui::Text("= \"%s\"", Memory::GetCharPointerUnchecked(address)); + } else { + ImGui::Text("= %x ", Memory::Read_U8(address), address); + } + break; + case BuiltInType::Float: + ImGui::Text("= %f", Memory::Read_Float(address)); + break; + case BuiltInType::Void: + ImGui::Text(""); + default: + return; + } + DrawBuiltInEditPopup(builtIn, address); +} + +static u64 ReadMemoryInt(const u32 address, const u32 length) { + switch (length) { + case 1: + return Memory::Read_U8(address); + case 2: + return Memory::Read_U16(address); + case 4: + return Memory::Read_U32(address); + case 8: + return Memory::Read_U64(address); + default: + return 0; + } +} + +static constexpr int COLUMN_NAME = 0; +static constexpr int COLUMN_TYPE = 1; +static constexpr int COLUMN_CONTENT = 2; + +void ImStructViewer::Draw(MIPSDebugInterface* mipsDebug, bool* open) { + mipsDebug_ = mipsDebug; + ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Struct viewer", open) || !mipsDebug->isAlive() || !Memory::IsActive()) { + ImGui::End(); + return; + } + if (ghidraClient_.Ready()) { + ghidraClient_.UpdateResult(); + if (!fetchedAtLeastOnce_ && !ghidraClient_.Failed()) { + fetchedAtLeastOnce_ = true; + } + } + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + if (fetchedAtLeastOnce_) { + DrawStructViewer(); + } else { + DrawConnectionSetup(); + } + ImGui::PopStyleVar(); + + ImGui::End(); +} + +void ImStructViewer::DrawConnectionSetup() { + ImGui::TextWrapped("Struct viewer visualizes data in memory using types from your Ghidra project."); + ImGui::TextWrapped("To get started install the ghidra-rest-api plugin and start the Rest API server."); + ImGui::TextWrapped("When ready press the connect button below."); + + ImGui::BeginDisabled(!ghidraClient_.Idle()); + ImGui::PushItemWidth(120); + ImGui::InputText("Host", ghidraHost_, IM_ARRAYSIZE(ghidraHost_)); + ImGui::SameLine(); + ImGui::InputInt("Port", &ghidraPort_, 0); + ImGui::SameLine(); + if (ImGui::Button("Connect")) { + ghidraClient_.FetchAll(ghidraHost_, ghidraPort_); + } + ImGui::PopItemWidth(); + ImGui::EndDisabled(); + + if (ghidraClient_.Idle() && ghidraClient_.Failed()) { + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str()); + ImGui::PopStyleColor(); + } +} + +void ImStructViewer::DrawStructViewer() { + ImGui::BeginDisabled(!ghidraClient_.Idle()); + if (ImGui::Button("Refresh data types")) { + ghidraClient_.FetchAll(ghidraHost_, ghidraPort_); + } + ImGui::EndDisabled(); + + if (ghidraClient_.Idle() && ghidraClient_.Failed()) { + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + ImGui::SameLine(); + ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str()); + ImGui::PopStyleColor(); + } + + if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_Reorderable)) { + if (ImGui::BeginTabItem("Globals")) { + DrawGlobals(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Watch")) { + DrawWatch(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} + +void ImStructViewer::DrawGlobals() { + globalFilter_.Draw(); + if (ImGui::BeginTable("##globals", 3, + ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_RowBg)) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Field"); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Content"); + ImGui::TableHeadersRow(); + + for (const auto& symbol: ghidraClient_.result.symbols) { + if (!symbol.label || !symbol.userDefined || symbol.dataTypePathName.empty()) { + continue; + } + if (!globalFilter_.PassFilter(symbol.name.c_str())) { + continue; + } + DrawType(symbol.address, 0, symbol.dataTypePathName, nullptr, symbol.name.c_str(), -1); + } + + ImGui::EndTable(); + } +} + +void ImStructViewer::DrawWatch() { + DrawNewWatchEntry(); + ImGui::Dummy(ImVec2(1, 6)); + + watchFilter_.Draw(); + if (ImGui::BeginTable("##watch", 3, + ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_RowBg)) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Field"); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Content"); + ImGui::TableHeadersRow(); + + int watchIndex = -1; + for (const auto& watch: watches_) { + watchIndex++; + if (!watchFilter_.PassFilter(watch.name.c_str())) { + continue; + } + u32 address = 0; + if (!watch.expression.empty()) { + u32 val; + PostfixExpression postfix; + if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)) { + if (mipsDebug_->parseExpression(postfix, val)) { + address = val; + } + } + } else { + address = watch.address; + } + DrawType(address, 0, watch.typePathName, nullptr, watch.name.c_str(), watchIndex); + } + if (removeWatchIndex_ != -1) { + watches_.erase(watches_.begin() + removeWatchIndex_); + removeWatchIndex_ = -1; + } + if (addWatch_.address != 0) { + watches_.push_back(addWatch_); + addWatch_ = Watch(); + } + + ImGui::EndTable(); + } +} + +void ImStructViewer::DrawNewWatchEntry() { + ImGui::PushItemWidth(150); + ImGui::InputText("Name", newWatch_.name, IM_ARRAYSIZE(newWatch_.name)); + + ImGui::SameLine(); + if (ImGui::BeginCombo("Type", newWatch_.typeDisplayName.c_str())) { + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(0); + } + newWatch_.typeFilter.Draw(); + for (const auto& entry: ghidraClient_.result.types) { + const auto& type = entry.second; + if (newWatch_.typeFilter.PassFilter(type.displayName.c_str())) { + ImGui::PushID(type.pathName.c_str()); + if (ImGui::Selectable(type.displayName.c_str(), newWatch_.typePathName == type.pathName)) { + newWatch_.typePathName = type.pathName; + newWatch_.typeDisplayName = type.displayName; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) { + ImGui::Text("%s (%s)", type.displayName.c_str(), type.pathName.c_str()); + ImGui::Text("Length: %x (aligned: %x)", type.length, type.alignedLength); + ImGui::EndTooltip(); + } + ImGui::PopID(); + } + } + ImGui::EndCombo(); + } + + ImGui::SameLine(); + ImGui::InputText("Expression", newWatch_.expression, IM_ARRAYSIZE(newWatch_.expression)); + ImGui::SameLine(); + ImGui::Checkbox("Dynamic", &newWatch_.dynamic); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal)) + ImGui::SetTooltip("When checked the expression will be\nre-evaluated on each frame."); + + ImGui::PopItemWidth(); + + ImGui::SameLine(); + if (ImGui::Button("Add watch")) { + u32 val; + PostfixExpression postfix; + if (newWatch_.typePathName.empty()) { + newWatch_.error = "type can't be empty"; + } else if (!mipsDebug_->initExpression(newWatch_.expression, postfix) + || !mipsDebug_->parseExpression(postfix, val)) { + newWatch_.error = "invalid expression"; + } else { + std::string watchName = newWatch_.name; + if (watchName.empty()) { + watchName = ""; + } + watches_.emplace_back(Watch{ + newWatch_.dynamic ? newWatch_.expression : "", + newWatch_.dynamic ? 0 : val, + newWatch_.typePathName, + newWatch_.dynamic ? watchName + " (" + newWatch_.expression + ")" : watchName + }); + memset(newWatch_.name, 0, sizeof(newWatch_.name)); + memset(newWatch_.expression, 0, sizeof(newWatch_.name)); + newWatch_.dynamic = false; + newWatch_.error = ""; + newWatch_.typeFilter.Clear(); + // Not clearing the actual selected type on purpose here, user will have to reselect one anyway and maybe + // there is a chance they will reuse the current one + } + } + if (!newWatch_.error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + ImGui::TextWrapped("Error: %s", newWatch_.error.c_str()); + ImGui::PopStyleColor(); + } +} + +static void DrawTypeColumn( + const std::string& format, + const std::string& typeDisplayName, + const u32 base, + const u32 offset +) { + ImGui::TableSetColumnIndex(COLUMN_TYPE); + ImGui::Text(format.c_str(), typeDisplayName.c_str()); + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::SameLine(); + if (offset != 0) { + ImGui::Text("@ %x+%x", base, offset); + } else { + ImGui::Text("@ %x", base); + } + ImGui::PopStyleColor(); +} + +static void DrawArrayContent( + const std::unordered_map& types, + const GhidraType& type, + const u32 address +) { + if (type.arrayElementLength != 1 || !types.count(type.arrayTypePathName)) { + return; + } + const auto& arrayType = types.at(type.arrayTypePathName); + bool charElement = false; + if (arrayType.kind == TYPEDEF && types.count(arrayType.typedefBaseTypePathName)) { + const auto& baseArrayType = types.at(arrayType.typedefBaseTypePathName); + charElement = baseArrayType.pathName == "/char"; + } else { + charElement = arrayType.pathName == "/char"; + } + if (!charElement) { + return; + } + const char* charPointer = Memory::GetCharPointerUnchecked(address); + std::string text(charPointer, charPointer + type.arrayElementCount); + text = std::regex_replace(text, std::regex("\n"), "\\n"); + ImGui::Text("= \"%s\"", text.c_str()); +} + +static void DrawPointerText(const u32 value) { + if (Memory::IsValidAddress(value)) { + ImGui::Text("* %x", value); + return; + } + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + if (value == 0) { + ImGui::Text("* NULL"); + } else { + ImGui::Text("* ", value); + } + ImGui::PopStyleColor(); +} + +static void DrawPointerContent( + const std::unordered_map& types, + const GhidraType& type, + const u32 value +) { + if (!types.count(type.pointerTypePathName)) { + DrawPointerText(value); + return; + } + const auto& pointedType = types.at(type.pointerTypePathName); + bool charPointerElement = false; + if (pointedType.kind == TYPEDEF && types.count(pointedType.typedefBaseTypePathName)) { + const auto& basePointedType = types.at(pointedType.typedefBaseTypePathName); + charPointerElement = basePointedType.pathName == "/char"; + } else { + charPointerElement = pointedType.pathName == "/char"; + } + if (!charPointerElement || !Memory::IsValidNullTerminatedString(value)) { + DrawPointerText(value); + return; + } + const char* charPointer = Memory::GetCharPointerUnchecked(value); + std::string text(charPointer); + text = std::regex_replace(text, std::regex("\n"), "\\n"); + ImGui::Text("= \"%s\"", text.c_str()); +} + +// Formatting enum value to a nice string as it would look in code with 'or' operator (e.g. "ALIGN_TOP | ALIGN_LEFT") +static std::string FormatEnumValue(const std::vector& enumMembers, const u64 value) { + std::stringstream ss; + bool hasPrevious = false; + for (const auto& member: enumMembers) { + if (value & member.value) { + if (hasPrevious) { + ss << " | "; + } + ss << member.name; + hasPrevious = true; + } + } + return ss.str(); +} + +// This will be potentially called a lot of times in a frame so not using string here +static void FormatIndexedMember(char* buffer, const size_t bufferSize, const std::string& name, const u32 index) { + snprintf(buffer, bufferSize, "%s[%x]", name.c_str(), index); +} + +void ImStructViewer::DrawType( + const u32 base, + const u32 offset, + const std::string& typePathName, + const char* typeDisplayNameOverride, + const char* name, + const int watchIndex, + const ImGuiTreeNodeFlags extraTreeNodeFlags +) { + const auto& types = ghidraClient_.result.types; + // Generic pointer is not included in the type listing, need to resolve it manually to void* + if (typePathName == "/pointer") { + DrawType(base, offset, "/void *", "pointer", name, watchIndex); + return; + } + // Undefined itself doesn't exist as a type, let's just display first byte in that case + if (typePathName == "/undefined") { + DrawType(base, offset, "/undefined1", "undefined", name, watchIndex); + return; + } + + const bool hasType = types.count(typePathName) != 0; + + // Resolve typedefs as early as possible + if (hasType) { + const auto& type = types.at(typePathName); + if (type.kind == TYPEDEF) { + DrawType(base, offset, type.typedefBaseTypePathName, type.displayName.c_str(), name, + watchIndex); + return; + } + } + + const u32 address = base + offset; + ImGui::PushID(static_cast(address)); + ImGui::PushID(watchIndex); + + // Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing + // to make the tree lines equal high. + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(COLUMN_NAME); + ImGui::AlignTextToFramePadding(); + // Flags used for nodes that can't be further opened + const ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | + ImGuiTreeNodeFlags_Bullet | extraTreeNodeFlags; + + // Type is missing in fetched types, this can happen e.g. if type used for watch is removed from Ghidra + if (!hasType) { + ImGui::TreeNodeEx("Field", leafFlags, "%s", name); + DrawContextMenu(base, offset, 0, typePathName, name, watchIndex); + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + DrawTypeColumn("", typePathName, base, offset); + ImGui::PopStyleColor(); + ImGui::PopID(); + ImGui::PopID(); + return; + } + + const auto& type = types.at(typePathName); + const std::string typeDisplayName = + typeDisplayNameOverride == nullptr ? type.displayName : typeDisplayNameOverride; + + // Handle cases where pointers or expressions point to invalid memory + if (!Memory::IsValidAddress(address)) { + ImGui::TreeNodeEx("Field", leafFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::Text("", address); + ImGui::PopStyleColor(); + ImGui::PopID(); + ImGui::PopID(); + return; + } + + // For each type we create tree node with the field name and fill type column + // Content column and edit popup is only set for types where it makes sense + switch (type.kind) { + case ENUM: { + ImGui::TreeNodeEx("Enum", leafFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + const u64 value = ReadMemoryInt(address, type.length); + const std::string stringValue = FormatEnumValue(type.enumMembers, value); + ImGui::Text("= %llx (%s)", value, stringValue.c_str()); + DrawIntBuiltInEditPopup(address, type.length); + break; + } + case POINTER: { + const bool nodeOpen = ImGui::TreeNodeEx("Pointer", extraTreeNodeFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + const u32 pointer = Memory::Read_U32(address); + DrawPointerContent(types, type, pointer); + + if (nodeOpen) { + if (types.count(type.pointerTypePathName)) { + const auto& pointedType = types.at(type.pointerTypePathName); + + const auto countStateId = ImGui::GetID("PointerElementCount"); + const int pointerElementCount = ImGui::GetStateStorage()->GetInt(countStateId, 1); + + // A pointer to unsized type (e.g. function or void) can't have more than one element + if (pointedType.alignedLength > 0) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(COLUMN_NAME); + if (ImGui::Button("Show more")) { + ImGui::GetStateStorage()->SetInt(countStateId, pointerElementCount + 1); + } + if (pointerElementCount > 1) { + ImGui::SameLine(); + ImGui::Text("(showing %x)", pointerElementCount); + } + } + + for (int i = 0; i < pointerElementCount; i++) { + char nameBuffer[256]; + FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i); + // A pointer always creates extra node in the tree so using DefaultOpen to spare user one click + DrawType(pointer, i * pointedType.alignedLength, type.pointerTypePathName, + nullptr, nameBuffer, -1, ImGuiTreeNodeFlags_DefaultOpen); + } + } + ImGui::TreePop(); + } + break; + } + case ARRAY: { + const bool nodeOpen = ImGui::TreeNodeEx("Array", extraTreeNodeFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + DrawArrayContent(types, type, address); + + if (nodeOpen) { + for (int i = 0; i < type.arrayElementCount; i++) { + char nameBuffer[256]; + FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i); + DrawType(base, offset + i * type.arrayElementLength, type.arrayTypePathName, + nullptr, nameBuffer, -1); + } + ImGui::TreePop(); + } + break; + } + case STRUCTURE: + case UNION: { + const bool nodeOpen = ImGui::TreeNodeEx("Composite", extraTreeNodeFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + + if (nodeOpen) { + for (const auto& member: type.compositeMembers) { + DrawType(base, offset + member.offset, member.typePathName, nullptr, + member.fieldName.c_str(), -1); + } + ImGui::TreePop(); + } + break; + } + case FUNCTION_DEFINITION: + ImGui::TreeNodeEx("Field", leafFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + DrawTypeColumn("%s", typeDisplayName, base, offset); + + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + ImGui::Text(""); // TODO could be go to in disassembler here + break; + case BUILT_IN: { + ImGui::TreeNodeEx("Field", leafFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + + if (knownBuiltIns.count(typePathName)) { + DrawTypeColumn("%s", typeDisplayName, base, offset); + ImGui::TableSetColumnIndex(COLUMN_CONTENT); + DrawBuiltInContent(knownBuiltIns.at(typePathName), address); + } else { + // Some built in types are rather obscure so we don't handle every possible one + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + DrawTypeColumn("", typePathName, base, offset); + ImGui::PopStyleColor(); + } + break; + } + default: { + // At this point there is most likely some issue in the Ghidra plugin and the type wasn't + // classified to any category + ImGui::TreeNodeEx("Field", leafFlags, "%s", name); + DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex); + ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED); + DrawTypeColumn("", typeDisplayName, base, offset); + ImGui::PopStyleColor(); + break; + } + } + + ImGui::PopID(); + ImGui::PopID(); +} + +void ImStructViewer::DrawContextMenu( + const u32 base, + const u32 offset, + const int length, + const std::string& typePathName, + const char* name, + const int watchIndex +) { + ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + if (ImGui::BeginPopup("context")) { + const u32 address = base + offset; + + // This might be called when iterating over existing watches so can't modify the watch vector directly here + if (watchIndex < 0) { + if (ImGui::MenuItem("Add watch")) { + addWatch_.address = address; + addWatch_.typePathName = typePathName; + addWatch_.name = name; + } + } else if (watchIndex < watches_.size()) { + if (ImGui::MenuItem("Remove watch")) { + removeWatchIndex_ = watchIndex; + } + } + + // if (ImGui::MenuItem("Go to in memory view")) { + // TODO imgui memory view not yet implemented + // } + + // Memory breakpoints are only possible for sized types + if (length > 0) { + const u32 end = address + length; + MemCheck memCheck; + const bool hasMemCheck = CBreakPoints::GetMemCheck(address, end, &memCheck); + if (hasMemCheck) { + if (ImGui::MenuItem("Remove memory breakpoint")) { + CBreakPoints::RemoveMemCheck(address, end); + } + } + if (!hasMemCheck || !(memCheck.cond & MEMCHECK_READ)) { + if (ImGui::MenuItem("Add memory read breakpoint")) { + CBreakPoints::AddMemCheck(address, end, MEMCHECK_READ, BREAK_ACTION_PAUSE); + } + } + if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE)) { + if (ImGui::MenuItem("Add memory write breakpoint")) { + CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE); + } + } + if (!hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE)) { + if (ImGui::MenuItem("Add memory write on change breakpoint")) { + constexpr auto cond = static_cast(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE); + CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE); + } + } + } + + ImGui::EndPopup(); + } +} diff --git a/UI/ImDebugger/ImStructViewer.h b/UI/ImDebugger/ImStructViewer.h new file mode 100644 index 0000000000..473f4b4f5d --- /dev/null +++ b/UI/ImDebugger/ImStructViewer.h @@ -0,0 +1,71 @@ +#pragma once + +#include "ext/imgui/imgui.h" + +#include "Common/GhidraClient.h" +#include "Core/MIPS/MIPSDebugInterface.h" + +class ImStructViewer { + struct Watch { + std::string expression; + u32 address = 0; + std::string typePathName; + std::string name; + }; + + struct NewWatch { + char name[256] = {}; + std::string typeDisplayName; + std::string typePathName; + char expression[256] = {}; + bool dynamic = false; + std::string error; + ImGuiTextFilter typeFilter; + }; + +public: + void Draw(MIPSDebugInterface* mipsDebug, bool* open); + +private: + MIPSDebugInterface* mipsDebug_ = nullptr; + + ImGuiTextFilter globalFilter_; + ImGuiTextFilter watchFilter_; + + GhidraClient ghidraClient_; + char ghidraHost_[128] = "localhost"; + int ghidraPort_ = 18489; + bool fetchedAtLeastOnce_ = false; + + std::vector watches_; + int removeWatchIndex_ = -1; + Watch addWatch_; + NewWatch newWatch_; + + void DrawConnectionSetup(); + + void DrawStructViewer(); + + void DrawGlobals(); + + void DrawWatch(); + + void DrawNewWatchEntry(); + + void DrawType( + u32 base, + u32 offset, + const std::string& typePathName, + const char* typeDisplayNameOverride, + const char* name, + int watchIndex, + ImGuiTreeNodeFlags extraTreeNodeFlags = 0); + + void DrawContextMenu( + u32 base, + u32 offset, + int length, + const std::string& typePathName, + const char* name, + int watchIndex); +}; diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index f8fbc3f26f..b1b2ba0c90 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -53,6 +53,7 @@ + @@ -95,6 +96,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index 288c8556a0..6c71cd8ab9 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -104,6 +104,9 @@ ImDebugger + + ImDebugger + @@ -208,6 +211,9 @@ ImDebugger + + ImDebugger + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index 88f2bc539e..20fab0de8e 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -128,6 +128,7 @@ + @@ -170,6 +171,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index 656c6c61e2..ba0db38ee8 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -45,6 +45,9 @@ ImDebugger + + ImDebugger + @@ -91,6 +94,9 @@ ImDebugger + + ImDebugger + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 6ef541a9c8..ebebbfe480 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -877,6 +877,7 @@ LOCAL_SRC_FILES := \ $(SRC)/android/jni/OpenSLContext.cpp \ $(SRC)/UI/ImDebugger/ImDebugger.cpp \ $(SRC)/UI/ImDebugger/ImDisasmView.cpp \ + $(SRC)/UI/ImDebugger/ImStructViewer.cpp \ $(SRC)/UI/AudioCommon.cpp \ $(SRC)/UI/BackgroundAudio.cpp \ $(SRC)/UI/DiscordIntegration.cpp \