From cf2e617f08df118494dde8845e00b0fe4efc5134 Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Sun, 13 Oct 2024 09:02:22 -0300 Subject: [PATCH] Devtools - Inspect regs/User data/Shader disassembly (#1358) * devtools: pm4 - show markers * SaveDataDialogLib: fix compile with mingw * devtools: pm4 - show program state * devtools: pm4 - show program disassembly * devtools: pm4 - show frame regs * devtools: pm4 - show color buffer info as popup add ux improvements for open new windows with shift+click better window titles * imgui: skip all textures to avoid hanging with crash diagnostic enabled not sure why this happens :c * devtools: pm4 - show reg depth buffer --- CMakeLists.txt | 10 + src/core/debug_state.cpp | 65 +- src/core/debug_state.h | 46 +- src/core/devtools/layer.cpp | 111 +- src/core/devtools/options.cpp | 24 + src/core/devtools/options.h | 21 + src/core/devtools/widget/cmd_list.cpp | 558 ++-- src/core/devtools/widget/cmd_list.h | 69 +- src/core/devtools/widget/common.h | 100 + src/core/devtools/widget/frame_dump.cpp | 87 +- src/core/devtools/widget/frame_dump.h | 6 +- src/core/devtools/widget/reg_popup.cpp | 182 ++ src/core/devtools/widget/reg_popup.h | 38 + src/core/devtools/widget/reg_view.cpp | 305 +++ src/core/devtools/widget/reg_view.h | 50 + src/core/devtools/widget/text_editor.cpp | 2334 +++++++++++++++++ src/core/devtools/widget/text_editor.h | 408 +++ src/core/libraries/dialogs/error_dialog.cpp | 2 +- src/core/libraries/gnmdriver/gnmdriver.cpp | 31 +- src/core/libraries/gnmdriver/gnmdriver.h | 2 +- .../save_data/dialog/savedatadialog_ui.cpp | 4 +- src/core/libraries/system/msgdialog_ui.cpp | 2 +- src/imgui/imgui_std.h | 7 +- src/imgui/renderer/texture_manager.cpp | 6 + src/video_core/amdgpu/liverpool.cpp | 27 + src/video_core/amdgpu/liverpool.h | 6 +- src/video_core/amdgpu/pm4_cmds.h | 1 + 27 files changed, 4215 insertions(+), 287 deletions(-) create mode 100644 src/core/devtools/options.cpp create mode 100644 src/core/devtools/options.h create mode 100644 src/core/devtools/widget/common.h create mode 100644 src/core/devtools/widget/reg_popup.cpp create mode 100644 src/core/devtools/widget/reg_popup.h create mode 100644 src/core/devtools/widget/reg_view.cpp create mode 100644 src/core/devtools/widget/reg_view.h create mode 100644 src/core/devtools/widget/text_editor.cpp create mode 100644 src/core/devtools/widget/text_editor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 37021746..781e93e1 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -354,15 +354,25 @@ set(MISC_LIBS src/core/libraries/screenshot/screenshot.cpp set(DEV_TOOLS src/core/devtools/layer.cpp src/core/devtools/layer.h + src/core/devtools/options.cpp + src/core/devtools/options.h src/core/devtools/gcn/gcn_context_regs.cpp src/core/devtools/gcn/gcn_op_names.cpp src/core/devtools/gcn/gcn_shader_regs.cpp src/core/devtools/widget/cmd_list.cpp src/core/devtools/widget/cmd_list.h + src/core/devtools/widget/common.h src/core/devtools/widget/frame_dump.cpp src/core/devtools/widget/frame_dump.h src/core/devtools/widget/frame_graph.cpp src/core/devtools/widget/frame_graph.h + src/core/devtools/widget/imgui_memory_editor.h + src/core/devtools/widget/reg_popup.cpp + src/core/devtools/widget/reg_popup.h + src/core/devtools/widget/reg_view.cpp + src/core/devtools/widget/reg_view.h + src/core/devtools/widget/text_editor.cpp + src/core/devtools/widget/text_editor.h ) set(COMMON src/common/logging/backend.cpp diff --git a/src/core/debug_state.cpp b/src/core/debug_state.cpp index 050143e6..93b00285 100644 --- a/src/core/debug_state.cpp +++ b/src/core/debug_state.cpp @@ -1,13 +1,16 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "common/assert.h" #include "common/native_clock.h" #include "common/singleton.h" #include "debug_state.h" -#include "libraries/kernel/event_queues.h" +#include "devtools/widget/common.h" #include "libraries/kernel/time_management.h" #include "libraries/system/msgdialog.h" +#include "video_core/amdgpu/pm4_cmds.h" using namespace DebugStateType; @@ -95,8 +98,68 @@ void DebugStateImpl::ResumeGuestThreads() { } void DebugStateImpl::RequestFrameDump(s32 count) { + ASSERT(!DumpingCurrentFrame()); gnm_frame_dump_request_count = count; frame_dump_list.clear(); frame_dump_list.resize(count); waiting_submit_pause = true; } + +void DebugStateImpl::PushQueueDump(QueueDump dump) { + ASSERT(DumpingCurrentFrame()); + std::unique_lock lock{frame_dump_list_mutex}; + auto& frame = GetFrameDump(); + { // Find draw calls + auto data = std::span{dump.data}; + auto initial_data = data.data(); + while (!data.empty()) { + const auto* header = reinterpret_cast(data.data()); + const auto type = header->type; + if (type == 2) { + data = data.subspan(1); + } else if (type != 3) { + UNREACHABLE(); + } + const AmdGpu::PM4ItOpcode opcode = header->opcode; + if (Core::Devtools::Widget::IsDrawCall(opcode)) { + const auto offset = + reinterpret_cast(header) - reinterpret_cast(initial_data); + const auto addr = dump.base_addr + offset; + waiting_reg_dumps.emplace(addr, &frame); + waiting_reg_dumps_dbg.emplace( + addr, + fmt::format("#{} h({}) queue {} {} {}", + frame_dump_list.size() - gnm_frame_dump_request_count, addr, + magic_enum::enum_name(dump.type), dump.submit_num, dump.num2)); + } + data = data.subspan(header->NumWords() + 1); + } + } + frame.queues.push_back(std::move(dump)); +} + +void DebugStateImpl::PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, + const AmdGpu::Liverpool::Regs& regs) { + std::scoped_lock lock{frame_dump_list_mutex}; + const auto it = waiting_reg_dumps.find(header_addr); + if (it == waiting_reg_dumps.end()) { + return; + } + auto& frame = *it->second; + waiting_reg_dumps.erase(it); + waiting_reg_dumps_dbg.erase(waiting_reg_dumps_dbg.find(header_addr)); + auto& dump = frame.regs[header_addr - base_addr]; + dump.regs = regs; + for (int i = 0; i < RegDump::MaxShaderStages; i++) { + if (regs.stage_enable.IsStageEnabled(i)) { + auto stage = regs.ProgramForStage(i); + if (stage->address_lo != 0) { + auto code = stage->Code(); + dump.stages[i] = ShaderDump{ + .user_data = *stage, + .code = std::vector{code.begin(), code.end()}, + }; + } + } + } +} diff --git a/src/core/debug_state.h b/src/core/debug_state.h index 00c687fa..26dfa202 100644 --- a/src/core/debug_state.h +++ b/src/core/debug_state.h @@ -5,10 +5,14 @@ #include #include +#include +#include #include #include #include "common/types.h" +#include "video_core/amdgpu/liverpool.h" +#include "video_core/renderer_vulkan/vk_pipeline_cache.h" #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -42,10 +46,23 @@ struct QueueDump { u32 submit_num; u32 num2; // acb: queue_num; else: buffer_in_submit std::vector data; + uintptr_t base_addr; +}; + +struct ShaderDump { + Vulkan::Liverpool::ShaderProgram user_data{}; + std::vector code{}; +}; + +struct RegDump { + static constexpr size_t MaxShaderStages = 5; + Vulkan::Liverpool::Regs regs{}; + std::array stages{}; }; struct FrameDump { std::vector queues; + std::unordered_map regs; // address -> reg dump }; class DebugStateImpl { @@ -61,15 +78,24 @@ class DebugStateImpl { std::atomic_int32_t gnm_frame_count = 0; s32 gnm_frame_dump_request_count = -1; + std::unordered_map waiting_reg_dumps; + std::unordered_map waiting_reg_dumps_dbg; bool waiting_submit_pause = false; bool should_show_frame_dump = false; - std::mutex frame_dump_list_mutex; + std::shared_mutex frame_dump_list_mutex; std::vector frame_dump_list{}; std::queue debug_message_popup; public: + void ShowDebugMessage(std::string message) { + if (message.empty()) { + return; + } + debug_message_popup.push(std::move(message)); + } + void AddCurrentThreadToGuestList(); void RemoveCurrentThreadFromGuestList(); @@ -99,6 +125,11 @@ public: return gnm_frame_dump_request_count > 0; } + bool DumpingCurrentReg() { + std::shared_lock lock{frame_dump_list_mutex}; + return !waiting_reg_dumps.empty(); + } + bool ShouldPauseInSubmit() const { return waiting_submit_pause && gnm_frame_dump_request_count == 0; } @@ -109,17 +140,10 @@ public: return frame_dump_list[frame_dump_list.size() - gnm_frame_dump_request_count]; } - void PushQueueDump(QueueDump dump) { - std::unique_lock lock{frame_dump_list_mutex}; - GetFrameDump().queues.push_back(std::move(dump)); - } + void PushQueueDump(QueueDump dump); - void ShowDebugMessage(std::string message) { - if (message.empty()) { - return; - } - debug_message_popup.push(std::move(message)); - } + void PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, + const AmdGpu::Liverpool::Regs& regs); }; } // namespace DebugStateType diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 0c7e85e4..17ef43bb 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -10,6 +10,7 @@ #include "imgui/imgui_std.h" #include "imgui_internal.h" #include "layer.h" +#include "options.h" #include "widget/frame_dump.h" #include "widget/frame_graph.h" @@ -18,10 +19,9 @@ using namespace Core::Devtools; using L = Core::Devtools::Layer; static bool show_simple_fps = false; + static float fps_scale = 1.0f; - static bool show_advanced_debug = false; - static int dump_frame_count = 1; static Widget::FrameGraph frame_graph; @@ -29,12 +29,16 @@ static std::vector frame_viewers; static float debug_popup_timing = 3.0f; +static bool just_opened_options = false; + void L::DrawMenuBar() { const auto& ctx = *GImGui; const auto& io = ctx.IO; auto isSystemPaused = DebugState.IsGuestThreadsPaused(); + bool open_popup_options = false; + if (BeginMainMenuBar()) { if (BeginMenu("Options")) { if (MenuItemEx("Emulator Paused", nullptr, nullptr, isSystemPaused)) { @@ -55,6 +59,7 @@ void L::DrawMenuBar() { } ImGui::EndMenu(); } + open_popup_options = MenuItem("Options"); ImGui::EndMenu(); } EndMainMenuBar(); @@ -74,6 +79,11 @@ void L::DrawMenuBar() { } } } + + if (open_popup_options) { + OpenPopup("GPU Tools Options"); + just_opened_options = true; + } } void L::DrawAdvanced() { @@ -91,13 +101,19 @@ void L::DrawAdvanced() { ->AddText({10.0f, io.DisplaySize.y - 40.0f}, IM_COL32_WHITE, "Emulator paused"); } - if (DebugState.should_show_frame_dump) { + if (DebugState.should_show_frame_dump && DebugState.waiting_reg_dumps.empty()) { DebugState.should_show_frame_dump = false; std::unique_lock lock{DebugState.frame_dump_list_mutex}; while (!DebugState.frame_dump_list.empty()) { - auto frame_dump = std::move(DebugState.frame_dump_list.back()); - DebugState.frame_dump_list.pop_back(); + const auto& frame_dump = DebugState.frame_dump_list.back(); frame_viewers.emplace_back(frame_dump); + DebugState.frame_dump_list.pop_back(); + } + static bool first_time = true; + if (first_time) { + first_time = false; + DebugState.ShowDebugMessage("Tip: You can shift+click any\n" + "popup to open a new window"); } } @@ -133,6 +149,30 @@ void L::DrawAdvanced() { debug_popup_timing = 3.0f; } } + + bool close_popup_options = true; + if (BeginPopupModal("GPU Tools Options", &close_popup_options, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { + static char disassembly_cli[512]; + + if (just_opened_options) { + just_opened_options = false; + auto s = Options.disassembly_cli.copy(disassembly_cli, sizeof(disassembly_cli) - 1); + disassembly_cli[s] = '\0'; + } + + InputText("Shader disassembler: ", disassembly_cli, sizeof(disassembly_cli)); + if (IsItemHovered()) { + SetTooltip(R"(Command to disassemble shaders. Example "dis.exe" --raw "{src}")"); + } + + if (Button("Save")) { + Options.disassembly_cli = disassembly_cli; + SaveIniSettingsToDisk(io.IniFilename); + CloseCurrentPopup(); + } + EndPopup(); + } } void L::DrawSimple() { @@ -140,26 +180,54 @@ void L::DrawSimple() { Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); } +static void LoadSettings(const char* line) { + int i; + float f; + if (sscanf(line, "fps_scale=%f", &f) == 1) { + fps_scale = f; + return; + } + if (sscanf(line, "show_advanced_debug=%d", &i) == 1) { + show_advanced_debug = i != 0; + return; + } + if (sscanf(line, "show_frame_graph=%d", &i) == 1) { + frame_graph.is_open = i != 0; + return; + } + if (sscanf(line, "dump_frame_count=%d", &i) == 1) { + dump_frame_count = i; + return; + } +} + void L::SetupSettings() { frame_graph.is_open = true; + using SettingLoader = void (*)(const char*); + ImGuiSettingsHandler handler{}; handler.TypeName = "DevtoolsLayer"; handler.TypeHash = ImHashStr(handler.TypeName); handler.ReadOpenFn = [](ImGuiContext*, ImGuiSettingsHandler*, const char* name) { - return std::string_view("Data") == name ? (void*)1 : nullptr; + if (std::string_view("Data") == name) { + static_assert(std::is_same_v); + return (void*)&LoadSettings; + } + if (std::string_view("CmdList") == name) { + static_assert( + std::is_same_v); + return (void*)&Widget::CmdListViewer::LoadConfig; + } + if (std::string_view("Options") == name) { + static_assert(std::is_same_v); + return (void*)&LoadOptionsConfig; + } + return (void*)nullptr; }; - handler.ReadLineFn = [](ImGuiContext*, ImGuiSettingsHandler*, void*, const char* line) { - int v; - float f; - if (sscanf(line, "fps_scale=%f", &f) == 1) { - fps_scale = f; - } else if (sscanf(line, "show_advanced_debug=%d", &v) == 1) { - show_advanced_debug = v != 0; - } else if (sscanf(line, "show_frame_graph=%d", &v) == 1) { - frame_graph.is_open = v != 0; - } else if (sscanf(line, "dump_frame_count=%d", &v) == 1) { - dump_frame_count = v; + handler.ReadLineFn = [](ImGuiContext*, ImGuiSettingsHandler*, void* handle, const char* line) { + if (handle != nullptr) { + reinterpret_cast(handle)(line); } }; handler.WriteAllFn = [](ImGuiContext*, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) { @@ -169,12 +237,19 @@ void L::SetupSettings() { buf->appendf("show_frame_graph=%d\n", frame_graph.is_open); buf->appendf("dump_frame_count=%d\n", dump_frame_count); buf->append("\n"); + buf->appendf("[%s][CmdList]\n", handler->TypeName); + Widget::CmdListViewer::SerializeConfig(buf); + buf->append("\n"); + buf->appendf("[%s][Options]\n", handler->TypeName); + SerializeOptionsConfig(buf); + buf->append("\n"); }; AddSettingsHandler(&handler); const ImGuiID dock_id = ImHashStr("FrameDumpDock"); DockBuilderAddNode(dock_id, 0); - DockBuilderSetNodePos(dock_id, ImVec2{50.0, 50.0}); + DockBuilderSetNodePos(dock_id, ImVec2{450.0, 150.0}); + DockBuilderSetNodeSize(dock_id, ImVec2{400.0, 500.0}); DockBuilderFinish(dock_id); } diff --git a/src/core/devtools/options.cpp b/src/core/devtools/options.cpp new file mode 100644 index 00000000..82fa6e87 --- /dev/null +++ b/src/core/devtools/options.cpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "options.h" + +namespace Core::Devtools { + +TOptions Options; + +void LoadOptionsConfig(const char* line) { + char str[512]; + if (sscanf(line, "disassembly_cli=%511[^\n]", str) == 1) { + Options.disassembly_cli = str; + return; + } +} + +void SerializeOptionsConfig(ImGuiTextBuffer* buf) { + buf->appendf("disassembly_cli=%s\n", Options.disassembly_cli.c_str()); +} + +} // namespace Core::Devtools diff --git a/src/core/devtools/options.h b/src/core/devtools/options.h new file mode 100644 index 00000000..9d291d76 --- /dev/null +++ b/src/core/devtools/options.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +struct ImGuiTextBuffer; + +namespace Core::Devtools { + +struct TOptions { + std::string disassembly_cli; +}; + +extern TOptions Options; + +void LoadOptionsConfig(const char* line); +void SerializeOptionsConfig(ImGuiTextBuffer* buf); + +} // namespace Core::Devtools diff --git a/src/core/devtools/widget/cmd_list.cpp b/src/core/devtools/widget/cmd_list.cpp index 012891c3..f5d31efe 100644 --- a/src/core/devtools/widget/cmd_list.cpp +++ b/src/core/devtools/widget/cmd_list.cpp @@ -32,6 +32,26 @@ const char* GetOpCodeName(u32 op); namespace Core::Devtools::Widget { +static bool group_batches = true; +static bool show_markers = false; + +void CmdListViewer::LoadConfig(const char* line) { + int i; + if (sscanf(line, "group_batches=%d", &i) == 1) { + group_batches = i != 0; + return; + } + if (sscanf(line, "show_markers=%d", &i) == 1) { + show_markers = i != 0; + return; + } +} + +void CmdListViewer::SerializeConfig(ImGuiTextBuffer* buf) { + buf->appendf("group_batches=%d\n", group_batches); + buf->appendf("show_markers=%d\n", show_markers); +} + template static HdrType GetNext(HdrType this_pm4, uint32_t n) { HdrType curr_pm4 = this_pm4; @@ -43,10 +63,11 @@ static HdrType GetNext(HdrType this_pm4, uint32_t n) { return curr_pm4; } -static void ParsePolygonControl(u32 value) { +void ParsePolygonControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("PA_SU_SC_MODE_CNTL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("PA_SU_SC_MODE_CNTL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("CULL_FRONT"); @@ -126,14 +147,17 @@ static void ParsePolygonControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.multi_prim_ib_ena.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseAaConfig(u32 value) { +void ParseAaConfig(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("PA_SC_AA_CONFIG", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("PA_SC_AA_CONFIG", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("MSAA_NUM_SAMPLES"); @@ -164,14 +188,17 @@ static void ParseAaConfig(u32 value) { TableSetColumnIndex(1); Text("%X", reg.detail_to_exposed_mode.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseViewportControl(u32 value) { +void ParseViewportControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("PA_CL_VTE_CNTL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("PA_CL_VTE_CNTL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("VPORT_X_SCALE_ENA"); @@ -232,14 +259,17 @@ static void ParseViewportControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.perfcounter_ref.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseColorControl(u32 value) { +void ParseColorControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("CB_COLOR_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("CB_COLOR_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("DISABLE_DUAL_QUAD__VI"); @@ -264,14 +294,17 @@ static void ParseColorControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.rop3.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseColor0Info(u32 value) { +void ParseColor0Info(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("CB_COLOR_INFO", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("CB_COLOR_INFO", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("ENDIAN"); @@ -380,14 +413,17 @@ static void ParseColor0Info(u32 value) { TableSetColumnIndex(1); Text("%X", reg.cmask_addr_type.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseColor0Attrib(u32 value) { +void ParseColor0Attrib(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("CB_COLOR_ATTRIB", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("CB_COLOR_ATTRIB", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("TILE_MODE_INDEX"); @@ -424,14 +460,17 @@ static void ParseColor0Attrib(u32 value) { TableSetColumnIndex(1); Text("%X", reg.force_dst_alpha_1.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseBlendControl(u32 value) { +void ParseBlendControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("CB_BLEND_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("CB_BLEND_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("COLOR_SRCBLEND"); @@ -490,14 +529,17 @@ static void ParseBlendControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.disable_rop3.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseDepthRenderControl(u32 value) { +void ParseDepthRenderControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("DB_RENDER_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("DB_RENDER_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("DEPTH_CLEAR_ENABLE"); @@ -558,14 +600,17 @@ static void ParseDepthRenderControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.decompress_enable.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseDepthControl(u32 value) { +void ParseDepthControl(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("STENCIL_ENABLE"); @@ -628,14 +673,17 @@ static void ParseDepthControl(u32 value) { TableSetColumnIndex(1); Text("%X", reg.disable_color_writes_on_depth_pass.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseEqaa(u32 value) { +void ParseEqaa(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("MAX_ANCHOR_SAMPLES"); @@ -708,14 +756,17 @@ static void ParseEqaa(u32 value) { TableSetColumnIndex(1); Text("%X", reg.enable_postz_overrasterization.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } -static void ParseZInfo(u32 value) { +void ParseZInfo(u32 value, bool begin_table) { auto const reg = reinterpret_cast(value); - if (BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (!begin_table || + BeginTable("DB_DEPTH_CONTROL", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { TableNextRow(); TableSetColumnIndex(0); Text("FORMAT"); @@ -776,40 +827,41 @@ static void ParseZInfo(u32 value) { TableSetColumnIndex(1); Text("%X", reg.zrange_precision.Value()); - EndTable(); + if (begin_table) { + EndTable(); + } } } void CmdListViewer::OnNop(AmdGpu::PM4Type3Header const* header, u32 const* body) { using namespace std::string_view_literals; - enum class NOP_PAYLOAD : u32 { - ACB_SUBMIT_MRK = 0x68750013, - ALLOC_ALIGN8 = 0x68753000, - PUSH_MARKER = 0x68750001, - SET_VSHARP = 0x68750004, - SET_TSHARP = 0x68750005, - SET_SSHARP = 0x68750006, - SET_USER_DATA = 0x6875000d, - }; - auto get_noppayload_text = [](NOP_PAYLOAD const nop_payload) { +#define NOP_PAYLOAD \ + P(PUSH_MARKER, 0x68750001) \ + P(POP_MARKER, 0x68750002) \ + P(SET_MARKER, 0x68750003) \ + P(SET_VSHARP, 0x68750004) \ + P(SET_TSHARP, 0x68750005) \ + P(SET_SSHARP, 0x68750006) \ + P(ACB_SUBMIT_MRK, 0x68750013) \ + P(SET_USER_DATA, 0x6875000D) \ + P(PATCHED_FLIP, 0x68750776) \ + P(PREPARE_FLIP, 0x68750777) \ + P(PREPARE_FLIP_LABEL, 0x68750778) \ + P(PREPARE_FLIP_INTERRUPT, 0x68750780) \ + P(PREPARE_FLIP_INTERRUPT_LABEL, 0x68750781) \ + P(ALLOC_ALIGN8, 0x68753000) + + auto get_nop_payload_text = [](u32 const nop_payload) { switch (nop_payload) { - case NOP_PAYLOAD::ACB_SUBMIT_MRK: - return "ACB_SUBMIT_MRK"sv; - case NOP_PAYLOAD::ALLOC_ALIGN8: - return "ALLOC_ALIGN8"sv; - case NOP_PAYLOAD::PUSH_MARKER: - return "PUSH_MARKER"sv; - case NOP_PAYLOAD::SET_VSHARP: - return "SET_VSHARP"sv; - case NOP_PAYLOAD::SET_TSHARP: - return "SET_TSHARP"sv; - case NOP_PAYLOAD::SET_SSHARP: - return "SET_SSHARP"sv; - case NOP_PAYLOAD::SET_USER_DATA: - return "SET_USER_DATA"sv; +#define P(name, value) \ + case value: \ + return #name##sv; + NOP_PAYLOAD +#undef P + default: + return ""sv; } - return ""sv; }; Separator(); @@ -822,7 +874,7 @@ void CmdListViewer::OnNop(AmdGpu::PM4Type3Header const* header, u32 const* body) for (unsigned i = 0; i < pkt->header.count + 1; ++i) { Text("%02X: %08X", i, payload[i]); if ((payload[i] & 0xffff0000) == 0x68750000) { - const auto& e = get_noppayload_text((NOP_PAYLOAD)payload[i]); + const auto& e = get_nop_payload_text(payload[i]); if (!e.empty()) { SameLine(); Text("(%s)", e.data()); @@ -836,7 +888,7 @@ void CmdListViewer::OnSetBase(AmdGpu::PM4Type3Header const* header, u32 const* b Separator(); BeginGroup(); - auto const* pkt = reinterpret_cast(header); + // auto const* pkt = reinterpret_cast(header); Text("BASE_INDEX: %08X", body[0]); Text("ADDRESS0 : %08X", body[1]); Text("ADDRESS1 : %08X", body[2]); @@ -1025,20 +1077,31 @@ void CmdListViewer::OnDispatch(AmdGpu::PM4Type3Header const* header, u32 const* EndGroup(); } -CmdListViewer::CmdListViewer(FrameDumpViewer* parent, const std::vector& cmd_list) - : parent(parent) { +CmdListViewer::CmdListViewer(DebugStateType::FrameDump* _frame_dump, + const std::vector& cmd_list, uintptr_t _base_addr, + std::string _name) + : frame_dump(_frame_dump), base_addr(_base_addr), name(std::move(_name)) { using namespace AmdGpu; cmdb_addr = (uintptr_t)cmd_list.data(); cmdb_size = cmd_list.size() * sizeof(u32); + cmdb_view_name = fmt::format("[GFX] Command buffer {}###cmdview_hex_{}", this->name, cmdb_addr); + cmdb_view.Open = false; + cmdb_view.ReadOnly = true; + auto const* pm4_hdr = reinterpret_cast(cmdb_addr); size_t processed_size = 0; size_t prev_offset = 0; + u32 batch_id = 0; std::string marker{}; + if (cmdb_size > 0) { + events.emplace_back(BatchBegin{.id = 0}); + } + while (processed_size < cmdb_size) { auto* next_pm4_hdr = GetNext(pm4_hdr, 1); auto processed_len = @@ -1048,22 +1111,28 @@ CmdListViewer::CmdListViewer(FrameDumpViewer* parent, const std::vector& cm if (pm4_hdr->type == PM4Type3Header::TYPE) { auto const* pm4_t3 = reinterpret_cast(pm4_hdr); + auto opcode = pm4_t3->opcode; - if (pm4_t3->opcode == PM4ItOpcode::Nop) { + if (opcode == PM4ItOpcode::Nop) { auto const* it_body = reinterpret_cast(pm4_hdr + 1); - if (it_body[0] == 0x68750001) { + switch (it_body[0]) { + case PM4CmdNop::PayloadType::DebugSetMarker: marker = std::string{(char*)&it_body[1]}; + break; + case PM4CmdNop::PayloadType::DebugMarkerPush: + events.emplace_back(PushMarker{ + .name = std::string{(char*)&it_body[1]}, + }); + break; + case PM4CmdNop::PayloadType::DebugMarkerPop: + events.emplace_back(PopMarker{}); + break; + default: + break; } } - if (pm4_t3->opcode == PM4ItOpcode::DispatchDirect || - pm4_t3->opcode == PM4ItOpcode::DispatchIndirect || - pm4_t3->opcode == PM4ItOpcode::DrawIndex2 || - pm4_t3->opcode == PM4ItOpcode::DrawIndexAuto || - pm4_t3->opcode == PM4ItOpcode::DrawIndexOffset2 || - pm4_t3->opcode == PM4ItOpcode::DrawIndexIndirect - // ... - ) { + if (IsDrawCall(opcode)) { // All these commands are terminated by NOP at the end, so // it is safe to skip it to be even with CP // next_pm4_hdr = get_next(next_pm4_hdr, 1); @@ -1071,15 +1140,17 @@ CmdListViewer::CmdListViewer(FrameDumpViewer* parent, const std::vector& cm // processed_len += nop_len; // processed_size += nop_len; - batches.emplace_back(BatchInfo{ - marker, - prev_offset, - processed_size, - processed_size - processed_len, - pm4_t3->opcode, + events.emplace_back(BatchInfo{ + .id = batch_id++, + .marker = marker, + .start_addr = prev_offset, + .end_addr = processed_size, + .command_addr = processed_size - processed_len, + .type = opcode, }); prev_offset = processed_size; marker.clear(); + events.emplace_back(BatchBegin{.id = batch_id}); } } @@ -1088,18 +1159,58 @@ CmdListViewer::CmdListViewer(FrameDumpViewer* parent, const std::vector& cm // state batch (last) if (processed_size - prev_offset > 0) { - batches.emplace_back(BatchInfo{ - marker, - prev_offset, - processed_size, - 0, - static_cast(0xFF), + events.emplace_back(BatchInfo{ + .id = batch_id++, + .marker = marker, + .start_addr = prev_offset, + .end_addr = processed_size, + .command_addr = 0, + .type = static_cast(0xFF), }); } + if (!events.empty() && std::holds_alternative(events.back())) { + events.pop_back(); + } } void CmdListViewer::Draw() { + const auto& ctx = *GetCurrentContext(); + + if (batch_view.open) { + batch_view.Draw(); + } + for (auto it = extra_batch_view.begin(); it != extra_batch_view.end();) { + if (!it->open) { + it = extra_batch_view.erase(it); + continue; + } + it->Draw(); + ++it; + } + + if (cmdb_view.Open) { + MemoryEditor::Sizes s; + cmdb_view.CalcSizes(s, cmdb_size, cmdb_addr); + SetNextWindowSize({s.WindowWidth, s.WindowWidth * 0.6f}, ImGuiCond_FirstUseEver); + SetNextWindowSizeConstraints({0.0f}, {s.WindowWidth, FLT_MAX}); + if (Begin(cmdb_view_name.c_str(), &cmdb_view.Open, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings)) { + cmdb_view.DrawContents((void*)cmdb_addr, cmdb_size, base_addr); + if (cmdb_view.ContentsWidthChanged) { + cmdb_view.CalcSizes(s, cmdb_size, cmdb_addr); + SetWindowSize({s.WindowWidth, s.WindowWidth * 0.6f}); + } + } + End(); + } + + PushID(name.c_str()); if (BeginChild("cmd_queue", {})) { + + Checkbox("Group batches", &group_batches); + SameLine(); + Checkbox("Show markers", &show_markers); + char queue_name[32]{}; if (vqid < 254) { std::snprintf(queue_name, sizeof(queue_name), "%s %d", vqid > 254 ? "GFX" : "ASC", @@ -1111,113 +1222,240 @@ void CmdListViewer::Draw() { Text("queue : %s", queue_name); Text("base addr: %08llX", cmdb_addr); SameLine(); - if (SmallButton(">")) { - parent->cmdb_view.Open ^= true; + if (SmallButton("Memory >")) { + cmdb_view.Open ^= true; } Text("size : %04llX", cmdb_size); Separator(); - char batch_hdr[128]; - for (int batch_id = 0; batch_id < batches.size(); ++batch_id) { - auto processed_size = 0ull; - auto const* pm4_hdr = - reinterpret_cast(cmdb_addr + batches[batch_id].start_addr); + if (TreeNode("Batches")) { + int tree_depth = 0; + int tree_depth_show = 0; - sprintf(batch_hdr, "%08llX: batch-%03d | %s", cmdb_addr + batches[batch_id].start_addr, - batch_id, batches[batch_id].marker.c_str()); - - if (batch_id == batch_bp) { // highlight batch at breakpoint - PushStyleColor(ImGuiCol_Header, ImVec4{1.0f, 0.5f, 0.5f, 0.5f}); + u32 last_batch_id = ~0u; + if (!events.empty() && std::holds_alternative(events.back())) { + last_batch_id = std::get(events.back()).id; } - if (batches[batch_id].type == static_cast(0xFF) || - CollapsingHeader(batch_hdr)) { - auto const batch_sz = batches[batch_id].end_addr - batches[batch_id].start_addr; - while (processed_size < batch_sz) { - AmdGpu::PM4ItOpcode op{0xFFu}; + u32 batch_id = ~0u; + u32 current_highlight_batch = ~0u; - if (pm4_hdr->type == AmdGpu::PM4Type3Header::TYPE) { - auto const* pm4_t3 = - reinterpret_cast(pm4_hdr); - op = pm4_t3->opcode; + int id = 0; + PushID(0); + for (const auto& event : events) { + PopID(); + PushID(id++); - static char header_name[128]; - sprintf(header_name, "%08llX: %s", - cmdb_addr + batches[batch_id].start_addr + processed_size, - Gcn::GetOpCodeName((u32)op)); + if (std::holds_alternative(event)) { + batch_id = std::get(event).id; + } - if (TreeNode(header_name)) { - bool just_opened = IsItemToggledOpen(); - if (BeginTable("split", 1)) { - TableNextColumn(); - Text("size: %d", pm4_hdr->count + 1); + if (show_markers) { + if (std::holds_alternative(event)) { + if (tree_depth_show >= tree_depth) { + auto& marker = std::get(event); + bool show = TreeNode(&event, "%s", marker.name.c_str()); + if (show) { + tree_depth_show++; + } + } + tree_depth++; + continue; + } + if (std::holds_alternative(event)) { + if (tree_depth_show >= tree_depth) { + tree_depth_show--; + TreePop(); + } + tree_depth--; + continue; + } + if (tree_depth_show < tree_depth) { + continue; + } + } - if (just_opened) { + if (!std::holds_alternative(event)) { + continue; + } + + auto& batch = std::get(event); + auto const* pm4_hdr = + reinterpret_cast(cmdb_addr + batch.start_addr); + + char batch_hdr[128]; + if (batch.type == static_cast(0xFF)) { + snprintf(batch_hdr, sizeof(batch_hdr), "State batch"); + } else if (!batch.marker.empty()) { + snprintf(batch_hdr, sizeof(batch_hdr), "%08llX: batch-%03d %s | %s", + cmdb_addr + batch.start_addr, batch.id, + Gcn::GetOpCodeName(static_cast(batch.type)), + batch.marker.c_str()); + } else { + snprintf(batch_hdr, sizeof(batch_hdr), "%08llX: batch-%03d %s", + cmdb_addr + batch.start_addr, batch.id, + Gcn::GetOpCodeName(static_cast(batch.type))); + } + + if (batch.id == batch_bp) { // highlight batch at breakpoint + PushStyleColor(ImGuiCol_Header, ImVec4{1.0f, 0.5f, 0.5f, 0.5f}); + } + if (batch.id == highlight_batch) { + PushStyleColor(ImGuiCol_Text, ImVec4{1.0f, 0.7f, 0.7f, 1.0f}); + } + + const auto open_batch_view = [&, this] { + if (frame_dump->regs.contains(batch.command_addr)) { + auto data = frame_dump->regs.at(batch.command_addr); + if (GetIO().KeyShift) { + auto& pop = extra_batch_view.emplace_back(); + pop.SetData(data, batch_id); + pop.open = true; + } else { + batch_view.SetData(data, batch_id); + batch_view.open = true; + } + } + }; + + bool show_batch_content = true; + + if (group_batches) { + show_batch_content = + CollapsingHeader(batch_hdr, ImGuiTreeNodeFlags_AllowOverlap); + SameLine(GetContentRegionAvail().x - 40.0f); + if (Button("->", {40.0f, 0.0f})) { + open_batch_view(); + } + } + + if (show_batch_content) { + auto processed_size = 0ull; + auto bb = ctx.LastItemData.Rect; + if (group_batches) { + Indent(); + } + auto const batch_sz = batch.end_addr - batch.start_addr; + + while (processed_size < batch_sz) { + AmdGpu::PM4ItOpcode op{0xFFu}; + + if (pm4_hdr->type == AmdGpu::PM4Type3Header::TYPE) { + auto const* pm4_t3 = + reinterpret_cast(pm4_hdr); + op = pm4_t3->opcode; + + char header_name[128]; + sprintf(header_name, "%08llX: %s", + cmdb_addr + batch.start_addr + processed_size, + Gcn::GetOpCodeName((u32)op)); + + bool open_pm4 = TreeNode(header_name); + if (!group_batches) { + if (IsDrawCall(op)) { + SameLine(GetContentRegionAvail().x - 40.0f); + if (Button("->", {40.0f, 0.0f})) { + open_batch_view(); + } + } + if (IsItemHovered() && ctx.IO.KeyShift) { + if (BeginTooltip()) { + Text("Batch %d", batch_id); + EndTooltip(); + } + } + } + if (open_pm4) { + if (IsItemToggledOpen()) { // Editor - parent->cmdb_view.GotoAddrAndHighlight( + cmdb_view.GotoAddrAndHighlight( reinterpret_cast(pm4_hdr) - cmdb_addr, reinterpret_cast(pm4_hdr) - cmdb_addr + (pm4_hdr->count + 2) * 4); } - auto const* it_body = - reinterpret_cast(pm4_hdr + 1); + if (BeginTable("split", 1)) { + TableNextColumn(); + Text("size: %d", pm4_hdr->count + 1); - switch (op) { - case AmdGpu::PM4ItOpcode::Nop: { - OnNop(pm4_t3, it_body); - break; - } - case AmdGpu::PM4ItOpcode::SetBase: { - OnSetBase(pm4_t3, it_body); - break; - } - case AmdGpu::PM4ItOpcode::SetContextReg: { - OnSetContextReg(pm4_t3, it_body); - break; - } - case AmdGpu::PM4ItOpcode::SetShReg: { - OnSetShReg(pm4_t3, it_body); - break; - } - case AmdGpu::PM4ItOpcode::DispatchDirect: { - OnDispatch(pm4_t3, it_body); - break; - } - default: { - auto const* payload = &it_body[0]; - for (unsigned i = 0; i < pm4_hdr->count + 1; ++i) { - Text("%02X: %08X", i, payload[i]); + auto const* it_body = + reinterpret_cast(pm4_hdr + 1); + + switch (op) { + case AmdGpu::PM4ItOpcode::Nop: { + OnNop(pm4_t3, it_body); + break; + } + case AmdGpu::PM4ItOpcode::SetBase: { + OnSetBase(pm4_t3, it_body); + break; + } + case AmdGpu::PM4ItOpcode::SetContextReg: { + OnSetContextReg(pm4_t3, it_body); + break; + } + case AmdGpu::PM4ItOpcode::SetShReg: { + OnSetShReg(pm4_t3, it_body); + break; + } + case AmdGpu::PM4ItOpcode::DispatchDirect: { + OnDispatch(pm4_t3, it_body); + break; + } + default: { + auto const* payload = &it_body[0]; + for (unsigned i = 0; i < pm4_hdr->count + 1; ++i) { + Text("%02X: %08X", i, payload[i]); + } + } } - } - } - EndTable(); + EndTable(); + } + TreePop(); } - TreePop(); + + } else { + Text(""); } - } else { - Text(""); + + auto const* next_pm4_hdr = GetNext(pm4_hdr, 1); + auto const processed_len = reinterpret_cast(next_pm4_hdr) - + reinterpret_cast(pm4_hdr); + pm4_hdr = next_pm4_hdr; + processed_size += processed_len; } - auto const* next_pm4_hdr = GetNext(pm4_hdr, 1); - auto const processed_len = reinterpret_cast(next_pm4_hdr) - - reinterpret_cast(pm4_hdr); - pm4_hdr = next_pm4_hdr; - processed_size += processed_len; + if (group_batches) { + Unindent(); + }; + bb = {{0.0f, bb.Max.y}, ctx.LastItemData.Rect.Max}; + if (bb.Contains(ctx.IO.MousePos)) { + current_highlight_batch = batch.id; + } + } + + if (batch.id == highlight_batch) { + PopStyleColor(); + } + + if (batch.id == batch_bp) { + PopStyleColor(); + } + + if (batch.id == last_batch_id) { + Separator(); } } + PopID(); - if (batch_id == batch_bp) { - PopStyleColor(); - } + highlight_batch = current_highlight_batch; - if (batch_id == batches.size() - 2) { - Separator(); - } + TreePop(); } } EndChild(); + PopID(); } } // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/cmd_list.h b/src/core/devtools/widget/cmd_list.h index a6ecd932..971c8fff 100644 --- a/src/core/devtools/widget/cmd_list.h +++ b/src/core/devtools/widget/cmd_list.h @@ -5,10 +5,14 @@ #pragma once +#include #include +#include +#include "common.h" #include "common/types.h" -#include "video_core/buffer_cache/buffer_cache.h" +#include "imgui_memory_editor.h" +#include "reg_view.h" namespace AmdGpu { union PM4Type3Header; @@ -19,43 +23,50 @@ namespace Core::Devtools::Widget { class FrameDumpViewer; -class CmdListViewer { - /* - * Generic PM4 header - */ - union PM4Header { - struct { - u32 reserved : 16; - u32 count : 14; - u32 type : 2; // PM4_TYPE - }; - u32 u32All; - }; - struct BatchInfo { - std::string marker{}; - size_t start_addr; - size_t end_addr; - size_t command_addr; - AmdGpu::PM4ItOpcode type; - bool bypass{false}; - }; +void ParsePolygonControl(u32 value, bool begin_table = true); +void ParseAaConfig(u32 value, bool begin_table = true); +void ParseViewportControl(u32 value, bool begin_table = true); +void ParseColorControl(u32 value, bool begin_table = true); +void ParseColor0Info(u32 value, bool begin_table = true); +void ParseColor0Attrib(u32 value, bool begin_table = true); +void ParseBlendControl(u32 value, bool begin_table = true); +void ParseDepthRenderControl(u32 value, bool begin_table = true); +void ParseDepthControl(u32 value, bool begin_table = true); +void ParseEqaa(u32 value, bool begin_table = true); +void ParseZInfo(u32 value, bool begin_table = true); - FrameDumpViewer* parent; - std::vector batches{}; +class CmdListViewer { + + DebugStateType::FrameDump* frame_dump; + + uintptr_t base_addr; + std::string name; + std::vector events{}; uintptr_t cmdb_addr; size_t cmdb_size; + std::string cmdb_view_name; + MemoryEditor cmdb_view; + int batch_bp{-1}; int vqid{255}; + u32 highlight_batch{~0u}; - void OnNop(AmdGpu::PM4Type3Header const* header, u32 const* body); - void OnSetBase(AmdGpu::PM4Type3Header const* header, u32 const* body); - void OnSetContextReg(AmdGpu::PM4Type3Header const* header, u32 const* body); - void OnSetShReg(AmdGpu::PM4Type3Header const* header, u32 const* body); - void OnDispatch(AmdGpu::PM4Type3Header const* header, u32 const* body); + RegView batch_view; + std::vector extra_batch_view; + + static void OnNop(AmdGpu::PM4Type3Header const* header, u32 const* body); + static void OnSetBase(AmdGpu::PM4Type3Header const* header, u32 const* body); + static void OnSetContextReg(AmdGpu::PM4Type3Header const* header, u32 const* body); + static void OnSetShReg(AmdGpu::PM4Type3Header const* header, u32 const* body); + static void OnDispatch(AmdGpu::PM4Type3Header const* header, u32 const* body); public: - explicit CmdListViewer(FrameDumpViewer* parent, const std::vector& cmd_list); + static void LoadConfig(const char* line); + static void SerializeConfig(ImGuiTextBuffer* buf); + + explicit CmdListViewer(DebugStateType::FrameDump* frame_dump, const std::vector& cmd_list, + uintptr_t base_addr = 0, std::string name = ""); void Draw(); }; diff --git a/src/core/devtools/widget/common.h b/src/core/devtools/widget/common.h new file mode 100644 index 00000000..701d1639 --- /dev/null +++ b/src/core/devtools/widget/common.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include + +#include "common/types.h" +#include "video_core/amdgpu/pm4_opcodes.h" + +namespace Core::Devtools::Widget { + +/* + * Generic PM4 header + */ +union PM4Header { + struct { + u32 reserved : 16; + u32 count : 14; + u32 type : 2; // PM4_TYPE + }; + u32 u32All; +}; + +struct PushMarker { + std::string name{}; +}; + +struct PopMarker {}; + +struct BatchBegin { + u32 id; +}; + +struct BatchInfo { + u32 id; + std::string marker{}; + size_t start_addr; + size_t end_addr; + size_t command_addr; + AmdGpu::PM4ItOpcode type; + bool bypass{false}; +}; + +using GPUEvent = std::variant; + +template +void DrawRow(const char* text, const char* fmt, Args... args) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(text); + ImGui::TableNextColumn(); + char buf[128]; + snprintf(buf, sizeof(buf), fmt, args...); + ImGui::TextUnformatted(buf); +} + +template +void DrawEnumRow(const char* text, T value) { + DrawRow(text, "%X (%s)", V(value), magic_enum::enum_name(value).data()); +} + +template +void DrawMultipleRow(const char* text, const char* fmt, V arg, Extra&&... extra_args) { + DrawRow(text, fmt, arg); + if constexpr (sizeof...(extra_args) > 0) { + DrawMultipleRow(std::forward(extra_args)...); + } +} + +template +static void DoTooltip(const char* str_id, Args&&... args) { + if (ImGui::BeginTooltip()) { + if (ImGui::BeginTable(str_id, 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + DrawMultipleRow(std::forward(args)...); + ImGui::EndTable(); + } + ImGui::EndTooltip(); + } +} + +static bool IsDrawCall(AmdGpu::PM4ItOpcode opcode) { + using AmdGpu::PM4ItOpcode; + switch (opcode) { + case PM4ItOpcode::DrawIndex2: + case PM4ItOpcode::DrawIndexOffset2: + case PM4ItOpcode::DrawIndexAuto: + case PM4ItOpcode::DrawIndirect: + case PM4ItOpcode::DrawIndexIndirect: + case PM4ItOpcode::DispatchDirect: + case PM4ItOpcode::DispatchIndirect: + return true; + default: + return false; + } +} + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/frame_dump.cpp b/src/core/devtools/widget/frame_dump.cpp index d27bab90..29b5cb8e 100644 --- a/src/core/devtools/widget/frame_dump.cpp +++ b/src/core/devtools/widget/frame_dump.cpp @@ -36,7 +36,8 @@ static std::array small_int_to_str(const s32 i) { namespace Core::Devtools::Widget { -FrameDumpViewer::FrameDumpViewer(FrameDump _frame_dump) : frame_dump(std::move(_frame_dump)) { +FrameDumpViewer::FrameDumpViewer(const FrameDump& _frame_dump) + : frame_dump(std::make_shared(_frame_dump)) { static int unique_id = 0; id = unique_id++; @@ -44,20 +45,19 @@ FrameDumpViewer::FrameDumpViewer(FrameDump _frame_dump) : frame_dump(std::move(_ selected_submit_num = 0; selected_queue_num2 = 0; - cmd_list_viewer.reserve(frame_dump.queues.size()); - for (const auto& cmd : frame_dump.queues) { - cmd_list_viewer.emplace_back(this, cmd.data); - if (cmd.type == QueueType::dcb && cmd.submit_num == selected_submit_num && - cmd.num2 == selected_queue_num2) { - selected_cmd = cmd_list_viewer.size() - 1; + cmd_list_viewer.reserve(frame_dump->queues.size()); + for (const auto& cmd : frame_dump->queues) { + const auto fname = + fmt::format("{}_{}_{:02}_{:02}", id, magic_enum::enum_name(selected_queue_type), + selected_submit_num, selected_queue_num2); + cmd_list_viewer.emplace_back(frame_dump.get(), cmd.data, cmd.base_addr, fname); + if (cmd.type == QueueType::dcb && cmd.submit_num == 0 && cmd.num2 == 0) { + selected_cmd = static_cast(cmd_list_viewer.size() - 1); } } - - cmdb_view.Open = false; - cmdb_view.ReadOnly = true; } -FrameDumpViewer::~FrameDumpViewer() {} +FrameDumpViewer::~FrameDumpViewer() = default; void FrameDumpViewer::Draw() { if (!is_open) { @@ -66,11 +66,11 @@ void FrameDumpViewer::Draw() { char name[32]; snprintf(name, sizeof(name), "Frame #%d dump", id); - static ImGuiID dock_id = ImHashStr("FrameDumpDock"); - SetNextWindowDockID(dock_id, ImGuiCond_Appearing); if (Begin(name, &is_open, ImGuiWindowFlags_NoSavedSettings)) { if (IsWindowAppearing()) { auto window = GetCurrentWindow(); + static ImGuiID dock_id = ImHashStr("FrameDumpDock"); + SetWindowDock(window, dock_id, ImGuiCond_Once | ImGuiCond_FirstUseEver); SetWindowSize(window, ImVec2{470.0f, 600.0f}); } BeginGroup(); @@ -89,12 +89,30 @@ void FrameDumpViewer::Draw() { EndCombo(); } SameLine(); + BeginDisabled(selected_cmd == -1); + if (SmallButton("Dump cmd")) { + auto now_time = fmt::localtime(std::time(nullptr)); + const auto fname = fmt::format("{:%F %H-%M-%S} {}_{}_{}.bin", now_time, + magic_enum::enum_name(selected_queue_type), + selected_submit_num, selected_queue_num2); + Common::FS::IOFile file(fname, Common::FS::FileAccessMode::Write); + const auto& data = frame_dump->queues[selected_cmd].data; + if (file.IsOpen()) { + DebugState.ShowDebugMessage(fmt::format("Dumping cmd as {}", fname)); + file.Write(data); + } else { + DebugState.ShowDebugMessage(fmt::format("Failed to save {}", fname)); + LOG_ERROR(Core, "Failed to open file {}", fname); + } + } + EndDisabled(); + TextEx("Submit num"); SameLine(); if (BeginCombo("##select_submit_num", small_int_to_str(selected_submit_num).data(), ImGuiComboFlags_WidthFitPreview)) { std::array available_submits{}; - for (const auto& cmd : frame_dump.queues) { + for (const auto& cmd : frame_dump->queues) { if (cmd.type == selected_queue_type) { available_submits[cmd.submit_num] = true; } @@ -119,7 +137,7 @@ void FrameDumpViewer::Draw() { if (BeginCombo("##select_queue_num2", small_int_to_str(selected_queue_num2).data(), ImGuiComboFlags_WidthFitPreview)) { std::array available_queues{}; - for (const auto& cmd : frame_dump.queues) { + for (const auto& cmd : frame_dump->queues) { if (cmd.type == selected_queue_type && cmd.submit_num == selected_submit_num) { available_queues[cmd.num2] = true; } @@ -134,34 +152,16 @@ void FrameDumpViewer::Draw() { } } if (selected) { - const auto it = std::ranges::find_if(frame_dump.queues, [&](const auto& cmd) { + const auto it = std::ranges::find_if(frame_dump->queues, [&](const auto& cmd) { return cmd.type == selected_queue_type && cmd.submit_num == selected_submit_num && cmd.num2 == selected_queue_num2; }); - if (it != frame_dump.queues.end()) { - selected_cmd = std::distance(frame_dump.queues.begin(), it); + if (it != frame_dump->queues.end()) { + selected_cmd = static_cast(std::distance(frame_dump->queues.begin(), it)); } } EndCombo(); } - SameLine(); - BeginDisabled(selected_cmd == -1); - if (SmallButton("Dump cmd")) { - auto now_time = fmt::localtime(std::time(nullptr)); - const auto fname = fmt::format("{:%F %H-%M-%S} {}_{}_{}.bin", now_time, - magic_enum::enum_name(selected_queue_type), - selected_submit_num, selected_queue_num2); - Common::FS::IOFile file(fname, Common::FS::FileAccessMode::Write); - auto& data = frame_dump.queues[selected_cmd].data; - if (file.IsOpen()) { - DebugState.ShowDebugMessage(fmt::format("Dumping cmd as {}", fname)); - file.Write(data); - } else { - DebugState.ShowDebugMessage(fmt::format("Failed to save {}", fname)); - LOG_ERROR(Core, "Failed to open file {}", fname); - } - } - EndDisabled(); EndGroup(); if (selected_cmd != -1) { @@ -169,21 +169,6 @@ void FrameDumpViewer::Draw() { } } End(); - - if (cmdb_view.Open && selected_cmd != -1) { - auto& cmd = frame_dump.queues[selected_cmd].data; - auto cmd_size = cmd.size() * sizeof(u32); - MemoryEditor::Sizes s; - cmdb_view.CalcSizes(s, cmd_size, (size_t)cmd.data()); - SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(s.WindowWidth, FLT_MAX)); - - char name[64]; - snprintf(name, sizeof(name), "[GFX] Command buffer %d###cmdbuf_hex_%d", id, id); - if (Begin(name, &cmdb_view.Open, ImGuiWindowFlags_NoScrollbar)) { - cmdb_view.DrawContents(cmd.data(), cmd_size, (size_t)cmd.data()); - } - End(); - } } } // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/frame_dump.h b/src/core/devtools/widget/frame_dump.h index d9d11f82..2b3ff241 100644 --- a/src/core/devtools/widget/frame_dump.h +++ b/src/core/devtools/widget/frame_dump.h @@ -8,7 +8,6 @@ #include "cmd_list.h" #include "core/debug_state.h" -#include "imgui_memory_editor.h" namespace Core::Devtools::Widget { @@ -17,11 +16,10 @@ class CmdListViewer; class FrameDumpViewer { friend class CmdListViewer; - DebugStateType::FrameDump frame_dump; + std::shared_ptr frame_dump; int id; std::vector cmd_list_viewer; - MemoryEditor cmdb_view; DebugStateType::QueueType selected_queue_type; s32 selected_submit_num; @@ -31,7 +29,7 @@ class FrameDumpViewer { public: bool is_open = true; - explicit FrameDumpViewer(DebugStateType::FrameDump frame_dump); + explicit FrameDumpViewer(const DebugStateType::FrameDump& frame_dump); ~FrameDumpViewer(); diff --git a/src/core/devtools/widget/reg_popup.cpp b/src/core/devtools/widget/reg_popup.cpp new file mode 100644 index 00000000..d012437c --- /dev/null +++ b/src/core/devtools/widget/reg_popup.cpp @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "reg_popup.h" + +#include +#include +#include + +#include "cmd_list.h" +#include "common.h" + +using namespace ImGui; +using magic_enum::enum_name; + +namespace Core::Devtools::Widget { + +void RegPopup::DrawColorBuffer(const AmdGpu::Liverpool::ColorBuffer& buffer) { + if (BeginTable("COLOR_BUFFER", 2, ImGuiTableFlags_Borders)) { + TableNextRow(); + + // clang-format off + + DrawMultipleRow( + "BASE_ADDR", "%X", buffer.base_address, + "PITCH.TILE_MAX", "%X", buffer.pitch.tile_max, + "PITCH.FMASK_TILE_MAX", "%X", buffer.pitch.fmask_tile_max, + "SLICE.TILE_MAX", "%X", buffer.slice.tile_max, + "VIEW.SLICE_START", "%X", buffer.view.slice_start, + "VIEW.SLICE_MAX", "%X", buffer.view.slice_max + ); + + TableNextRow(); + TableNextColumn(); + if (TreeNode("Color0Info")) { + TableNextRow(); + TableNextColumn(); + ParseColor0Info(buffer.info.u32all, false); + TreePop(); + } + + TableNextRow(); + TableNextColumn(); + if (TreeNode("Color0Attrib")) { + TableNextRow(); + TableNextColumn(); + ParseColor0Attrib(buffer.attrib.u32all, false); + TreePop(); + } + + TableNextRow(); + DrawMultipleRow( + "CMASK_BASE_EXT", "%X", buffer.cmask_base_address, + "FMASK_BASE_EXT", "%X", buffer.fmask_base_address, + "FMASK_SLICE.TILE_MAX", "%X", buffer.fmask_slice.tile_max, + "CLEAR_WORD0", "%X", buffer.clear_word0, + "CLEAR_WORD1", "%X", buffer.clear_word1 + ); + + DrawMultipleRow( + "Pitch()", "%X", buffer.Pitch(), + "Height()", "%X", buffer.Height(), + "Address()", "%X", buffer.Address(), + "CmaskAddress", "%X", buffer.CmaskAddress(), + "FmaskAddress", "%X", buffer.FmaskAddress(), + "NumSamples()", "%X", buffer.NumSamples(), + "NumSlices()", "%X", buffer.NumSlices(), + "GetColorSliceSize()", "%X", buffer.GetColorSliceSize() + ); + + auto tiling_mode = buffer.GetTilingMode(); + auto num_format = buffer.NumFormat(); + DrawEnumRow("GetTilingMode()", tiling_mode); + DrawRow("IsTiled()", "%X", buffer.IsTiled()); + DrawEnumRow("NumFormat()", num_format); + + // clang-format on + + EndTable(); + } +} + +void RegPopup::DrawDepthBuffer(const DepthBuffer& depth_data) { + const auto& [depth_buffer, depth_control] = depth_data; + + SeparatorText("Depth buffer"); + + if (BeginTable("DEPTH_BUFFER", 2, ImGuiTableFlags_Borders)) { + TableNextRow(); + + // clang-format off + DrawEnumRow("Z_INFO.FORMAT", depth_buffer.z_info.format.Value()); + DrawMultipleRow( + "Z_INFO.NUM_SAMPLES", "%X", depth_buffer.z_info.num_samples, + "Z_INFO.TILE_SPLIT", "%X", depth_buffer.z_info.tile_split, + "Z_INFO.TILE_MODE_INDEX", "%X", depth_buffer.z_info.tile_mode_index, + "Z_INFO.DECOMPRESS_ON_N_ZPLANES", "%X", depth_buffer.z_info.decompress_on_n_zplanes, + "Z_INFO.ALLOW_EXPCLEAR", "%X", depth_buffer.z_info.allow_expclear, + "Z_INFO.READ_SIZE", "%X", depth_buffer.z_info.read_size, + "Z_INFO.TILE_SURFACE_EN", "%X", depth_buffer.z_info.tile_surface_en, + "Z_INFO.CLEAR_DISALLOWED", "%X", depth_buffer.z_info.clear_disallowed, + "Z_INFO.ZRANGE_PRECISION", "%X", depth_buffer.z_info.zrange_precision + ); + + DrawEnumRow("STENCIL_INFO.FORMAT", depth_buffer.stencil_info.format.Value()); + + DrawMultipleRow( + "Z_READ_BASE", "%X", depth_buffer.z_read_base, + "STENCIL_READ_BASE", "%X", depth_buffer.stencil_read_base, + "Z_WRITE_BASE", "%X", depth_buffer.z_write_base, + "STENCIL_WRITE_BASE", "%X", depth_buffer.stencil_write_base, + "DEPTH_SIZE.PITCH_TILE_MAX", "%X", depth_buffer.depth_size.pitch_tile_max, + "DEPTH_SIZE.HEIGHT_TILE_MAX", "%X", depth_buffer.depth_size.height_tile_max, + "DEPTH_SLICE.TILE_MAX", "%X", depth_buffer.depth_slice.tile_max, + "Pitch()", "%X", depth_buffer.Pitch(), + "Height()", "%X", depth_buffer.Height(), + "Address()", "%X", depth_buffer.Address(), + "NumSamples()", "%X", depth_buffer.NumSamples(), + "NumBits()", "%X", depth_buffer.NumBits(), + "GetDepthSliceSize()", "%X", depth_buffer.GetDepthSliceSize() + ); + // clang-format on + EndTable(); + } + SeparatorText("Depth control"); + if (BeginTable("DEPTH_CONTROL", 2, ImGuiTableFlags_Borders)) { + TableNextRow(); + + // clang-format off + DrawMultipleRow( + "STENCIL_ENABLE", "%X", depth_control.stencil_enable, + "DEPTH_ENABLE", "%X", depth_control.depth_enable, + "DEPTH_WRITE_ENABLE", "%X", depth_control.depth_write_enable, + "DEPTH_BOUNDS_ENABLE", "%X", depth_control.depth_bounds_enable + ); + DrawEnumRow("DEPTH_FUNC", depth_control.depth_func.Value()); + DrawRow("BACKFACE_ENABLE", "%X", depth_control.backface_enable); + DrawEnumRow("STENCIL_FUNC", depth_control.stencil_ref_func.Value()); + DrawEnumRow("STENCIL_FUNC_BF", depth_control.stencil_bf_func.Value()); + DrawMultipleRow( + "ENABLE_COLOR_WRITES_ON_DEPTH_FAIL", "%X", depth_control.enable_color_writes_on_depth_fail, + "DISABLE_COLOR_WRITES_ON_DEPTH_PASS", "%X", depth_control.disable_color_writes_on_depth_pass + ); + // clang-format on + + EndTable(); + } +} + +RegPopup::RegPopup() { + static int unique_id = 0; + id = unique_id++; +} + +void RegPopup::SetData(AmdGpu::Liverpool::ColorBuffer color_buffer, u32 batch_id, u32 cb_id) { + this->data = color_buffer; + this->title = fmt::format("Batch #{} CB #{}", batch_id, cb_id); +} + +void RegPopup::SetData(AmdGpu::Liverpool::DepthBuffer depth_buffer, + AmdGpu::Liverpool::DepthControl depth_control, u32 batch_id) { + this->data = std::make_tuple(depth_buffer, depth_control); + this->title = fmt::format("Batch #{} Depth", batch_id); +} + +void RegPopup::Draw() { + + char name[128]; + snprintf(name, sizeof(name), "%s###reg_popup_%d", title.c_str(), id); + + SetNextWindowSize({250.0f, 300.0f}, ImGuiCond_FirstUseEver); + if (Begin(name, &open, ImGuiWindowFlags_NoSavedSettings)) { + if (const auto* buffer = std::get_if(&data)) { + DrawColorBuffer(*buffer); + } else if (const auto* depth_data = std::get_if(&data)) { + DrawDepthBuffer(*depth_data); + } + } + End(); +} + +} // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/reg_popup.h b/src/core/devtools/widget/reg_popup.h new file mode 100644 index 00000000..ba4224d7 --- /dev/null +++ b/src/core/devtools/widget/reg_popup.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/types.h" +#include "video_core/renderer_vulkan/liverpool_to_vk.h" + +namespace Core::Devtools::Widget { + +class RegPopup { + int id; + + using DepthBuffer = std::tuple; + + std::variant data; + std::string title{}; + + void DrawColorBuffer(const AmdGpu::Liverpool::ColorBuffer& buffer); + + void DrawDepthBuffer(const DepthBuffer& depth_data); + +public: + bool open = false; + + RegPopup(); + + void SetData(AmdGpu::Liverpool::ColorBuffer color_buffer, u32 batch_id, u32 cb_id); + + void SetData(AmdGpu::Liverpool::DepthBuffer depth_buffer, + AmdGpu::Liverpool::DepthControl depth_control, u32 batch_id); + + void Draw(); +}; + +} // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/reg_view.cpp b/src/core/devtools/widget/reg_view.cpp new file mode 100644 index 00000000..2e8bb8f5 --- /dev/null +++ b/src/core/devtools/widget/reg_view.cpp @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "common/io_file.h" +#include "core/devtools/options.h" +#include "imgui_internal.h" +#include "reg_view.h" + +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif + +using namespace ImGui; +using magic_enum::enum_name; + +static std::optional exec_cli(const char* cli) { + std::array buffer{}; + std::string output; + const auto f = popen(cli, "r"); + if (!f) { + pclose(f); + return {}; + } + while (fgets(buffer.data(), buffer.size(), f)) { + output += buffer.data(); + } + pclose(f); + return output; +} + +namespace Core::Devtools::Widget { + +void RegView::ProcessShader(int shader_id) { + auto shader = data.stages[shader_id]; + + std::string shader_dis; + + if (Options.disassembly_cli.empty()) { + shader_dis = "No disassembler set"; + } else { + auto bin_path = std::filesystem::temp_directory_path() / "shadps4_tmp_shader.bin"; + + constexpr std::string_view src_arg = "{src}"; + std::string cli = Options.disassembly_cli; + const auto pos = cli.find(src_arg); + if (pos == std::string::npos) { + DebugState.ShowDebugMessage("Disassembler CLI does not contain {src} argument"); + } else { + cli.replace(pos, src_arg.size(), "\"" + bin_path.string() + "\""); + Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Write); + file.Write(shader.code); + file.Close(); + + auto result = exec_cli(cli.c_str()); + shader_dis = result.value_or("Could not disassemble shader"); + if (shader_dis.empty()) { + shader_dis = "Disassembly empty or failed"; + } + + std::filesystem::remove(bin_path); + } + } + + MemoryEditor hex_view; + hex_view.Open = true; + hex_view.ReadOnly = true; + hex_view.Cols = 16; + hex_view.OptShowAscii = false; + + TextEditor dis_view; + dis_view.SetPalette(TextEditor::GetDarkPalette()); + dis_view.SetReadOnly(true); + dis_view.SetText(shader_dis); + + ShaderCache cache{ + .hex_view = hex_view, + .dis_view = dis_view, + .user_data = shader.user_data.user_data, + }; + shader_decomp.emplace(shader_id, std::move(cache)); +} +void RegView::SelectShader(int id) { + selected_shader = id; + if (!shader_decomp.contains(id)) { + ProcessShader(id); + } +} + +void RegView::DrawRegs() { + const auto& regs = data.regs; + + if (BeginTable("REGS", 2, ImGuiTableFlags_Borders)) { + + auto& s = regs.screen_scissor; + DrawRow("Scissor", "(%d, %d, %d, %d)", s.top_left_x, s.top_left_y, s.bottom_right_x, + s.bottom_right_y); + + auto cc_mode = regs.color_control.mode.Value(); + DrawRow("Color control", "%X (%s)", cc_mode, enum_name(cc_mode).data()); + + const auto open_new_popup = [&](int cb, auto... args) { + if (GetIO().KeyShift) { + auto& pop = extra_reg_popup.emplace_back(); + pop.SetData(args...); + pop.open = true; + } else if (last_selected_cb == cb && default_reg_popup.open) { + default_reg_popup.open = false; + } else { + last_selected_cb = cb; + default_reg_popup.SetData(args...); + if (!default_reg_popup.open) { + default_reg_popup.open = true; + auto popup_pos = + GetCurrentContext()->LastItemData.Rect.Max + ImVec2(5.0f, 0.0f); + SetNextWindowPos(popup_pos, ImGuiCond_Always); + default_reg_popup.Draw(); + } + } + }; + + for (int cb = 0; cb < AmdGpu::Liverpool::NumColorBuffers; ++cb) { + PushID(cb); + + TableNextRow(); + TableNextColumn(); + + const auto& buffer = regs.color_buffers[cb]; + + Text("Color buffer %d", cb); + TableNextColumn(); + if (!buffer || !regs.color_target_mask.GetMask(cb)) { + TextUnformatted("N/A"); + } else { + const char* text = last_selected_cb == cb && default_reg_popup.open ? "x" : "->"; + if (SmallButton(text)) { + open_new_popup(cb, buffer, batch_id, cb); + } + } + + PopID(); + } + + TableNextRow(); + TableNextColumn(); + TextUnformatted("Depth buffer"); + TableNextColumn(); + if (regs.depth_buffer.Address() == 0 || !regs.depth_control.depth_enable) { + TextUnformatted("N/A"); + } else { + constexpr auto depth_id = 0xF3; + const char* text = last_selected_cb == depth_id && default_reg_popup.open ? "x" : "->"; + if (SmallButton(text)) { + open_new_popup(depth_id, regs.depth_buffer, regs.depth_control, batch_id); + } + } + + EndTable(); + } +} + +RegView::RegView() { + static int unique_id = 0; + id = unique_id++; + + char name[128]; + snprintf(name, sizeof(name), "BatchView###reg_dump_%d", id); + SetNextWindowPos({400.0f, 200.0f}); + SetNextWindowSize({450.0f, 500.0f}); + ImGuiID root_dock_id; + Begin(name); + { + char dock_name[64]; + snprintf(dock_name, sizeof(dock_name), "BatchView###reg_dump_%d/dock_space", id); + root_dock_id = ImHashStr(dock_name); + DockSpace(root_dock_id); + } + End(); + + ImGuiID up1, down1; + + DockBuilderRemoveNodeChildNodes(root_dock_id); + DockBuilderSplitNode(root_dock_id, ImGuiDir_Up, 0.2f, &up1, &down1); + + snprintf(name, sizeof(name), "User data###reg_dump_%d/user_data", id); + DockBuilderDockWindow(name, up1); + + snprintf(name, sizeof(name), "Regs###reg_dump_%d/regs", id); + DockBuilderDockWindow(name, down1); + + snprintf(name, sizeof(name), "Disassembly###reg_dump_%d/disassembly", id); + DockBuilderDockWindow(name, down1); + + DockBuilderFinish(root_dock_id); +} + +void RegView::SetData(DebugStateType::RegDump data, u32 batch_id) { + this->data = std::move(data); + this->batch_id = batch_id; + // clear cache + selected_shader = -1; + shader_decomp.clear(); + default_reg_popup.open = false; + extra_reg_popup.clear(); +} + +void RegView::Draw() { + + char name[128]; + snprintf(name, sizeof(name), "BatchView %u###reg_dump_%d", batch_id, id); + if (Begin(name, &open, ImGuiWindowFlags_MenuBar)) { + const char* names[] = {"vs", "ps", "gs", "es", "hs", "ls"}; + + if (BeginMenuBar()) { + if (BeginMenu("Stage")) { + for (int i = 0; i < DebugStateType::RegDump::MaxShaderStages; i++) { + if (data.regs.stage_enable.IsStageEnabled(i)) { + bool selected = selected_shader == i; + if (Selectable(names[i], &selected)) { + SelectShader(i); + } + } + } + ImGui::EndMenu(); + } + if (BeginMenu("Windows")) { + Checkbox("Registers", &show_registers); + Checkbox("User data", &show_user_data); + Checkbox("Disassembly", &show_disassembly); + ImGui::EndMenu(); + } + EndMenuBar(); + } + + char dock_name[64]; + snprintf(dock_name, sizeof(dock_name), "BatchView###reg_dump_%d/dock_space", id); + auto root_dock_id = ImHashStr(dock_name); + DockSpace(root_dock_id); + } + End(); + + auto get_shader = [&]() -> ShaderCache* { + auto shader_cache = shader_decomp.find(selected_shader); + if (shader_cache == shader_decomp.end()) { + return nullptr; + } + return &shader_cache->second; + }; + + if (show_user_data) { + snprintf(name, sizeof(name), "User data###reg_dump_%d/user_data", id); + if (Begin(name, &show_user_data)) { + auto shader = get_shader(); + if (!shader) { + Text("Select a stage"); + } else { + shader->hex_view.DrawContents(shader->user_data.data(), shader->user_data.size()); + } + } + End(); + } + + if (show_disassembly) { + snprintf(name, sizeof(name), "Disassembly###reg_dump_%d/disassembly", id); + if (Begin(name, &show_disassembly)) { + auto shader = get_shader(); + if (!shader) { + Text("Select a stage"); + } else { + shader->dis_view.Render("Disassembly", GetContentRegionAvail()); + } + } + End(); + } + + if (show_registers) { + snprintf(name, sizeof(name), "Regs###reg_dump_%d/regs", id); + if (Begin(name, &show_registers)) { + DrawRegs(); + } + End(); + } + + if (default_reg_popup.open) { + default_reg_popup.Draw(); + } + for (auto it = extra_reg_popup.begin(); it != extra_reg_popup.end();) { + if (!it->open) { + it = extra_reg_popup.erase(it); + continue; + } + it->Draw(); + ++it; + } +} + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/reg_view.h b/src/core/devtools/widget/reg_view.h new file mode 100644 index 00000000..67ab1e04 --- /dev/null +++ b/src/core/devtools/widget/reg_view.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include "core/debug_state.h" +#include "imgui_memory_editor.h" +#include "reg_popup.h" +#include "text_editor.h" + +namespace Core::Devtools::Widget { + +struct ShaderCache { + MemoryEditor hex_view; + TextEditor dis_view; + Vulkan::Liverpool::UserData user_data; +}; + +class RegView { + int id; + + DebugStateType::RegDump data; + u32 batch_id{~0u}; + + std::unordered_map shader_decomp; + int selected_shader{-1}; + RegPopup default_reg_popup; + int last_selected_cb{-1}; + std::vector extra_reg_popup; + + bool show_registers{true}; + bool show_user_data{true}; + bool show_disassembly{true}; + + void ProcessShader(int shader_id); + + void SelectShader(int shader_id); + + void DrawRegs(); + +public: + bool open = false; + + RegView(); + + void SetData(DebugStateType::RegDump data, u32 batch_id); + + void Draw(); +}; + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/text_editor.cpp b/src/core/devtools/widget/text_editor.cpp new file mode 100644 index 00000000..f447e45a --- /dev/null +++ b/src/core/devtools/widget/text_editor.cpp @@ -0,0 +1,2334 @@ +// SPDX-FileCopyrightText: Copyright (c) 2017 BalazsJako +// SPDX-License-Identifier: MIT + +// source: https://github.com/BalazsJako/ImGuiColorTextEdit + +#include +#include +#include +#include +#include + +#include "text_editor.h" + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" // for imGui::GetCurrentWindow() + +// TODO +// - multiline comments vs single-line: latter is blocking start of a ML + +namespace Core::Devtools::Widget { + +template +bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p) { + for (; first1 != last1 && first2 != last2; ++first1, ++first2) { + if (!p(*first1, *first2)) + return false; + } + return first1 == last1 && first2 == last2; +} + +TextEditor::TextEditor() + : mLineSpacing(1.0f), mUndoIndex(0), mTabSize(4), mOverwrite(false), mReadOnly(false), + mWithinRender(false), mScrollToCursor(false), mScrollToTop(false), mTextChanged(false), + mColorizerEnabled(true), mTextStart(20.0f), mLeftMargin(10), mCursorPositionChanged(false), + mColorRangeMin(0), mColorRangeMax(0), mSelectionMode(SelectionMode::Normal), + mCheckComments(true), mLastClick(-1.0f), mHandleKeyboardInputs(true), + mHandleMouseInputs(true), mIgnoreImGuiChild(false), mShowWhitespaces(true), + mStartTime(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()) { + SetPalette(GetDarkPalette()); + mLines.push_back(Line()); +} + +TextEditor::~TextEditor() {} + +void TextEditor::SetLanguageDefinition(const LanguageDefinition& aLanguageDef) { + mLanguageDefinition = aLanguageDef; + mRegexList.clear(); + + for (auto& r : mLanguageDefinition.mTokenRegexStrings) + mRegexList.push_back( + std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); +} + +void TextEditor::SetPalette(const Palette& aValue) { + mPaletteBase = aValue; +} + +std::string TextEditor::GetText(const Coordinates& aStart, const Coordinates& aEnd) const { + std::string result; + + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; + + for (size_t i = lstart; i < lend; i++) + s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) { + if (lstart >= (int)mLines.size()) + break; + + auto& line = mLines[lstart]; + if (istart < (int)line.size()) { + result += line[istart].mChar; + istart++; + } else { + istart = 0; + ++lstart; + result += '\n'; + } + } + + return result; +} + +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const { + return SanitizeCoordinates(mState.mCursorPosition); +} + +TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates& aValue) const { + auto line = aValue.mLine; + auto column = aValue.mColumn; + if (line >= (int)mLines.size()) { + if (mLines.empty()) { + line = 0; + column = 0; + } else { + line = (int)mLines.size() - 1; + column = GetLineMaxColumn(line); + } + return Coordinates(line, column); + } else { + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return Coordinates(line, column); + } +} + +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code +// sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) { + if ((c & 0xFE) == 0xFC) + return 6; + if ((c & 0xFC) == 0xF8) + return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) { + if (c < 0x80) { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) { + if (buf_size < 2) + return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) { + if (buf_size < 4) + return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c) & 0x3f)); + return 4; + } + // else if (c < 0x10000) + { + if (buf_size < 3) + return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c) & 0x3f)); + return 3; + } +} + +void TextEditor::Advance(Coordinates& aCoordinates) const { + if (aCoordinates.mLine < (int)mLines.size()) { + auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); + + if (cindex + 1 < (int)line.size()) { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int)line.size() - 1); + } else { + ++aCoordinates.mLine; + cindex = 0; + } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); + } +} + +void TextEditor::DeleteRange(const Coordinates& aStart, const Coordinates& aEnd) { + ASSERT(aEnd >= aStart); + ASSERT(!mReadOnly); + + // printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn); + + if (aEnd == aStart) + return; + + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + + if (aStart.mLine == aEnd.mLine) { + auto& line = mLines[aStart.mLine]; + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); + else + line.erase(line.begin() + start, line.begin() + end); + } else { + auto& firstLine = mLines[aStart.mLine]; + auto& lastLine = mLines[aEnd.mLine]; + + firstLine.erase(firstLine.begin() + start, firstLine.end()); + lastLine.erase(lastLine.begin(), lastLine.begin() + end); + + if (aStart.mLine < aEnd.mLine) + firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + + if (aStart.mLine < aEnd.mLine) + RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + } + + mTextChanged = true; +} + +int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char* aValue) { + ASSERT(!mReadOnly); + + int cindex = GetCharacterIndex(aWhere); + int totalLines = 0; + while (*aValue != '\0') { + ASSERT(!mLines.empty()); + + if (*aValue == '\r') { + // skip + ++aValue; + } else if (*aValue == '\n') { + if (cindex < (int)mLines[aWhere.mLine].size()) { + auto& newLine = InsertLine(aWhere.mLine + 1); + auto& line = mLines[aWhere.mLine]; + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); + } else { + InsertLine(aWhere.mLine + 1); + } + ++aWhere.mLine; + aWhere.mColumn = 0; + cindex = 0; + ++totalLines; + ++aValue; + } else { + auto& line = mLines[aWhere.mLine]; + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); + ++aWhere.mColumn; + } + + mTextChanged = true; + } + + return totalLines; +} + +void TextEditor::AddUndo(UndoRecord& aValue) { + ASSERT(!mReadOnly); + // printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n", + // aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn, + // aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, + // aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, aValue.mRemoved.c_str(), + // aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine, + // aValue.mRemovedEnd.mColumn, aValue.mAfter.mCursorPosition.mLine, + // aValue.mAfter.mCursorPosition.mColumn + // ); + + mUndoBuffer.resize((size_t)(mUndoIndex + 1)); + mUndoBuffer.back() = aValue; + ++mUndoIndex; +} + +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const { + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); + + int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); + + int columnCoord = 0; + + if (lineNo >= 0 && lineNo < (int)mLines.size()) { + auto& line = mLines.at(lineNo); + + int columnIndex = 0; + float columnX = 0.0f; + + while ((size_t)columnIndex < line.size()) { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') { + float spaceSize = + ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; + float oldX = columnX; + float newColumnX = + (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } else { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) + buf[i++] = line[columnIndex++].mChar; + buf[i] = '\0'; + columnWidth = + ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX += columnWidth; + columnCoord++; + } + } + } + + return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); +} + +TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + while (cindex > 0 && isspace(line[cindex].mChar)) + --cindex; + + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex > 0) { + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) { + cindex++; + break; + } + if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex) + break; + } + --cindex; + } + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + bool prevspace = (bool)isspace(line[cindex].mChar); + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex < (int)line.size()) { + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex)line[cindex].mColorIndex) + break; + + if (prevspace != !!isspace(c)) { + if (isspace(c)) + while (cindex < (int)line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; + } + return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int)mLines[at.mLine].size()) { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar); + skip = isword; + } + + while (!isword || skip) { + if (at.mLine >= mLines.size()) { + auto l = std::max(0, (int)mLines.size() - 1); + return Coordinates(l, GetLineMaxColumn(l)); + } + + auto& line = mLines[at.mLine]; + if (cindex < (int)line.size()) { + isword = isalnum(line[cindex].mChar); + + if (isword && !skip) + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); + + if (!isword) + skip = false; + + cindex++; + } else { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + + return at; +} + +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const { + if (aCoordinates.mLine >= mLines.size()) + return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const { + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int)line.size()) { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const { + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) + i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const { + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size();) { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + +bool TextEditor::IsOnWordBoundary(const Coordinates& aAt) const { + if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0) + return true; + + auto& line = mLines[aAt.mLine]; + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int)line.size()) + return true; + + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); +} + +void TextEditor::RemoveLine(int aStart, int aEnd) { + ASSERT(!mReadOnly); + ASSERT(aEnd >= aStart); + ASSERT(mLines.size() > (size_t)(aEnd - aStart)); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) { + ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); + if (e.first >= aStart && e.first <= aEnd) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) { + if (i >= aStart && i <= aEnd) + continue; + btmp.insert(i >= aStart ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); + ASSERT(!mLines.empty()); + + mTextChanged = true; +} + +void TextEditor::RemoveLine(int aIndex) { + ASSERT(!mReadOnly); + ASSERT(mLines.size() > 1); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) { + ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); + if (e.first - 1 == aIndex) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) { + if (i == aIndex) + continue; + btmp.insert(i >= aIndex ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aIndex); + ASSERT(!mLines.empty()); + + mTextChanged = true; +} + +TextEditor::Line& TextEditor::InsertLine(int aIndex) { + ASSERT(!mReadOnly); + + auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + btmp.insert(i >= aIndex ? i + 1 : i); + mBreakpoints = std::move(btmp); + + return result; +} + +std::string TextEditor::GetWordUnderCursor() const { + auto c = GetCursorPosition(); + return GetWordAt(c); +} + +std::string TextEditor::GetWordAt(const Coordinates& aCoords) const { + auto start = FindWordStart(aCoords); + auto end = FindWordEnd(aCoords); + + std::string r; + + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(mLines[aCoords.mLine][it].mChar); + + return r; +} + +ImU32 TextEditor::GetGlyphColor(const Glyph& aGlyph) const { + if (!mColorizerEnabled) + return mPalette[(int)PaletteIndex::Default]; + if (aGlyph.mComment) + return mPalette[(int)PaletteIndex::Comment]; + if (aGlyph.mMultiLineComment) + return mPalette[(int)PaletteIndex::MultiLineComment]; + auto const color = mPalette[(int)aGlyph.mColorIndex]; + if (aGlyph.mPreprocessor) { + const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor]; + const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; + const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; + const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; + const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; + return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); + } + return color; +} + +void TextEditor::HandleKeyboardInputs() { + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowFocused()) { + if (ImGui::IsWindowHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); + // ImGui::CaptureKeyboardFromApp(true); + + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + + if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z))) + Undo(); + else if (!IsReadOnly() && !ctrl && !shift && alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) + Undo(); + else if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Y))) + Redo(); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow))) + MoveUp(1, shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow))) + MoveDown(1, shift); + else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow))) + MoveLeft(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow))) + MoveRight(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageUp))) + MoveUp(GetPageSize() - 4, shift); + else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageDown))) + MoveDown(GetPageSize() - 4, shift); + else if (!alt && ctrl && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home))) + MoveTop(shift); + else if (ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End))) + MoveBottom(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home))) + MoveHome(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End))) + MoveEnd(shift); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) + Delete(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) + Backspace(); + else if (!ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) + mOverwrite ^= true; + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) + Copy(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) + Copy(); + else if (!IsReadOnly() && !ctrl && shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) + Paste(); + else if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V))) + Paste(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X))) + Cut(); + else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) + Cut(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_A))) + SelectAll(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter))) + EnterCharacter('\n', false); + else if (!IsReadOnly() && !ctrl && !alt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab))) + EnterCharacter('\t', shift); + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) { + for (int i = 0; i < io.InputQueueCharacters.Size; i++) { + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) + EnterCharacter(c, shift); + } + io.InputQueueCharacters.resize(0); + } + } +} + +void TextEditor::HandleMouseInputs() { + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowHovered()) { + if (!shift && !alt) { + auto click = ImGui::IsMouseClicked(0); + auto doubleClick = ImGui::IsMouseDoubleClicked(0); + auto t = ImGui::GetTime(); + auto tripleClick = click && !doubleClick && + (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); + + /* + Left mouse button triple click + */ + + if (tripleClick) { + if (!ctrl) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = -1.0f; + } + + /* + Left mouse button double click + */ + + else if (doubleClick) { + if (!ctrl) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = (float)ImGui::GetTime(); + } + + /* + Left mouse button click + */ + else if (click) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + if (ctrl) + mSelectionMode = SelectionMode::Word; + else + mSelectionMode = SelectionMode::Normal; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + + mLastClick = (float)ImGui::GetTime(); + } + // Mouse left button dragging (=> update selection) + else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) { + io.WantCaptureMouse = true; + mState.mCursorPosition = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + } + } +} + +void TextEditor::Render() { + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr) + .x; + mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); + + /* Update palette with the current alpha from style */ + for (int i = 0; i < (int)PaletteIndex::Max; ++i) { + auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + color.w *= ImGui::GetStyle().Alpha; + mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); + } + + ASSERT(mLineBuffer.empty()); + + auto contentSize = ImGui::GetWindowContentRegionMax(); + auto drawList = ImGui::GetWindowDrawList(); + float longest(mTextStart); + + if (mScrollToTop) { + mScrollToTop = false; + ImGui::SetScrollY(0.f); + } + + ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); + auto scrollX = ImGui::GetScrollX(); + auto scrollY = ImGui::GetScrollY(); + + auto lineNo = (int)floor(scrollY / mCharAdvance.y); + auto globalLineMax = (int)mLines.size(); + auto lineMax = + std::max(0, std::min((int)mLines.size() - 1, + lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y))); + + // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width + char buf[16]; + snprintf(buf, 16, " %d ", globalLineMax); + mTextStart = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr) + .x + + mLeftMargin; + + if (!mLines.empty()) { + float spaceSize = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr) + .x; + + while (lineNo <= lineMax) { + ImVec2 lineStartScreenPos = + ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y); + ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); + + auto& line = mLines[lineNo]; + longest = std::max( + mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), + longest); + auto columnNo = 0; + Coordinates lineStartCoord(lineNo, 0); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); + + // Draw selection for the current line + float sstart = -1.0f; + float ssend = -1.0f; + + ASSERT(mState.mSelectionStart <= mState.mSelectionEnd); + if (mState.mSelectionStart <= lineEndCoord) + sstart = mState.mSelectionStart > lineStartCoord + ? TextDistanceToLineStart(mState.mSelectionStart) + : 0.0f; + if (mState.mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart( + mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord); + + if (mState.mSelectionEnd.mLine > lineNo) + ssend += mCharAdvance.x; + + if (sstart != -1 && ssend != -1 && sstart < ssend) { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]); + } + + // Draw breakpoints + auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); + + if (mBreakpoints.count(lineNo + 1) != 0) { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::Breakpoint]); + } + + // Draw error markers + auto errorIt = mErrorMarkers.find(lineNo + 1); + if (errorIt != mErrorMarkers.end()) { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]); + + if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", errorIt->first); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); + ImGui::Text("%s", errorIt->second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + + // Draw line number (right aligned) + snprintf(buf, 16, "%d ", lineNo + 1); + + auto lineNoWidth = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr) + .x; + drawList->AddText( + ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), + mPalette[(int)PaletteIndex::LineNumber], buf); + + if (mState.mCursorPosition.mLine == lineNo) { + auto focused = ImGui::IsWindowFocused(); + + // Highlight the current line (where the cursor is) + if (!HasSelection()) { + auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y); + drawList->AddRectFilled( + start, end, + mPalette[(int)(focused ? PaletteIndex::CurrentLineFill + : PaletteIndex::CurrentLineFillInactive)]); + drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], + 1.0f); + } + + // Render the cursor + if (focused) { + auto timeEnd = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + auto elapsed = timeEnd - mStartTime; + if (elapsed > 400) { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int)line.size()) { + auto c = line[cindex].mChar; + if (c == '\t') { + auto x = (1.0f + + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + width = x - cx; + } else { + char buf2[2]; + buf2[0] = line[cindex].mChar; + buf2[1] = '\0'; + width = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2) + .x; + } + } + ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); + ImVec2 cend(textScreenPos.x + cx + width, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); + if (elapsed > 800) + mStartTime = timeEnd; + } + } + } + + // Render colorized text + auto prevColor = + line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]); + ImVec2 bufferOffset; + + for (int i = 0; i < line.size();) { + auto& glyph = line[i]; + auto color = GetGlyphColor(glyph); + + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && + !mLineBuffer.empty()) { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, + textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = + ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, + mLineBuffer.c_str(), nullptr, nullptr); + bufferOffset.x += textSize.x; + mLineBuffer.clear(); + } + prevColor = color; + + if (glyph.mChar == '\t') { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / + (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } else if (glyph.mChar == ' ') { + if (mShowWhitespaces) { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } else { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) + mLineBuffer.push_back(line[i++].mChar); + } + ++columnNo; + } + + if (!mLineBuffer.empty()) { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, + textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); + } + + ++lineNo; + } + + // Draw a tooltip on known identifiers/preprocessor symbols + if (ImGui::IsMousePosValid()) { + auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); + if (!id.empty()) { + auto it = mLanguageDefinition.mIdentifiers.find(id); + if (it != mLanguageDefinition.mIdentifiers.end()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(it->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } else { + auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); + if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + } + } + } + } + + ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y)); + + if (mScrollToCursor) { + EnsureCursorVisible(); + ImGui::SetWindowFocus(); + mScrollToCursor = false; + } +} + +void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) { + mWithinRender = true; + mTextChanged = false; + mCursorPositionChanged = false; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, + ImGuiWindowFlags_HorizontalScrollbar | + ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) { + HandleKeyboardInputs(); + ImGui::PushAllowKeyboardFocus(true); + } + + if (mHandleMouseInputs) + HandleMouseInputs(); + + ColorizeInternal(); + Render(); + + if (mHandleKeyboardInputs) + ImGui::PopAllowKeyboardFocus(); + + if (!mIgnoreImGuiChild) + ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + mWithinRender = false; +} + +void TextEditor::SetText(const std::string& aText) { + mLines.clear(); + mLines.emplace_back(Line()); + for (auto chr : aText) { + if (chr == '\r') { + // ignore the carriage return character + } else if (chr == '\n') + mLines.emplace_back(Line()); + else { + mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::SetTextLines(const std::vector& aLines) { + mLines.clear(); + + if (aLines.empty()) { + mLines.emplace_back(Line()); + } else { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) { + const std::string& aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (size_t j = 0; j < aLine.size(); ++j) + mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) { + ASSERT(!mReadOnly); + + UndoRecord u; + + u.mBefore = mState; + + if (HasSelection()) { + if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) { + + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + auto originalEnd = end; + + if (start > end) + std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) + --end.mLine; + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); + + // if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + + u.mRemovedStart = start; + u.mRemovedEnd = end; + u.mRemoved = GetText(start, end); + + bool modified = false; + + for (int i = start.mLine; i <= end.mLine; i++) { + auto& line = mLines[i]; + if (aShift) { + if (!line.empty()) { + if (line.front().mChar == '\t') { + line.erase(line.begin()); + modified = true; + } else { + for (int j = 0; + j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) { + line.erase(line.begin()); + modified = true; + } + } + } + } else { + line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } + } + + if (modified) { + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + if (originalEnd.mColumn != 0) { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + u.mAdded = GetText(start, end); + } else { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + + u.mAddedStart = start; + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); + + mTextChanged = true; + + EnsureCursorVisible(); + } + + return; + } // c == '\t' + else { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + } // HasSelection + + auto coord = GetActualCursorCoordinates(); + u.mAddedStart = coord; + + ASSERT(!mLines.empty()); + + if (aChar == '\n') { + InsertLine(coord.mLine + 1); + auto& line = mLines[coord.mLine]; + auto& newLine = mLines[coord.mLine + 1]; + + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; + it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) + newLine.push_back(line[it]); + + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + newLine.insert(newLine.end(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.begin() + line.size()); + SetCursorPosition( + Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize))); + u.mAdded = (char)aChar; + } else { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < (int)line.size()) { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = + Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } else + return; + } + + mTextChanged = true; + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + + AddUndo(u); + + Colorize(coord.mLine - 1, 3); + EnsureCursorVisible(); +} + +void TextEditor::SetReadOnly(bool aValue) { + mReadOnly = aValue; +} + +void TextEditor::SetColorizerEnable(bool aValue) { + mColorizerEnabled = aValue; +} + +void TextEditor::SetCursorPosition(const Coordinates& aPosition) { + if (mState.mCursorPosition != aPosition) { + mState.mCursorPosition = aPosition; + mCursorPositionChanged = true; + EnsureCursorVisible(); + } +} + +void TextEditor::SetSelectionStart(const Coordinates& aPosition) { + mState.mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelectionEnd(const Coordinates& aPosition) { + mState.mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelection(const Coordinates& aStart, const Coordinates& aEnd, + SelectionMode aMode) { + auto oldSelStart = mState.mSelectionStart; + auto oldSelEnd = mState.mSelectionEnd; + + mState.mSelectionStart = SanitizeCoordinates(aStart); + mState.mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); + + switch (aMode) { + case TextEditor::SelectionMode::Normal: + break; + case TextEditor::SelectionMode::Word: { + mState.mSelectionStart = FindWordStart(mState.mSelectionStart); + if (!IsOnWordBoundary(mState.mSelectionEnd)) + mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); + break; + } + case TextEditor::SelectionMode::Line: { + const auto lineNo = mState.mSelectionEnd.mLine; + const auto lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; + mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + break; + } + default: + break; + } + + if (mState.mSelectionStart != oldSelStart || mState.mSelectionEnd != oldSelEnd) + mCursorPositionChanged = true; +} + +void TextEditor::SetTabSize(int aValue) { + mTabSize = std::max(0, std::min(32, aValue)); +} + +void TextEditor::InsertText(const std::string& aValue) { + InsertText(aValue.c_str()); +} + +void TextEditor::InsertText(const char* aValue) { + if (aValue == nullptr) + return; + + auto pos = GetActualCursorCoordinates(); + auto start = std::min(pos, mState.mSelectionStart); + int totalLines = pos.mLine - start.mLine; + + totalLines += InsertTextAt(pos, aValue); + + SetSelection(pos, pos); + SetCursorPosition(pos); + Colorize(start.mLine - 1, totalLines + 2); +} + +void TextEditor::DeleteSelection() { + ASSERT(mState.mSelectionEnd >= mState.mSelectionStart); + + if (mState.mSelectionEnd == mState.mSelectionStart) + return; + + DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + + SetSelection(mState.mSelectionStart, mState.mSelectionStart); + SetCursorPosition(mState.mSelectionStart); + Colorize(mState.mSelectionStart.mLine, 1); +} + +void TextEditor::MoveUp(int aAmount, bool aSelect) { + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursorPosition) { + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +void TextEditor::MoveDown(int aAmount, bool aSelect) { + ASSERT(mState.mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = + std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +static bool IsUTFSequence(char c) { + return (c & 0xC0) == 0x80; +} + +void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) { + if (mLines.empty()) + return; + + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + + while (aAmount-- > 0) { + if (cindex == 0) { + if (line > 0) { + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; + } + } else { + --cindex; + if (cindex > 0) { + if ((int)mLines.size() > line) { + while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) + --cindex; + } + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) { + mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + cindex = GetCharacterIndex(mState.mCursorPosition); + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + + ASSERT(mState.mCursorPosition.mColumn >= 0); + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, + aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) { + auto oldPos = mState.mCursorPosition; + + if (mLines.empty() || oldPos.mLine >= mLines.size()) + return; + + auto cindex = GetCharacterIndex(mState.mCursorPosition); + while (aAmount-- > 0) { + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) { + if (mState.mCursorPosition.mLine < mLines.size() - 1) { + mState.mCursorPosition.mLine = + std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); + mState.mCursorPosition.mColumn = 0; + } else + return; + } else { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + } + } + + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, + aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveTop(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(0, 0)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + mInteractiveEnd = oldPos; + mInteractiveStart = mState.mCursorPosition; + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::TextEditor::MoveBottom(bool aSelect) { + auto oldPos = GetCursorPosition(); + auto newPos = Coordinates((int)mLines.size() - 1, 0); + SetCursorPosition(newPos); + if (aSelect) { + mInteractiveStart = oldPos; + mInteractiveEnd = newPos; + } else + mInteractiveStart = mInteractiveEnd = newPos; + SetSelection(mInteractiveStart, mInteractiveEnd); +} + +void TextEditor::MoveHome(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::MoveEnd(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::Delete() { + ASSERT(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } else { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + auto& line = mLines[pos.mLine]; + + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) { + if (pos.mLine == (int)mLines.size() - 1) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + Advance(u.mRemovedEnd); + + auto& nextLine = mLines[pos.mLine + 1]; + line.insert(line.end(), nextLine.begin(), nextLine.end()); + RemoveLine(pos.mLine + 1); + } else { + auto cindex = GetCharacterIndex(pos); + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + line.erase(line.begin() + cindex); + } + + mTextChanged = true; + + Colorize(pos.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::Backspace() { + ASSERT(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } else { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + + if (mState.mCursorPosition.mColumn == 0) { + if (mState.mCursorPosition.mLine == 0) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = + Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Advance(u.mRemovedEnd); + + auto& line = mLines[mState.mCursorPosition.mLine]; + auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); + prevLine.insert(prevLine.end(), line.begin(), line.end()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type( + i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + RemoveLine(mState.mCursorPosition.mLine); + --mState.mCursorPosition.mLine; + mState.mCursorPosition.mColumn = prevSize; + } else { + auto& line = mLines[mState.mCursorPosition.mLine]; + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) + --cindex; + + // if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; + + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + --u.mRemovedStart.mColumn; + --mState.mCursorPosition.mColumn; + + while (cindex < line.size() && cend-- > cindex) { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + mTextChanged = true; + + EnsureCursorVisible(); + Colorize(mState.mCursorPosition.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::SelectWordUnderCursor() { + auto c = GetCursorPosition(); + SetSelection(FindWordStart(c), FindWordEnd(c)); +} + +void TextEditor::SelectAll() { + SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0)); +} + +bool TextEditor::HasSelection() const { + return mState.mSelectionEnd > mState.mSelectionStart; +} + +void TextEditor::Copy() { + if (HasSelection()) { + ImGui::SetClipboardText(GetSelectedText().c_str()); + } else { + if (!mLines.empty()) { + std::string str; + auto& line = mLines[GetActualCursorCoordinates().mLine]; + for (auto& g : line) + str.push_back(g.mChar); + ImGui::SetClipboardText(str.c_str()); + } + } +} + +void TextEditor::Cut() { + if (IsReadOnly()) { + Copy(); + } else { + if (HasSelection()) { + UndoRecord u; + u.mBefore = mState; + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + Copy(); + DeleteSelection(); + + u.mAfter = mState; + AddUndo(u); + } + } +} + +void TextEditor::Paste() { + if (IsReadOnly()) + return; + + auto clipText = ImGui::GetClipboardText(); + if (clipText != nullptr && strlen(clipText) > 0) { + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + + u.mAdded = clipText; + u.mAddedStart = GetActualCursorCoordinates(); + + InsertText(clipText); + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + AddUndo(u); + } +} + +bool TextEditor::CanUndo() const { + return !mReadOnly && mUndoIndex > 0; +} + +bool TextEditor::CanRedo() const { + return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); +} + +void TextEditor::Undo(int aSteps) { + while (CanUndo() && aSteps-- > 0) + mUndoBuffer[--mUndoIndex].Undo(this); +} + +void TextEditor::Redo(int aSteps) { + while (CanRedo() && aSteps-- > 0) + mUndoBuffer[mUndoIndex++].Redo(this); +} + +const TextEditor::Palette& TextEditor::GetDarkPalette() { + const static Palette p = {{ + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // Char literal + 0xffffffff, // Punctuation + 0xff408080, // Preprocessor + 0xffaaaaaa, // Identifier + 0xff9bc64d, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff206020, // Comment (single line) + 0xff406020, // Comment (multi line) + 0xff101010, // Background + 0xffe0e0e0, // Cursor + 0x80a06020, // Selection + 0x800020ff, // ErrorMarker + 0x40f08000, // Breakpoint + 0xff707000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40a0a0a0, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetLightPalette() { + const static Palette p = {{ + 0xff7f7f7f, // None + 0xffff0c06, // Keyword + 0xff008000, // Number + 0xff2020a0, // String + 0xff304070, // Char literal + 0xff000000, // Punctuation + 0xff406060, // Preprocessor + 0xff404040, // Identifier + 0xff606010, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff205020, // Comment (single line) + 0xff405020, // Comment (multi line) + 0xffffffff, // Background + 0xff000000, // Cursor + 0x80600000, // Selection + 0xa00010ff, // ErrorMarker + 0x80f08000, // Breakpoint + 0xff505000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetRetroBluePalette() { + const static Palette p = {{ + 0xff00ffff, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // Identifier + 0xffffffff, // Known identifier + 0xffff00ff, // Preproc identifier + 0xff808080, // Comment (single line) + 0xff404040, // Comment (multi line) + 0xff800000, // Background + 0xff0080ff, // Cursor + 0x80ffff00, // Selection + 0xa00000ff, // ErrorMarker + 0x80ff8000, // Breakpoint + 0xff808000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + +std::string TextEditor::GetText() const { + return GetText(Coordinates(), Coordinates((int)mLines.size(), 0)); +} + +std::vector TextEditor::GetTextLines() const { + std::vector result; + + result.reserve(mLines.size()); + + for (auto& line : mLines) { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) + text[i] = line[i].mChar; + + result.emplace_back(std::move(text)); + } + + return result; +} + +std::string TextEditor::GetSelectedText() const { + return GetText(mState.mSelectionStart, mState.mSelectionEnd); +} + +std::string TextEditor::GetCurrentLineText() const { + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText(Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); +} + +void TextEditor::ProcessInputs() {} + +void TextEditor::Colorize(int aFromLine, int aLines) { + int toLine = + aLines == -1 ? (int)mLines.size() : std::min((int)mLines.size(), aFromLine + aLines); + mColorRangeMin = std::min(mColorRangeMin, aFromLine); + mColorRangeMax = std::max(mColorRangeMax, toLine); + mColorRangeMin = std::max(0, mColorRangeMin); + mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); + mCheckComments = true; +} + +void TextEditor::ColorizeRange(int aFromLine, int aToLine) { + if (mLines.empty() || aFromLine >= aToLine) + return; + + std::string buffer; + std::cmatch results; + std::string id; + + int endLine = std::max(0, std::min((int)mLines.size(), aToLine)); + for (int i = aFromLine; i < endLine; ++i) { + auto& line = mLines[i]; + + if (line.empty()) + continue; + + buffer.resize(line.size()); + for (size_t j = 0; j < line.size(); ++j) { + auto& col = line[j]; + buffer[j] = col.mChar; + col.mColorIndex = PaletteIndex::Default; + } + + const char* bufferBegin = &buffer.front(); + const char* bufferEnd = bufferBegin + buffer.size(); + + auto last = bufferEnd; + + for (auto first = bufferBegin; first != last;) { + const char* token_begin = nullptr; + const char* token_end = nullptr; + PaletteIndex token_color = PaletteIndex::Default; + + bool hasTokenizeResult = false; + + if (mLanguageDefinition.mTokenize != nullptr) { + if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color)) + hasTokenizeResult = true; + } + + if (hasTokenizeResult == false) { + // todo : remove + // printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), + // first); + + for (auto& p : mRegexList) { + if (std::regex_search(first, last, results, p.first, + std::regex_constants::match_continuous)) { + hasTokenizeResult = true; + + auto& v = *results.begin(); + token_begin = v.first; + token_end = v.second; + token_color = p.second; + break; + } + } + } + + if (hasTokenizeResult == false) { + first++; + } else { + const size_t token_length = token_end - token_begin; + + if (token_color == PaletteIndex::Identifier) { + id.assign(token_begin, token_end); + + // todo : allmost all language definitions use lower case to specify keywords, + // so shouldn't this use ::tolower ? + if (!mLanguageDefinition.mCaseSensitive) + std::transform(id.begin(), id.end(), id.begin(), ::toupper); + + if (!line[first - bufferBegin].mPreprocessor) { + if (mLanguageDefinition.mKeywords.count(id) != 0) + token_color = PaletteIndex::Keyword; + else if (mLanguageDefinition.mIdentifiers.count(id) != 0) + token_color = PaletteIndex::KnownIdentifier; + else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } else { + if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + } + + for (size_t j = 0; j < token_length; ++j) + line[(token_begin - bufferBegin) + j].mColorIndex = token_color; + + first = token_end; + } + } + } +} + +void TextEditor::ColorizeInternal() { + if (mLines.empty() || !mColorizerEnabled) + return; + + if (mCheckComments) { + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; + auto withinString = false; + auto withinSingleLineComment = false; + auto withinPreproc = false; + auto firstChar = true; // there is no other non-whitespace characters in the line before + auto concatenate = false; // '\' on the very end of the line + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) { + auto& line = mLines[currentLine]; + + if (currentIndex == 0 && !concatenate) { + withinSingleLineComment = false; + withinPreproc = false; + firstChar = true; + } + + concatenate = false; + + if (!line.empty()) { + auto& g = line[currentIndex]; + auto c = g.mChar; + + if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) + firstChar = false; + + if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\') + concatenate = true; + + bool inComment = + (commentStartLine < currentLine || + (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + if (withinString) { + line[currentIndex].mMultiLineComment = inComment; + + if (c == '\"') { + if (currentIndex + 1 < (int)line.size() && + line[currentIndex + 1].mChar == '\"') { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } else + withinString = false; + } else if (c == '\\') { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + } else { + if (firstChar && c == mLanguageDefinition.mPreprocChar) + withinPreproc = true; + + if (c == '\"') { + withinString = true; + line[currentIndex].mMultiLineComment = inComment; + } else { + auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; + auto from = line.begin() + currentIndex; + auto& startStr = mLanguageDefinition.mCommentStart; + auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + + if (singleStartStr.size() > 0 && + currentIndex + singleStartStr.size() <= line.size() && + equals(singleStartStr.begin(), singleStartStr.end(), from, + from + singleStartStr.size(), pred)) { + withinSingleLineComment = true; + } else if (!withinSingleLineComment && + currentIndex + startStr.size() <= line.size() && + equals(startStr.begin(), startStr.end(), from, + from + startStr.size(), pred)) { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } + + inComment = inComment = + (commentStartLine < currentLine || (commentStartLine == currentLine && + commentStartIndex <= currentIndex)); + + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; + + auto& endStr = mLanguageDefinition.mCommentEnd; + if (currentIndex + 1 >= (int)endStr.size() && + equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, + pred)) { + commentStartIndex = endIndex; + commentStartLine = endLine; + } + } + } + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= (int)line.size()) { + currentIndex = 0; + ++currentLine; + } + } else { + currentIndex = 0; + ++currentLine; + } + } + mCheckComments = false; + } + + if (mColorRangeMin < mColorRangeMax) { + const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; + const int to = std::min(mColorRangeMin + increment, mColorRangeMax); + ColorizeRange(mColorRangeMin, to); + mColorRangeMin = to; + + if (mColorRangeMax == mColorRangeMin) { + mColorRangeMin = std::numeric_limits::max(); + mColorRangeMax = 0; + } + return; + } +} + +float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const { + auto& line = mLines[aFrom.mLine]; + float distance = 0.0f; + float spaceSize = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr) + .x; + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex;) { + if (line[it].mChar == '\t') { + distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + ++it; + } else { + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++) + tempCString[i] = line[it].mChar; + + tempCString[i] = '\0'; + distance += ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString, + nullptr, nullptr) + .x; + } + } + + return distance; +} + +void TextEditor::EnsureCursorVisible() { + if (!mWithinRender) { + mScrollToCursor = true; + return; + } + + float scrollX = ImGui::GetScrollX(); + float scrollY = ImGui::GetScrollY(); + + auto height = ImGui::GetWindowHeight(); + auto width = ImGui::GetWindowWidth(); + + auto top = 1 + (int)ceil(scrollY / mCharAdvance.y); + auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y); + + auto left = (int)ceil(scrollX / mCharAdvance.x); + auto right = (int)ceil((scrollX + width) / mCharAdvance.x); + + auto pos = GetActualCursorCoordinates(); + auto len = TextDistanceToLineStart(pos); + + if (pos.mLine < top) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y)); + if (pos.mLine > bottom - 4) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height)); + if (len + mTextStart < left + 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); + if (len + mTextStart > right - 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); +} + +int TextEditor::GetPageSize() const { + auto height = ImGui::GetWindowHeight() - 20.0f; + return (int)floor(height / mCharAdvance.y); +} + +TextEditor::UndoRecord::UndoRecord( + const std::string& aAdded, const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, const TextEditor::Coordinates aRemovedEnd, + TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter) + : mAdded(aAdded), mAddedStart(aAddedStart), mAddedEnd(aAddedEnd), mRemoved(aRemoved), + mRemovedStart(aRemovedStart), mRemovedEnd(aRemovedEnd), mBefore(aBefore), mAfter(aAfter) { + ASSERT(mAddedStart <= mAddedEnd); + ASSERT(mRemovedStart <= mRemovedEnd); +} + +void TextEditor::UndoRecord::Undo(TextEditor* aEditor) { + if (!mAdded.empty()) { + aEditor->DeleteRange(mAddedStart, mAddedEnd); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); + } + + if (!mRemoved.empty()) { + auto start = mRemovedStart; + aEditor->InsertTextAt(start, mRemoved.c_str()); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); + } + + aEditor->mState = mBefore; + aEditor->EnsureCursorVisible(); +} + +void TextEditor::UndoRecord::Redo(TextEditor* aEditor) { + if (!mRemoved.empty()) { + aEditor->DeleteRange(mRemovedStart, mRemovedEnd); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + } + + if (!mAdded.empty()) { + auto start = mAddedStart; + aEditor->InsertTextAt(start, mAdded.c_str()); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + } + + aEditor->mState = mAfter; + aEditor->EnsureCursorVisible(); +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", + "default", "do", "double", "else", "enum", "extern", + "float", "for", "goto", "if", "inline", "int", + "long", "register", "restrict", "return", "short", "signed", + "sizeof", "static", "struct", "switch", "typedef", "union", + "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", + "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn", + "_Static_assert", "_Thread_local"}; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", + "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", + "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", + "isalpha", "isdigit", "isgraph", "ispunct", "isspace", "isupper", "kbhit", + "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", + "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", + "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +} // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/text_editor.h b/src/core/devtools/widget/text_editor.h new file mode 100644 index 00000000..5c3f29f1 --- /dev/null +++ b/src/core/devtools/widget/text_editor.h @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright (c) 2017 BalazsJako +// SPDX-License-Identifier: MIT + +// source: https://github.com/BalazsJako/ImGuiColorTextEdit + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/assert.h" +#include "imgui.h" + +namespace Core::Devtools::Widget { + +class TextEditor { +public: + enum class PaletteIndex { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + enum class SelectionMode { Normal, Word, Line }; + + struct Breakpoint { + int mLine; + bool mEnabled; + std::string mCondition; + + Breakpoint() : mLine(-1), mEnabled(false) {} + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize + // = 4, because it is rendered as " ABC" on the screen. + struct Coordinates { + int mLine, mColumn; + Coordinates() : mLine(0), mColumn(0) {} + Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) { + ASSERT(aLine >= 0); + ASSERT(aColumn >= 0); + } + static Coordinates Invalid() { + static Coordinates invalid(-1, -1); + return invalid; + } + + bool operator==(const Coordinates& o) const { + return mLine == o.mLine && mColumn == o.mColumn; + } + + bool operator!=(const Coordinates& o) const { + return mLine != o.mLine || mColumn != o.mColumn; + } + + bool operator<(const Coordinates& o) const { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator>(const Coordinates& o) const { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator<=(const Coordinates& o) const { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator>=(const Coordinates& o) const { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn >= o.mColumn; + } + }; + + struct Identifier { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::string String; + typedef std::unordered_map Identifiers; + typedef std::unordered_set Keywords; + typedef std::map ErrorMarkers; + typedef std::unordered_set Breakpoints; + typedef std::array Palette; + typedef uint8_t Char; + + struct Glyph { + Char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(Char aChar, PaletteIndex aColorIndex) + : mChar(aChar), mColorIndex(aColorIndex), mComment(false), mMultiLineComment(false), + mPreprocessor(false) {} + }; + + typedef std::vector Line; + typedef std::vector Lines; + + struct LanguageDefinition { + typedef std::pair TokenRegexString; + typedef std::vector TokenRegexStrings; + typedef bool (*TokenizeCallback)(const char* in_begin, const char* in_end, + const char*& out_begin, const char*& out_end, + PaletteIndex& paletteIndex); + + std::string mName; + Keywords mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar; + bool mAutoIndentation; + + TokenizeCallback mTokenize; + + TokenRegexStrings mTokenRegexStrings; + + bool mCaseSensitive; + + LanguageDefinition() + : mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true) {} + + static const LanguageDefinition& GLSL(); + }; + + TextEditor(); + ~TextEditor(); + + void SetLanguageDefinition(const LanguageDefinition& aLanguageDef); + const LanguageDefinition& GetLanguageDefinition() const { + return mLanguageDefinition; + } + + const Palette& GetPalette() const { + return mPaletteBase; + } + void SetPalette(const Palette& aValue); + + void SetErrorMarkers(const ErrorMarkers& aMarkers) { + mErrorMarkers = aMarkers; + } + void SetBreakpoints(const Breakpoints& aMarkers) { + mBreakpoints = aMarkers; + } + + void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + std::string GetSelectedText() const; + std::string GetCurrentLineText() const; + + int GetTotalLines() const { + return (int)mLines.size(); + } + bool IsOverwrite() const { + return mOverwrite; + } + + void SetReadOnly(bool aValue); + bool IsReadOnly() const { + return mReadOnly; + } + bool IsTextChanged() const { + return mTextChanged; + } + bool IsCursorPositionChanged() const { + return mCursorPositionChanged; + } + + bool IsColorizerEnabled() const { + return mColorizerEnabled; + } + void SetColorizerEnable(bool aValue); + + Coordinates GetCursorPosition() const { + return GetActualCursorCoordinates(); + } + void SetCursorPosition(const Coordinates& aPosition); + + inline void SetHandleMouseInputs(bool aValue) { + mHandleMouseInputs = aValue; + } + inline bool IsHandleMouseInputsEnabled() const { + return mHandleKeyboardInputs; + } + + inline void SetHandleKeyboardInputs(bool aValue) { + mHandleKeyboardInputs = aValue; + } + inline bool IsHandleKeyboardInputsEnabled() const { + return mHandleKeyboardInputs; + } + + inline void SetImGuiChildIgnored(bool aValue) { + mIgnoreImGuiChild = aValue; + } + inline bool IsImGuiChildIgnored() const { + return mIgnoreImGuiChild; + } + + inline void SetShowWhitespaces(bool aValue) { + mShowWhitespaces = aValue; + } + inline bool IsShowingWhitespaces() const { + return mShowWhitespaces; + } + + void SetTabSize(int aValue); + inline int GetTabSize() const { + return mTabSize; + } + + void InsertText(const std::string& aValue); + void InsertText(const char* aValue); + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + + void SetSelectionStart(const Coordinates& aPosition); + void SetSelectionEnd(const Coordinates& aPosition); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, + SelectionMode aMode = SelectionMode::Normal); + void SelectWordUnderCursor(); + void SelectAll(); + bool HasSelection() const; + + void Copy(); + void Cut(); + void Paste(); + void Delete(); + + bool CanUndo() const; + bool CanRedo() const; + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + + static const Palette& GetDarkPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + +private: + typedef std::vector> RegexList; + + struct EditorState { + Coordinates mSelectionStart; + Coordinates mSelectionEnd; + Coordinates mCursorPosition; + }; + + class UndoRecord { + public: + UndoRecord() {} + ~UndoRecord() {} + + UndoRecord(const std::string& aAdded, const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + const std::string& aRemoved, const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + + TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::string mAdded; + Coordinates mAddedStart; + Coordinates mAddedEnd; + + std::string mRemoved; + Coordinates mRemovedStart; + Coordinates mRemovedEnd; + + EditorState mBefore; + EditorState mAfter; + }; + + typedef std::vector UndoBuffer; + + void ProcessInputs(); + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + float TextDistanceToLineStart(const Coordinates& aFrom) const; + void EnsureCursorVisible(); + int GetPageSize() const; + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + Coordinates GetActualCursorCoordinates() const; + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + void Advance(Coordinates& aCoordinates) const; + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void AddUndo(UndoRecord& aValue); + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndex(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetLineCharacterCount(int aLine) const; + int GetLineMaxColumn(int aLine) const; + bool IsOnWordBoundary(const Coordinates& aAt) const; + void RemoveLine(int aStart, int aEnd); + void RemoveLine(int aIndex); + Line& InsertLine(int aIndex); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); + void DeleteSelection(); + std::string GetWordUnderCursor() const; + std::string GetWordAt(const Coordinates& aCoords) const; + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(); + void HandleMouseInputs(); + void Render(); + + float mLineSpacing; + Lines mLines; + EditorState mState; + UndoBuffer mUndoBuffer; + int mUndoIndex; + + int mTabSize; + bool mOverwrite; + bool mReadOnly; + bool mWithinRender; + bool mScrollToCursor; + bool mScrollToTop; + bool mTextChanged; + bool mColorizerEnabled; + float mTextStart; // position (in pixels) where a code line starts relative to the left of the + // TextEditor. + int mLeftMargin; + bool mCursorPositionChanged; + int mColorRangeMin, mColorRangeMax; + SelectionMode mSelectionMode; + bool mHandleKeyboardInputs; + bool mHandleMouseInputs; + bool mIgnoreImGuiChild; + bool mShowWhitespaces; + + Palette mPaletteBase; + Palette mPalette; + LanguageDefinition mLanguageDefinition; + RegexList mRegexList; + + bool mCheckComments; + Breakpoints mBreakpoints; + ErrorMarkers mErrorMarkers; + ImVec2 mCharAdvance; + Coordinates mInteractiveStart, mInteractiveEnd; + std::string mLineBuffer; + uint64_t mStartTime; + + float mLastClick; +}; + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/libraries/dialogs/error_dialog.cpp b/src/core/libraries/dialogs/error_dialog.cpp index b122e2d0..811f2cb9 100644 --- a/src/core/libraries/dialogs/error_dialog.cpp +++ b/src/core/libraries/dialogs/error_dialog.cpp @@ -75,7 +75,7 @@ public: std::min(io.DisplaySize.y, 300.0f), }; - CentralizeWindow(); + CentralizeNextWindow(); SetNextWindowSize(window_size); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index ce30895c..4d8aa881 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -519,10 +519,12 @@ void PS4_SYSV_ABI sceGnmDingDong(u32 gnm_vqid, u32 next_offs_dw) { // Dumping them using the current ring pointer would result in files containing only the // `IndirectBuffer` command. To access the actual command stream, we need to unwrap the IB. auto acb = acb_span; + auto base_addr = reinterpret_cast(acb_ptr); const auto* indirect_buffer = reinterpret_cast(acb_span.data()); if (indirect_buffer->header.opcode == PM4ItOpcode::IndirectBuffer) { - acb = {indirect_buffer->Address(), indirect_buffer->ib_size}; + base_addr = reinterpret_cast(indirect_buffer->Address()); + acb = {reinterpret_cast(base_addr), indirect_buffer->ib_size}; } using namespace DebugStateType; @@ -532,9 +534,9 @@ void PS4_SYSV_ABI sceGnmDingDong(u32 gnm_vqid, u32 next_offs_dw) { .submit_num = seq_num, .num2 = gnm_vqid, .data = {acb.begin(), acb.end()}, + .base_addr = base_addr, }); } - liverpool->SubmitAsc(vqid, acb_span); *asc_queue.read_addr += acb_size; @@ -1125,9 +1127,25 @@ int PS4_SYSV_ABI sceGnmInsertSetColorMarker() { return ORBIS_OK; } -int PS4_SYSV_ABI sceGnmInsertSetMarker() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceGnmInsertSetMarker(u32* cmdbuf, u32 size, const char* marker) { + LOG_TRACE(Lib_GnmDriver, "called"); + + if (cmdbuf && marker) { + const auto len = std::strlen(marker); + const u32 packet_size = ((len + 8) >> 2) + ((len + 0xc) >> 3) * 2; + if (packet_size + 2 == size) { + auto* nop = reinterpret_cast(cmdbuf); + nop->header = + PM4Type3Header{PM4ItOpcode::Nop, packet_size, PM4ShaderType::ShaderGraphics}; + nop->data_block[0] = PM4CmdNop::PayloadType::DebugSetMarker; + const auto marker_len = len + 1; + std::memcpy(&nop->data_block[1], marker, marker_len); + std::memset(reinterpret_cast(&nop->data_block[1]) + marker_len, 0, + packet_size * 4 - marker_len); + return ORBIS_OK; + } + } + return -1; } int PS4_SYSV_ABI sceGnmInsertThreadTraceMarker() { @@ -2163,15 +2181,16 @@ s32 PS4_SYSV_ABI sceGnmSubmitCommandBuffers(u32 count, const u32* dcb_gpu_addrs[ .submit_num = seq_num, .num2 = cbpair, .data = {dcb_span.begin(), dcb_span.end()}, + .base_addr = reinterpret_cast(dcb_gpu_addrs[cbpair]), }); DebugState.PushQueueDump({ .type = QueueType::ccb, .submit_num = seq_num, .num2 = cbpair, .data = {ccb_span.begin(), ccb_span.end()}, + .base_addr = reinterpret_cast(ccb), }); } - liverpool->SubmitGfx(dcb_span, ccb_span); } diff --git a/src/core/libraries/gnmdriver/gnmdriver.h b/src/core/libraries/gnmdriver/gnmdriver.h index 33bccf42..a95daa90 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.h +++ b/src/core/libraries/gnmdriver/gnmdriver.h @@ -108,7 +108,7 @@ s32 PS4_SYSV_ABI sceGnmInsertPopMarker(u32* cmdbuf, u32 size); s32 PS4_SYSV_ABI sceGnmInsertPushColorMarker(u32* cmdbuf, u32 size, const char* marker, u32 color); s32 PS4_SYSV_ABI sceGnmInsertPushMarker(u32* cmdbuf, u32 size, const char* marker); int PS4_SYSV_ABI sceGnmInsertSetColorMarker(); -int PS4_SYSV_ABI sceGnmInsertSetMarker(); +s32 PS4_SYSV_ABI sceGnmInsertSetMarker(u32* cmdbuf, u32 size, const char* marker); int PS4_SYSV_ABI sceGnmInsertThreadTraceMarker(); s32 PS4_SYSV_ABI sceGnmInsertWaitFlipDone(u32* cmdbuf, u32 size, s32 vo_handle, u32 buf_idx); int PS4_SYSV_ABI sceGnmIsCoredumpValid(); diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp index c4bf8425..01e56f8b 100644 --- a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp @@ -98,7 +98,7 @@ SaveDialogState::SaveDialogState(const OrbisSaveDataDialogParam& param) { param_sfo.Open(param_sfo_path); auto last_write = param_sfo.GetLastWrite(); -#ifdef _WIN32 +#if defined(_WIN32) && !defined(__GNUC__) && !defined(__MINGW32__) && !defined(__MINGW64__) auto utc_time = std::chrono::file_clock::to_utc(last_write); #else auto utc_time = std::chrono::file_clock::to_sys(last_write); @@ -402,7 +402,7 @@ void SaveDialogUi::Draw() { }; } - CentralizeWindow(); + CentralizeNextWindow(); SetNextWindowSize(window_size); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { diff --git a/src/core/libraries/system/msgdialog_ui.cpp b/src/core/libraries/system/msgdialog_ui.cpp index ae1dced1..862f5a56 100644 --- a/src/core/libraries/system/msgdialog_ui.cpp +++ b/src/core/libraries/system/msgdialog_ui.cpp @@ -256,7 +256,7 @@ void MsgDialogUi::Draw() { std::min(io.DisplaySize.y, 300.0f), }; - CentralizeWindow(); + CentralizeNextWindow(); SetNextWindowSize(window_size); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { diff --git a/src/imgui/imgui_std.h b/src/imgui/imgui_std.h index 168204ea..ce79da70 100644 --- a/src/imgui/imgui_std.h +++ b/src/imgui/imgui_std.h @@ -25,11 +25,16 @@ inline float FastInFastOutCubic(float x) { } // namespace Easing -inline void CentralizeWindow() { +inline void CentralizeNextWindow() { const auto display_size = GetIO().DisplaySize; SetNextWindowPos(display_size / 2.0f, ImGuiCond_Always, {0.5f}); } +inline void CentralizeWindow() { + const auto display_size = GetIO().DisplaySize; + SetWindowPos(display_size / 2.0f); +} + inline void KeepWindowInside(ImVec2 display_size = GetIO().DisplaySize) { const auto cur_pos = GetWindowPos(); if (cur_pos.x < 0.0f || cur_pos.y < 0.0f) { diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp index ba4a05d0..7f9c69d4 100644 --- a/src/imgui/renderer/texture_manager.cpp +++ b/src/imgui/renderer/texture_manager.cpp @@ -7,6 +7,7 @@ #include #include "common/assert.h" +#include "common/config.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "imgui_impl_vulkan.h" @@ -147,6 +148,11 @@ void WorkerLoop() { g_job_list.pop_front(); g_job_list_mtx.unlock(); + if (Config::vkCrashDiagnosticEnabled()) { + // FIXME: Crash diagnostic hangs when building the command buffer here + continue; + } + if (!path.empty()) { // Decode PNG from file Common::FS::IOFile file(path, Common::FS::FileAccessMode::Read); if (!file.IsOpen()) { diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index b3b71883..d4ebeb88 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -6,6 +6,7 @@ #include "common/debug.h" #include "common/polyfill_thread.h" #include "common/thread.h" +#include "core/debug_state.h" #include "core/libraries/videoout/driver.h" #include "video_core/amdgpu/liverpool.h" #include "video_core/amdgpu/pm4_cmds.h" @@ -187,6 +188,7 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(dcb.data()); while (!dcb.empty()) { const auto* header = reinterpret_cast(dcb.data()); const u32 type = header->type; @@ -359,6 +361,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spanindex_base_hi); regs.num_indices = draw_index->index_count; regs.draw_initiator = draw_index->draw_initiator; + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin(fmt::format("dcb:{}:DrawIndex2", cmd_address)); @@ -373,6 +378,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spanmax_size; regs.num_indices = draw_index_off->index_count; regs.draw_initiator = draw_index_off->draw_initiator; + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin( @@ -386,6 +394,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); regs.num_indices = draw_index->index_count; regs.draw_initiator = draw_index->draw_initiator; + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin(fmt::format("dcb:{}:DrawIndexAuto", cmd_address)); @@ -399,6 +410,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spandata_offset; const auto ib_address = mapped_queues[GfxQueueId].indirect_args_addr; const auto size = sizeof(PM4CmdDrawIndirect::DrawInstancedArgs); + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin(fmt::format("dcb:{}:DrawIndirect", cmd_address)); @@ -413,6 +427,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spandata_offset; const auto ib_address = mapped_queues[GfxQueueId].indirect_args_addr; const auto size = sizeof(PM4CmdDrawIndexIndirect::DrawIndexInstancedArgs); + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin( @@ -428,6 +445,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spandim_y; regs.cs_program.dim_z = dispatch_direct->dim_z; regs.cs_program.dispatch_initiator = dispatch_direct->dispatch_initiator; + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer && (regs.cs_program.dispatch_initiator & 1)) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin(fmt::format("dcb:{}:Dispatch", cmd_address)); @@ -442,6 +462,9 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spandata_offset; const auto ib_address = mapped_queues[GfxQueueId].indirect_args_addr; const auto size = sizeof(PM4CmdDispatchIndirect::GroupDimensions); + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer && (regs.cs_program.dispatch_initiator & 1)) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin( @@ -576,6 +599,7 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span acb, int vqid) { TracyFiberEnter(acb_task_name); + auto base_addr = reinterpret_cast(acb.data()); while (!acb.empty()) { const auto* header = reinterpret_cast(acb.data()); const u32 type = header->type; @@ -620,6 +644,9 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, int vqid) { regs.cs_program.dim_y = dispatch_direct->dim_y; regs.cs_program.dim_z = dispatch_direct->dim_z; regs.cs_program.dispatch_initiator = dispatch_direct->dispatch_initiator; + if (DebugState.DumpingCurrentReg()) { + DebugState.PushRegsDump(base_addr, reinterpret_cast(header), regs); + } if (rasterizer && (regs.cs_program.dispatch_initiator & 1)) { const auto cmd_address = reinterpret_cast(header); rasterizer->ScopeMarkerBegin(fmt::format("acb[{}]:{}:Dispatch", vqid, cmd_address)); diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index 1c994d0a..a4cf7933 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -772,6 +772,8 @@ struct Liverpool { BitField<27, 1, u32> fmask_compress_1frag_only; BitField<28, 1, u32> dcc_enable; BitField<29, 1, u32> cmask_addr_type; + + u32 u32all; } info; union Color0Attrib { BitField<0, 5, TilingMode> tile_mode_index; @@ -780,6 +782,8 @@ struct Liverpool { BitField<12, 3, u32> num_samples_log2; BitField<15, 2, u32> num_fragments_log2; BitField<17, 1, u32> force_dst_alpha_1; + + u32 u32all; } attrib; INSERT_PADDING_WORDS(1); u32 cmask_base_address; @@ -935,7 +939,7 @@ struct Liverpool { BitField<5, 1, u32> gs_en; BitField<6, 1, u32> vs_en; - bool IsStageEnabled(u32 stage) { + bool IsStageEnabled(u32 stage) const { switch (stage) { case 0: case 1: diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index b9fbfcb8..a7a862ea 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -213,6 +213,7 @@ struct PM4CmdNop { enum PayloadType : u32 { DebugMarkerPush = 0x68750001u, ///< Begin of GPU event scope DebugMarkerPop = 0x68750002u, ///< End of GPU event scope + DebugSetMarker = 0x68750003u, ///< Set GPU event marker SetVsharpInUdata = 0x68750004u, ///< Indicates that V# will be set in the next packet SetTsharpInUdata = 0x68750005u, ///< Indicates that T# will be set in the next packet SetSsharpInUdata = 0x68750006u, ///< Indicates that S# will be set in the next packet