1612 lines
53 KiB
C++
1612 lines
53 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2022 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/debug/ui/debug_window.h"
|
|
|
|
#include <cinttypes>
|
|
|
|
#include "third_party/capstone/include/capstone/capstone.h"
|
|
#include "third_party/capstone/include/capstone/x86.h"
|
|
#include "third_party/imgui/imgui.h"
|
|
#include "third_party/imgui/imgui_internal.h"
|
|
#include "xenia/base/clock.h"
|
|
#include "xenia/base/fuzzy.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/base/math.h"
|
|
#include "xenia/base/platform.h"
|
|
#include "xenia/base/string_util.h"
|
|
#include "xenia/base/threading.h"
|
|
#include "xenia/cpu/breakpoint.h"
|
|
#include "xenia/cpu/ppc/ppc_opcode_info.h"
|
|
#include "xenia/cpu/stack_walker.h"
|
|
#include "xenia/gpu/graphics_system.h"
|
|
#include "xenia/kernel/xmodule.h"
|
|
#include "xenia/kernel/xthread.h"
|
|
#include "xenia/ui/graphics_provider.h"
|
|
#include "xenia/ui/imgui_drawer.h"
|
|
#include "xenia/ui/immediate_drawer.h"
|
|
#include "xenia/ui/presenter.h"
|
|
#include "xenia/ui/windowed_app_context.h"
|
|
|
|
DEFINE_bool(imgui_debug, false, "Show ImGui debugging tools.", "UI");
|
|
|
|
namespace xe {
|
|
namespace debug {
|
|
namespace ui {
|
|
|
|
using xe::cpu::Breakpoint;
|
|
using xe::kernel::XModule;
|
|
using xe::kernel::XObject;
|
|
using xe::kernel::XThread;
|
|
using xe::ui::KeyEvent;
|
|
using xe::ui::MenuItem;
|
|
using xe::ui::MouseEvent;
|
|
using xe::ui::UIEvent;
|
|
|
|
void DebugWindow::DebugDialog::OnDraw(ImGuiIO& io) {
|
|
debug_window_.DrawFrame(io);
|
|
}
|
|
|
|
static constexpr std::string_view kBaseTitle = "Xenia Debugger";
|
|
|
|
DebugWindow::DebugWindow(Emulator* emulator,
|
|
xe::ui::WindowedAppContext& app_context)
|
|
: emulator_(emulator),
|
|
processor_(emulator->processor()),
|
|
app_context_(app_context),
|
|
window_(xe::ui::Window::Create(app_context_, kBaseTitle, 1500, 1000)) {
|
|
if (cs_open(CS_ARCH_X86, CS_MODE_64, &capstone_handle_) != CS_ERR_OK) {
|
|
assert_always("Failed to initialize capstone");
|
|
}
|
|
cs_option(capstone_handle_, CS_OPT_SYNTAX, CS_OPT_SYNTAX_INTEL);
|
|
cs_option(capstone_handle_, CS_OPT_DETAIL, CS_OPT_OFF);
|
|
}
|
|
|
|
DebugWindow::~DebugWindow() {
|
|
// Make sure pending functions referencing the DebugWindow are executed.
|
|
app_context_.ExecutePendingFunctionsFromUIThread();
|
|
|
|
if (capstone_handle_) {
|
|
cs_close(&capstone_handle_);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<DebugWindow> DebugWindow::Create(
|
|
Emulator* emulator, xe::ui::WindowedAppContext& app_context) {
|
|
std::unique_ptr<DebugWindow> debug_window(
|
|
new DebugWindow(emulator, app_context));
|
|
if (!debug_window->Initialize()) {
|
|
xe::FatalError("Failed to initialize debug window");
|
|
return nullptr;
|
|
}
|
|
|
|
return debug_window;
|
|
}
|
|
|
|
bool DebugWindow::Initialize() {
|
|
// Main menu.
|
|
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
|
|
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
|
|
{
|
|
file_menu->AddChild(
|
|
MenuItem::Create(MenuItem::Type::kString, "&Close", "Alt+F4",
|
|
[this]() { window_->RequestClose(); }));
|
|
}
|
|
main_menu->AddChild(std::move(file_menu));
|
|
window_->SetMainMenu(std::move(main_menu));
|
|
|
|
// Open the window once it's configured.
|
|
if (!window_->Open()) {
|
|
XELOGE("Failed to open the platform window for the debugger");
|
|
return false;
|
|
}
|
|
|
|
// Setup drawing to the window.
|
|
|
|
xe::ui::GraphicsProvider& graphics_provider =
|
|
*emulator_->graphics_system()->provider();
|
|
|
|
presenter_ = graphics_provider.CreatePresenter();
|
|
if (!presenter_) {
|
|
XELOGE("Failed to initialize the presenter for the debugger");
|
|
return false;
|
|
}
|
|
|
|
immediate_drawer_ = graphics_provider.CreateImmediateDrawer();
|
|
if (!immediate_drawer_) {
|
|
XELOGE("Failed to initialize the immediate drawer for the debugger");
|
|
return false;
|
|
}
|
|
immediate_drawer_->SetPresenter(presenter_.get());
|
|
|
|
imgui_drawer_ = std::make_unique<xe::ui::ImGuiDrawer>(window_.get(), 0);
|
|
imgui_drawer_->SetPresenterAndImmediateDrawer(presenter_.get(),
|
|
immediate_drawer_.get());
|
|
debug_dialog_ =
|
|
std::unique_ptr<DebugDialog>(new DebugDialog(imgui_drawer_.get(), *this));
|
|
|
|
// Update the cache before the first frame.
|
|
UpdateCache();
|
|
|
|
// Begin drawing.
|
|
window_->SetPresenter(presenter_.get());
|
|
|
|
return true;
|
|
}
|
|
|
|
void DebugWindow::DrawFrame(ImGuiIO& io) {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(-1, 0));
|
|
ImGui::Begin("main_window", nullptr,
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoSavedSettings);
|
|
ImGui::SetWindowPos(ImVec2(0, 0));
|
|
ImGui::SetWindowSize(io.DisplaySize);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 4));
|
|
|
|
constexpr float kSplitterWidth = 5.0f;
|
|
static float function_pane_width = 150.0f;
|
|
static float source_pane_width = 600.0f;
|
|
static float registers_pane_width = 150.0f;
|
|
static float bottom_panes_height = 300.0f;
|
|
static float breakpoints_pane_width = 300.0f;
|
|
float top_panes_height =
|
|
ImGui::GetContentRegionAvail().y - bottom_panes_height;
|
|
float log_pane_width =
|
|
ImGui::GetContentRegionAvail().x - breakpoints_pane_width;
|
|
|
|
ImGui::BeginChild("##toolbar", ImVec2(0, 25), true);
|
|
DrawToolbar();
|
|
ImGui::EndChild();
|
|
|
|
if (!cache_.is_running) {
|
|
// Disabled state?
|
|
// https://github.com/ocornut/imgui/issues/211
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
|
ImGui::BeginChild("##function_pane",
|
|
ImVec2(function_pane_width, top_panes_height), true);
|
|
DrawFunctionsPane();
|
|
ImGui::EndChild();
|
|
ImGui::SameLine();
|
|
ImGui::InvisibleButton("##vsplitter0",
|
|
ImVec2(kSplitterWidth, top_panes_height));
|
|
if (ImGui::IsItemActive()) {
|
|
function_pane_width += io.MouseDelta.x;
|
|
function_pane_width = xe::clamp_float(function_pane_width, 30.0f, FLT_MAX);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginChild("##source_pane",
|
|
ImVec2(source_pane_width, top_panes_height), true);
|
|
DrawSourcePane();
|
|
ImGui::EndChild();
|
|
ImGui::SameLine();
|
|
ImGui::InvisibleButton("##vsplitter1",
|
|
ImVec2(kSplitterWidth, top_panes_height));
|
|
if (ImGui::IsItemActive()) {
|
|
source_pane_width += io.MouseDelta.x;
|
|
source_pane_width = xe::clamp_float(source_pane_width, 30.0f, FLT_MAX);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginChild("##registers_pane",
|
|
ImVec2(registers_pane_width, top_panes_height), true);
|
|
DrawRegistersPane();
|
|
ImGui::EndChild();
|
|
ImGui::SameLine();
|
|
ImGui::InvisibleButton("##vsplitter2",
|
|
ImVec2(kSplitterWidth, top_panes_height));
|
|
if (ImGui::IsItemActive()) {
|
|
registers_pane_width += io.MouseDelta.x;
|
|
registers_pane_width =
|
|
xe::clamp_float(registers_pane_width, 30.0f, FLT_MAX);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginChild("##right_pane", ImVec2(0, top_panes_height), true);
|
|
ImGui::BeginGroup();
|
|
ImGui::RadioButton("Threads", &state_.right_pane_tab,
|
|
ImState::kRightPaneThreads);
|
|
ImGui::SameLine();
|
|
ImGui::RadioButton("Memory", &state_.right_pane_tab,
|
|
ImState::kRightPaneMemory);
|
|
ImGui::EndGroup();
|
|
ImGui::Separator();
|
|
switch (state_.right_pane_tab) {
|
|
case ImState::kRightPaneThreads:
|
|
ImGui::BeginChild("##threads_pane");
|
|
DrawThreadsPane();
|
|
ImGui::EndChild();
|
|
break;
|
|
case ImState::kRightPaneMemory:
|
|
ImGui::BeginChild("##memory_pane");
|
|
DrawMemoryPane();
|
|
ImGui::EndChild();
|
|
break;
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::InvisibleButton("##hsplitter0", ImVec2(-1, kSplitterWidth));
|
|
if (ImGui::IsItemActive()) {
|
|
bottom_panes_height -= io.MouseDelta.y;
|
|
bottom_panes_height = xe::clamp_float(bottom_panes_height, 30.0f, FLT_MAX);
|
|
}
|
|
ImGui::BeginChild("##log_pane", ImVec2(log_pane_width, bottom_panes_height),
|
|
true);
|
|
DrawLogPane();
|
|
ImGui::EndChild();
|
|
ImGui::SameLine();
|
|
ImGui::InvisibleButton("##vsplitter3",
|
|
ImVec2(kSplitterWidth, bottom_panes_height));
|
|
if (ImGui::IsItemActive()) {
|
|
breakpoints_pane_width -= io.MouseDelta.x;
|
|
breakpoints_pane_width =
|
|
xe::clamp_float(breakpoints_pane_width, 30.0f, FLT_MAX);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginChild("##breakpoints_pane", ImVec2(0, 0), true);
|
|
DrawBreakpointsPane();
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
|
|
if (cvars::imgui_debug) {
|
|
ImGui::ShowMetricsWindow();
|
|
}
|
|
}
|
|
|
|
void DebugWindow::DrawToolbar() {
|
|
// TODO(benvanik): save/load database? other app options?
|
|
|
|
// Program control.
|
|
if (cache_.is_running) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.0f, 0.0f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
|
ImVec4(0.9f, 0.0f, 0.0f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
|
ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
|
|
if (ImGui::Button("Pause", ImVec2(80, 0))) {
|
|
processor_->Pause();
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Interrupt the program and break into the debugger.");
|
|
}
|
|
ImGui::PopStyleColor(3);
|
|
} else {
|
|
if (ImGui::Button("Continue", ImVec2(80, 0))) {
|
|
processor_->Continue();
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Resume the program from the current location.");
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
// Thread dropdown.
|
|
// Fast selection of the thread and reports the currently active thread for
|
|
// global operations.
|
|
// TODO(benvanik): use a popup + custom rendering to get richer view.
|
|
int current_thread_index = 0;
|
|
StringBuffer thread_combo;
|
|
int i = 0;
|
|
for (auto thread_info : cache_.thread_debug_infos) {
|
|
if (thread_info == state_.thread_info) {
|
|
current_thread_index = i;
|
|
}
|
|
|
|
// Threads can be briefly invalid once destroyed and before a cache update.
|
|
// This ensures we are accessing threads that are still valid.
|
|
switch (thread_info->state) {
|
|
case cpu::ThreadDebugInfo::State::kAlive:
|
|
case cpu::ThreadDebugInfo::State::kExited:
|
|
case cpu::ThreadDebugInfo::State::kWaiting:
|
|
if (!thread_info->thread_handle || thread_info->thread == nullptr) {
|
|
thread_combo.Append("(invalid)");
|
|
} else {
|
|
thread_combo.Append(thread_info->thread->thread_name());
|
|
}
|
|
break;
|
|
case cpu::ThreadDebugInfo::State::kZombie:
|
|
thread_combo.Append("(zombie)");
|
|
break;
|
|
default:
|
|
thread_combo.Append("(invalid)");
|
|
}
|
|
|
|
thread_combo.Append('\0');
|
|
++i;
|
|
}
|
|
if (ImGui::Combo("##thread_combo", ¤t_thread_index,
|
|
thread_combo.buffer(), 10)) {
|
|
// Thread changed.
|
|
SelectThreadStackFrame(cache_.thread_debug_infos[current_thread_index], 0,
|
|
true);
|
|
}
|
|
}
|
|
|
|
void DebugWindow::DrawFunctionsPane() {
|
|
ImGui::Text("<functions>");
|
|
// bar: analyze (?), goto current
|
|
// combo with module (+ emulator itself?)
|
|
// list with all functions known
|
|
// filter box + search button -> search dialog
|
|
}
|
|
|
|
void DebugWindow::DrawSourcePane() {
|
|
auto function = state_.function;
|
|
if (!function) {
|
|
ImGui::Text("(no function selected)");
|
|
return;
|
|
}
|
|
ImGui::BeginGroup();
|
|
// top bar:
|
|
// copy button
|
|
// address start - end
|
|
// name text box (editable)
|
|
// combo for interleaved + [ppc, hir, opt hir, x64 + byte with sizes]
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("%s", function->module()->name().c_str());
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
char address_str[9];
|
|
std::snprintf(address_str, xe::countof(address_str), "%.8X",
|
|
function->address());
|
|
ImGui::PushItemWidth(50);
|
|
ImGui::InputText("##address", address_str, xe::countof(address_str),
|
|
ImGuiInputTextFlags_AutoSelectAll);
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
ImGui::Text(" - %.8X", function->end_address());
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
char name[256];
|
|
std::strcpy(name, function->name().c_str());
|
|
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - 10);
|
|
if (ImGui::InputText("##name", name, sizeof(name),
|
|
ImGuiInputTextFlags_AutoSelectAll)) {
|
|
function->set_name(name);
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::BeginGroup();
|
|
ImGui::PushButtonRepeat(true);
|
|
bool can_step = !cache_.is_running && state_.thread_info;
|
|
if (ImGui::ButtonEx("Step PPC", ImVec2(0, 0),
|
|
can_step ? 0 : ImGuiItemFlags_Disabled)) {
|
|
// By enabling the button when stepping we allow repeat behavior.
|
|
if (processor_->execution_state() != cpu::ExecutionState::kStepping) {
|
|
processor_->StepGuestInstruction(state_.thread_info->thread_id);
|
|
}
|
|
}
|
|
ImGui::PopButtonRepeat();
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Step one PPC instruction on the current thread (hold for many).");
|
|
}
|
|
ImGui::SameLine();
|
|
if (state_.source_display_mode > 0) {
|
|
// Only show x64 step button if we have x64 visible.
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
ImGui::PushButtonRepeat(true);
|
|
if (ImGui::ButtonEx("Step x64", ImVec2(0, 0),
|
|
can_step ? 0 : ImGuiItemFlags_Disabled)) {
|
|
// By enabling the button when stepping we allow repeat behavior.
|
|
if (processor_->execution_state() != cpu::ExecutionState::kStepping) {
|
|
processor_->StepHostInstruction(state_.thread_info->thread_id);
|
|
}
|
|
}
|
|
ImGui::PopButtonRepeat();
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Step one x64 instruction on the current thread (hold for many).");
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::Dummy(ImVec2(16, 0));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Copy")) {
|
|
// TODO(benvanik): move clipboard abstraction into ui?
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
if (function->is_guest()) {
|
|
const char* kSourceDisplayModes[] = {
|
|
"PPC",
|
|
"PPC+HIR+x64",
|
|
"PPC+HIR (opt)+x64",
|
|
"PPC+x64",
|
|
};
|
|
ImGui::PushItemWidth(90);
|
|
ImGui::Combo("##display_mode", &state_.source_display_mode,
|
|
kSourceDisplayModes,
|
|
static_cast<int>(xe::countof(kSourceDisplayModes)));
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Text("(profile options?)");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Text("(hit count)");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Text("(code size?)");
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::Separator();
|
|
ImGui::BeginChild("##code");
|
|
if (function->is_guest()) {
|
|
DrawGuestFunctionSource();
|
|
} else {
|
|
// TODO(benvanik): load PDB and display source code?
|
|
ImGui::Text("(system function?)");
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void DebugWindow::DrawGuestFunctionSource() {
|
|
// source code:
|
|
// | 8200000 ppc_label:
|
|
// | [I] 8200000 FFCCDDEE ppc disam # comment
|
|
// | v0 = add v1, v2
|
|
// | A123456 xadd rax, rbx
|
|
// icon = active lines/etc (other threads too)
|
|
// color gutter background for trace data? (hit count, etc)
|
|
// show address + code bytes + ppc diasm with annotation comments
|
|
// labels get their own line with duped addresses
|
|
// show xrefs to labels?
|
|
// hir greyed and offset (background color change?)
|
|
// x64 greyed and offset with native address
|
|
// hover on registers/etc for tooltip/highlight others
|
|
// click register to go to location of last write
|
|
// click code address to jump to code
|
|
// click memory address to jump to memory browser
|
|
// if historical data for memory/etc present, show combo boxes
|
|
auto memory = emulator_->memory();
|
|
auto function = static_cast<cpu::GuestFunction*>(state_.function);
|
|
auto& source_map = function->source_map();
|
|
uint32_t source_map_index = 0;
|
|
|
|
bool draw_hir = false;
|
|
bool draw_hir_opt = false;
|
|
bool draw_x64 = false;
|
|
switch (state_.source_display_mode) {
|
|
case 1:
|
|
draw_hir = true;
|
|
draw_x64 = true;
|
|
break;
|
|
case 2:
|
|
draw_hir_opt = true;
|
|
draw_x64 = true;
|
|
break;
|
|
case 3:
|
|
draw_x64 = true;
|
|
break;
|
|
}
|
|
|
|
auto guest_pc =
|
|
state_.thread_info
|
|
? state_.thread_info->frames[state_.thread_stack_frame_index].guest_pc
|
|
: 0;
|
|
|
|
if (draw_hir) {
|
|
// TODO(benvanik): get HIR and draw preamble.
|
|
}
|
|
if (draw_hir_opt) {
|
|
// TODO(benvanik): get HIR and draw preamble.
|
|
}
|
|
if (draw_x64) {
|
|
// x64 preamble.
|
|
DrawMachineCodeSource(function->machine_code(), source_map[0].code_offset);
|
|
}
|
|
|
|
StringBuffer str;
|
|
for (uint32_t address = function->address();
|
|
address <= function->end_address(); address += 4) {
|
|
ImGui::PushID(address);
|
|
|
|
// TODO(benvanik): check other threads?
|
|
bool is_current_instr = address == guest_pc;
|
|
if (is_current_instr) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
|
|
if (!draw_x64) {
|
|
ScrollToSourceIfPcChanged();
|
|
}
|
|
}
|
|
|
|
bool has_guest_bp =
|
|
LookupBreakpointAtAddress(Breakpoint::AddressType::kGuest, address) !=
|
|
nullptr;
|
|
DrawBreakpointGutterButton(has_guest_bp, Breakpoint::AddressType::kGuest,
|
|
address);
|
|
ImGui::SameLine();
|
|
|
|
ImGui::Text(" %c ", is_current_instr ? '>' : ' ');
|
|
ImGui::SameLine();
|
|
|
|
uint32_t code =
|
|
xe::load_and_swap<uint32_t>(memory->TranslateVirtual(address));
|
|
cpu::ppc::DisasmPPC(address, code, &str);
|
|
ImGui::Text("%.8X %.8X %s", address, code, str.buffer());
|
|
str.Reset();
|
|
|
|
if (is_current_instr) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
while (source_map_index < source_map.size() &&
|
|
source_map[source_map_index].guest_address != address) {
|
|
++source_map_index;
|
|
}
|
|
if (source_map_index < source_map.size()) {
|
|
if (draw_hir) {
|
|
// TODO(benvanik): get HIR and draw for this PPC function.
|
|
}
|
|
if (draw_hir_opt) {
|
|
// TODO(benvanik): get HIR and draw for this PPC function.
|
|
}
|
|
if (draw_x64) {
|
|
const uint8_t* machine_code_start =
|
|
function->machine_code() + source_map[source_map_index].code_offset;
|
|
const size_t machine_code_length =
|
|
(source_map_index == source_map.size() - 1
|
|
? function->machine_code_length()
|
|
: source_map[source_map_index + 1].code_offset) -
|
|
source_map[source_map_index].code_offset;
|
|
DrawMachineCodeSource(machine_code_start, machine_code_length);
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
void DebugWindow::DrawMachineCodeSource(const uint8_t* machine_code_ptr,
|
|
size_t length) {
|
|
auto host_pc =
|
|
state_.thread_info
|
|
? state_.thread_info->frames[state_.thread_stack_frame_index].host_pc
|
|
: 0;
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.5f));
|
|
auto machine_code_start_ptr = machine_code_ptr;
|
|
size_t remaining_machine_code_size = length;
|
|
uint64_t host_address = uint64_t(machine_code_ptr);
|
|
cs_insn insn = {0};
|
|
while (remaining_machine_code_size &&
|
|
cs_disasm_iter(capstone_handle_, &machine_code_ptr,
|
|
&remaining_machine_code_size, &host_address, &insn)) {
|
|
ImGui::PushID(reinterpret_cast<void*>(insn.address));
|
|
|
|
bool is_current_instr = state_.thread_info && insn.address == host_pc;
|
|
if (is_current_instr) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
|
|
ScrollToSourceIfPcChanged();
|
|
}
|
|
|
|
bool has_host_bp = LookupBreakpointAtAddress(Breakpoint::AddressType::kHost,
|
|
insn.address) != nullptr;
|
|
DrawBreakpointGutterButton(has_host_bp, Breakpoint::AddressType::kHost,
|
|
insn.address);
|
|
ImGui::SameLine();
|
|
|
|
ImGui::Text(" %c ", is_current_instr ? '>' : ' ');
|
|
ImGui::SameLine();
|
|
ImGui::Text(" %.8X %-10s %s", uint32_t(insn.address), insn.mnemonic,
|
|
insn.op_str);
|
|
|
|
if (is_current_instr) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
void DebugWindow::DrawBreakpointGutterButton(
|
|
bool has_breakpoint, Breakpoint::AddressType address_type,
|
|
uint64_t address) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
has_breakpoint
|
|
? ImVec4(1.0f, 0.0f, 0.0f, 0.6f)
|
|
: ImGui::GetStyle().Colors[ImGuiCol_FrameBg]);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
|
!has_breakpoint
|
|
? ImVec4(1.0f, 0.0f, 0.0f, 0.8f)
|
|
: ImGui::GetStyle().Colors[ImGuiCol_FrameBg]);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
|
!has_breakpoint
|
|
? ImVec4(1.0f, 0.0f, 0.0f, 1.0f)
|
|
: ImGui::GetStyle().Colors[ImGuiCol_FrameBg]);
|
|
if (ImGui::Button(" ##toggle_line_bp")) {
|
|
if (!has_breakpoint) {
|
|
CreateCodeBreakpoint(address_type, address);
|
|
} else {
|
|
auto breakpoint = LookupBreakpointAtAddress(address_type, address);
|
|
assert_not_null(breakpoint);
|
|
DeleteCodeBreakpoint(breakpoint);
|
|
}
|
|
}
|
|
ImGui::PopStyleColor(3);
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(has_breakpoint ? "Remove breakpoint at this address."
|
|
: "Add a breakpoint at this address.");
|
|
}
|
|
}
|
|
|
|
void DebugWindow::ScrollToSourceIfPcChanged() {
|
|
if (state_.has_changed_pc) {
|
|
// TODO(benvanik): not so annoying scroll.
|
|
ImGui::SetScrollHereY(0.5f);
|
|
state_.has_changed_pc = false;
|
|
}
|
|
}
|
|
|
|
bool DebugWindow::DrawRegisterTextBox(int id, uint32_t* value) {
|
|
char buffer[256] = {0};
|
|
ImGuiInputTextFlags input_flags =
|
|
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank;
|
|
if (state_.register_input_hex) {
|
|
input_flags |= ImGuiInputTextFlags_CharsHexadecimal |
|
|
ImGuiInputTextFlags_AlwaysOverwrite |
|
|
ImGuiInputTextFlags_NoHorizontalScroll;
|
|
auto src_value = xe::string_util::to_hex_string(*value);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
} else {
|
|
input_flags |=
|
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll;
|
|
auto src_value = std::to_string(*value);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
}
|
|
char label[16] = {0};
|
|
std::snprintf(label, xe::countof(label), "##iregister%d", id);
|
|
bool any_changed = false;
|
|
ImGui::PushItemWidth(50);
|
|
if (ImGui::InputText(label, buffer,
|
|
state_.register_input_hex ? 9 : sizeof(buffer),
|
|
input_flags)) {
|
|
if (state_.register_input_hex) {
|
|
*value = string_util::from_string<uint32_t>(buffer, true);
|
|
} else {
|
|
*value = string_util::from_string<int32_t>(buffer);
|
|
}
|
|
any_changed = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
if (ImGui::IsItemHovered()) {
|
|
auto alt_value = state_.register_input_hex
|
|
? std::to_string(*value)
|
|
: string_util::to_hex_string(*value);
|
|
ImGui::SetTooltip("%s", alt_value.c_str());
|
|
}
|
|
return any_changed;
|
|
}
|
|
|
|
bool DebugWindow::DrawRegisterTextBox(int id, uint64_t* value) {
|
|
char buffer[256] = {0};
|
|
ImGuiInputTextFlags input_flags =
|
|
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank;
|
|
if (state_.register_input_hex) {
|
|
input_flags |= ImGuiInputTextFlags_CharsHexadecimal |
|
|
ImGuiInputTextFlags_AlwaysOverwrite |
|
|
ImGuiInputTextFlags_NoHorizontalScroll;
|
|
auto src_value = xe::string_util::to_hex_string(*value);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
} else {
|
|
input_flags |=
|
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll;
|
|
auto src_value = std::to_string(*value);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
}
|
|
char label[16] = {0};
|
|
std::snprintf(label, xe::countof(label), "##lregister%d", id);
|
|
bool any_changed = false;
|
|
ImGui::PushItemWidth(95);
|
|
if (ImGui::InputText(label, buffer,
|
|
state_.register_input_hex ? 17 : sizeof(buffer),
|
|
input_flags)) {
|
|
if (state_.register_input_hex) {
|
|
*value = string_util::from_string<uint64_t>(buffer, true);
|
|
} else {
|
|
*value = string_util::from_string<int64_t>(buffer);
|
|
}
|
|
any_changed = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
if (ImGui::IsItemHovered()) {
|
|
auto alt_value = state_.register_input_hex
|
|
? std::to_string(*value)
|
|
: string_util::to_hex_string(*value);
|
|
ImGui::SetTooltip("%s", alt_value.c_str());
|
|
}
|
|
return any_changed;
|
|
}
|
|
|
|
bool DebugWindow::DrawRegisterTextBox(int id, double* value) {
|
|
char buffer[256] = {0};
|
|
ImGuiInputTextFlags input_flags =
|
|
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank;
|
|
if (state_.register_input_hex) {
|
|
input_flags |= ImGuiInputTextFlags_CharsHexadecimal |
|
|
ImGuiInputTextFlags_AlwaysOverwrite |
|
|
ImGuiInputTextFlags_NoHorizontalScroll;
|
|
auto src_value = xe::string_util::to_hex_string(*value);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
} else {
|
|
input_flags |=
|
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll;
|
|
std::snprintf(buffer, xe::countof(buffer), "%.8F", *value);
|
|
}
|
|
char label[16] = {0};
|
|
std::snprintf(label, xe::countof(label), "##dregister%d", id);
|
|
bool any_changed = false;
|
|
ImGui::PushItemWidth(95);
|
|
if (ImGui::InputText(label, buffer,
|
|
state_.register_input_hex ? 17 : sizeof(buffer),
|
|
input_flags)) {
|
|
if (state_.register_input_hex) {
|
|
*value = string_util::from_string<double>(buffer, true);
|
|
} else {
|
|
*value = string_util::from_string<double>(buffer);
|
|
}
|
|
any_changed = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
if (ImGui::IsItemHovered()) {
|
|
auto alt_value = state_.register_input_hex
|
|
? std::to_string(*value)
|
|
: string_util::to_hex_string(*value);
|
|
ImGui::SetTooltip("%s", alt_value.c_str());
|
|
}
|
|
return any_changed;
|
|
}
|
|
|
|
bool DebugWindow::DrawRegisterTextBoxes(int id, float* value) {
|
|
char buffer[256] = {0};
|
|
ImGuiInputTextFlags input_flags =
|
|
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank;
|
|
if (state_.register_input_hex) {
|
|
input_flags |= ImGuiInputTextFlags_CharsHexadecimal |
|
|
ImGuiInputTextFlags_AlwaysOverwrite |
|
|
ImGuiInputTextFlags_NoHorizontalScroll;
|
|
} else {
|
|
input_flags |=
|
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll;
|
|
}
|
|
bool any_changed = false;
|
|
char label[16] = {0};
|
|
for (int i = 0; i < 4; ++i) {
|
|
if (state_.register_input_hex) {
|
|
auto src_value = xe::string_util::to_hex_string(value[i]);
|
|
std::strcpy(buffer, src_value.c_str());
|
|
} else {
|
|
std::snprintf(buffer, xe::countof(buffer), "%F", value[i]);
|
|
}
|
|
std::snprintf(label, xe::countof(label), "##vregister%d_%d", id, i);
|
|
ImGui::PushItemWidth(50);
|
|
if (ImGui::InputText(label, buffer,
|
|
state_.register_input_hex ? 9 : sizeof(buffer),
|
|
input_flags)) {
|
|
if (state_.register_input_hex) {
|
|
value[i] = string_util::from_string<float>(buffer, true);
|
|
} else {
|
|
value[i] = string_util::from_string<float>(buffer);
|
|
}
|
|
any_changed = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
if (ImGui::IsItemHovered()) {
|
|
auto alt_value = state_.register_input_hex
|
|
? std::to_string(value[i])
|
|
: string_util::to_hex_string(value[i]);
|
|
ImGui::SetTooltip("%s", alt_value.c_str());
|
|
}
|
|
if (i < 3) {
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(1, 0));
|
|
ImGui::SameLine();
|
|
}
|
|
}
|
|
return any_changed;
|
|
}
|
|
|
|
void DebugWindow::DrawRegistersPane() {
|
|
if (state_.register_group == RegisterGroup::kGuestGeneral) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
ImGui::Button("GPR");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
if (ImGui::Button("GPR")) {
|
|
state_.register_group = RegisterGroup::kGuestGeneral;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (state_.register_group == RegisterGroup::kGuestFloat) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
ImGui::Button("FPR");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
if (ImGui::Button("FPR")) {
|
|
state_.register_group = RegisterGroup::kGuestFloat;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (state_.register_group == RegisterGroup::kGuestVector) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
ImGui::Button("VMX");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
if (ImGui::Button("VMX")) {
|
|
state_.register_group = RegisterGroup::kGuestVector;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (state_.register_group == RegisterGroup::kHostGeneral) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
ImGui::Button("x64");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
if (ImGui::Button("x64")) {
|
|
state_.register_group = RegisterGroup::kHostGeneral;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (state_.register_group == RegisterGroup::kHostVector) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
ImGui::Button("XMM");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
if (ImGui::Button("XMM")) {
|
|
state_.register_group = RegisterGroup::kHostVector;
|
|
}
|
|
}
|
|
|
|
ImGui::Checkbox("Hex", &state_.register_input_hex);
|
|
|
|
if (!state_.thread_info) {
|
|
return;
|
|
}
|
|
auto thread_info = state_.thread_info;
|
|
|
|
bool dirty_guest_context = false;
|
|
bool dirty_host_context = false;
|
|
switch (state_.register_group) {
|
|
case RegisterGroup::kGuestGeneral: {
|
|
if (!thread_info->guest_context.physical_membase) {
|
|
return;
|
|
}
|
|
ImGui::BeginChild("##guest_general");
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text(" lr");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBox(100, &thread_info->guest_context.lr);
|
|
ImGui::EndGroup();
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("ctr");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBox(101, &thread_info->guest_context.ctr);
|
|
ImGui::EndGroup();
|
|
// CR
|
|
// XER
|
|
// FPSCR
|
|
// VSCR
|
|
for (int i = 0; i < 32; ++i) {
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text(i < 10 ? " r%d" : "r%d", i);
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBox(i, &thread_info->guest_context.r[i]);
|
|
ImGui::EndGroup();
|
|
}
|
|
ImGui::EndChild();
|
|
} break;
|
|
case RegisterGroup::kGuestFloat: {
|
|
if (!thread_info->guest_context.physical_membase) {
|
|
return;
|
|
}
|
|
ImGui::BeginChild("##guest_float");
|
|
for (int i = 0; i < 32; ++i) {
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text(i < 10 ? " f%d" : "f%d", i);
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBox(i, &thread_info->guest_context.f[i]);
|
|
ImGui::EndGroup();
|
|
}
|
|
ImGui::EndChild();
|
|
} break;
|
|
case RegisterGroup::kGuestVector: {
|
|
if (!thread_info->guest_context.physical_membase) {
|
|
return;
|
|
}
|
|
ImGui::BeginChild("##guest_vector");
|
|
for (int i = 0; i < 128; ++i) {
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text(i < 10 ? " v%d" : (i < 100 ? " v%d" : "v%d"), i);
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBoxes(i, thread_info->guest_context.v[i].f32);
|
|
ImGui::EndGroup();
|
|
}
|
|
ImGui::EndChild();
|
|
} break;
|
|
case RegisterGroup::kHostGeneral: {
|
|
ImGui::BeginChild("##host_general");
|
|
for (int i = 0; i < 18; ++i) {
|
|
auto reg = static_cast<X64Register>(i);
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("%3s", HostThreadContext::GetRegisterName(reg));
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
if (reg == X64Register::kRip) {
|
|
dirty_guest_context |=
|
|
DrawRegisterTextBox(i, &thread_info->host_context.rip);
|
|
} else if (reg == X64Register::kEflags) {
|
|
dirty_guest_context =
|
|
DrawRegisterTextBox(i, &thread_info->host_context.eflags);
|
|
} else {
|
|
dirty_guest_context |= DrawRegisterTextBox(
|
|
i, &thread_info->host_context.int_registers[i - 2]);
|
|
}
|
|
ImGui::EndGroup();
|
|
}
|
|
ImGui::EndChild();
|
|
} break;
|
|
case RegisterGroup::kHostVector: {
|
|
ImGui::BeginChild("##host_vector");
|
|
for (int i = 0; i < 16; ++i) {
|
|
auto reg =
|
|
static_cast<X64Register>(static_cast<int>(X64Register::kXmm0) + i);
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("%5s", HostThreadContext::GetRegisterName(reg));
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
dirty_host_context |= DrawRegisterTextBoxes(
|
|
i, thread_info->host_context.xmm_registers[i].f32);
|
|
ImGui::EndGroup();
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
}
|
|
|
|
if (dirty_guest_context) {
|
|
// TODO(benvanik): write context to thread.
|
|
// NOTE: this will only work if context promotion is disabled!
|
|
}
|
|
if (dirty_host_context) {
|
|
// TODO(benvanik): write context to thread.
|
|
}
|
|
}
|
|
|
|
void DebugWindow::DrawThreadsPane() {
|
|
ImGui::BeginGroup();
|
|
// checkbox to show host threads
|
|
// expand all toggle
|
|
ImGui::EndGroup();
|
|
ImGui::BeginChild("##threads_listing");
|
|
for (size_t i = 0; i < cache_.thread_debug_infos.size(); ++i) {
|
|
auto thread_info = cache_.thread_debug_infos[i];
|
|
bool is_current_thread = thread_info == state_.thread_info;
|
|
auto thread =
|
|
emulator_->kernel_state()->GetThreadByID(thread_info->thread_id);
|
|
if (!thread) {
|
|
// TODO(benvanik): better display of zombie thread states.
|
|
continue;
|
|
}
|
|
if (is_current_thread && state_.has_changed_thread) {
|
|
ImGui::SetScrollHereY(0.5f);
|
|
state_.has_changed_thread = false;
|
|
}
|
|
if (!is_current_thread) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 0.6f));
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Header,
|
|
ImGui::GetStyle().Colors[ImGuiCol_HeaderActive]);
|
|
}
|
|
ImGui::PushID(thread_info);
|
|
if (is_current_thread) {
|
|
ImGui::SetNextItemOpen(true, ImGuiCond_Always);
|
|
}
|
|
const char* state_label = "?";
|
|
if (thread->can_debugger_suspend()) {
|
|
if (thread->is_running()) {
|
|
if (thread->suspend_count() > 1) {
|
|
state_label = "SUSPEND";
|
|
} else {
|
|
state_label = "RUNNING";
|
|
}
|
|
} else {
|
|
state_label = "ZOMBIE";
|
|
}
|
|
}
|
|
char thread_label[256];
|
|
std::snprintf(thread_label, xe::countof(thread_label),
|
|
"%-5s %-7s id=%.4X hnd=%.4X %s",
|
|
thread->is_guest_thread() ? "guest" : "host", state_label,
|
|
thread->thread_id(), thread->handle(),
|
|
thread->name().c_str());
|
|
if (ImGui::CollapsingHeader(
|
|
thread_label,
|
|
is_current_thread ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
|
|
// | (log button) detail of kernel call categories
|
|
// log button toggles only logging that thread
|
|
ImGui::BulletText("Call Stack");
|
|
ImGui::Indent();
|
|
for (size_t j = 0; j < thread_info->frames.size(); ++j) {
|
|
bool is_current_frame =
|
|
is_current_thread && j == state_.thread_stack_frame_index;
|
|
auto& frame = thread_info->frames[j];
|
|
if (is_current_thread && !frame.guest_pc) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.6f));
|
|
}
|
|
char host_label[64];
|
|
std::snprintf(host_label, xe::countof(host_label), "%016" PRIX64 "##%p",
|
|
frame.host_pc, &frame);
|
|
if (ImGui::Selectable(host_label, is_current_frame,
|
|
ImGuiSelectableFlags_SpanAllColumns)) {
|
|
SelectThreadStackFrame(thread_info, j, true);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(8, 0));
|
|
ImGui::SameLine();
|
|
if (frame.guest_pc) {
|
|
ImGui::Text("%08X", frame.guest_pc);
|
|
} else {
|
|
ImGui::Text(" ");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(8, 0));
|
|
ImGui::SameLine();
|
|
// breakpoints set? or something?
|
|
ImGui::Text(" ");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(8, 0));
|
|
ImGui::SameLine();
|
|
if (frame.guest_function) {
|
|
ImGui::Text("%s", frame.guest_function->name().c_str());
|
|
} else {
|
|
ImGui::Text("%s", frame.name);
|
|
}
|
|
if (is_current_thread && !frame.guest_pc) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
ImGui::Unindent();
|
|
}
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void DebugWindow::DrawMemoryPane() {
|
|
ImGui::Text("<memory>");
|
|
// tools for searching:
|
|
// search bytes | text | pattern
|
|
// https://github.com/ocornut/imgui/wiki/memory_editor_example
|
|
}
|
|
|
|
void DebugWindow::DrawBreakpointsPane() {
|
|
auto& state = state_.breakpoints;
|
|
|
|
ImGui::BeginChild("##toolbar", ImVec2(-1, 20));
|
|
bool all_breakpoints_enabled = true;
|
|
for (auto& breakpoint : state.all_breakpoints) {
|
|
if (!breakpoint->is_enabled()) {
|
|
all_breakpoints_enabled = false;
|
|
break;
|
|
}
|
|
}
|
|
if (ImGui::Checkbox("##toggle", &all_breakpoints_enabled)) {
|
|
// Toggle all breakpoints on/off.
|
|
// If any are not enabled we will enable all, otherwise disable all.
|
|
for (auto& breakpoint : state.all_breakpoints) {
|
|
breakpoint->set_enabled(all_breakpoints_enabled);
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Toggle all breakpoints.");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(8, 0));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+ Code")) {
|
|
ImGui::OpenPopup("##add_code_breakpoint");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Add a code breakpoint for either PPC or x64.");
|
|
}
|
|
// TODO(benvanik): remove this set focus workaround when imgui is fixed:
|
|
// https://github.com/ocornut/imgui/issues/343
|
|
static int add_code_popup_render_count = 0;
|
|
if (ImGui::BeginPopup("##add_code_breakpoint")) {
|
|
++add_code_popup_render_count;
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("PPC");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(2, 0));
|
|
ImGui::SameLine();
|
|
if (add_code_popup_render_count == 2) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
}
|
|
static char ppc_buffer[32] = {0};
|
|
ImGuiInputTextFlags input_flags = ImGuiInputTextFlags_CharsUppercase |
|
|
ImGuiInputTextFlags_CharsNoBlank |
|
|
ImGuiInputTextFlags_CharsHexadecimal |
|
|
ImGuiInputTextFlags_AlwaysOverwrite |
|
|
ImGuiInputTextFlags_NoHorizontalScroll |
|
|
ImGuiInputTextFlags_EnterReturnsTrue;
|
|
ImGui::PushItemWidth(50);
|
|
if (ImGui::InputText("##guest_address", ppc_buffer, 9, input_flags)) {
|
|
uint32_t address = string_util::from_string<uint32_t>(ppc_buffer, true);
|
|
ppc_buffer[0] = 0;
|
|
CreateCodeBreakpoint(Breakpoint::AddressType::kGuest, address);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::Dummy(ImVec2(0, 2));
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::Text("x64");
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(2, 0));
|
|
ImGui::SameLine();
|
|
static char x64_buffer[64] = {0};
|
|
ImGui::PushItemWidth(100);
|
|
if (ImGui::InputText("##host_address", x64_buffer, 17, input_flags)) {
|
|
uint64_t address = string_util::from_string<uint64_t>(x64_buffer, true);
|
|
x64_buffer[0] = 0;
|
|
CreateCodeBreakpoint(Breakpoint::AddressType::kHost, address);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::EndPopup();
|
|
} else {
|
|
add_code_popup_render_count = 0;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(2, 0));
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("+ Data")) {
|
|
ImGui::OpenPopup("##add_data_breakpoint");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Add a memory data breakpoint.");
|
|
}
|
|
if (ImGui::BeginPopup("##add_data_breakpoint")) {
|
|
ImGui::Text("TODO: data stuff");
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(2, 0));
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("+ Kernel")) {
|
|
ImGui::OpenPopup("##add_kernel_breakpoint");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Select active kernel breakpoints.");
|
|
}
|
|
// TODO(benvanik): remove this set focus workaround when imgui is fixed:
|
|
// https://github.com/ocornut/imgui/issues/343
|
|
static int kernel_popup_render_count = 0;
|
|
if (ImGui::BeginPopup("##add_kernel_breakpoint")) {
|
|
++kernel_popup_render_count;
|
|
|
|
auto export_tables = emulator_->export_resolver()->tables();
|
|
|
|
ImGui::PushItemWidth(300);
|
|
// TODO(benvanik): tag filtering.
|
|
const char* its[] = {
|
|
"All",
|
|
};
|
|
int ci = 0;
|
|
ImGui::Combo("##kernel_categories", &ci, its, 1, 1);
|
|
ImGui::Dummy(ImVec2(0, 3));
|
|
ImGui::BeginListBox("##kernel_calls", ImVec2(1000, 15));
|
|
auto& all_exports = emulator_->export_resolver()->all_exports_by_name();
|
|
auto call_rankings = xe::fuzzy_filter(state.kernel_call_filter, all_exports,
|
|
offsetof(cpu::Export, name));
|
|
bool has_any_call_filter = std::strlen(state.kernel_call_filter) > 0;
|
|
if (has_any_call_filter) {
|
|
std::sort(call_rankings.begin(), call_rankings.end(),
|
|
[](std::pair<size_t, int>& a, std::pair<size_t, int>& b) {
|
|
if (a.second == b.second) {
|
|
return a.first > b.first;
|
|
} else {
|
|
return a.second > b.second;
|
|
}
|
|
});
|
|
}
|
|
for (size_t i = 0; i < call_rankings.size(); ++i) {
|
|
if (has_any_call_filter && !call_rankings[i].second) {
|
|
continue;
|
|
}
|
|
auto export_entry = all_exports[call_rankings[i].first];
|
|
if (export_entry->get_type() != cpu::Export::Type::kFunction ||
|
|
!export_entry->is_implemented()) {
|
|
continue;
|
|
}
|
|
// TODO(benvanik): skip unused kernel calls.
|
|
ImGui::PushID(export_entry);
|
|
// TODO(benvanik): selection, hover info (module name, ordinal, etc).
|
|
bool is_pre_enabled = false;
|
|
bool is_post_enabled = false;
|
|
if (ImGui::Checkbox("##pre", &is_pre_enabled)) {
|
|
// TODO(benvanik): add pre breakpoint (lookup thunk, add before
|
|
// syscall).
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Break immediately before this export is called.");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(1, 0));
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("##post", &is_post_enabled)) {
|
|
// TODO(benvanik): add pre breakpoint (lookup thunk, add after
|
|
// syscall).
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Break immediately after this export returns.");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Text("%s", export_entry->name);
|
|
ImGui::Dummy(ImVec2(0, 1));
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndListBox();
|
|
ImGui::Dummy(ImVec2(0, 3));
|
|
if (kernel_popup_render_count == 2) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
}
|
|
ImGui::InputText(
|
|
"##kernel_call_filter", state.kernel_call_filter,
|
|
xe::countof(state.kernel_call_filter),
|
|
ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_CharsNoBlank);
|
|
ImGui::PopItemWidth();
|
|
ImGui::EndPopup();
|
|
} else {
|
|
kernel_popup_render_count = 0;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(2, 0));
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("+ GPU")) {
|
|
ImGui::OpenPopup("##add_gpu_breakpoint");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Select active GPU breakpoints.");
|
|
}
|
|
if (ImGui::BeginPopup("##add_gpu_breakpoint")) {
|
|
ImGui::Text("TODO: swap, kick, etc");
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(8, 0));
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Clear")) {
|
|
// Unregister and delete all breakpoints.
|
|
while (!state.all_breakpoints.empty()) {
|
|
DeleteCodeBreakpoint(state.all_breakpoints.front().get());
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Clear all breakpoints.");
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::Separator();
|
|
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::BeginListBox("##empty",
|
|
ImVec2(-1, ImGui::GetContentRegionAvail().y))) {
|
|
std::vector<Breakpoint*> to_delete;
|
|
for (auto& breakpoint : state.all_breakpoints) {
|
|
ImGui::PushID(breakpoint.get());
|
|
bool is_enabled = breakpoint->is_enabled();
|
|
if (ImGui::Checkbox("##toggle", &is_enabled)) {
|
|
breakpoint->set_enabled(is_enabled);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(4, 0));
|
|
ImGui::SameLine();
|
|
auto breakpoint_str = breakpoint->to_string();
|
|
bool is_selected = false; // in function/stopped on line?
|
|
if (ImGui::Selectable(breakpoint_str.c_str(), &is_selected,
|
|
ImGuiSelectableFlags_SpanAllColumns)) {
|
|
auto function = breakpoint->guest_function();
|
|
assert_not_null(function);
|
|
if (breakpoint->address_type() == Breakpoint::AddressType::kGuest) {
|
|
NavigateToFunction(breakpoint->guest_function(),
|
|
breakpoint->guest_address(),
|
|
function->MapGuestAddressToMachineCode(
|
|
breakpoint->guest_address()));
|
|
} else {
|
|
NavigateToFunction(function,
|
|
function->MapMachineCodeToGuestAddress(
|
|
breakpoint->host_address()),
|
|
breakpoint->host_address());
|
|
}
|
|
}
|
|
if (ImGui::BeginPopupContextItem("##breakpoint_context_menu")) {
|
|
if (ImGui::MenuItem("Delete")) {
|
|
to_delete.push_back(breakpoint.get());
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndListBox();
|
|
if (!to_delete.empty()) {
|
|
for (auto breakpoint : to_delete) {
|
|
DeleteCodeBreakpoint(breakpoint);
|
|
}
|
|
}
|
|
}
|
|
ImGui::PopItemWidth();
|
|
}
|
|
|
|
void DebugWindow::DrawLogPane() {
|
|
ImGui::Text("<log>");
|
|
// bar:
|
|
// combo for log level
|
|
// check combo for areas (cpu/gpu/etc)
|
|
// combo for thread checkboxes
|
|
// filter text box
|
|
// ...
|
|
// copy visible button
|
|
// follow button
|
|
// clear button
|
|
// visible lines / total lines
|
|
// log box:
|
|
// line per log line
|
|
// if big, click to open dialog with contents
|
|
}
|
|
|
|
void DebugWindow::SelectThreadStackFrame(cpu::ThreadDebugInfo* thread_info,
|
|
size_t stack_frame_index,
|
|
bool always_navigate) {
|
|
state_.has_changed_thread = false;
|
|
if (thread_info != state_.thread_info) {
|
|
state_.has_changed_thread = true;
|
|
state_.thread_info = thread_info;
|
|
}
|
|
if (state_.thread_info) {
|
|
stack_frame_index =
|
|
std::min(state_.thread_info->frames.size() - 1, stack_frame_index);
|
|
}
|
|
if (stack_frame_index != state_.thread_stack_frame_index) {
|
|
state_.thread_stack_frame_index = stack_frame_index;
|
|
state_.has_changed_thread = true;
|
|
}
|
|
if (state_.thread_info && state_.thread_info->frames.empty()) {
|
|
return;
|
|
}
|
|
if (state_.thread_info) {
|
|
auto new_host_pc =
|
|
state_.thread_info->frames[state_.thread_stack_frame_index].host_pc;
|
|
if (new_host_pc != state_.last_host_pc) {
|
|
state_.last_host_pc = new_host_pc;
|
|
state_.has_changed_pc = true;
|
|
}
|
|
}
|
|
if ((always_navigate || state_.has_changed_thread) && state_.thread_info) {
|
|
auto& frame = state_.thread_info->frames[state_.thread_stack_frame_index];
|
|
if (frame.guest_function) {
|
|
NavigateToFunction(frame.guest_function, frame.guest_pc, frame.host_pc);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DebugWindow::NavigateToFunction(cpu::Function* function, uint32_t guest_pc,
|
|
uint64_t host_pc) {
|
|
state_.function = function;
|
|
state_.last_host_pc = host_pc;
|
|
state_.has_changed_pc = true;
|
|
}
|
|
|
|
void DebugWindow::UpdateCache() {
|
|
auto kernel_state = emulator_->kernel_state();
|
|
auto object_table = kernel_state->object_table();
|
|
|
|
app_context_.CallInUIThread([this]() {
|
|
std::string title = std::string(kBaseTitle);
|
|
switch (processor_->execution_state()) {
|
|
case cpu::ExecutionState::kEnded:
|
|
title += " (ended)";
|
|
break;
|
|
case cpu::ExecutionState::kPaused:
|
|
title += " (paused)";
|
|
break;
|
|
case cpu::ExecutionState::kRunning:
|
|
title += " (running)";
|
|
break;
|
|
case cpu::ExecutionState::kStepping:
|
|
title += " (stepping)";
|
|
break;
|
|
}
|
|
window_->SetTitle(title);
|
|
});
|
|
|
|
cache_.is_running =
|
|
processor_->execution_state() == cpu::ExecutionState::kRunning;
|
|
if (cache_.is_running) {
|
|
// Early exit - the rest of the data is kept stale on purpose.
|
|
return;
|
|
}
|
|
|
|
// Fetch module listing.
|
|
// We hold refs so that none are unloaded.
|
|
cache_.modules =
|
|
object_table->GetObjectsByType<XModule>(XObject::Type::Module);
|
|
|
|
cache_.thread_debug_infos = processor_->QueryThreadDebugInfos();
|
|
|
|
SelectThreadStackFrame(state_.thread_info, state_.thread_stack_frame_index,
|
|
false);
|
|
}
|
|
|
|
void DebugWindow::CreateCodeBreakpoint(Breakpoint::AddressType address_type,
|
|
uint64_t address) {
|
|
auto& state = state_.breakpoints;
|
|
auto breakpoint = std::make_unique<Breakpoint>(
|
|
processor_, address_type, address,
|
|
[this](Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info,
|
|
uint64_t host_address) {
|
|
OnBreakpointHit(breakpoint, thread_info);
|
|
});
|
|
if (breakpoint->address_type() == Breakpoint::AddressType::kGuest) {
|
|
auto& map = state.code_breakpoints_by_guest_address;
|
|
auto it = map.find(breakpoint->guest_address());
|
|
if (it != map.end()) {
|
|
// Already exists!
|
|
return;
|
|
}
|
|
map.emplace(breakpoint->guest_address(), breakpoint.get());
|
|
} else {
|
|
auto& map = state.code_breakpoints_by_host_address;
|
|
auto it = map.find(breakpoint->host_address());
|
|
if (it != map.end()) {
|
|
// Already exists!
|
|
return;
|
|
}
|
|
map.emplace(breakpoint->host_address(), breakpoint.get());
|
|
}
|
|
processor_->AddBreakpoint(breakpoint.get());
|
|
state.all_breakpoints.emplace_back(std::move(breakpoint));
|
|
}
|
|
|
|
void DebugWindow::DeleteCodeBreakpoint(Breakpoint* breakpoint) {
|
|
auto& state = state_.breakpoints;
|
|
for (size_t i = 0; i < state.all_breakpoints.size(); ++i) {
|
|
if (state.all_breakpoints[i].get() != breakpoint) {
|
|
continue;
|
|
}
|
|
processor_->RemoveBreakpoint(breakpoint);
|
|
if (breakpoint->address_type() == Breakpoint::AddressType::kGuest) {
|
|
auto& map = state.code_breakpoints_by_guest_address;
|
|
auto it = map.find(breakpoint->guest_address());
|
|
if (it != map.end()) {
|
|
map.erase(it);
|
|
}
|
|
} else {
|
|
auto& map = state.code_breakpoints_by_host_address;
|
|
auto it = map.find(breakpoint->host_address());
|
|
if (it != map.end()) {
|
|
map.erase(it);
|
|
}
|
|
}
|
|
state.all_breakpoints.erase(state.all_breakpoints.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
Breakpoint* DebugWindow::LookupBreakpointAtAddress(
|
|
Breakpoint::AddressType address_type, uint64_t address) {
|
|
auto& state = state_.breakpoints;
|
|
if (address_type == Breakpoint::AddressType::kGuest) {
|
|
auto& map = state.code_breakpoints_by_guest_address;
|
|
auto it = map.find(static_cast<uint32_t>(address));
|
|
return it == map.end() ? nullptr : it->second;
|
|
} else {
|
|
auto& map = state.code_breakpoints_by_host_address;
|
|
auto it = map.find(static_cast<uintptr_t>(address));
|
|
return it == map.end() ? nullptr : it->second;
|
|
}
|
|
}
|
|
|
|
void DebugWindow::OnFocus() { Focus(); }
|
|
|
|
void DebugWindow::OnDetached() {
|
|
UpdateCache();
|
|
|
|
// Remove all breakpoints.
|
|
while (!state_.breakpoints.all_breakpoints.empty()) {
|
|
DeleteCodeBreakpoint(state_.breakpoints.all_breakpoints.front().get());
|
|
}
|
|
}
|
|
|
|
void DebugWindow::OnExecutionPaused() {
|
|
UpdateCache();
|
|
Focus();
|
|
}
|
|
|
|
void DebugWindow::OnExecutionContinued() {
|
|
UpdateCache();
|
|
Focus();
|
|
}
|
|
|
|
void DebugWindow::OnExecutionEnded() {
|
|
UpdateCache();
|
|
Focus();
|
|
}
|
|
|
|
void DebugWindow::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) {
|
|
UpdateCache();
|
|
SelectThreadStackFrame(thread_info, 0, true);
|
|
Focus();
|
|
}
|
|
|
|
void DebugWindow::OnBreakpointHit(Breakpoint* breakpoint,
|
|
cpu::ThreadDebugInfo* thread_info) {
|
|
UpdateCache();
|
|
SelectThreadStackFrame(thread_info, 0, true);
|
|
Focus();
|
|
}
|
|
|
|
void DebugWindow::Focus() const {
|
|
app_context_.CallInUIThread([this]() { window_->Focus(); });
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace debug
|
|
} // namespace xe
|