Add struct viewer debugging tool

This commit is contained in:
kotcrab 2024-11-11 21:34:19 +01:00
parent fdf8ff7d94
commit 3182cc29e4
10 changed files with 881 additions and 0 deletions

View File

@ -1525,6 +1525,8 @@ list(APPEND NativeAppSource
UI/ImDebugger/ImDebugger.h UI/ImDebugger/ImDebugger.h
UI/ImDebugger/ImDisasmView.cpp UI/ImDebugger/ImDisasmView.cpp
UI/ImDebugger/ImDisasmView.h UI/ImDebugger/ImDisasmView.h
UI/ImDebugger/ImStructViewer.cpp
UI/ImDebugger/ImStructViewer.h
UI/DiscordIntegration.cpp UI/DiscordIntegration.cpp
UI/NativeApp.cpp UI/NativeApp.cpp
UI/BackgroundAudio.h UI/BackgroundAudio.h

View File

@ -377,6 +377,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen); ImGui::Checkbox("HLE Modules", &cfg_.modulesOpen);
ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen); ImGui::Checkbox("HLE Threads", &cfg_.threadsOpen);
ImGui::Checkbox("sceAtrac", &cfg_.atracOpen); ImGui::Checkbox("sceAtrac", &cfg_.atracOpen);
ImGui::Checkbox("Struct viewer", &cfg_.structViewerOpen);
ImGui::EndMenu(); ImGui::EndMenu();
} }
ImGui::EndMainMenuBar(); ImGui::EndMainMenuBar();
@ -411,6 +412,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug) {
} }
DrawHLEModules(cfg_); DrawHLEModules(cfg_);
if (&cfg_.structViewerOpen) {
structViewer_.Draw(mipsDebug, &cfg_.structViewerOpen);
}
} }
void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) { void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, bool *open, CoreState coreState) {

View File

@ -14,6 +14,7 @@
#include "Core/Debugger/DebugInterface.h" #include "Core/Debugger/DebugInterface.h"
#include "UI/ImDebugger/ImDisasmView.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. // 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 modulesOpen = true;
bool hleModulesOpen = false; bool hleModulesOpen = false;
bool atracOpen = true; bool atracOpen = true;
bool structViewerOpen = false;
// HLE explorer settings // HLE explorer settings
// bool filterByUsed = true; // bool filterByUsed = true;
@ -71,6 +73,7 @@ struct ImDebugger {
ImDisasmWindow disasm_; ImDisasmWindow disasm_;
ImLuaConsole luaConsole_; ImLuaConsole luaConsole_;
ImStructViewer structViewer_;
// Open variables. // Open variables.
ImConfig cfg_; ImConfig cfg_;

View File

@ -0,0 +1,783 @@
#include <regex>
#include <sstream>
#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<std::string, BuiltIn> 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 <invalid string @ %x>", Memory::Read_U8(address), address);
}
break;
case BuiltInType::Float:
ImGui::Text("= %f", Memory::Read_Float(address));
break;
case BuiltInType::Void:
ImGui::Text("<void type>");
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 = "<watch>";
}
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<std::string, GhidraType>& 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("* <invalid pointer: %x>", value);
}
ImGui::PopStyleColor();
}
static void DrawPointerContent(
const std::unordered_map<std::string, GhidraType>& 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<GhidraEnumMember>& 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<int>(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("<missing type: %s>", 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("<invalid address: %x>", 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("<function definition>"); // 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("<unsupported built in: %s>", 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("<not implemented type: %s>", 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<MemCheckCondition>(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
}
}
}
ImGui::EndPopup();
}
}

View File

@ -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<Watch> 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);
};

View File

@ -53,6 +53,7 @@
<ClCompile Include="GPUDriverTestScreen.cpp" /> <ClCompile Include="GPUDriverTestScreen.cpp" />
<ClCompile Include="ImDebugger\ImDebugger.cpp" /> <ClCompile Include="ImDebugger\ImDebugger.cpp" />
<ClCompile Include="ImDebugger\ImDisasmView.cpp" /> <ClCompile Include="ImDebugger\ImDisasmView.cpp" />
<ClCompile Include="ImDebugger\ImStructViewer.cpp" />
<ClCompile Include="JitCompareScreen.cpp" /> <ClCompile Include="JitCompareScreen.cpp" />
<ClCompile Include="JoystickHistoryView.cpp" /> <ClCompile Include="JoystickHistoryView.cpp" />
<ClCompile Include="MainScreen.cpp" /> <ClCompile Include="MainScreen.cpp" />
@ -95,6 +96,7 @@
<ClInclude Include="GPUDriverTestScreen.h" /> <ClInclude Include="GPUDriverTestScreen.h" />
<ClInclude Include="ImDebugger\ImDebugger.h" /> <ClInclude Include="ImDebugger\ImDebugger.h" />
<ClInclude Include="ImDebugger\ImDisasmView.h" /> <ClInclude Include="ImDebugger\ImDisasmView.h" />
<ClInclude Include="ImDebugger\ImStructViewer.h" />
<ClInclude Include="JitCompareScreen.h" /> <ClInclude Include="JitCompareScreen.h" />
<ClInclude Include="JoystickHistoryView.h" /> <ClInclude Include="JoystickHistoryView.h" />
<ClInclude Include="MainScreen.h" /> <ClInclude Include="MainScreen.h" />

View File

@ -104,6 +104,9 @@
<ClCompile Include="ImDebugger\ImDisasmView.cpp"> <ClCompile Include="ImDebugger\ImDisasmView.cpp">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ImDebugger\ImStructViewer.cpp">
<Filter>ImDebugger</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="GameInfoCache.h" /> <ClInclude Include="GameInfoCache.h" />
@ -208,6 +211,9 @@
<ClInclude Include="ImDebugger\ImDisasmView.h"> <ClInclude Include="ImDebugger\ImDisasmView.h">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="ImDebugger\ImStructViewer.h">
<Filter>ImDebugger</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="Screens"> <Filter Include="Screens">

View File

@ -128,6 +128,7 @@
<ClInclude Include="..\..\UI\GPUDriverTestScreen.h" /> <ClInclude Include="..\..\UI\GPUDriverTestScreen.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImDebugger.h" /> <ClInclude Include="..\..\UI\ImDebugger\ImDebugger.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h" /> <ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h" />
<ClInclude Include="..\..\UI\InstallZipScreen.h" /> <ClInclude Include="..\..\UI\InstallZipScreen.h" />
<ClInclude Include="..\..\UI\JitCompareScreen.h" /> <ClInclude Include="..\..\UI\JitCompareScreen.h" />
<ClInclude Include="..\..\UI\JoystickHistoryView.h" /> <ClInclude Include="..\..\UI\JoystickHistoryView.h" />
@ -170,6 +171,7 @@
<ClCompile Include="..\..\UI\GPUDriverTestScreen.cpp" /> <ClCompile Include="..\..\UI\GPUDriverTestScreen.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImDebugger.cpp" /> <ClCompile Include="..\..\UI\ImDebugger\ImDebugger.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp" /> <ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp" />
<ClCompile Include="..\..\UI\InstallZipScreen.cpp" /> <ClCompile Include="..\..\UI\InstallZipScreen.cpp" />
<ClCompile Include="..\..\UI\JitCompareScreen.cpp" /> <ClCompile Include="..\..\UI\JitCompareScreen.cpp" />
<ClCompile Include="..\..\UI\JoystickHistoryView.cpp" /> <ClCompile Include="..\..\UI\JoystickHistoryView.cpp" />

View File

@ -45,6 +45,9 @@
<ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp"> <ClCompile Include="..\..\UI\ImDebugger\ImDisasmView.cpp">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp">
<Filter>ImDebugger</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@ -91,6 +94,9 @@
<ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h"> <ClInclude Include="..\..\UI\ImDebugger\ImDisasmView.h">
<Filter>ImDebugger</Filter> <Filter>ImDebugger</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h">
<Filter>ImDebugger</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="ImDebugger"> <Filter Include="ImDebugger">

View File

@ -877,6 +877,7 @@ LOCAL_SRC_FILES := \
$(SRC)/android/jni/OpenSLContext.cpp \ $(SRC)/android/jni/OpenSLContext.cpp \
$(SRC)/UI/ImDebugger/ImDebugger.cpp \ $(SRC)/UI/ImDebugger/ImDebugger.cpp \
$(SRC)/UI/ImDebugger/ImDisasmView.cpp \ $(SRC)/UI/ImDebugger/ImDisasmView.cpp \
$(SRC)/UI/ImDebugger/ImStructViewer.cpp \
$(SRC)/UI/AudioCommon.cpp \ $(SRC)/UI/AudioCommon.cpp \
$(SRC)/UI/BackgroundAudio.cpp \ $(SRC)/UI/BackgroundAudio.cpp \
$(SRC)/UI/DiscordIntegration.cpp \ $(SRC)/UI/DiscordIntegration.cpp \