diff options
Diffstat (limited to 'editor/debugger')
19 files changed, 6416 insertions, 0 deletions
diff --git a/editor/debugger/SCsub b/editor/debugger/SCsub new file mode 100644 index 0000000000..359d04e5df --- /dev/null +++ b/editor/debugger/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.editor_sources, "*.cpp") diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp new file mode 100644 index 0000000000..dcd7220ed0 --- /dev/null +++ b/editor/debugger/editor_debugger_inspector.cpp @@ -0,0 +1,269 @@ +/*************************************************************************/ +/* editor_debugger_inspector.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_debugger_inspector.h" + +#include "core/debugger/debugger_marshalls.h" +#include "core/io/marshalls.h" +#include "editor/editor_node.h" +#include "scene/debugger/scene_debugger.h" + +bool EditorDebuggerRemoteObject::_set(const StringName &p_name, const Variant &p_value) { + if (!editable || !prop_values.has(p_name) || String(p_name).begins_with("Constants/")) { + return false; + } + + prop_values[p_name] = p_value; + emit_signal("value_edited", remote_object_id, p_name, p_value); + return true; +} + +bool EditorDebuggerRemoteObject::_get(const StringName &p_name, Variant &r_ret) const { + if (!prop_values.has(p_name)) { + return false; + } + + r_ret = prop_values[p_name]; + return true; +} + +void EditorDebuggerRemoteObject::_get_property_list(List<PropertyInfo> *p_list) const { + p_list->clear(); //sorry, no want category + for (const List<PropertyInfo>::Element *E = prop_list.front(); E; E = E->next()) { + p_list->push_back(E->get()); + } +} + +String EditorDebuggerRemoteObject::get_title() { + if (remote_object_id.is_valid()) { + return TTR("Remote ") + String(type_name) + ": " + itos(remote_object_id); + } else { + return "<null>"; + } +} + +Variant EditorDebuggerRemoteObject::get_variant(const StringName &p_name) { + Variant var; + _get(p_name, var); + return var; +} + +void EditorDebuggerRemoteObject::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_title"), &EditorDebuggerRemoteObject::get_title); + ClassDB::bind_method(D_METHOD("get_variant"), &EditorDebuggerRemoteObject::get_variant); + ClassDB::bind_method(D_METHOD("clear"), &EditorDebuggerRemoteObject::clear); + ClassDB::bind_method(D_METHOD("get_remote_object_id"), &EditorDebuggerRemoteObject::get_remote_object_id); + + ADD_SIGNAL(MethodInfo("value_edited", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"))); +} + +EditorDebuggerInspector::EditorDebuggerInspector() { + variables = memnew(EditorDebuggerRemoteObject); + variables->editable = false; +} + +EditorDebuggerInspector::~EditorDebuggerInspector() { + clear_cache(); + memdelete(variables); +} + +void EditorDebuggerInspector::_bind_methods() { + ADD_SIGNAL(MethodInfo("object_selected", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("object_edited", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"))); + ADD_SIGNAL(MethodInfo("object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"))); +} + +void EditorDebuggerInspector::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: + connect("object_id_selected", callable_mp(this, &EditorDebuggerInspector::_object_selected)); + break; + case NOTIFICATION_ENTER_TREE: + edit(variables); + break; + default: + break; + } +} + +void EditorDebuggerInspector::_object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value) { + emit_signal("object_edited", p_id, p_prop, p_value); +} + +void EditorDebuggerInspector::_object_selected(ObjectID p_object) { + emit_signal("object_selected", p_object); +} + +ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) { + EditorDebuggerRemoteObject *debugObj = nullptr; + + SceneDebuggerObject obj; + obj.deserialize(p_arr); + ERR_FAIL_COND_V(obj.id.is_null(), ObjectID()); + + if (remote_objects.has(obj.id)) { + debugObj = remote_objects[obj.id]; + } else { + debugObj = memnew(EditorDebuggerRemoteObject); + debugObj->remote_object_id = obj.id; + debugObj->type_name = obj.class_name; + remote_objects[obj.id] = debugObj; + debugObj->connect("value_edited", callable_mp(this, &EditorDebuggerInspector::_object_edited)); + } + + int old_prop_size = debugObj->prop_list.size(); + + debugObj->prop_list.clear(); + int new_props_added = 0; + Set<String> changed; + for (int i = 0; i < obj.properties.size(); i++) { + PropertyInfo &pinfo = obj.properties[i].first; + Variant &var = obj.properties[i].second; + + if (pinfo.type == Variant::OBJECT) { + if (var.get_type() == Variant::STRING) { + String path = var; + if (path.find("::") != -1) { + // built-in resource + String base_path = path.get_slice("::", 0); + RES dependency = ResourceLoader::load(base_path); + if (dependency.is_valid()) { + remote_dependencies.insert(dependency); + } + } + var = ResourceLoader::load(path); + + if (pinfo.hint_string == "Script") { + if (debugObj->get_script() != var) { + debugObj->set_script(REF()); + Ref<Script> script(var); + if (!script.is_null()) { + ScriptInstance *script_instance = script->placeholder_instance_create(debugObj); + debugObj->set_script_and_instance(var, script_instance); + } + } + } + } + } + + //always add the property, since props may have been added or removed + debugObj->prop_list.push_back(pinfo); + + if (!debugObj->prop_values.has(pinfo.name)) { + new_props_added++; + debugObj->prop_values[pinfo.name] = var; + } else { + if (bool(Variant::evaluate(Variant::OP_NOT_EQUAL, debugObj->prop_values[pinfo.name], var))) { + debugObj->prop_values[pinfo.name] = var; + changed.insert(pinfo.name); + } + } + } + + if (old_prop_size == debugObj->prop_list.size() && new_props_added == 0) { + //only some may have changed, if so, then update those, if exist + for (Set<String>::Element *E = changed.front(); E; E = E->next()) { + emit_signal("object_property_updated", debugObj->remote_object_id, E->get()); + } + } else { + //full update, because props were added or removed + debugObj->update(); + } + return obj.id; +} + +void EditorDebuggerInspector::clear_cache() { + for (Map<ObjectID, EditorDebuggerRemoteObject *>::Element *E = remote_objects.front(); E; E = E->next()) { + EditorNode *editor = EditorNode::get_singleton(); + if (editor->get_editor_history()->get_current() == E->value()->get_instance_id()) { + editor->push_item(nullptr); + } + memdelete(E->value()); + } + remote_objects.clear(); + remote_dependencies.clear(); +} + +Object *EditorDebuggerInspector::get_object(ObjectID p_id) { + if (remote_objects.has(p_id)) { + return remote_objects[p_id]; + } + return nullptr; +} + +void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { + DebuggerMarshalls::ScriptStackVariable var; + var.deserialize(p_array); + String n = var.name; + Variant v = var.value; + + PropertyHint h = PROPERTY_HINT_NONE; + String hs = String(); + + if (v.get_type() == Variant::OBJECT) { + v = Object::cast_to<EncodedObjectAsID>(v)->get_object_id(); + h = PROPERTY_HINT_OBJECT_ID; + hs = "Object"; + } + String type; + switch (var.type) { + case 0: + type = "Locals/"; + break; + case 1: + type = "Members/"; + break; + case 2: + type = "Globals/"; + break; + default: + type = "Unknown/"; + } + + PropertyInfo pinfo; + pinfo.name = type + n; + pinfo.type = v.get_type(); + pinfo.hint = h; + pinfo.hint_string = hs; + + variables->prop_list.push_back(pinfo); + variables->prop_values[type + n] = v; + variables->update(); + edit(variables); +} + +void EditorDebuggerInspector::clear_stack_variables() { + variables->clear(); + variables->update(); +} + +String EditorDebuggerInspector::get_stack_variable(const String &p_var) { + return variables->get_variant(p_var); +} diff --git a/editor/debugger/editor_debugger_inspector.h b/editor/debugger/editor_debugger_inspector.h new file mode 100644 index 0000000000..7d13a4c362 --- /dev/null +++ b/editor/debugger/editor_debugger_inspector.h @@ -0,0 +1,97 @@ +/*************************************************************************/ +/* editor_debugger_inspector.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_DEBUGGER_INSPECTOR_H +#define EDITOR_DEBUGGER_INSPECTOR_H +#include "editor/editor_inspector.h" + +class EditorDebuggerRemoteObject : public Object { + GDCLASS(EditorDebuggerRemoteObject, Object); + +protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + static void _bind_methods(); + +public: + bool editable = false; + ObjectID remote_object_id; + String type_name; + List<PropertyInfo> prop_list; + Map<StringName, Variant> prop_values; + + ObjectID get_remote_object_id() { return remote_object_id; }; + String get_title(); + + Variant get_variant(const StringName &p_name); + + void clear() { + prop_list.clear(); + prop_values.clear(); + } + + void update() { _change_notify(); } + + EditorDebuggerRemoteObject() {} +}; + +class EditorDebuggerInspector : public EditorInspector { + GDCLASS(EditorDebuggerInspector, EditorInspector); + +private: + ObjectID inspected_object_id; + Map<ObjectID, EditorDebuggerRemoteObject *> remote_objects; + Set<RES> remote_dependencies; + EditorDebuggerRemoteObject *variables; + + void _object_selected(ObjectID p_object); + void _object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + EditorDebuggerInspector(); + ~EditorDebuggerInspector(); + + // Remote Object cache + ObjectID add_object(const Array &p_arr); + Object *get_object(ObjectID p_id); + void clear_cache(); + + // Stack Dump variables + String get_stack_variable(const String &p_var); + void add_stack_variable(const Array &p_arr); + void clear_stack_variables(); +}; + +#endif // EDITOR_DEBUGGER_INSPECTOR_H diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp new file mode 100644 index 0000000000..b461ac4f35 --- /dev/null +++ b/editor/debugger/editor_debugger_node.cpp @@ -0,0 +1,647 @@ +/*************************************************************************/ +/* editor_debugger_node.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_debugger_node.h" + +#include "editor/debugger/editor_debugger_tree.h" +#include "editor/debugger/script_editor_debugger.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" +#include "editor/plugins/editor_debugger_plugin.h" +#include "editor/plugins/script_editor_plugin.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/tab_container.h" + +template <typename Func> +void _for_all(TabContainer *p_node, const Func &p_func) { + for (int i = 0; i < p_node->get_tab_count(); i++) { + ScriptEditorDebugger *dbg = Object::cast_to<ScriptEditorDebugger>(p_node->get_tab_control(i)); + ERR_FAIL_COND(!dbg); + p_func(dbg); + } +} + +EditorDebuggerNode *EditorDebuggerNode::singleton = nullptr; + +EditorDebuggerNode::EditorDebuggerNode() { + if (!singleton) { + singleton = this; + } + + add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("BottomPanelDebuggerOverride", "EditorStyles")->get_margin(MARGIN_LEFT)); + add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("BottomPanelDebuggerOverride", "EditorStyles")->get_margin(MARGIN_RIGHT)); + + tabs = memnew(TabContainer); + tabs->set_tab_align(TabContainer::ALIGN_LEFT); + tabs->set_tabs_visible(false); + tabs->connect("tab_changed", callable_mp(this, &EditorDebuggerNode::_debugger_changed)); + add_child(tabs); + + Ref<StyleBoxEmpty> empty; + empty.instance(); + tabs->add_theme_style_override("panel", empty); + + auto_switch_remote_scene_tree = EDITOR_DEF("debugger/auto_switch_to_remote_scene_tree", false); + _add_debugger(); + + // Remote scene tree + remote_scene_tree = memnew(EditorDebuggerTree); + remote_scene_tree->connect("object_selected", callable_mp(this, &EditorDebuggerNode::_remote_object_requested)); + remote_scene_tree->connect("save_node", callable_mp(this, &EditorDebuggerNode::_save_node_requested)); + EditorNode::get_singleton()->get_scene_tree_dock()->add_remote_tree_editor(remote_scene_tree); + EditorNode::get_singleton()->get_scene_tree_dock()->connect("remote_tree_selected", callable_mp(this, &EditorDebuggerNode::request_remote_tree)); + + remote_scene_tree_timeout = EDITOR_DEF("debugger/remote_scene_tree_refresh_interval", 1.0); + inspect_edited_object_timeout = EDITOR_DEF("debugger/remote_inspect_refresh_interval", 0.2); + + EditorNode *editor = EditorNode::get_singleton(); + editor->get_undo_redo()->set_method_notify_callback(_method_changeds, this); + editor->get_undo_redo()->set_property_notify_callback(_property_changeds, this); + editor->get_pause_button()->connect("pressed", callable_mp(this, &EditorDebuggerNode::_paused)); +} + +ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() { + ScriptEditorDebugger *node = memnew(ScriptEditorDebugger(EditorNode::get_singleton())); + + int id = tabs->get_tab_count(); + node->connect("stop_requested", callable_mp(this, &EditorDebuggerNode::_debugger_wants_stop), varray(id)); + node->connect("stopped", callable_mp(this, &EditorDebuggerNode::_debugger_stopped), varray(id)); + node->connect("stack_frame_selected", callable_mp(this, &EditorDebuggerNode::_stack_frame_selected), varray(id)); + node->connect("error_selected", callable_mp(this, &EditorDebuggerNode::_error_selected), varray(id)); + node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution)); + node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked), varray(id)); + node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated), varray(id)); + node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated), varray(id)); + node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated), varray(id)); + node->connect("remote_object_requested", callable_mp(this, &EditorDebuggerNode::_remote_object_requested), varray(id)); + + if (tabs->get_tab_count() > 0) { + get_debugger(0)->clear_style(); + } + + tabs->add_child(node); + + node->set_name("Session " + itos(tabs->get_tab_count())); + if (tabs->get_tab_count() > 1) { + node->clear_style(); + tabs->set_tabs_visible(true); + tabs->add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("DebuggerPanel", "EditorStyles")); + } + + if (!debugger_plugins.empty()) { + for (Set<Ref<Script>>::Element *i = debugger_plugins.front(); i; i = i->next()) { + node->add_debugger_plugin(i->get()); + } + } + + return node; +} + +void EditorDebuggerNode::_stack_frame_selected(int p_debugger) { + const ScriptEditorDebugger *dbg = get_debugger(p_debugger); + ERR_FAIL_COND(!dbg); + if (dbg != get_current_debugger()) { + return; + } + _text_editor_stack_goto(dbg); +} + +void EditorDebuggerNode::_error_selected(const String &p_file, int p_line, int p_debugger) { + Ref<Script> s = ResourceLoader::load(p_file); + emit_signal("goto_script_line", s, p_line - 1); +} + +void EditorDebuggerNode::_text_editor_stack_goto(const ScriptEditorDebugger *p_debugger) { + const String file = p_debugger->get_stack_script_file(); + if (file.empty()) { + return; + } + stack_script = ResourceLoader::load(file); + const int line = p_debugger->get_stack_script_line() - 1; + emit_signal("goto_script_line", stack_script, line); + emit_signal("set_execution", stack_script, line); + stack_script.unref(); // Why?!? +} + +void EditorDebuggerNode::_bind_methods() { + // LiveDebug. + ClassDB::bind_method("live_debug_create_node", &EditorDebuggerNode::live_debug_create_node); + ClassDB::bind_method("live_debug_instance_node", &EditorDebuggerNode::live_debug_instance_node); + ClassDB::bind_method("live_debug_remove_node", &EditorDebuggerNode::live_debug_remove_node); + ClassDB::bind_method("live_debug_remove_and_keep_node", &EditorDebuggerNode::live_debug_remove_and_keep_node); + ClassDB::bind_method("live_debug_restore_node", &EditorDebuggerNode::live_debug_restore_node); + ClassDB::bind_method("live_debug_duplicate_node", &EditorDebuggerNode::live_debug_duplicate_node); + ClassDB::bind_method("live_debug_reparent_node", &EditorDebuggerNode::live_debug_reparent_node); + + ADD_SIGNAL(MethodInfo("goto_script_line")); + ADD_SIGNAL(MethodInfo("set_execution", PropertyInfo("script"), PropertyInfo(Variant::INT, "line"))); + ADD_SIGNAL(MethodInfo("clear_execution", PropertyInfo("script"))); + ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug"))); +} + +EditorDebuggerRemoteObject *EditorDebuggerNode::get_inspected_remote_object() { + return Object::cast_to<EditorDebuggerRemoteObject>(ObjectDB::get_instance(EditorNode::get_singleton()->get_editor_history()->get_current())); +} + +ScriptEditorDebugger *EditorDebuggerNode::get_debugger(int p_id) const { + return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(p_id)); +} + +ScriptEditorDebugger *EditorDebuggerNode::get_current_debugger() const { + return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(tabs->get_current_tab())); +} + +ScriptEditorDebugger *EditorDebuggerNode::get_default_debugger() const { + return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(0)); +} + +Error EditorDebuggerNode::start(const String &p_protocol) { + stop(); + if (EDITOR_GET("run/output/always_open_output_on_play")) { + EditorNode::get_singleton()->make_bottom_panel_item_visible(EditorNode::get_log()); + } else { + EditorNode::get_singleton()->make_bottom_panel_item_visible(this); + } + + server = Ref<EditorDebuggerServer>(EditorDebuggerServer::create(p_protocol)); + const Error err = server->start(); + if (err != OK) { + return err; + } + set_process(true); + EditorNode::get_log()->add_message("--- Debugging process started ---", EditorLog::MSG_TYPE_EDITOR); + return OK; +} + +void EditorDebuggerNode::stop() { + if (server.is_valid()) { + server->stop(); + EditorNode::get_log()->add_message("--- Debugging process stopped ---", EditorLog::MSG_TYPE_EDITOR); + server.unref(); + } + // Also close all debugging sessions. + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + if (dbg->is_session_active()) { + dbg->stop(); + } + }); + _break_state_changed(); + if (hide_on_stop) { + if (is_visible_in_tree()) { + EditorNode::get_singleton()->hide_bottom_panel(); + } + } + breakpoints.clear(); + set_process(false); +} + +void EditorDebuggerNode::_notification(int p_what) { + switch (p_what) { + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (tabs->get_tab_count() > 1) { + add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("BottomPanelDebuggerOverride", "EditorStyles")->get_margin(MARGIN_LEFT)); + add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("BottomPanelDebuggerOverride", "EditorStyles")->get_margin(MARGIN_RIGHT)); + + tabs->add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("DebuggerPanel", "EditorStyles")); + } + } break; + default: + break; + } + + if (p_what != NOTIFICATION_PROCESS || !server.is_valid()) { + return; + } + + if (!server.is_valid() || !server->is_active()) { + stop(); + return; + } + server->poll(); + + // Errors and warnings + int error_count = 0; + int warning_count = 0; + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + error_count += dbg->get_error_count(); + warning_count += dbg->get_warning_count(); + }); + + if (error_count != last_error_count || warning_count != last_warning_count) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->update_tabs(); + }); + + if (error_count == 0 && warning_count == 0) { + debugger_button->set_text(TTR("Debugger")); + debugger_button->set_icon(Ref<Texture2D>()); + } else { + debugger_button->set_text(TTR("Debugger") + " (" + itos(error_count + warning_count) + ")"); + if (error_count >= 1 && warning_count >= 1) { + debugger_button->set_icon(get_theme_icon("ErrorWarning", "EditorIcons")); + } else if (error_count >= 1) { + debugger_button->set_icon(get_theme_icon("Error", "EditorIcons")); + } else { + debugger_button->set_icon(get_theme_icon("Warning", "EditorIcons")); + } + } + last_error_count = error_count; + last_warning_count = warning_count; + } + + // Remote scene tree update + remote_scene_tree_timeout -= get_process_delta_time(); + if (remote_scene_tree_timeout < 0) { + remote_scene_tree_timeout = EditorSettings::get_singleton()->get("debugger/remote_scene_tree_refresh_interval"); + if (remote_scene_tree->is_visible_in_tree()) { + get_current_debugger()->request_remote_tree(); + } + } + + // Remote inspector update + inspect_edited_object_timeout -= get_process_delta_time(); + if (inspect_edited_object_timeout < 0) { + inspect_edited_object_timeout = EditorSettings::get_singleton()->get("debugger/remote_inspect_refresh_interval"); + if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) { + get_current_debugger()->request_remote_object(obj->remote_object_id); + } + } + + // Take connections. + if (server->is_connection_available()) { + ScriptEditorDebugger *debugger = nullptr; + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + if (debugger || dbg->is_session_active()) { + return; + } + debugger = dbg; + }); + if (debugger == nullptr) { + if (tabs->get_tab_count() <= 4) { // Max 4 debugging sessions active. + debugger = _add_debugger(); + } else { + // We already have too many sessions, disconnecting new clients to prevent them from hanging. + server->take_connection()->close(); + return; // Can't add, stop here. + } + } + + EditorNode::get_singleton()->get_pause_button()->set_disabled(false); + // Switch to remote tree view if so desired. + auto_switch_remote_scene_tree = (bool)EditorSettings::get_singleton()->get("debugger/auto_switch_to_remote_scene_tree"); + if (auto_switch_remote_scene_tree) { + EditorNode::get_singleton()->get_scene_tree_dock()->show_remote_tree(); + } + // Good to go. + EditorNode::get_singleton()->get_scene_tree_dock()->show_tab_buttons(); + debugger->set_editor_remote_tree(remote_scene_tree); + debugger->start(server->take_connection()); + // Send breakpoints. + for (Map<Breakpoint, bool>::Element *E = breakpoints.front(); E; E = E->next()) { + const Breakpoint &bp = E->key(); + debugger->set_breakpoint(bp.source, bp.line, E->get()); + } // Will arrive too late, how does the regular run work? + + debugger->update_live_edit_root(); + } +} + +void EditorDebuggerNode::_debugger_stopped(int p_id) { + ScriptEditorDebugger *dbg = get_debugger(p_id); + ERR_FAIL_COND(!dbg); + + bool found = false; + _for_all(tabs, [&](ScriptEditorDebugger *p_debugger) { + if (p_debugger->is_session_active()) { + found = true; + } + }); + if (!found) { + EditorNode::get_singleton()->get_pause_button()->set_pressed(false); + EditorNode::get_singleton()->get_pause_button()->set_disabled(true); + EditorNode::get_singleton()->get_scene_tree_dock()->hide_remote_tree(); + EditorNode::get_singleton()->get_scene_tree_dock()->hide_tab_buttons(); + EditorNode::get_singleton()->notify_all_debug_sessions_exited(); + } +} + +void EditorDebuggerNode::_debugger_wants_stop(int p_id) { + // Ask editor to kill PID. + int pid = get_debugger(p_id)->get_remote_pid(); + if (pid) { + EditorNode::get_singleton()->call_deferred("stop_child_process", pid); + } +} + +void EditorDebuggerNode::_debugger_changed(int p_tab) { + if (get_inspected_remote_object()) { + // Clear inspected object, you can only inspect objects in selected debugger. + // Hopefully, in the future, we will have one inspector per debugger. + EditorNode::get_singleton()->push_item(nullptr); + } + if (remote_scene_tree->is_visible_in_tree()) { + get_current_debugger()->request_remote_tree(); + } + if (get_current_debugger()->is_breaked()) { + _text_editor_stack_goto(get_current_debugger()); + } +} + +void EditorDebuggerNode::set_script_debug_button(MenuButton *p_button) { + script_menu = p_button; + script_menu->set_text(TTR("Debug")); + script_menu->set_switch_on_hover(true); + PopupMenu *p = script_menu->get_popup(); + p->add_shortcut(ED_GET_SHORTCUT("debugger/step_into"), DEBUG_STEP); + p->add_shortcut(ED_GET_SHORTCUT("debugger/step_over"), DEBUG_NEXT); + p->add_separator(); + p->add_shortcut(ED_GET_SHORTCUT("debugger/break"), DEBUG_BREAK); + p->add_shortcut(ED_GET_SHORTCUT("debugger/continue"), DEBUG_CONTINUE); + p->add_separator(); + p->add_check_shortcut(ED_GET_SHORTCUT("debugger/keep_debugger_open"), DEBUG_SHOW_KEEP_OPEN); + p->add_check_shortcut(ED_GET_SHORTCUT("debugger/debug_with_external_editor"), DEBUG_WITH_EXTERNAL_EDITOR); + p->connect("id_pressed", callable_mp(this, &EditorDebuggerNode::_menu_option)); + + _break_state_changed(); + script_menu->show(); +} + +void EditorDebuggerNode::_break_state_changed() { + const bool breaked = get_current_debugger()->is_breaked(); + const bool can_debug = get_current_debugger()->is_debuggable(); + if (breaked) { // Show debugger. + EditorNode::get_singleton()->make_bottom_panel_item_visible(this); + } + + // Update script menu. + if (!script_menu) { + return; + } + PopupMenu *p = script_menu->get_popup(); + p->set_item_disabled(p->get_item_index(DEBUG_NEXT), !(breaked && can_debug)); + p->set_item_disabled(p->get_item_index(DEBUG_STEP), !(breaked && can_debug)); + p->set_item_disabled(p->get_item_index(DEBUG_BREAK), breaked); + p->set_item_disabled(p->get_item_index(DEBUG_CONTINUE), !breaked); +} + +void EditorDebuggerNode::_menu_option(int p_id) { + switch (p_id) { + case DEBUG_NEXT: { + debug_next(); + } break; + case DEBUG_STEP: { + debug_step(); + } break; + case DEBUG_BREAK: { + debug_break(); + } break; + case DEBUG_CONTINUE: { + debug_continue(); + } break; + + case DEBUG_SHOW_KEEP_OPEN: { + bool visible = script_menu->get_popup()->is_item_checked(script_menu->get_popup()->get_item_index(DEBUG_SHOW_KEEP_OPEN)); + hide_on_stop = visible; + script_menu->get_popup()->set_item_checked(script_menu->get_popup()->get_item_index(DEBUG_SHOW_KEEP_OPEN), !visible); + } break; + case DEBUG_WITH_EXTERNAL_EDITOR: { + bool checked = !script_menu->get_popup()->is_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR)); + debug_with_external_editor = checked; + script_menu->get_popup()->set_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR), checked); + } break; + } +} + +void EditorDebuggerNode::_paused() { + const bool paused = EditorNode::get_singleton()->get_pause_button()->is_pressed(); + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + if (paused && !dbg->is_breaked()) { + dbg->debug_break(); + } else if (!paused && dbg->is_breaked()) { + dbg->debug_continue(); + } + }); +} + +void EditorDebuggerNode::_breaked(bool p_breaked, bool p_can_debug, int p_debugger) { + if (get_current_debugger() != get_debugger(p_debugger)) { + if (!p_breaked) { + return; + } + tabs->set_current_tab(p_debugger); + } + _break_state_changed(); + EditorNode::get_singleton()->get_pause_button()->set_pressed(p_breaked); + emit_signal("breaked", p_breaked, p_can_debug); +} + +bool EditorDebuggerNode::is_skip_breakpoints() const { + return get_default_debugger()->is_skip_breakpoints(); +} + +void EditorDebuggerNode::set_breakpoint(const String &p_path, int p_line, bool p_enabled) { + breakpoints[Breakpoint(p_path, p_line)] = p_enabled; + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->set_breakpoint(p_path, p_line, p_enabled); + }); +} + +void EditorDebuggerNode::reload_scripts() { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->reload_scripts(); + }); +} + +void EditorDebuggerNode::debug_next() { + get_default_debugger()->debug_next(); +} + +void EditorDebuggerNode::debug_step() { + get_default_debugger()->debug_step(); +} + +void EditorDebuggerNode::debug_break() { + get_default_debugger()->debug_break(); +} + +void EditorDebuggerNode::debug_continue() { + get_default_debugger()->debug_continue(); +} + +String EditorDebuggerNode::get_var_value(const String &p_var) const { + return get_default_debugger()->get_var_value(p_var); +} + +// LiveEdit/Inspector +void EditorDebuggerNode::request_remote_tree() { + get_current_debugger()->request_remote_tree(); +} + +void EditorDebuggerNode::_remote_tree_updated(int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + remote_scene_tree->clear(); + remote_scene_tree->update_scene_tree(get_current_debugger()->get_remote_tree(), p_debugger); +} + +void EditorDebuggerNode::_remote_object_updated(ObjectID p_id, int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) { + if (obj->remote_object_id == p_id) { + return; // Already being edited + } + } + + EditorNode::get_singleton()->push_item(get_current_debugger()->get_remote_object(p_id)); +} + +void EditorDebuggerNode::_remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) { + if (obj->remote_object_id != p_id) { + return; + } + EditorNode::get_singleton()->get_inspector()->update_property(p_property); + } +} + +void EditorDebuggerNode::_remote_object_requested(ObjectID p_id, int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + inspect_edited_object_timeout = 0.7; // Temporarily disable timeout to avoid multiple requests. + get_current_debugger()->request_remote_object(p_id); +} + +void EditorDebuggerNode::_save_node_requested(ObjectID p_id, const String &p_file, int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + get_current_debugger()->save_node(p_id, p_file); +} + +// Remote inspector/edit. +void EditorDebuggerNode::_method_changeds(void *p_ud, Object *p_base, const StringName &p_name, VARIANT_ARG_DECLARE) { + if (!singleton) { + return; + } + _for_all(singleton->tabs, [&](ScriptEditorDebugger *dbg) { + dbg->_method_changed(p_base, p_name, VARIANT_ARG_PASS); + }); +} + +void EditorDebuggerNode::_property_changeds(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value) { + if (!singleton) { + return; + } + _for_all(singleton->tabs, [&](ScriptEditorDebugger *dbg) { + dbg->_property_changed(p_base, p_property, p_value); + }); +} + +// LiveDebug +void EditorDebuggerNode::set_live_debugging(bool p_enabled) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->set_live_debugging(p_enabled); + }); +} + +void EditorDebuggerNode::update_live_edit_root() { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->update_live_edit_root(); + }); +} + +void EditorDebuggerNode::live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_create_node(p_parent, p_type, p_name); + }); +} + +void EditorDebuggerNode::live_debug_instance_node(const NodePath &p_parent, const String &p_path, const String &p_name) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_instance_node(p_parent, p_path, p_name); + }); +} + +void EditorDebuggerNode::live_debug_remove_node(const NodePath &p_at) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_remove_node(p_at); + }); +} + +void EditorDebuggerNode::live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_remove_and_keep_node(p_at, p_keep_id); + }); +} + +void EditorDebuggerNode::live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_restore_node(p_id, p_at, p_at_pos); + }); +} + +void EditorDebuggerNode::live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_duplicate_node(p_at, p_new_name); + }); +} + +void EditorDebuggerNode::live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos) { + _for_all(tabs, [&](ScriptEditorDebugger *dbg) { + dbg->live_debug_reparent_node(p_at, p_new_place, p_new_name, p_at_pos); + }); +} + +void EditorDebuggerNode::add_debugger_plugin(const Ref<Script> &p_script) { + ERR_FAIL_COND_MSG(debugger_plugins.has(p_script), "Debugger plugin already exists."); + ERR_FAIL_COND_MSG(p_script.is_null(), "Debugger plugin script is null"); + ERR_FAIL_COND_MSG(String(p_script->get_instance_base_type()) == "", "Debugger plugin script has error."); + ERR_FAIL_COND_MSG(String(p_script->get_instance_base_type()) != "EditorDebuggerPlugin", "Base type of debugger plugin is not 'EditorDebuggerPlugin'."); + ERR_FAIL_COND_MSG(!p_script->is_tool(), "Debugger plugin script is not in tool mode."); + debugger_plugins.insert(p_script); + for (int i = 0; get_debugger(i); i++) { + get_debugger(i)->add_debugger_plugin(p_script); + } +} + +void EditorDebuggerNode::remove_debugger_plugin(const Ref<Script> &p_script) { + ERR_FAIL_COND_MSG(!debugger_plugins.has(p_script), "Debugger plugin doesn't exists."); + debugger_plugins.erase(p_script); + for (int i = 0; get_debugger(i); i++) { + get_debugger(i)->remove_debugger_plugin(p_script); + } +} diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h new file mode 100644 index 0000000000..8d70a7f961 --- /dev/null +++ b/editor/debugger/editor_debugger_node.h @@ -0,0 +1,195 @@ +/*************************************************************************/ +/* editor_debugger_node.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_DEBUGGER_NODE_H +#define EDITOR_DEBUGGER_NODE_H + +#include "editor/debugger/editor_debugger_server.h" +#include "scene/gui/margin_container.h" + +class Button; +class EditorDebuggerTree; +class EditorDebuggerRemoteObject; +class MenuButton; +class ScriptEditorDebugger; +class TabContainer; + +class EditorDebuggerNode : public MarginContainer { + GDCLASS(EditorDebuggerNode, MarginContainer); + +public: + enum CameraOverride { + OVERRIDE_NONE, + OVERRIDE_2D, + OVERRIDE_3D_1, // 3D Viewport 1 + OVERRIDE_3D_2, // 3D Viewport 2 + OVERRIDE_3D_3, // 3D Viewport 3 + OVERRIDE_3D_4 // 3D Viewport 4 + }; + +private: + enum Options { + DEBUG_NEXT, + DEBUG_STEP, + DEBUG_BREAK, + DEBUG_CONTINUE, + DEBUG_SHOW_KEEP_OPEN, + DEBUG_WITH_EXTERNAL_EDITOR, + }; + + class Breakpoint { + public: + String source; + int line = 0; + + bool operator<(const Breakpoint &p_b) const { + if (line == p_b.line) { + return source < p_b.source; + } + return line < p_b.line; + } + + Breakpoint() {} + + Breakpoint(const String &p_source, int p_line) { + line = p_line; + source = p_source; + } + }; + + Ref<EditorDebuggerServer> server; + TabContainer *tabs = nullptr; + Button *debugger_button = nullptr; + MenuButton *script_menu = nullptr; + + Ref<Script> stack_script; // Why?!? + + int last_error_count = 0; + int last_warning_count = 0; + + float inspect_edited_object_timeout = 0; + EditorDebuggerTree *remote_scene_tree = nullptr; + float remote_scene_tree_timeout = 0.0; + bool auto_switch_remote_scene_tree = false; + bool debug_with_external_editor = false; + bool hide_on_stop = true; + CameraOverride camera_override = OVERRIDE_NONE; + Map<Breakpoint, bool> breakpoints; + + Set<Ref<Script>> debugger_plugins; + + ScriptEditorDebugger *_add_debugger(); + EditorDebuggerRemoteObject *get_inspected_remote_object(); + + friend class DebuggerEditorPlugin; + static EditorDebuggerNode *singleton; + EditorDebuggerNode(); + +protected: + void _debugger_stopped(int p_id); + void _debugger_wants_stop(int p_id); + void _debugger_changed(int p_tab); + void _remote_tree_updated(int p_debugger); + void _remote_object_updated(ObjectID p_id, int p_debugger); + void _remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger); + void _remote_object_requested(ObjectID p_id, int p_debugger); + void _save_node_requested(ObjectID p_id, const String &p_file, int p_debugger); + + void _clear_execution(REF p_script) { + emit_signal("clear_execution", p_script); + } + + void _text_editor_stack_goto(const ScriptEditorDebugger *p_debugger); + void _stack_frame_selected(int p_debugger); + void _error_selected(const String &p_file, int p_line, int p_debugger); + void _breaked(bool p_breaked, bool p_can_debug, int p_debugger); + void _paused(); + void _break_state_changed(); + void _menu_option(int p_id); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + static EditorDebuggerNode *get_singleton() { return singleton; } + + ScriptEditorDebugger *get_current_debugger() const; + ScriptEditorDebugger *get_default_debugger() const; + ScriptEditorDebugger *get_debugger(int p_debugger) const; + + void debug_next(); + void debug_step(); + void debug_break(); + void debug_continue(); + + void set_script_debug_button(MenuButton *p_button); + + void set_tool_button(Button *p_button) { + debugger_button = p_button; + } + + String get_var_value(const String &p_var) const; + Ref<Script> get_dump_stack_script() const { return stack_script; } // Why do we need this? + + bool get_debug_with_external_editor() { return debug_with_external_editor; } + + bool is_skip_breakpoints() const; + void set_breakpoint(const String &p_path, int p_line, bool p_enabled); + void reload_scripts(); + + // Remote inspector/edit. + void request_remote_tree(); + static void _method_changeds(void *p_ud, Object *p_base, const StringName &p_name, VARIANT_ARG_DECLARE); + static void _property_changeds(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value); + + // LiveDebug + void set_live_debugging(bool p_enabled); + void update_live_edit_root(); + void live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name); + void live_debug_instance_node(const NodePath &p_parent, const String &p_path, const String &p_name); + void live_debug_remove_node(const NodePath &p_at); + void live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id); + void live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos); + void live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name); + void live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos); + + // Camera + void set_camera_override(CameraOverride p_override) { camera_override = p_override; } + CameraOverride get_camera_override() { return camera_override; } + + Error start(const String &p_protocol = "tcp://"); + + void stop(); + + void add_debugger_plugin(const Ref<Script> &p_script); + void remove_debugger_plugin(const Ref<Script> &p_script); +}; +#endif // EDITOR_DEBUGGER_NODE_H diff --git a/editor/debugger/editor_debugger_server.cpp b/editor/debugger/editor_debugger_server.cpp new file mode 100644 index 0000000000..0b655044a8 --- /dev/null +++ b/editor/debugger/editor_debugger_server.cpp @@ -0,0 +1,112 @@ +/*************************************************************************/ +/* editor_debugger_server.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_debugger_server.h" + +#include "core/io/marshalls.h" +#include "core/io/tcp_server.h" +#include "core/os/mutex.h" +#include "core/os/thread.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" +#include "editor/editor_settings.h" + +class EditorDebuggerServerTCP : public EditorDebuggerServer { +private: + Ref<TCP_Server> server; + +public: + static EditorDebuggerServer *create(const String &p_protocol); + virtual void poll() {} + virtual Error start(); + virtual void stop(); + virtual bool is_active() const; + virtual bool is_connection_available() const; + virtual Ref<RemoteDebuggerPeer> take_connection(); + + EditorDebuggerServerTCP(); +}; + +EditorDebuggerServer *EditorDebuggerServerTCP::create(const String &p_protocol) { + ERR_FAIL_COND_V(p_protocol != "tcp://", nullptr); + return memnew(EditorDebuggerServerTCP); +} + +EditorDebuggerServerTCP::EditorDebuggerServerTCP() { + server.instance(); +} + +Error EditorDebuggerServerTCP::start() { + int remote_port = (int)EditorSettings::get_singleton()->get("network/debug/remote_port"); + const Error err = server->listen(remote_port); + if (err != OK) { + EditorNode::get_log()->add_message(String("Error listening on port ") + itos(remote_port), EditorLog::MSG_TYPE_ERROR); + return err; + } + return err; +} + +void EditorDebuggerServerTCP::stop() { + server->stop(); +} + +bool EditorDebuggerServerTCP::is_active() const { + return server->is_listening(); +} + +bool EditorDebuggerServerTCP::is_connection_available() const { + return server->is_listening() && server->is_connection_available(); +} + +Ref<RemoteDebuggerPeer> EditorDebuggerServerTCP::take_connection() { + ERR_FAIL_COND_V(!is_connection_available(), Ref<RemoteDebuggerPeer>()); + return memnew(RemoteDebuggerPeerTCP(server->take_connection())); +} + +/// EditorDebuggerServer +Map<StringName, EditorDebuggerServer::CreateServerFunc> EditorDebuggerServer::protocols; + +EditorDebuggerServer *EditorDebuggerServer::create(const String &p_protocol) { + ERR_FAIL_COND_V(!protocols.has(p_protocol), nullptr); + return protocols[p_protocol](p_protocol); +} + +void EditorDebuggerServer::register_protocol_handler(const String &p_protocol, CreateServerFunc p_func) { + ERR_FAIL_COND(protocols.has(p_protocol)); + protocols[p_protocol] = p_func; +} + +void EditorDebuggerServer::initialize() { + register_protocol_handler("tcp://", EditorDebuggerServerTCP::create); +} + +void EditorDebuggerServer::deinitialize() { + protocols.clear(); +} diff --git a/editor/debugger/editor_debugger_server.h b/editor/debugger/editor_debugger_server.h new file mode 100644 index 0000000000..10a9a232ab --- /dev/null +++ b/editor/debugger/editor_debugger_server.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* editor_debugger_server.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_DEBUGGER_CONNECTION_H +#define EDITOR_DEBUGGER_CONNECTION_H + +#include "core/debugger/remote_debugger_peer.h" +#include "core/reference.h" + +class EditorDebuggerServer : public Reference { +public: + typedef EditorDebuggerServer *(*CreateServerFunc)(const String &p_uri); + +private: + static Map<StringName, CreateServerFunc> protocols; + +public: + static void initialize(); + static void deinitialize(); + + static void register_protocol_handler(const String &p_protocol, CreateServerFunc p_func); + static EditorDebuggerServer *create(const String &p_protocol); + virtual void poll() = 0; + virtual Error start() = 0; + virtual void stop() = 0; + virtual bool is_active() const = 0; + virtual bool is_connection_available() const = 0; + virtual Ref<RemoteDebuggerPeer> take_connection() = 0; +}; + +#endif // EDITOR_DEBUGGER_CONNECTION_H diff --git a/editor/debugger/editor_debugger_tree.cpp b/editor/debugger/editor_debugger_tree.cpp new file mode 100644 index 0000000000..ebac9b3482 --- /dev/null +++ b/editor/debugger/editor_debugger_tree.cpp @@ -0,0 +1,268 @@ +/*************************************************************************/ +/* editor_debugger_tree.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_debugger_tree.h" + +#include "editor/editor_node.h" +#include "scene/debugger/scene_debugger.h" +#include "scene/resources/packed_scene.h" +#include "servers/display_server.h" + +EditorDebuggerTree::EditorDebuggerTree() { + set_v_size_flags(SIZE_EXPAND_FILL); + set_allow_rmb_select(true); + + // Popup + item_menu = memnew(PopupMenu); + item_menu->connect("id_pressed", callable_mp(this, &EditorDebuggerTree::_item_menu_id_pressed)); + add_child(item_menu); + + // File Dialog + file_dialog = memnew(EditorFileDialog); + file_dialog->connect("file_selected", callable_mp(this, &EditorDebuggerTree::_file_selected)); + add_child(file_dialog); +} + +void EditorDebuggerTree::_notification(int p_what) { + if (p_what == NOTIFICATION_POSTINITIALIZE) { + connect("cell_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selected)); + connect("item_collapsed", callable_mp(this, &EditorDebuggerTree::_scene_tree_folded)); + connect("item_rmb_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_rmb_selected)); + } +} + +void EditorDebuggerTree::_bind_methods() { + ADD_SIGNAL(MethodInfo("object_selected", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::INT, "debugger"))); + ADD_SIGNAL(MethodInfo("save_node", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "filename"), PropertyInfo(Variant::INT, "debugger"))); +} + +void EditorDebuggerTree::_scene_tree_selected() { + if (updating_scene_tree) { + return; + } + + TreeItem *item = get_selected(); + if (!item) { + return; + } + + inspected_object_id = uint64_t(item->get_metadata(0)); + + emit_signal("object_selected", inspected_object_id, debugger_id); +} + +void EditorDebuggerTree::_scene_tree_folded(Object *p_obj) { + if (updating_scene_tree) { + return; + } + TreeItem *item = Object::cast_to<TreeItem>(p_obj); + + if (!item) { + return; + } + + ObjectID id = ObjectID(uint64_t(item->get_metadata(0))); + if (unfold_cache.has(id)) { + unfold_cache.erase(id); + } else { + unfold_cache.insert(id); + } +} + +void EditorDebuggerTree::_scene_tree_rmb_selected(const Vector2 &p_position) { + TreeItem *item = get_item_at_position(p_position); + if (!item) { + return; + } + + item->select(0); + + item_menu->clear(); + item_menu->add_icon_item(get_theme_icon("CreateNewSceneFrom", "EditorIcons"), TTR("Save Branch as Scene"), ITEM_MENU_SAVE_REMOTE_NODE); + item_menu->add_icon_item(get_theme_icon("CopyNodePath", "EditorIcons"), TTR("Copy Node Path"), ITEM_MENU_COPY_NODE_PATH); + item_menu->set_position(get_screen_transform().xform(get_local_mouse_position())); + item_menu->popup(); +} + +/// Populates inspect_scene_tree given data in nodes as a flat list, encoded depth first. +/// +/// Given a nodes array like [R,A,B,C,D,E] the following Tree will be generated, assuming +/// filter is an empty String, R and A child count are 2, B is 1 and C, D and E are 0. +/// +/// R +/// |-A +/// | |-B +/// | | |-C +/// | | +/// | |-D +/// | +/// |-E +/// +void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger) { + updating_scene_tree = true; + const String last_path = get_selected_path(); + const String filter = EditorNode::get_singleton()->get_scene_tree_dock()->get_filter(); + + // Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion. + List<Pair<TreeItem *, int>> parents; + for (int i = 0; i < p_tree->nodes.size(); i++) { + TreeItem *parent = nullptr; + if (parents.size()) { // Find last parent. + Pair<TreeItem *, int> &p = parents[0]; + parent = p.first; + if (!(--p.second)) { // If no child left, remove it. + parents.pop_front(); + } + } + // Add this node. + const SceneDebuggerTree::RemoteNode &node = p_tree->nodes[i]; + TreeItem *item = create_item(parent); + item->set_text(0, node.name); + item->set_tooltip(0, TTR("Type:") + " " + node.type_name); + Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(node.type_name, ""); + if (icon.is_valid()) { + item->set_icon(0, icon); + } + item->set_metadata(0, node.id); + + // Set current item as collapsed if necessary (root is never collapsed) + if (parent) { + if (!unfold_cache.has(node.id)) { + item->set_collapsed(true); + } + } + // Select previously selected node. + if (debugger_id == p_debugger) { // Can use remote id. + if (node.id == inspected_object_id) { + item->select(0); + } + } else { // Must use path + if (last_path == _get_path(item)) { + updating_scene_tree = false; // Force emission of new selection + item->select(0); + updating_scene_tree = true; + } + } + + // Add in front of the parents stack if children are expected. + if (node.child_count) { + parents.push_front(Pair<TreeItem *, int>(item, node.child_count)); + } else { + // Apply filters. + while (parent) { + const bool had_siblings = item->get_prev() || item->get_next(); + if (filter.is_subsequence_ofi(item->get_text(0))) { + break; // Filter matches, must survive. + } + parent->remove_child(item); + memdelete(item); + if (had_siblings) { + break; // Parent must survive. + } + item = parent; + parent = item->get_parent(); + // Check if parent expects more children. + for (int j = 0; j < parents.size(); j++) { + if (parents[j].first == item) { + parent = nullptr; + break; // Might have more children. + } + } + } + } + } + debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree + updating_scene_tree = false; +} + +String EditorDebuggerTree::get_selected_path() { + if (!get_selected()) { + return ""; + } + return _get_path(get_selected()); +} + +String EditorDebuggerTree::_get_path(TreeItem *p_item) { + ERR_FAIL_COND_V(!p_item, ""); + + if (p_item->get_parent() == nullptr) { + return "/root"; + } + String text = p_item->get_text(0); + TreeItem *cur = p_item->get_parent(); + while (cur) { + text = cur->get_text(0) + "/" + text; + cur = cur->get_parent(); + } + return "/" + text; +} + +void EditorDebuggerTree::_item_menu_id_pressed(int p_option) { + switch (p_option) { + case ITEM_MENU_SAVE_REMOTE_NODE: { + file_dialog->set_access(EditorFileDialog::ACCESS_RESOURCES); + file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + + List<String> extensions; + Ref<PackedScene> sd = memnew(PackedScene); + ResourceSaver::get_recognized_extensions(sd, &extensions); + file_dialog->clear_filters(); + for (int i = 0; i < extensions.size(); i++) { + file_dialog->add_filter("*." + extensions[i] + " ; " + extensions[i].to_upper()); + } + + file_dialog->popup_file_dialog(); + } break; + case ITEM_MENU_COPY_NODE_PATH: { + String text = get_selected_path(); + if (text.empty()) { + return; + } else if (text == "/root") { + text = "."; + } else { + text = text.replace("/root/", ""); + int slash = text.find("/"); + if (slash < 0) { + text = "."; + } else { + text = text.substr(slash + 1); + } + } + DisplayServer::get_singleton()->clipboard_set(text); + } break; + } +} + +void EditorDebuggerTree::_file_selected(const String &p_file) { + if (inspected_object_id.is_null()) { + return; + } + emit_signal("save_node", inspected_object_id, p_file, debugger_id); +} diff --git a/editor/debugger/editor_debugger_tree.h b/editor/debugger/editor_debugger_tree.h new file mode 100644 index 0000000000..5ec1423c07 --- /dev/null +++ b/editor/debugger/editor_debugger_tree.h @@ -0,0 +1,73 @@ +/*************************************************************************/ +/* editor_debugger_tree.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "scene/gui/tree.h" + +#ifndef EDITOR_DEBUGGER_TREE_H +#define EDITOR_DEBUGGER_TREE_H + +class SceneDebuggerTree; +class EditorFileDialog; + +class EditorDebuggerTree : public Tree { + GDCLASS(EditorDebuggerTree, Tree); + +private: + enum ItemMenu { + ITEM_MENU_SAVE_REMOTE_NODE, + ITEM_MENU_COPY_NODE_PATH, + }; + + ObjectID inspected_object_id; + int debugger_id = 0; + bool updating_scene_tree = false; + Set<ObjectID> unfold_cache; + PopupMenu *item_menu = nullptr; + EditorFileDialog *file_dialog = nullptr; + + String _get_path(TreeItem *p_item); + void _scene_tree_folded(Object *p_obj); + void _scene_tree_selected(); + void _scene_tree_rmb_selected(const Vector2 &p_position); + void _item_menu_id_pressed(int p_option); + void _file_selected(const String &p_file); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + String get_selected_path(); + ObjectID get_selected_object(); + int get_current_debugger(); // Would love to have one tree for every debugger. + void update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger); + EditorDebuggerTree(); +}; +#endif // EDITOR_DEBUGGER_TREE_H diff --git a/editor/debugger/editor_network_profiler.cpp b/editor/debugger/editor_network_profiler.cpp new file mode 100644 index 0000000000..baa88bcdbc --- /dev/null +++ b/editor/debugger/editor_network_profiler.cpp @@ -0,0 +1,201 @@ +/*************************************************************************/ +/* editor_network_profiler.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_network_profiler.h" + +#include "core/os/os.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void EditorNetworkProfiler::_bind_methods() { + ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); +} + +void EditorNetworkProfiler::_notification(int p_what) { + if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + clear_button->set_icon(get_theme_icon("Clear", "EditorIcons")); + incoming_bandwidth_text->set_right_icon(get_theme_icon("ArrowDown", "EditorIcons")); + outgoing_bandwidth_text->set_right_icon(get_theme_icon("ArrowUp", "EditorIcons")); + + // This needs to be done here to set the faded color when the profiler is first opened + incoming_bandwidth_text->add_theme_color_override("font_color_uneditable", get_theme_color("font_color", "Editor") * Color(1, 1, 1, 0.5)); + outgoing_bandwidth_text->add_theme_color_override("font_color_uneditable", get_theme_color("font_color", "Editor") * Color(1, 1, 1, 0.5)); + } +} + +void EditorNetworkProfiler::_update_frame() { + counters_display->clear(); + + TreeItem *root = counters_display->create_item(); + + for (Map<ObjectID, DebuggerMarshalls::MultiplayerNodeInfo>::Element *E = nodes_data.front(); E; E = E->next()) { + TreeItem *node = counters_display->create_item(root); + + for (int j = 0; j < counters_display->get_columns(); ++j) { + node->set_text_align(j, j > 0 ? TreeItem::ALIGN_RIGHT : TreeItem::ALIGN_LEFT); + } + + node->set_text(0, E->get().node_path); + node->set_text(1, E->get().incoming_rpc == 0 ? "-" : itos(E->get().incoming_rpc)); + node->set_text(2, E->get().incoming_rset == 0 ? "-" : itos(E->get().incoming_rset)); + node->set_text(3, E->get().outgoing_rpc == 0 ? "-" : itos(E->get().outgoing_rpc)); + node->set_text(4, E->get().outgoing_rset == 0 ? "-" : itos(E->get().outgoing_rset)); + } +} + +void EditorNetworkProfiler::_activate_pressed() { + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon("Stop", "EditorIcons")); + activate->set_text(TTR("Stop")); + } else { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + activate->set_text(TTR("Start")); + } + emit_signal("enable_profiling", activate->is_pressed()); +} + +void EditorNetworkProfiler::_clear_pressed() { + nodes_data.clear(); + set_bandwidth(0, 0); + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +void EditorNetworkProfiler::add_node_frame_data(const DebuggerMarshalls::MultiplayerNodeInfo p_frame) { + if (!nodes_data.has(p_frame.node)) { + nodes_data.insert(p_frame.node, p_frame); + } else { + nodes_data[p_frame.node].incoming_rpc += p_frame.incoming_rpc; + nodes_data[p_frame.node].incoming_rset += p_frame.incoming_rset; + nodes_data[p_frame.node].outgoing_rpc += p_frame.outgoing_rpc; + nodes_data[p_frame.node].outgoing_rset += p_frame.outgoing_rset; + } + + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +void EditorNetworkProfiler::set_bandwidth(int p_incoming, int p_outgoing) { + incoming_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_incoming))); + outgoing_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_outgoing))); + + // Make labels more prominent when the bandwidth is greater than 0 to attract user attention + incoming_bandwidth_text->add_theme_color_override( + "font_color_uneditable", + get_theme_color("font_color", "Editor") * Color(1, 1, 1, p_incoming > 0 ? 1 : 0.5)); + outgoing_bandwidth_text->add_theme_color_override( + "font_color_uneditable", + get_theme_color("font_color", "Editor") * Color(1, 1, 1, p_outgoing > 0 ? 1 : 0.5)); +} + +bool EditorNetworkProfiler::is_profiling() { + return activate->is_pressed(); +} + +EditorNetworkProfiler::EditorNetworkProfiler() { + HBoxContainer *hb = memnew(HBoxContainer); + hb->add_theme_constant_override("separation", 8 * EDSCALE); + add_child(hb); + + activate = memnew(Button); + activate->set_toggle_mode(true); + activate->set_text(TTR("Start")); + activate->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_activate_pressed)); + hb->add_child(activate); + + clear_button = memnew(Button); + clear_button->set_text(TTR("Clear")); + clear_button->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_clear_pressed)); + hb->add_child(clear_button); + + hb->add_spacer(); + + Label *lb = memnew(Label); + lb->set_text(TTR("Down")); + hb->add_child(lb); + + incoming_bandwidth_text = memnew(LineEdit); + incoming_bandwidth_text->set_editable(false); + incoming_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + incoming_bandwidth_text->set_align(LineEdit::Align::ALIGN_RIGHT); + hb->add_child(incoming_bandwidth_text); + + Control *down_up_spacer = memnew(Control); + down_up_spacer->set_custom_minimum_size(Size2(30, 0) * EDSCALE); + hb->add_child(down_up_spacer); + + lb = memnew(Label); + lb->set_text(TTR("Up")); + hb->add_child(lb); + + outgoing_bandwidth_text = memnew(LineEdit); + outgoing_bandwidth_text->set_editable(false); + outgoing_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + outgoing_bandwidth_text->set_align(LineEdit::Align::ALIGN_RIGHT); + hb->add_child(outgoing_bandwidth_text); + + // Set initial texts in the incoming/outgoing bandwidth labels + set_bandwidth(0, 0); + + counters_display = memnew(Tree); + counters_display->set_custom_minimum_size(Size2(300, 0) * EDSCALE); + counters_display->set_v_size_flags(SIZE_EXPAND_FILL); + counters_display->set_hide_folding(true); + counters_display->set_hide_root(true); + counters_display->set_columns(5); + counters_display->set_column_titles_visible(true); + counters_display->set_column_title(0, TTR("Node")); + counters_display->set_column_expand(0, true); + counters_display->set_column_min_width(0, 60 * EDSCALE); + counters_display->set_column_title(1, TTR("Incoming RPC")); + counters_display->set_column_expand(1, false); + counters_display->set_column_min_width(1, 120 * EDSCALE); + counters_display->set_column_title(2, TTR("Incoming RSET")); + counters_display->set_column_expand(2, false); + counters_display->set_column_min_width(2, 120 * EDSCALE); + counters_display->set_column_title(3, TTR("Outgoing RPC")); + counters_display->set_column_expand(3, false); + counters_display->set_column_min_width(3, 120 * EDSCALE); + counters_display->set_column_title(4, TTR("Outgoing RSET")); + counters_display->set_column_expand(4, false); + counters_display->set_column_min_width(4, 120 * EDSCALE); + add_child(counters_display); + + frame_delay = memnew(Timer); + frame_delay->set_wait_time(0.1); + frame_delay->set_one_shot(true); + add_child(frame_delay); + frame_delay->connect("timeout", callable_mp(this, &EditorNetworkProfiler::_update_frame)); +} diff --git a/editor/debugger/editor_network_profiler.h b/editor/debugger/editor_network_profiler.h new file mode 100644 index 0000000000..cf65fb5316 --- /dev/null +++ b/editor/debugger/editor_network_profiler.h @@ -0,0 +1,72 @@ +/*************************************************************************/ +/* editor_network_profiler.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITORNETWORKPROFILER_H +#define EDITORNETWORKPROFILER_H + +#include "core/debugger/debugger_marshalls.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/label.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tree.h" + +class EditorNetworkProfiler : public VBoxContainer { + GDCLASS(EditorNetworkProfiler, VBoxContainer) + +private: + Button *activate; + Button *clear_button; + Tree *counters_display; + LineEdit *incoming_bandwidth_text; + LineEdit *outgoing_bandwidth_text; + + Timer *frame_delay; + + Map<ObjectID, DebuggerMarshalls::MultiplayerNodeInfo> nodes_data; + + void _update_frame(); + + void _activate_pressed(); + void _clear_pressed(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void add_node_frame_data(const DebuggerMarshalls::MultiplayerNodeInfo p_frame); + void set_bandwidth(int p_incoming, int p_outgoing); + bool is_profiling(); + + EditorNetworkProfiler(); +}; + +#endif //EDITORNETWORKPROFILER_H diff --git a/editor/debugger/editor_performance_profiler.cpp b/editor/debugger/editor_performance_profiler.cpp new file mode 100644 index 0000000000..47fe282758 --- /dev/null +++ b/editor/debugger/editor_performance_profiler.cpp @@ -0,0 +1,394 @@ +/*************************************************************************/ +/* editor_performance_profiler.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_performance_profiler.h" + +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" +#include "main/performance.h" + +EditorPerformanceProfiler::Monitor::Monitor() {} + +EditorPerformanceProfiler::Monitor::Monitor(String p_name, String p_base, int p_frame_index, Performance::MonitorType p_type, TreeItem *p_item) { + type = p_type; + item = p_item; + frame_index = p_frame_index; + name = p_name; + base = p_base; +} + +void EditorPerformanceProfiler::Monitor::update_value(float p_value) { + ERR_FAIL_COND(!item); + String label = EditorPerformanceProfiler::_create_label(p_value, type); + String tooltip = label; + switch (type) { + case Performance::MONITOR_TYPE_MEMORY: { + tooltip = label; + } break; + case Performance::MONITOR_TYPE_TIME: { + tooltip = label; + } break; + default: { + tooltip += " " + item->get_text(0); + } break; + } + item->set_text(1, label); + item->set_tooltip(1, tooltip); + + if (p_value > max) { + max = p_value; + } +} + +void EditorPerformanceProfiler::Monitor::reset() { + history.clear(); + max = 0.0f; + if (item) { + item->set_text(1, ""); + item->set_tooltip(1, ""); + } +} + +String EditorPerformanceProfiler::_create_label(float p_value, Performance::MonitorType p_type) { + switch (p_type) { + case Performance::MONITOR_TYPE_MEMORY: { + return String::humanize_size(p_value); + } + case Performance::MONITOR_TYPE_TIME: { + return rtos(p_value * 1000).pad_decimals(2) + " ms"; + } + default: { + return rtos(p_value); + } + } +} + +void EditorPerformanceProfiler::_monitor_select() { + monitor_draw->update(); +} + +void EditorPerformanceProfiler::_monitor_draw() { + Vector<StringName> active; + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + if (i.value().item->is_checked(0)) { + active.push_back(i.key()); + } + } + + if (active.empty()) { + info_message->show(); + return; + } + + info_message->hide(); + + Ref<StyleBox> graph_style_box = get_theme_stylebox("normal", "TextEdit"); + Ref<Font> graph_font = get_theme_font("font", "TextEdit"); + + int columns = int(Math::ceil(Math::sqrt(float(active.size())))); + int rows = int(Math::ceil(float(active.size()) / float(columns))); + if (active.size() == 1) { + rows = 1; + } + Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows); + float spacing = float(POINT_SEPARATION) / float(columns); + float value_multiplier = EditorSettings::get_singleton()->is_dark_theme() ? 1.4f : 0.55f; + float hue_shift = 1.0f / float(monitors.size()); + + for (int i = 0; i < active.size(); i++) { + Monitor ¤t = monitors[active[i]]; + Rect2i rect(Point2i(i % columns, i / columns) * cell_size + Point2i(MARGIN, MARGIN), cell_size - Point2i(MARGIN, MARGIN) * 2); + monitor_draw->draw_style_box(graph_style_box, rect); + + rect.position += graph_style_box->get_offset(); + rect.size -= graph_style_box->get_minimum_size(); + Color draw_color = get_theme_color("accent_color", "Editor"); + draw_color.set_hsv(Math::fmod(hue_shift * float(current.frame_index), 0.9f), draw_color.get_s() * 0.9f, draw_color.get_v() * value_multiplier, 0.6f); + monitor_draw->draw_string(graph_font, rect.position + Point2(0, graph_font->get_ascent()), current.item->get_text(0), draw_color, rect.size.x); + + draw_color.a = 0.9f; + float value_position = rect.size.width - graph_font->get_string_size(current.item->get_text(1)).width; + if (value_position < 0) { + value_position = 0; + } + monitor_draw->draw_string(graph_font, rect.position + Point2(value_position, graph_font->get_ascent()), current.item->get_text(1), draw_color, rect.size.x); + + rect.position.y += graph_font->get_height(); + rect.size.height -= graph_font->get_height(); + + int line_count = rect.size.height / (graph_font->get_height() * 2); + if (line_count > 5) { + line_count = 5; + } + if (line_count > 0) { + Color horizontal_line_color; + horizontal_line_color.set_hsv(draw_color.get_h(), draw_color.get_s() * 0.5f, draw_color.get_v() * 0.5f, 0.3f); + monitor_draw->draw_line(rect.position, rect.position + Vector2(rect.size.width, 0), horizontal_line_color, Math::round(EDSCALE)); + monitor_draw->draw_string(graph_font, rect.position + Vector2(0, graph_font->get_ascent()), _create_label(current.max, current.type), horizontal_line_color, rect.size.width); + + for (int j = 0; j < line_count; j++) { + Vector2 y_offset = Vector2(0, rect.size.height * (1.0f - float(j) / float(line_count))); + monitor_draw->draw_line(rect.position + y_offset, rect.position + Vector2(rect.size.width, 0) + y_offset, horizontal_line_color, Math::round(EDSCALE)); + monitor_draw->draw_string(graph_font, rect.position - Vector2(0, graph_font->get_descent()) + y_offset, _create_label(current.max * float(j) / float(line_count), current.type), horizontal_line_color, rect.size.width); + } + } + + float from = rect.size.width; + float prev = -1.0f; + int count = 0; + List<float>::Element *e = current.history.front(); + + while (from >= 0 && e) { + float m = current.max; + float h2 = 0; + if (m != 0) { + h2 = (e->get() / m); + } + h2 = (1.0f - h2) * float(rect.size.y); + if (e != current.history.front()) { + monitor_draw->draw_line(rect.position + Point2(from, h2), rect.position + Point2(from + spacing, prev), draw_color, Math::round(EDSCALE)); + } + + if (marker_key == active[i] && count == marker_frame) { + Color line_color; + line_color.set_hsv(draw_color.get_h(), draw_color.get_s() * 0.8f, draw_color.get_v(), 0.5f); + monitor_draw->draw_line(rect.position + Point2(from, 0), rect.position + Point2(from, rect.size.y), line_color, Math::round(EDSCALE)); + + String label = _create_label(e->get(), current.type); + Size2 size = graph_font->get_string_size(label); + Vector2 text_top_left_position = Vector2(from, h2) - (size + Vector2(MARKER_MARGIN, MARKER_MARGIN)); + if (text_top_left_position.x < 0) { + text_top_left_position.x = from + MARKER_MARGIN; + } + if (text_top_left_position.y < 0) { + text_top_left_position.y = h2 + MARKER_MARGIN; + } + monitor_draw->draw_string(graph_font, rect.position + text_top_left_position + Point2(0, graph_font->get_ascent()), label, line_color, rect.size.x); + } + prev = h2; + e = e->next(); + from -= spacing; + count++; + } + } +} + +void EditorPerformanceProfiler::_build_monitor_tree() { + Set<StringName> monitor_checked; + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + if (i.value().item && i.value().item->is_checked(0)) { + monitor_checked.insert(i.key()); + } + } + + base_map.clear(); + monitor_tree->get_root()->clear_children(); + + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + TreeItem *base = _get_monitor_base(i.value().base); + TreeItem *item = _create_monitor_item(i.value().name, base); + item->set_checked(0, monitor_checked.has(i.key())); + i.value().item = item; + if (!i.value().history.empty()) { + i.value().update_value(i.value().history.front()->get()); + } + } +} + +TreeItem *EditorPerformanceProfiler::_get_monitor_base(const StringName &p_base_name) { + if (base_map.has(p_base_name)) { + return base_map[p_base_name]; + } + + TreeItem *base = monitor_tree->create_item(monitor_tree->get_root()); + base->set_text(0, p_base_name); + base->set_editable(0, false); + base->set_selectable(0, false); + base->set_expand_right(0, true); + base_map.insert(p_base_name, base); + return base; +} + +TreeItem *EditorPerformanceProfiler::_create_monitor_item(const StringName &p_monitor_name, TreeItem *p_base) { + TreeItem *item = monitor_tree->create_item(p_base); + item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + item->set_editable(0, true); + item->set_selectable(0, false); + item->set_selectable(1, false); + item->set_text(0, p_monitor_name); + return item; +} + +void EditorPerformanceProfiler::_marker_input(const Ref<InputEvent> &p_event) { + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + Vector<StringName> active; + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + if (i.value().item->is_checked(0)) { + active.push_back(i.key()); + } + } + if (active.size() > 0) { + int columns = int(Math::ceil(Math::sqrt(float(active.size())))); + int rows = int(Math::ceil(float(active.size()) / float(columns))); + if (active.size() == 1) { + rows = 1; + } + Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows); + Vector2i index = mb->get_position() / cell_size; + Rect2i rect(index * cell_size + Point2i(MARGIN, MARGIN), cell_size - Point2i(MARGIN, MARGIN) * 2); + if (rect.has_point(mb->get_position())) { + if (index.x + index.y * columns < active.size()) { + marker_key = active[index.x + index.y * columns]; + } else { + marker_key = ""; + } + Ref<StyleBox> graph_style_box = get_theme_stylebox("normal", "TextEdit"); + rect.position += graph_style_box->get_offset(); + rect.size -= graph_style_box->get_minimum_size(); + Vector2 point = mb->get_position() - rect.position; + if (point.x >= rect.size.x) { + marker_frame = 0; + } else { + int point_sep = 5; + float spacing = float(point_sep) / float(columns); + marker_frame = (rect.size.x - point.x) / spacing; + } + monitor_draw->update(); + return; + } + } + marker_key = ""; + monitor_draw->update(); + } +} + +void EditorPerformanceProfiler::reset() { + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + if (String(i.key()).begins_with("custom:")) { + monitors.erase(i); + } else { + i.value().reset(); + } + } + + _build_monitor_tree(); + marker_key = ""; + marker_frame = 0; + monitor_draw->update(); +} + +void EditorPerformanceProfiler::update_monitors(const Vector<StringName> &p_names) { + OrderedHashMap<StringName, int> names; + for (int i = 0; i < p_names.size(); i++) { + names.insert("custom:" + p_names[i], Performance::MONITOR_MAX + i); + } + + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + if (String(i.key()).begins_with("custom:")) { + if (!names.has(i.key())) { + monitors.erase(i); + } else { + i.value().frame_index = names[i.key()]; + names.erase(i.key()); + } + } + } + + for (OrderedHashMap<StringName, int>::Element i = names.front(); i; i = i.next()) { + String name = String(i.key()).replace_first("custom:", ""); + String base = "Custom"; + if (name.get_slice_count("/") == 2) { + base = name.get_slicec('/', 0); + name = name.get_slicec('/', 1); + } + monitors.insert(i.key(), Monitor(name, base, i.value(), Performance::MONITOR_TYPE_QUANTITY, nullptr)); + } + + _build_monitor_tree(); +} + +void EditorPerformanceProfiler::add_profile_frame(const Vector<float> &p_values) { + for (OrderedHashMap<StringName, Monitor>::Element i = monitors.front(); i; i = i.next()) { + float data = 0.0f; + if (i.value().frame_index >= 0 && i.value().frame_index < p_values.size()) { + data = p_values[i.value().frame_index]; + } + i.value().history.push_front(data); + i.value().update_value(data); + } + marker_frame++; + monitor_draw->update(); +} + +List<float> *EditorPerformanceProfiler::get_monitor_data(const StringName &p_name) { + if (monitors.has(p_name)) { + return &monitors[p_name].history; + } + return nullptr; +} + +EditorPerformanceProfiler::EditorPerformanceProfiler() { + set_name(TTR("Monitors")); + set_split_offset(340 * EDSCALE); + + monitor_tree = memnew(Tree); + monitor_tree->set_columns(2); + monitor_tree->set_column_title(0, TTR("Monitor")); + monitor_tree->set_column_title(1, TTR("Value")); + monitor_tree->set_column_titles_visible(true); + monitor_tree->connect("item_edited", callable_mp(this, &EditorPerformanceProfiler::_monitor_select)); + monitor_tree->create_item(); + monitor_tree->set_hide_root(true); + add_child(monitor_tree); + + monitor_draw = memnew(Control); + monitor_draw->set_clip_contents(true); + monitor_draw->connect("draw", callable_mp(this, &EditorPerformanceProfiler::_monitor_draw)); + monitor_draw->connect("gui_input", callable_mp(this, &EditorPerformanceProfiler::_marker_input)); + add_child(monitor_draw); + + info_message = memnew(Label); + info_message->set_text(TTR("Pick one or more items from the list to display the graph.")); + info_message->set_valign(Label::VALIGN_CENTER); + info_message->set_align(Label::ALIGN_CENTER); + info_message->set_autowrap(true); + info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0)); + info_message->set_anchors_and_margins_preset(PRESET_WIDE, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE); + monitor_draw->add_child(info_message); + + for (int i = 0; i < Performance::MONITOR_MAX; i++) { + String base = Performance::get_singleton()->get_monitor_name(Performance::Monitor(i)).get_slicec('/', 0).capitalize(); + String name = Performance::get_singleton()->get_monitor_name(Performance::Monitor(i)).get_slicec('/', 1).capitalize(); + monitors.insert(Performance::get_singleton()->get_monitor_name(Performance::Monitor(i)), Monitor(name, base, i, Performance::get_singleton()->get_monitor_type(Performance::Monitor(i)), nullptr)); + } + + _build_monitor_tree(); +} diff --git a/editor/debugger/editor_performance_profiler.h b/editor/debugger/editor_performance_profiler.h new file mode 100644 index 0000000000..144dd34103 --- /dev/null +++ b/editor/debugger/editor_performance_profiler.h @@ -0,0 +1,90 @@ +/*************************************************************************/ +/* editor_performance_profiler.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_PERFORMANCE_PROFILER_H +#define EDITOR_PERFORMANCE_PROFILER_H + +#include "core/map.h" +#include "core/ordered_hash_map.h" +#include "main/performance.h" +#include "scene/gui/control.h" +#include "scene/gui/label.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tree.h" + +class EditorPerformanceProfiler : public HSplitContainer { + GDCLASS(EditorPerformanceProfiler, HSplitContainer); + +private: + class Monitor { + public: + String name; + String base; + List<float> history; + float max = 0.0f; + TreeItem *item = nullptr; + Performance::MonitorType type = Performance::MONITOR_TYPE_QUANTITY; + int frame_index = 0; + + Monitor(); + Monitor(String p_name, String p_base, int p_frame_index, Performance::MonitorType p_type, TreeItem *p_item); + void update_value(float p_value); + void reset(); + }; + + OrderedHashMap<StringName, Monitor> monitors; + + Map<StringName, TreeItem *> base_map; + Tree *monitor_tree; + Control *monitor_draw; + Label *info_message; + StringName marker_key; + int marker_frame; + const int MARGIN = 4; + const int POINT_SEPARATION = 5; + const int MARKER_MARGIN = 2; + + static String _create_label(float p_value, Performance::MonitorType p_type); + void _monitor_select(); + void _monitor_draw(); + void _build_monitor_tree(); + TreeItem *_get_monitor_base(const StringName &p_base_name); + TreeItem *_create_monitor_item(const StringName &p_monitor_name, TreeItem *p_base); + void _marker_input(const Ref<InputEvent> &p_event); + +public: + void reset(); + void update_monitors(const Vector<StringName> &p_names); + void add_profile_frame(const Vector<float> &p_values); + List<float> *get_monitor_data(const StringName &p_name); + EditorPerformanceProfiler(); +}; + +#endif // EDITOR_PERFORMANCE_PROFILER_H diff --git a/editor/debugger/editor_profiler.cpp b/editor/debugger/editor_profiler.cpp new file mode 100644 index 0000000000..8bd21fff5c --- /dev/null +++ b/editor/debugger/editor_profiler.cpp @@ -0,0 +1,751 @@ +/*************************************************************************/ +/* editor_profiler.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_profiler.h" + +#include "core/os/os.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void EditorProfiler::_make_metric_ptrs(Metric &m) { + for (int i = 0; i < m.categories.size(); i++) { + m.category_ptrs[m.categories[i].signature] = &m.categories.write[i]; + for (int j = 0; j < m.categories[i].items.size(); j++) { + m.item_ptrs[m.categories[i].items[j].signature] = &m.categories.write[i].items.write[j]; + } + } +} + +void EditorProfiler::add_frame_metric(const Metric &p_metric, bool p_final) { + ++last_metric; + if (last_metric >= frame_metrics.size()) { + last_metric = 0; + } + + frame_metrics.write[last_metric] = p_metric; + _make_metric_ptrs(frame_metrics.write[last_metric]); + + updating_frame = true; + cursor_metric_edit->set_max(frame_metrics[last_metric].frame_number); + cursor_metric_edit->set_min(MAX(frame_metrics[last_metric].frame_number - frame_metrics.size(), 0)); + + if (!seeking) { + cursor_metric_edit->set_value(frame_metrics[last_metric].frame_number); + if (hover_metric != -1) { + hover_metric++; + if (hover_metric >= frame_metrics.size()) { + hover_metric = 0; + } + } + } + updating_frame = false; + + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(p_final ? 0.1 : 1); + frame_delay->start(); + } + + if (plot_delay->is_stopped()) { + plot_delay->set_wait_time(0.1); + plot_delay->start(); + } +} + +void EditorProfiler::clear() { + int metric_size = EditorSettings::get_singleton()->get("debugger/profiler_frame_history_size"); + metric_size = CLAMP(metric_size, 60, 1024); + frame_metrics.clear(); + frame_metrics.resize(metric_size); + last_metric = -1; + variables->clear(); + plot_sigs.clear(); + plot_sigs.insert("physics_frame_time"); + plot_sigs.insert("category_frame_time"); + + updating_frame = true; + cursor_metric_edit->set_min(0); + cursor_metric_edit->set_max(100); // Doesn't make much sense, but we can't have min == max. Doesn't hurt. + cursor_metric_edit->set_value(0); + updating_frame = false; + hover_metric = -1; + seeking = false; +} + +static String _get_percent_txt(float p_value, float p_total) { + if (p_total == 0) { + p_total = 0.00001; + } + + return String::num((p_value / p_total) * 100, 1) + "%"; +} + +String EditorProfiler::_get_time_as_text(const Metric &m, float p_time, int p_calls) { + const int dmode = display_mode->get_selected(); + + if (dmode == DISPLAY_FRAME_TIME) { + return rtos(p_time * 1000).pad_decimals(2) + " ms"; + } else if (dmode == DISPLAY_AVERAGE_TIME) { + if (p_calls == 0) { + return "0.00 ms"; + } else { + return rtos((p_time / p_calls) * 1000).pad_decimals(2) + " ms"; + } + } else if (dmode == DISPLAY_FRAME_PERCENT) { + return _get_percent_txt(p_time, m.frame_time); + } else if (dmode == DISPLAY_PHYSICS_FRAME_PERCENT) { + return _get_percent_txt(p_time, m.physics_frame_time); + } + + return "err"; +} + +Color EditorProfiler::_get_color_from_signature(const StringName &p_signature) const { + Color bc = get_theme_color("error_color", "Editor"); + double rot = ABS(double(p_signature.hash()) / double(0x7FFFFFFF)); + Color c; + c.set_hsv(rot, bc.get_s(), bc.get_v()); + return c.lerp(get_theme_color("base_color", "Editor"), 0.07); +} + +void EditorProfiler::_item_edited() { + if (updating_frame) { + return; + } + + TreeItem *item = variables->get_edited(); + if (!item) { + return; + } + StringName signature = item->get_metadata(0); + bool checked = item->is_checked(0); + + if (checked) { + plot_sigs.insert(signature); + } else { + plot_sigs.erase(signature); + } + + if (!frame_delay->is_processing()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } + + _update_plot(); +} + +void EditorProfiler::_update_plot() { + const int w = graph->get_size().width; + const int h = graph->get_size().height; + bool reset_texture = false; + const int desired_len = w * h * 4; + + if (graph_image.size() != desired_len) { + reset_texture = true; + graph_image.resize(desired_len); + } + + uint8_t *wr = graph_image.ptrw(); + const Color background_color = get_theme_color("dark_color_2", "Editor"); + + // Clear the previous frame and set the background color. + for (int i = 0; i < desired_len; i += 4) { + wr[i + 0] = Math::fast_ftoi(background_color.r * 255); + wr[i + 1] = Math::fast_ftoi(background_color.g * 255); + wr[i + 2] = Math::fast_ftoi(background_color.b * 255); + wr[i + 3] = 255; + } + + //find highest value + + const bool use_self = display_time->get_selected() == DISPLAY_SELF_TIME; + float highest = 0; + + for (int i = 0; i < frame_metrics.size(); i++) { + const Metric &m = frame_metrics[i]; + if (!m.valid) { + continue; + } + + for (Set<StringName>::Element *E = plot_sigs.front(); E; E = E->next()) { + const Map<StringName, Metric::Category *>::Element *F = m.category_ptrs.find(E->get()); + if (F) { + highest = MAX(F->get()->total_time, highest); + } + + const Map<StringName, Metric::Category::Item *>::Element *G = m.item_ptrs.find(E->get()); + if (G) { + if (use_self) { + highest = MAX(G->get()->self, highest); + } else { + highest = MAX(G->get()->total, highest); + } + } + } + } + + if (highest > 0) { + //means some data exists.. + highest *= 1.2; //leave some upper room + graph_height = highest; + + Vector<int> columnv; + columnv.resize(h * 4); + + int *column = columnv.ptrw(); + + Map<StringName, int> plot_prev; + //Map<StringName,int> plot_max; + + for (int i = 0; i < w; i++) { + for (int j = 0; j < h * 4; j++) { + column[j] = 0; + } + + int current = i * frame_metrics.size() / w; + int next = (i + 1) * frame_metrics.size() / w; + if (next > frame_metrics.size()) { + next = frame_metrics.size(); + } + if (next == current) { + next = current + 1; //just because for loop must work + } + + for (Set<StringName>::Element *E = plot_sigs.front(); E; E = E->next()) { + int plot_pos = -1; + + for (int j = current; j < next; j++) { + //wrap + int idx = last_metric + 1 + j; + while (idx >= frame_metrics.size()) { + idx -= frame_metrics.size(); + } + + //get + const Metric &m = frame_metrics[idx]; + if (!m.valid) { + continue; //skip because invalid + } + + float value = 0; + + const Map<StringName, Metric::Category *>::Element *F = m.category_ptrs.find(E->get()); + if (F) { + value = F->get()->total_time; + } + + const Map<StringName, Metric::Category::Item *>::Element *G = m.item_ptrs.find(E->get()); + if (G) { + if (use_self) { + value = G->get()->self; + } else { + value = G->get()->total; + } + } + + plot_pos = MAX(CLAMP(int(value * h / highest), 0, h - 1), plot_pos); + } + + int prev_plot = plot_pos; + Map<StringName, int>::Element *H = plot_prev.find(E->get()); + if (H) { + prev_plot = H->get(); + H->get() = plot_pos; + } else { + plot_prev[E->get()] = plot_pos; + } + + if (plot_pos == -1 && prev_plot == -1) { + //don't bother drawing + continue; + } + + if (prev_plot != -1 && plot_pos == -1) { + plot_pos = prev_plot; + } + + if (prev_plot == -1 && plot_pos != -1) { + prev_plot = plot_pos; + } + + plot_pos = h - plot_pos - 1; + prev_plot = h - prev_plot - 1; + + if (prev_plot > plot_pos) { + SWAP(prev_plot, plot_pos); + } + + Color col = _get_color_from_signature(E->get()); + + for (int j = prev_plot; j <= plot_pos; j++) { + column[j * 4 + 0] += Math::fast_ftoi(CLAMP(col.r * 255, 0, 255)); + column[j * 4 + 1] += Math::fast_ftoi(CLAMP(col.g * 255, 0, 255)); + column[j * 4 + 2] += Math::fast_ftoi(CLAMP(col.b * 255, 0, 255)); + column[j * 4 + 3] += 1; + } + } + + for (int j = 0; j < h * 4; j += 4) { + const int a = column[j + 3]; + if (a > 0) { + column[j + 0] /= a; + column[j + 1] /= a; + column[j + 2] /= a; + } + + const uint8_t red = uint8_t(column[j + 0]); + const uint8_t green = uint8_t(column[j + 1]); + const uint8_t blue = uint8_t(column[j + 2]); + const bool is_filled = red >= 1 || green >= 1 || blue >= 1; + const int widx = ((j >> 2) * w + i) * 4; + + // If the pixel isn't filled by any profiler line, apply the background color instead. + wr[widx + 0] = is_filled ? red : Math::fast_ftoi(background_color.r * 255); + wr[widx + 1] = is_filled ? green : Math::fast_ftoi(background_color.g * 255); + wr[widx + 2] = is_filled ? blue : Math::fast_ftoi(background_color.b * 255); + wr[widx + 3] = 255; + } + } + } + + Ref<Image> img; + img.instance(); + img->create(w, h, false, Image::FORMAT_RGBA8, graph_image); + + if (reset_texture) { + if (graph_texture.is_null()) { + graph_texture.instance(); + } + graph_texture->create_from_image(img); + } + + graph_texture->update(img, true); + + graph->set_texture(graph_texture); + graph->update(); +} + +void EditorProfiler::_update_frame() { + int cursor_metric = _get_cursor_index(); + + ERR_FAIL_INDEX(cursor_metric, frame_metrics.size()); + + updating_frame = true; + variables->clear(); + + TreeItem *root = variables->create_item(); + const Metric &m = frame_metrics[cursor_metric]; + + int dtime = display_time->get_selected(); + + for (int i = 0; i < m.categories.size(); i++) { + TreeItem *category = variables->create_item(root); + category->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + category->set_editable(0, true); + category->set_metadata(0, m.categories[i].signature); + category->set_text(0, String(m.categories[i].name)); + category->set_text(1, _get_time_as_text(m, m.categories[i].total_time, 1)); + + if (plot_sigs.has(m.categories[i].signature)) { + category->set_checked(0, true); + category->set_custom_color(0, _get_color_from_signature(m.categories[i].signature)); + } + + for (int j = m.categories[i].items.size() - 1; j >= 0; j--) { + const Metric::Category::Item &it = m.categories[i].items[j]; + + TreeItem *item = variables->create_item(category); + item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + item->set_editable(0, true); + item->set_text(0, it.name); + item->set_metadata(0, it.signature); + item->set_metadata(1, it.script); + item->set_metadata(2, it.line); + item->set_text_align(2, TreeItem::ALIGN_RIGHT); + item->set_tooltip(0, it.script + ":" + itos(it.line)); + + float time = dtime == DISPLAY_SELF_TIME ? it.self : it.total; + + item->set_text(1, _get_time_as_text(m, time, it.calls)); + + item->set_text(2, itos(it.calls)); + + if (plot_sigs.has(it.signature)) { + item->set_checked(0, true); + item->set_custom_color(0, _get_color_from_signature(it.signature)); + } + } + } + + updating_frame = false; +} + +void EditorProfiler::_activate_pressed() { + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon("Stop", "EditorIcons")); + activate->set_text(TTR("Stop")); + } else { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + activate->set_text(TTR("Start")); + } + emit_signal("enable_profiling", activate->is_pressed()); +} + +void EditorProfiler::_clear_pressed() { + clear(); + _update_plot(); +} + +void EditorProfiler::_notification(int p_what) { + if (p_what == NOTIFICATION_ENTER_TREE) { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + clear_button->set_icon(get_theme_icon("Clear", "EditorIcons")); + } +} + +void EditorProfiler::_graph_tex_draw() { + if (last_metric < 0) { + return; + } + if (seeking) { + int max_frames = frame_metrics.size(); + int frame = cursor_metric_edit->get_value() - (frame_metrics[last_metric].frame_number - max_frames + 1); + if (frame < 0) { + frame = 0; + } + + int cur_x = frame * graph->get_size().x / max_frames; + + graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.8)); + } + + if (hover_metric != -1 && frame_metrics[hover_metric].valid) { + int max_frames = frame_metrics.size(); + int frame = frame_metrics[hover_metric].frame_number - (frame_metrics[last_metric].frame_number - max_frames + 1); + if (frame < 0) { + frame = 0; + } + + int cur_x = frame * graph->get_size().x / max_frames; + + graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.4)); + } +} + +void EditorProfiler::_graph_tex_mouse_exit() { + hover_metric = -1; + graph->update(); +} + +void EditorProfiler::_cursor_metric_changed(double) { + if (updating_frame) { + return; + } + + graph->update(); + _update_frame(); +} + +void EditorProfiler::_graph_tex_input(const Ref<InputEvent> &p_ev) { + if (last_metric < 0) { + return; + } + + Ref<InputEventMouse> me = p_ev; + Ref<InputEventMouseButton> mb = p_ev; + Ref<InputEventMouseMotion> mm = p_ev; + + if ( + (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && mb->is_pressed()) || + (mm.is_valid())) { + int x = me->get_position().x; + x = x * frame_metrics.size() / graph->get_size().width; + + bool show_hover = x >= 0 && x < frame_metrics.size(); + + if (x < 0) { + x = 0; + } + + if (x >= frame_metrics.size()) { + x = frame_metrics.size() - 1; + } + + int metric = frame_metrics.size() - x - 1; + metric = last_metric - metric; + while (metric < 0) { + metric += frame_metrics.size(); + } + + if (show_hover) { + hover_metric = metric; + + } else { + hover_metric = -1; + } + + if (mb.is_valid() || mm->get_button_mask() & BUTTON_MASK_LEFT) { + //cursor_metric=x; + updating_frame = true; + + //metric may be invalid, so look for closest metric that is valid, this makes snap feel better + bool valid = false; + for (int i = 0; i < frame_metrics.size(); i++) { + if (frame_metrics[metric].valid) { + valid = true; + break; + } + + metric++; + if (metric >= frame_metrics.size()) { + metric = 0; + } + } + + if (valid) { + cursor_metric_edit->set_value(frame_metrics[metric].frame_number); + } + + updating_frame = false; + + if (activate->is_pressed()) { + if (!seeking) { + emit_signal("break_request"); + } + } + + seeking = true; + + if (!frame_delay->is_processing()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } + } + + graph->update(); + } +} + +int EditorProfiler::_get_cursor_index() const { + if (last_metric < 0) { + return 0; + } + if (!frame_metrics[last_metric].valid) { + return 0; + } + + int diff = (frame_metrics[last_metric].frame_number - cursor_metric_edit->get_value()); + + int idx = last_metric - diff; + while (idx < 0) { + idx += frame_metrics.size(); + } + + return idx; +} + +void EditorProfiler::disable_seeking() { + seeking = false; + graph->update(); +} + +void EditorProfiler::_combo_changed(int) { + _update_frame(); + _update_plot(); +} + +void EditorProfiler::_bind_methods() { + ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); + ADD_SIGNAL(MethodInfo("break_request")); +} + +void EditorProfiler::set_enabled(bool p_enable) { + activate->set_disabled(!p_enable); +} + +bool EditorProfiler::is_profiling() { + return activate->is_pressed(); +} + +Vector<Vector<String>> EditorProfiler::get_data_as_csv() const { + Vector<Vector<String>> res; + + if (frame_metrics.empty()) { + return res; + } + + // signatures + Vector<String> signatures; + const Vector<EditorProfiler::Metric::Category> &categories = frame_metrics[0].categories; + + for (int j = 0; j < categories.size(); j++) { + const EditorProfiler::Metric::Category &c = categories[j]; + signatures.push_back(c.signature); + + for (int k = 0; k < c.items.size(); k++) { + signatures.push_back(c.items[k].signature); + } + } + res.push_back(signatures); + + // values + Vector<String> values; + values.resize(signatures.size()); + + int index = last_metric; + + for (int i = 0; i < frame_metrics.size(); i++) { + ++index; + + if (index >= frame_metrics.size()) { + index = 0; + } + + if (!frame_metrics[index].valid) { + continue; + } + int it = 0; + const Vector<EditorProfiler::Metric::Category> &frame_cat = frame_metrics[index].categories; + + for (int j = 0; j < frame_cat.size(); j++) { + const EditorProfiler::Metric::Category &c = frame_cat[j]; + values.write[it++] = String::num_real(c.total_time); + + for (int k = 0; k < c.items.size(); k++) { + values.write[it++] = String::num_real(c.items[k].total); + } + } + res.push_back(values); + } + + return res; +} + +EditorProfiler::EditorProfiler() { + HBoxContainer *hb = memnew(HBoxContainer); + add_child(hb); + activate = memnew(Button); + activate->set_toggle_mode(true); + activate->set_text(TTR("Start")); + activate->connect("pressed", callable_mp(this, &EditorProfiler::_activate_pressed)); + hb->add_child(activate); + + clear_button = memnew(Button); + clear_button->set_text(TTR("Clear")); + clear_button->connect("pressed", callable_mp(this, &EditorProfiler::_clear_pressed)); + hb->add_child(clear_button); + + hb->add_child(memnew(Label(TTR("Measure:")))); + + display_mode = memnew(OptionButton); + display_mode->add_item(TTR("Frame Time (sec)")); + display_mode->add_item(TTR("Average Time (sec)")); + display_mode->add_item(TTR("Frame %")); + display_mode->add_item(TTR("Physics Frame %")); + display_mode->connect("item_selected", callable_mp(this, &EditorProfiler::_combo_changed)); + + hb->add_child(display_mode); + + hb->add_child(memnew(Label(TTR("Time:")))); + + display_time = memnew(OptionButton); + display_time->add_item(TTR("Inclusive")); + display_time->add_item(TTR("Self")); + display_time->connect("item_selected", callable_mp(this, &EditorProfiler::_combo_changed)); + + hb->add_child(display_time); + + hb->add_spacer(); + + hb->add_child(memnew(Label(TTR("Frame #:")))); + + cursor_metric_edit = memnew(SpinBox); + cursor_metric_edit->set_h_size_flags(SIZE_FILL); + hb->add_child(cursor_metric_edit); + cursor_metric_edit->connect("value_changed", callable_mp(this, &EditorProfiler::_cursor_metric_changed)); + + hb->add_theme_constant_override("separation", 8 * EDSCALE); + + h_split = memnew(HSplitContainer); + add_child(h_split); + h_split->set_v_size_flags(SIZE_EXPAND_FILL); + + variables = memnew(Tree); + variables->set_custom_minimum_size(Size2(320, 0) * EDSCALE); + variables->set_hide_folding(true); + h_split->add_child(variables); + variables->set_hide_root(true); + variables->set_columns(3); + variables->set_column_titles_visible(true); + variables->set_column_title(0, TTR("Name")); + variables->set_column_expand(0, true); + variables->set_column_min_width(0, 60 * EDSCALE); + variables->set_column_title(1, TTR("Time")); + variables->set_column_expand(1, false); + variables->set_column_min_width(1, 100 * EDSCALE); + variables->set_column_title(2, TTR("Calls")); + variables->set_column_expand(2, false); + variables->set_column_min_width(2, 60 * EDSCALE); + variables->connect("item_edited", callable_mp(this, &EditorProfiler::_item_edited)); + + graph = memnew(TextureRect); + graph->set_expand(true); + graph->set_mouse_filter(MOUSE_FILTER_STOP); + graph->connect("draw", callable_mp(this, &EditorProfiler::_graph_tex_draw)); + graph->connect("gui_input", callable_mp(this, &EditorProfiler::_graph_tex_input)); + graph->connect("mouse_exited", callable_mp(this, &EditorProfiler::_graph_tex_mouse_exit)); + + h_split->add_child(graph); + graph->set_h_size_flags(SIZE_EXPAND_FILL); + + int metric_size = CLAMP(int(EDITOR_DEF("debugger/profiler_frame_history_size", 600)), 60, 1024); + frame_metrics.resize(metric_size); + last_metric = -1; + hover_metric = -1; + + EDITOR_DEF("debugger/profiler_frame_max_functions", 64); + + frame_delay = memnew(Timer); + frame_delay->set_wait_time(0.1); + frame_delay->set_one_shot(true); + add_child(frame_delay); + frame_delay->connect("timeout", callable_mp(this, &EditorProfiler::_update_frame)); + + plot_delay = memnew(Timer); + plot_delay->set_wait_time(0.1); + plot_delay->set_one_shot(true); + add_child(plot_delay); + plot_delay->connect("timeout", callable_mp(this, &EditorProfiler::_update_plot)); + + plot_sigs.insert("physics_frame_time"); + plot_sigs.insert("category_frame_time"); + + seeking = false; + graph_height = 1; +} diff --git a/editor/debugger/editor_profiler.h b/editor/debugger/editor_profiler.h new file mode 100644 index 0000000000..aa2ef58db4 --- /dev/null +++ b/editor/debugger/editor_profiler.h @@ -0,0 +1,173 @@ +/*************************************************************************/ +/* editor_profiler.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITORPROFILER_H +#define EDITORPROFILER_H + +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/label.h" +#include "scene/gui/option_button.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/split_container.h" +#include "scene/gui/texture_rect.h" +#include "scene/gui/tree.h" + +class EditorProfiler : public VBoxContainer { + GDCLASS(EditorProfiler, VBoxContainer); + +public: + struct Metric { + bool valid; + + int frame_number; + float frame_time; + float idle_time; + float physics_time; + float physics_frame_time; + + struct Category { + StringName signature; + String name; + float total_time; //total for category + + struct Item { + StringName signature; + String name; + String script; + int line; + float self; + float total; + int calls; + }; + + Vector<Item> items; + }; + + Vector<Category> categories; + + Map<StringName, Category *> category_ptrs; + Map<StringName, Category::Item *> item_ptrs; + + Metric() { + valid = false; + frame_number = 0; + } + }; + + enum DisplayMode { + DISPLAY_FRAME_TIME, + DISPLAY_AVERAGE_TIME, + DISPLAY_FRAME_PERCENT, + DISPLAY_PHYSICS_FRAME_PERCENT, + }; + + enum DisplayTime { + DISPLAY_TOTAL_TIME, + DISPLAY_SELF_TIME, + }; + +private: + Button *activate; + Button *clear_button; + TextureRect *graph; + Ref<ImageTexture> graph_texture; + Vector<uint8_t> graph_image; + Tree *variables; + HSplitContainer *h_split; + + Set<StringName> plot_sigs; + + OptionButton *display_mode; + OptionButton *display_time; + + SpinBox *cursor_metric_edit; + + Vector<Metric> frame_metrics; + int last_metric; + + int max_functions; + + bool updating_frame; + + //int cursor_metric; + int hover_metric; + + float graph_height; + + bool seeking; + + Timer *frame_delay; + Timer *plot_delay; + + void _update_frame(); + + void _activate_pressed(); + void _clear_pressed(); + + String _get_time_as_text(const Metric &m, float p_time, int p_calls); + + void _make_metric_ptrs(Metric &m); + void _item_edited(); + + void _update_plot(); + + void _graph_tex_mouse_exit(); + + void _graph_tex_draw(); + void _graph_tex_input(const Ref<InputEvent> &p_ev); + + int _get_cursor_index() const; + + Color _get_color_from_signature(const StringName &p_signature) const; + + void _cursor_metric_changed(double); + + void _combo_changed(int); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void add_frame_metric(const Metric &p_metric, bool p_final = false); + void set_enabled(bool p_enable); + bool is_profiling(); + bool is_seeking() { return seeking; } + void disable_seeking(); + + void clear(); + + Vector<Vector<String>> get_data_as_csv() const; + + EditorProfiler(); +}; + +#endif // EDITORPROFILER_H diff --git a/editor/debugger/editor_visual_profiler.cpp b/editor/debugger/editor_visual_profiler.cpp new file mode 100644 index 0000000000..81b42da08e --- /dev/null +++ b/editor/debugger/editor_visual_profiler.cpp @@ -0,0 +1,822 @@ +/*************************************************************************/ +/* editor_visual_profiler.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_visual_profiler.h" + +#include "core/os/os.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void EditorVisualProfiler::add_frame_metric(const Metric &p_metric) { + ++last_metric; + if (last_metric >= frame_metrics.size()) { + last_metric = 0; + } + + frame_metrics.write[last_metric] = p_metric; + // _make_metric_ptrs(frame_metrics.write[last_metric]); + + List<String> stack; + for (int i = 0; i < frame_metrics[last_metric].areas.size(); i++) { + String name = frame_metrics[last_metric].areas[i].name; + frame_metrics.write[last_metric].areas.write[i].color_cache = _get_color_from_signature(name); + String full_name; + + if (name[0] == '<') { + stack.pop_back(); + } + + if (stack.size()) { + full_name = stack.back()->get() + name; + } else { + full_name = name; + } + + if (name[0] == '>') { + stack.push_back(full_name + "/"); + } + + frame_metrics.write[last_metric].areas.write[i].fullpath_cache = full_name; + } + + updating_frame = true; + cursor_metric_edit->set_max(frame_metrics[last_metric].frame_number); + cursor_metric_edit->set_min(MAX(frame_metrics[last_metric].frame_number - frame_metrics.size(), 0)); + + if (!seeking) { + cursor_metric_edit->set_value(frame_metrics[last_metric].frame_number); + if (hover_metric != -1) { + hover_metric++; + if (hover_metric >= frame_metrics.size()) { + hover_metric = 0; + } + } + } + updating_frame = false; + + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } + + if (plot_delay->is_stopped()) { + plot_delay->set_wait_time(0.1); + plot_delay->start(); + } +} + +void EditorVisualProfiler::clear() { + int metric_size = EditorSettings::get_singleton()->get("debugger/profiler_frame_history_size"); + metric_size = CLAMP(metric_size, 60, 1024); + frame_metrics.clear(); + frame_metrics.resize(metric_size); + last_metric = -1; + variables->clear(); + //activate->set_pressed(false); + + updating_frame = true; + cursor_metric_edit->set_min(0); + cursor_metric_edit->set_max(0); + cursor_metric_edit->set_value(0); + updating_frame = false; + hover_metric = -1; + seeking = false; +} + +String EditorVisualProfiler::_get_time_as_text(float p_time) { + int dmode = display_mode->get_selected(); + + if (dmode == DISPLAY_FRAME_TIME) { + return rtos(p_time) + "ms"; + } else if (dmode == DISPLAY_FRAME_PERCENT) { + return String::num(p_time * 100 / graph_limit, 2) + "%"; + } + + return "err"; +} + +Color EditorVisualProfiler::_get_color_from_signature(const StringName &p_signature) const { + Color bc = get_theme_color("error_color", "Editor"); + double rot = ABS(double(p_signature.hash()) / double(0x7FFFFFFF)); + Color c; + c.set_hsv(rot, bc.get_s(), bc.get_v()); + return c.lerp(get_theme_color("base_color", "Editor"), 0.07); +} + +void EditorVisualProfiler::_item_selected() { + if (updating_frame) { + return; + } + + TreeItem *item = variables->get_selected(); + if (!item) { + return; + } + selected_area = item->get_metadata(0); + _update_plot(); +} + +void EditorVisualProfiler::_update_plot() { + int w = graph->get_size().width; + int h = graph->get_size().height; + + bool reset_texture = false; + + int desired_len = w * h * 4; + + if (graph_image.size() != desired_len) { + reset_texture = true; + graph_image.resize(desired_len); + } + + uint8_t *wr = graph_image.ptrw(); + + //clear + for (int i = 0; i < desired_len; i += 4) { + wr[i + 0] = 0; + wr[i + 1] = 0; + wr[i + 2] = 0; + wr[i + 3] = 255; + } + + //find highest value + + float highest_cpu = 0; + float highest_gpu = 0; + + for (int i = 0; i < frame_metrics.size(); i++) { + const Metric &m = frame_metrics[i]; + if (!m.valid) { + continue; + } + + if (m.areas.size()) { + highest_cpu = MAX(highest_cpu, m.areas[m.areas.size() - 1].cpu_time); + highest_gpu = MAX(highest_gpu, m.areas[m.areas.size() - 1].gpu_time); + } + } + + if (highest_cpu > 0 || highest_gpu > 0) { + if (frame_relative->is_pressed()) { + highest_cpu = MAX(graph_limit, highest_cpu); + highest_gpu = MAX(graph_limit, highest_gpu); + } + + if (linked->is_pressed()) { + float highest = MAX(highest_cpu, highest_gpu); + highest_cpu = highest_gpu = highest; + } + + //means some data exists.. + highest_cpu *= 1.2; //leave some upper room + highest_gpu *= 1.2; //leave some upper room + graph_height_cpu = highest_cpu; + graph_height_gpu = highest_gpu; + + Vector<Color> columnv_cpu; + columnv_cpu.resize(h); + Color *column_cpu = columnv_cpu.ptrw(); + + Vector<Color> columnv_gpu; + columnv_gpu.resize(h); + Color *column_gpu = columnv_gpu.ptrw(); + + int half_w = w / 2; + for (int i = 0; i < half_w; i++) { + for (int j = 0; j < h; j++) { + column_cpu[j] = Color(0, 0, 0, 0); + column_gpu[j] = Color(0, 0, 0, 0); + } + + int current = i * frame_metrics.size() / half_w; + int next = (i + 1) * frame_metrics.size() / half_w; + if (next > frame_metrics.size()) { + next = frame_metrics.size(); + } + if (next == current) { + next = current + 1; //just because for loop must work + } + + for (int j = current; j < next; j++) { + //wrap + int idx = last_metric + 1 + j; + while (idx >= frame_metrics.size()) { + idx -= frame_metrics.size(); + } + + int area_count = frame_metrics[idx].areas.size(); + const Metric::Area *areas = frame_metrics[idx].areas.ptr(); + int prev_cpu = 0; + int prev_gpu = 0; + for (int k = 1; k < area_count; k++) { + int ofs_cpu = int(areas[k].cpu_time * h / highest_cpu); + ofs_cpu = CLAMP(ofs_cpu, 0, h - 1); + Color color = selected_area == areas[k - 1].fullpath_cache ? Color(1, 1, 1, 1) : areas[k - 1].color_cache; + + for (int l = prev_cpu; l < ofs_cpu; l++) { + column_cpu[h - l - 1] += color; + } + prev_cpu = ofs_cpu; + + int ofs_gpu = int(areas[k].gpu_time * h / highest_gpu); + ofs_gpu = CLAMP(ofs_gpu, 0, h - 1); + for (int l = prev_gpu; l < ofs_gpu; l++) { + column_gpu[h - l - 1] += color; + } + + prev_gpu = ofs_gpu; + } + } + + //plot CPU + for (int j = 0; j < h; j++) { + uint8_t r, g, b; + + if (column_cpu[j].a == 0) { + r = 0; + g = 0; + b = 0; + } else { + r = CLAMP((column_cpu[j].r / column_cpu[j].a) * 255.0, 0, 255); + g = CLAMP((column_cpu[j].g / column_cpu[j].a) * 255.0, 0, 255); + b = CLAMP((column_cpu[j].b / column_cpu[j].a) * 255.0, 0, 255); + } + + int widx = (j * w + i) * 4; + wr[widx + 0] = r; + wr[widx + 1] = g; + wr[widx + 2] = b; + wr[widx + 3] = 255; + } + //plot GPU + for (int j = 0; j < h; j++) { + uint8_t r, g, b; + + if (column_gpu[j].a == 0) { + r = 0; + g = 0; + b = 0; + } else { + r = CLAMP((column_gpu[j].r / column_gpu[j].a) * 255.0, 0, 255); + g = CLAMP((column_gpu[j].g / column_gpu[j].a) * 255.0, 0, 255); + b = CLAMP((column_gpu[j].b / column_gpu[j].a) * 255.0, 0, 255); + } + + int widx = (j * w + w / 2 + i) * 4; + wr[widx + 0] = r; + wr[widx + 1] = g; + wr[widx + 2] = b; + wr[widx + 3] = 255; + } + } + } + + Ref<Image> img; + img.instance(); + img->create(w, h, false, Image::FORMAT_RGBA8, graph_image); + + if (reset_texture) { + if (graph_texture.is_null()) { + graph_texture.instance(); + } + graph_texture->create_from_image(img); + } + + graph_texture->update(img, true); + + graph->set_texture(graph_texture); + graph->update(); +} + +void EditorVisualProfiler::_update_frame(bool p_focus_selected) { + int cursor_metric = _get_cursor_index(); + + Ref<Texture> track_icon = get_theme_icon("TrackColor", "EditorIcons"); + + ERR_FAIL_INDEX(cursor_metric, frame_metrics.size()); + + updating_frame = true; + variables->clear(); + + TreeItem *root = variables->create_item(); + const Metric &m = frame_metrics[cursor_metric]; + + List<TreeItem *> stack; + List<TreeItem *> categories; + + TreeItem *ensure_selected = nullptr; + + for (int i = 1; i < m.areas.size() - 1; i++) { + TreeItem *parent = stack.size() ? stack.back()->get() : root; + + String name = m.areas[i].name; + + float cpu_time = m.areas[i].cpu_time; + float gpu_time = m.areas[i].gpu_time; + if (i < m.areas.size() - 1) { + cpu_time = m.areas[i + 1].cpu_time - cpu_time; + gpu_time = m.areas[i + 1].gpu_time - gpu_time; + } + + if (name.begins_with(">")) { + TreeItem *category = variables->create_item(parent); + + stack.push_back(category); + categories.push_back(category); + + name = name.substr(1, name.length()); + + category->set_text(0, name); + category->set_metadata(1, cpu_time); + category->set_metadata(2, gpu_time); + continue; + } + + if (name.begins_with("<")) { + stack.pop_back(); + continue; + } + TreeItem *category = variables->create_item(parent); + + for (List<TreeItem *>::Element *E = stack.front(); E; E = E->next()) { + float total_cpu = E->get()->get_metadata(1); + float total_gpu = E->get()->get_metadata(2); + total_cpu += cpu_time; + total_gpu += gpu_time; + E->get()->set_metadata(1, cpu_time); + E->get()->set_metadata(2, gpu_time); + } + + category->set_icon(0, track_icon); + category->set_icon_modulate(0, m.areas[i].color_cache); + category->set_selectable(0, true); + category->set_metadata(0, m.areas[i].fullpath_cache); + category->set_text(0, m.areas[i].name); + category->set_text(1, _get_time_as_text(cpu_time)); + category->set_metadata(1, m.areas[i].cpu_time); + category->set_text(2, _get_time_as_text(gpu_time)); + category->set_metadata(2, m.areas[i].gpu_time); + + if (selected_area == m.areas[i].fullpath_cache) { + category->select(0); + if (p_focus_selected) { + ensure_selected = category; + } + } + } + + for (List<TreeItem *>::Element *E = categories.front(); E; E = E->next()) { + float total_cpu = E->get()->get_metadata(1); + float total_gpu = E->get()->get_metadata(2); + E->get()->set_text(1, _get_time_as_text(total_cpu)); + E->get()->set_text(2, _get_time_as_text(total_gpu)); + } + + if (ensure_selected) { + variables->ensure_cursor_is_visible(); + } + updating_frame = false; +} + +void EditorVisualProfiler::_activate_pressed() { + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon("Stop", "EditorIcons")); + activate->set_text(TTR("Stop")); + _clear_pressed(); //always clear on start + } else { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + activate->set_text(TTR("Start")); + } + emit_signal("enable_profiling", activate->is_pressed()); +} + +void EditorVisualProfiler::_clear_pressed() { + clear(); + _update_plot(); +} + +void EditorVisualProfiler::_notification(int p_what) { + if (p_what == NOTIFICATION_ENTER_TREE) { + activate->set_icon(get_theme_icon("Play", "EditorIcons")); + clear_button->set_icon(get_theme_icon("Clear", "EditorIcons")); + } +} + +void EditorVisualProfiler::_graph_tex_draw() { + if (last_metric < 0) { + return; + } + Ref<Font> font = get_theme_font("font", "Label"); + if (seeking) { + int max_frames = frame_metrics.size(); + int frame = cursor_metric_edit->get_value() - (frame_metrics[last_metric].frame_number - max_frames + 1); + if (frame < 0) { + frame = 0; + } + + int half_width = graph->get_size().x / 2; + int cur_x = frame * half_width / max_frames; + //cur_x /= 2.0; + + graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.8)); + graph->draw_line(Vector2(cur_x + half_width, 0), Vector2(cur_x + half_width, graph->get_size().y), Color(1, 1, 1, 0.8)); + } + + if (graph_height_cpu > 0) { + int frame_y = graph->get_size().y - graph_limit * graph->get_size().y / graph_height_cpu - 1; + + int half_width = graph->get_size().x / 2; + + graph->draw_line(Vector2(0, frame_y), Vector2(half_width, frame_y), Color(1, 1, 1, 0.3)); + + String limit_str = String::num(graph_limit, 2); + graph->draw_string(font, Vector2(half_width - font->get_string_size(limit_str).x - 2, frame_y - 2), limit_str, Color(1, 1, 1, 0.6)); + } + + if (graph_height_gpu > 0) { + int frame_y = graph->get_size().y - graph_limit * graph->get_size().y / graph_height_gpu - 1; + + int half_width = graph->get_size().x / 2; + + graph->draw_line(Vector2(half_width, frame_y), Vector2(graph->get_size().x, frame_y), Color(1, 1, 1, 0.3)); + + String limit_str = String::num(graph_limit, 2); + graph->draw_string(font, Vector2(half_width * 2 - font->get_string_size(limit_str).x - 2, frame_y - 2), limit_str, Color(1, 1, 1, 0.6)); + } + + graph->draw_string(font, Vector2(font->get_string_size("X").x, font->get_ascent() + 2), "CPU:", Color(1, 1, 1, 0.8)); + graph->draw_string(font, Vector2(font->get_string_size("X").x + graph->get_size().width / 2, font->get_ascent() + 2), "GPU:", Color(1, 1, 1, 0.8)); + + /* + if (hover_metric != -1 && frame_metrics[hover_metric].valid) { + + int max_frames = frame_metrics.size(); + int frame = frame_metrics[hover_metric].frame_number - (frame_metrics[last_metric].frame_number - max_frames + 1); + if (frame < 0) + frame = 0; + + int cur_x = frame * graph->get_size().x / max_frames; + + graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.4)); + } +*/ +} + +void EditorVisualProfiler::_graph_tex_mouse_exit() { + hover_metric = -1; + graph->update(); +} + +void EditorVisualProfiler::_cursor_metric_changed(double) { + if (updating_frame) { + return; + } + + graph->update(); + _update_frame(); +} + +void EditorVisualProfiler::_graph_tex_input(const Ref<InputEvent> &p_ev) { + if (last_metric < 0) { + return; + } + + Ref<InputEventMouse> me = p_ev; + Ref<InputEventMouseButton> mb = p_ev; + Ref<InputEventMouseMotion> mm = p_ev; + + if ( + (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && mb->is_pressed()) || + (mm.is_valid())) { + int half_w = graph->get_size().width / 2; + int x = me->get_position().x; + if (x > half_w) { + x -= half_w; + } + x = x * frame_metrics.size() / half_w; + + bool show_hover = x >= 0 && x < frame_metrics.size(); + + if (x < 0) { + x = 0; + } + + if (x >= frame_metrics.size()) { + x = frame_metrics.size() - 1; + } + + int metric = frame_metrics.size() - x - 1; + metric = last_metric - metric; + while (metric < 0) { + metric += frame_metrics.size(); + } + + if (show_hover) { + hover_metric = metric; + + } else { + hover_metric = -1; + } + + if (mb.is_valid() || mm->get_button_mask() & BUTTON_MASK_LEFT) { + //cursor_metric=x; + updating_frame = true; + + //metric may be invalid, so look for closest metric that is valid, this makes snap feel better + bool valid = false; + for (int i = 0; i < frame_metrics.size(); i++) { + if (frame_metrics[metric].valid) { + valid = true; + break; + } + + metric++; + if (metric >= frame_metrics.size()) { + metric = 0; + } + } + + if (!valid) { + return; + } + + cursor_metric_edit->set_value(frame_metrics[metric].frame_number); + + updating_frame = false; + + if (activate->is_pressed()) { + if (!seeking) { + // Break request is not required, just stop profiling + } + } + + seeking = true; + + if (!frame_delay->is_processing()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } + + bool touched_cpu = me->get_position().x < graph->get_size().width * 0.5; + + const Metric::Area *areas = frame_metrics[metric].areas.ptr(); + int area_count = frame_metrics[metric].areas.size(); + float posy = (1.0 - (me->get_position().y / graph->get_size().height)) * (touched_cpu ? graph_height_cpu : graph_height_gpu); + int last_valid = -1; + bool found = false; + for (int i = 0; i < area_count - 1; i++) { + if (areas[i].name[0] != '<' && areas[i].name[0] != '>') { + last_valid = i; + } + float h = touched_cpu ? areas[i + 1].cpu_time : areas[i + 1].gpu_time; + + if (h > posy) { + found = true; + break; + } + } + + StringName area_found; + if (found && last_valid != -1) { + area_found = areas[last_valid].fullpath_cache; + } + + if (area_found != selected_area) { + selected_area = area_found; + _update_frame(true); + _update_plot(); + } + } + + graph->update(); + } +} + +int EditorVisualProfiler::_get_cursor_index() const { + if (last_metric < 0) { + return 0; + } + if (!frame_metrics[last_metric].valid) { + return 0; + } + + int diff = (frame_metrics[last_metric].frame_number - cursor_metric_edit->get_value()); + + int idx = last_metric - diff; + while (idx < 0) { + idx += frame_metrics.size(); + } + + return idx; +} + +void EditorVisualProfiler::disable_seeking() { + seeking = false; + graph->update(); +} + +void EditorVisualProfiler::_combo_changed(int) { + _update_frame(); + _update_plot(); +} + +void EditorVisualProfiler::_bind_methods() { + ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); +} + +void EditorVisualProfiler::set_enabled(bool p_enable) { + activate->set_disabled(!p_enable); +} + +bool EditorVisualProfiler::is_profiling() { + return activate->is_pressed(); +} + +Vector<Vector<String>> EditorVisualProfiler::get_data_as_csv() const { + Vector<Vector<String>> res; +#if 0 + if (frame_metrics.empty()) { + return res; + } + + // signatures + Vector<String> signatures; + const Vector<EditorFrameProfiler::Metric::Category> &categories = frame_metrics[0].categories; + + for (int j = 0; j < categories.size(); j++) { + + const EditorFrameProfiler::Metric::Category &c = categories[j]; + signatures.push_back(c.signature); + + for (int k = 0; k < c.items.size(); k++) { + signatures.push_back(c.items[k].signature); + } + } + res.push_back(signatures); + + // values + Vector<String> values; + values.resize(signatures.size()); + + int index = last_metric; + + for (int i = 0; i < frame_metrics.size(); i++) { + + ++index; + + if (index >= frame_metrics.size()) { + index = 0; + } + + if (!frame_metrics[index].valid) { + continue; + } + int it = 0; + const Vector<EditorFrameProfiler::Metric::Category> &frame_cat = frame_metrics[index].categories; + + for (int j = 0; j < frame_cat.size(); j++) { + + const EditorFrameProfiler::Metric::Category &c = frame_cat[j]; + values.write[it++] = String::num_real(c.total_time); + + for (int k = 0; k < c.items.size(); k++) { + values.write[it++] = String::num_real(c.items[k].total); + } + } + res.push_back(values); + } +#endif + return res; +} + +EditorVisualProfiler::EditorVisualProfiler() { + HBoxContainer *hb = memnew(HBoxContainer); + add_child(hb); + activate = memnew(Button); + activate->set_toggle_mode(true); + activate->set_text(TTR("Start")); + activate->connect("pressed", callable_mp(this, &EditorVisualProfiler::_activate_pressed)); + hb->add_child(activate); + + clear_button = memnew(Button); + clear_button->set_text(TTR("Clear")); + clear_button->connect("pressed", callable_mp(this, &EditorVisualProfiler::_clear_pressed)); + hb->add_child(clear_button); + + hb->add_child(memnew(Label(TTR("Measure:")))); + + display_mode = memnew(OptionButton); + display_mode->add_item(TTR("Frame Time (msec)")); + display_mode->add_item(TTR("Frame %")); + display_mode->connect("item_selected", callable_mp(this, &EditorVisualProfiler::_combo_changed)); + + hb->add_child(display_mode); + + frame_relative = memnew(CheckBox(TTR("Fit to Frame"))); + frame_relative->set_pressed(true); + hb->add_child(frame_relative); + frame_relative->connect("pressed", callable_mp(this, &EditorVisualProfiler::_update_plot)); + linked = memnew(CheckBox(TTR("Linked"))); + linked->set_pressed(true); + hb->add_child(linked); + linked->connect("pressed", callable_mp(this, &EditorVisualProfiler::_update_plot)); + + hb->add_spacer(); + + hb->add_child(memnew(Label(TTR("Frame #:")))); + + cursor_metric_edit = memnew(SpinBox); + cursor_metric_edit->set_h_size_flags(SIZE_FILL); + hb->add_child(cursor_metric_edit); + cursor_metric_edit->connect("value_changed", callable_mp(this, &EditorVisualProfiler::_cursor_metric_changed)); + + hb->add_theme_constant_override("separation", 8 * EDSCALE); + + h_split = memnew(HSplitContainer); + add_child(h_split); + h_split->set_v_size_flags(SIZE_EXPAND_FILL); + + variables = memnew(Tree); + variables->set_custom_minimum_size(Size2(300, 0) * EDSCALE); + variables->set_hide_folding(true); + h_split->add_child(variables); + variables->set_hide_root(true); + variables->set_columns(3); + variables->set_column_titles_visible(true); + variables->set_column_title(0, TTR("Name")); + variables->set_column_expand(0, true); + variables->set_column_min_width(0, 60); + variables->set_column_title(1, TTR("CPU")); + variables->set_column_expand(1, false); + variables->set_column_min_width(1, 60 * EDSCALE); + variables->set_column_title(2, TTR("GPU")); + variables->set_column_expand(2, false); + variables->set_column_min_width(2, 60 * EDSCALE); + variables->connect("cell_selected", callable_mp(this, &EditorVisualProfiler::_item_selected)); + + graph = memnew(TextureRect); + graph->set_expand(true); + graph->set_mouse_filter(MOUSE_FILTER_STOP); + //graph->set_ignore_mouse(false); + graph->connect("draw", callable_mp(this, &EditorVisualProfiler::_graph_tex_draw)); + graph->connect("gui_input", callable_mp(this, &EditorVisualProfiler::_graph_tex_input)); + graph->connect("mouse_exited", callable_mp(this, &EditorVisualProfiler::_graph_tex_mouse_exit)); + + h_split->add_child(graph); + graph->set_h_size_flags(SIZE_EXPAND_FILL); + + int metric_size = CLAMP(int(EDITOR_DEF("debugger/profiler_frame_history_size", 600)), 60, 1024); + frame_metrics.resize(metric_size); + last_metric = -1; + //cursor_metric=-1; + hover_metric = -1; + + //display_mode=DISPLAY_FRAME_TIME; + + frame_delay = memnew(Timer); + frame_delay->set_wait_time(0.1); + frame_delay->set_one_shot(true); + add_child(frame_delay); + frame_delay->connect("timeout", callable_mp(this, &EditorVisualProfiler::_update_frame), make_binds(false)); + + plot_delay = memnew(Timer); + plot_delay->set_wait_time(0.1); + plot_delay->set_one_shot(true); + add_child(plot_delay); + plot_delay->connect("timeout", callable_mp(this, &EditorVisualProfiler::_update_plot)); + + seeking = false; + graph_height_cpu = 1; + graph_height_gpu = 1; + + graph_limit = 1000 / 60.0; + + //activate->set_disabled(true); +} diff --git a/editor/debugger/editor_visual_profiler.h b/editor/debugger/editor_visual_profiler.h new file mode 100644 index 0000000000..3c1a55dc38 --- /dev/null +++ b/editor/debugger/editor_visual_profiler.h @@ -0,0 +1,152 @@ +/*************************************************************************/ +/* editor_visual_profiler.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_FRAME_PROFILER_H +#define EDITOR_FRAME_PROFILER_H + +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" +#include "scene/gui/label.h" +#include "scene/gui/option_button.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/split_container.h" +#include "scene/gui/texture_rect.h" +#include "scene/gui/tree.h" + +class EditorVisualProfiler : public VBoxContainer { + GDCLASS(EditorVisualProfiler, VBoxContainer); + +public: + struct Metric { + bool valid; + + uint64_t frame_number; + + struct Area { + String name; + Color color_cache; + StringName fullpath_cache; + float cpu_time = 0; + float gpu_time = 0; + }; + + Vector<Area> areas; + + Metric() { + valid = false; + } + }; + + enum DisplayTimeMode { + DISPLAY_FRAME_TIME, + DISPLAY_FRAME_PERCENT, + }; + +private: + Button *activate; + Button *clear_button; + + TextureRect *graph; + Ref<ImageTexture> graph_texture; + Vector<uint8_t> graph_image; + Tree *variables; + HSplitContainer *h_split; + CheckBox *frame_relative; + CheckBox *linked; + + OptionButton *display_mode; + + SpinBox *cursor_metric_edit; + + Vector<Metric> frame_metrics; + int last_metric; + + StringName selected_area; + + bool updating_frame; + + //int cursor_metric; + int hover_metric; + + float graph_height_cpu; + float graph_height_gpu; + + float graph_limit; + + bool seeking; + + Timer *frame_delay; + Timer *plot_delay; + + void _update_frame(bool p_focus_selected = false); + + void _activate_pressed(); + void _clear_pressed(); + + String _get_time_as_text(float p_time); + + //void _make_metric_ptrs(Metric &m); + void _item_selected(); + + void _update_plot(); + + void _graph_tex_mouse_exit(); + + void _graph_tex_draw(); + void _graph_tex_input(const Ref<InputEvent> &p_ev); + + int _get_cursor_index() const; + + Color _get_color_from_signature(const StringName &p_signature) const; + + void _cursor_metric_changed(double); + + void _combo_changed(int); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void add_frame_metric(const Metric &p_metric); + void set_enabled(bool p_enable); + bool is_profiling(); + bool is_seeking() { return seeking; } + void disable_seeking(); + + void clear(); + + Vector<Vector<String>> get_data_as_csv() const; + + EditorVisualProfiler(); +}; + +#endif // EDITOR_FRAME_PROFILER_H diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp new file mode 100644 index 0000000000..1fca95b6da --- /dev/null +++ b/editor/debugger/script_editor_debugger.cpp @@ -0,0 +1,1762 @@ +/*************************************************************************/ +/* script_editor_debugger.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "script_editor_debugger.h" + +#include "core/debugger/debugger_marshalls.h" +#include "core/debugger/remote_debugger.h" +#include "core/io/marshalls.h" +#include "core/project_settings.h" +#include "core/ustring.h" +#include "editor/debugger/editor_network_profiler.h" +#include "editor/debugger/editor_performance_profiler.h" +#include "editor/debugger/editor_profiler.h" +#include "editor/debugger/editor_visual_profiler.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" +#include "editor/plugins/canvas_item_editor_plugin.h" +#include "editor/plugins/editor_debugger_plugin.h" +#include "editor/plugins/node_3d_editor_plugin.h" +#include "editor/property_editor.h" +#include "main/performance.h" +#include "scene/3d/camera_3d.h" +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/label.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/margin_container.h" +#include "scene/gui/rich_text_label.h" +#include "scene/gui/separator.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tab_container.h" +#include "scene/gui/texture_button.h" +#include "scene/gui/tree.h" +#include "scene/resources/packed_scene.h" +#include "servers/display_server.h" + +using CameraOverride = EditorDebuggerNode::CameraOverride; + +void ScriptEditorDebugger::_put_msg(String p_message, Array p_data) { + if (is_session_active()) { + Array msg; + msg.push_back(p_message); + msg.push_back(p_data); + peer->put_message(msg); + } +} + +void ScriptEditorDebugger::debug_copy() { + String msg = reason->get_text(); + if (msg == "") { + return; + } + DisplayServer::get_singleton()->clipboard_set(msg); +} + +void ScriptEditorDebugger::debug_skip_breakpoints() { + skip_breakpoints_value = !skip_breakpoints_value; + if (skip_breakpoints_value) { + skip_breakpoints->set_icon(get_theme_icon("DebugSkipBreakpointsOn", "EditorIcons")); + } else { + skip_breakpoints->set_icon(get_theme_icon("DebugSkipBreakpointsOff", "EditorIcons")); + } + + Array msg; + msg.push_back(skip_breakpoints_value); + _put_msg("set_skip_breakpoints", msg); +} + +void ScriptEditorDebugger::debug_next() { + ERR_FAIL_COND(!breaked); + + _put_msg("next", Array()); + _clear_execution(); +} + +void ScriptEditorDebugger::debug_step() { + ERR_FAIL_COND(!breaked); + + _put_msg("step", Array()); + _clear_execution(); +} + +void ScriptEditorDebugger::debug_break() { + ERR_FAIL_COND(breaked); + + _put_msg("break", Array()); +} + +void ScriptEditorDebugger::debug_continue() { + ERR_FAIL_COND(!breaked); + + // Allow focus stealing only if we actually run this client for security. + if (remote_pid && EditorNode::get_singleton()->has_child_process(remote_pid)) { + DisplayServer::get_singleton()->enable_for_stealing_focus(remote_pid); + } + + _clear_execution(); + _put_msg("continue", Array()); +} + +void ScriptEditorDebugger::update_tabs() { + if (error_count == 0 && warning_count == 0) { + errors_tab->set_name(TTR("Errors")); + tabs->set_tab_icon(errors_tab->get_index(), Ref<Texture2D>()); + } else { + errors_tab->set_name(TTR("Errors") + " (" + itos(error_count + warning_count) + ")"); + if (error_count >= 1 && warning_count >= 1) { + tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("ErrorWarning", "EditorIcons")); + } else if (error_count >= 1) { + tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("Error", "EditorIcons")); + } else { + tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("Warning", "EditorIcons")); + } + } +} + +void ScriptEditorDebugger::clear_style() { + tabs->add_theme_style_override("panel", nullptr); +} + +void ScriptEditorDebugger::save_node(ObjectID p_id, const String &p_file) { + Array msg; + msg.push_back(p_id); + msg.push_back(p_file); + _put_msg("scene:save_node", msg); +} + +void ScriptEditorDebugger::_file_selected(const String &p_file) { + switch (file_dialog_purpose) { + case SAVE_MONITORS_CSV: { + Error err; + FileAccessRef file = FileAccess::open(p_file, FileAccess::WRITE, &err); + + if (err != OK) { + ERR_PRINT("Failed to open " + p_file); + return; + } + Vector<String> line; + line.resize(Performance::MONITOR_MAX); + + // signatures + for (int i = 0; i < Performance::MONITOR_MAX; i++) { + line.write[i] = Performance::get_singleton()->get_monitor_name(Performance::Monitor(i)); + } + file->store_csv_line(line); + + // values + Vector<List<float>::Element *> iterators; + iterators.resize(Performance::MONITOR_MAX); + bool continue_iteration = false; + for (int i = 0; i < Performance::MONITOR_MAX; i++) { + iterators.write[i] = performance_profiler->get_monitor_data(Performance::get_singleton()->get_monitor_name(Performance::Monitor(i)))->back(); + continue_iteration = continue_iteration || iterators[i]; + } + while (continue_iteration) { + continue_iteration = false; + for (int i = 0; i < Performance::MONITOR_MAX; i++) { + if (iterators[i]) { + line.write[i] = String::num_real(iterators[i]->get()); + iterators.write[i] = iterators[i]->prev(); + } else { + line.write[i] = ""; + } + continue_iteration = continue_iteration || iterators[i]; + } + file->store_csv_line(line); + } + file->store_string("\n"); + + Vector<Vector<String>> profiler_data = profiler->get_data_as_csv(); + for (int i = 0; i < profiler_data.size(); i++) { + file->store_csv_line(profiler_data[i]); + } + } break; + case SAVE_VRAM_CSV: { + Error err; + FileAccessRef file = FileAccess::open(p_file, FileAccess::WRITE, &err); + + if (err != OK) { + ERR_PRINT("Failed to open " + p_file); + return; + } + + Vector<String> headers; + headers.resize(vmem_tree->get_columns()); + for (int i = 0; i < vmem_tree->get_columns(); ++i) { + headers.write[i] = vmem_tree->get_column_title(i); + } + file->store_csv_line(headers); + + if (vmem_tree->get_root()) { + TreeItem *ti = vmem_tree->get_root()->get_children(); + while (ti) { + Vector<String> values; + values.resize(vmem_tree->get_columns()); + for (int i = 0; i < vmem_tree->get_columns(); ++i) { + values.write[i] = ti->get_text(i); + } + file->store_csv_line(values); + + ti = ti->get_next(); + } + } + } break; + } +} + +void ScriptEditorDebugger::request_remote_tree() { + _put_msg("scene:request_scene_tree", Array()); +} + +const SceneDebuggerTree *ScriptEditorDebugger::get_remote_tree() { + return scene_tree; +} + +void ScriptEditorDebugger::update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value) { + Array msg; + msg.push_back(p_obj_id); + msg.push_back(p_prop); + msg.push_back(p_value); + _put_msg("scene:set_object_property", msg); +} + +void ScriptEditorDebugger::request_remote_object(ObjectID p_obj_id) { + ERR_FAIL_COND(p_obj_id.is_null()); + Array msg; + msg.push_back(p_obj_id); + _put_msg("scene:inspect_object", msg); +} + +Object *ScriptEditorDebugger::get_remote_object(ObjectID p_id) { + return inspector->get_object(p_id); +} + +void ScriptEditorDebugger::_remote_object_selected(ObjectID p_id) { + emit_signal("remote_object_requested", p_id); +} + +void ScriptEditorDebugger::_remote_object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value) { + update_remote_object(p_id, p_prop, p_value); + request_remote_object(p_id); +} + +void ScriptEditorDebugger::_remote_object_property_updated(ObjectID p_id, const String &p_property) { + emit_signal("remote_object_property_updated", p_id, p_property); +} + +void ScriptEditorDebugger::_video_mem_request() { + _put_msg("core:memory", Array()); +} + +void ScriptEditorDebugger::_video_mem_export() { + file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + file_dialog->clear_filters(); + file_dialog_purpose = SAVE_VRAM_CSV; + file_dialog->popup_file_dialog(); +} + +Size2 ScriptEditorDebugger::get_minimum_size() const { + Size2 ms = MarginContainer::get_minimum_size(); + ms.y = MAX(ms.y, 250 * EDSCALE); + return ms; +} + +void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_data) { + if (p_msg == "debug_enter") { + _put_msg("get_stack_dump", Array()); + + ERR_FAIL_COND(p_data.size() != 2); + bool can_continue = p_data[0]; + String error = p_data[1]; + breaked = true; + can_debug = can_continue; + _update_buttons_state(); + _set_reason_text(error, MESSAGE_ERROR); + emit_signal("breaked", true, can_continue); + DisplayServer::get_singleton()->window_move_to_foreground(); + if (error != "") { + tabs->set_current_tab(0); + } + profiler->set_enabled(false); + inspector->clear_cache(); // Take a chance to force remote objects update. + + } else if (p_msg == "debug_exit") { + breaked = false; + can_debug = false; + _clear_execution(); + _update_buttons_state(); + _set_reason_text(TTR("Execution resumed."), MESSAGE_SUCCESS); + emit_signal("breaked", false, false); + profiler->set_enabled(true); + profiler->disable_seeking(); + } else if (p_msg == "set_pid") { + ERR_FAIL_COND(p_data.size() < 1); + remote_pid = p_data[0]; + } else if (p_msg == "scene:click_ctrl") { + ERR_FAIL_COND(p_data.size() < 2); + clicked_ctrl->set_text(p_data[0]); + clicked_ctrl_type->set_text(p_data[1]); + } else if (p_msg == "scene:scene_tree") { + scene_tree->nodes.clear(); + scene_tree->deserialize(p_data); + emit_signal("remote_tree_updated"); + _update_buttons_state(); + } else if (p_msg == "scene:inspect_object") { + ObjectID id = inspector->add_object(p_data); + if (id.is_valid()) { + emit_signal("remote_object_updated", id); + } + } else if (p_msg == "memory:usage") { + vmem_tree->clear(); + TreeItem *root = vmem_tree->create_item(); + DebuggerMarshalls::ResourceUsage usage; + usage.deserialize(p_data); + + int total = 0; + + for (List<DebuggerMarshalls::ResourceInfo>::Element *E = usage.infos.front(); E; E = E->next()) { + TreeItem *it = vmem_tree->create_item(root); + String type = E->get().type; + int bytes = E->get().vram; + it->set_text(0, E->get().path); + it->set_text(1, type); + it->set_text(2, E->get().format); + it->set_text(3, String::humanize_size(bytes)); + total += bytes; + + if (has_theme_icon(type, "EditorIcons")) { + it->set_icon(0, get_theme_icon(type, "EditorIcons")); + } + } + + vmem_total->set_tooltip(TTR("Bytes:") + " " + itos(total)); + vmem_total->set_text(String::humanize_size(total)); + + } else if (p_msg == "stack_dump") { + DebuggerMarshalls::ScriptStackDump stack; + stack.deserialize(p_data); + + stack_dump->clear(); + inspector->clear_stack_variables(); + TreeItem *r = stack_dump->create_item(); + + for (int i = 0; i < stack.frames.size(); i++) { + TreeItem *s = stack_dump->create_item(r); + Dictionary d; + d["frame"] = i; + d["file"] = stack.frames[i].file; + d["function"] = stack.frames[i].func; + d["line"] = stack.frames[i].line; + s->set_metadata(0, d); + + String line = itos(i) + " - " + String(d["file"]) + ":" + itos(d["line"]) + " - at function: " + d["function"]; + s->set_text(0, line); + + if (i == 0) { + s->select(0); + } + } + } else if (p_msg == "stack_frame_vars") { + inspector->clear_stack_variables(); + + } else if (p_msg == "stack_frame_var") { + inspector->add_stack_variable(p_data); + + } else if (p_msg == "output") { + ERR_FAIL_COND(p_data.size() != 2); + + ERR_FAIL_COND(p_data[0].get_type() != Variant::PACKED_STRING_ARRAY); + Vector<String> output_strings = p_data[0]; + + ERR_FAIL_COND(p_data[1].get_type() != Variant::PACKED_INT32_ARRAY); + Vector<int> output_types = p_data[1]; + + ERR_FAIL_COND(output_strings.size() != output_types.size()); + + for (int i = 0; i < output_strings.size(); i++) { + RemoteDebugger::MessageType type = (RemoteDebugger::MessageType)(int)(output_types[i]); + EditorLog::MessageType msg_type; + switch (type) { + case RemoteDebugger::MESSAGE_TYPE_LOG: { + msg_type = EditorLog::MSG_TYPE_STD; + } break; + case RemoteDebugger::MESSAGE_TYPE_ERROR: { + msg_type = EditorLog::MSG_TYPE_ERROR; + } break; + default: { + WARN_PRINT("Unhandled script debugger message type: " + itos(type)); + msg_type = EditorLog::MSG_TYPE_STD; + } break; + } + EditorNode::get_log()->add_message(output_strings[i], msg_type); + } + } else if (p_msg == "performance:profile_frame") { + Vector<float> frame_data; + frame_data.resize(p_data.size()); + for (int i = 0; i < p_data.size(); i++) { + frame_data.write[i] = p_data[i]; + } + performance_profiler->add_profile_frame(frame_data); + + } else if (p_msg == "visual:profile_frame") { + DebuggerMarshalls::VisualProfilerFrame frame; + frame.deserialize(p_data); + + EditorVisualProfiler::Metric metric; + metric.areas.resize(frame.areas.size()); + metric.frame_number = frame.frame_number; + metric.valid = true; + + { + EditorVisualProfiler::Metric::Area *areas_ptr = metric.areas.ptrw(); + for (int i = 0; i < frame.areas.size(); i++) { + areas_ptr[i].name = frame.areas[i].name; + areas_ptr[i].cpu_time = frame.areas[i].cpu_msec; + areas_ptr[i].gpu_time = frame.areas[i].gpu_msec; + } + } + visual_profiler->add_frame_metric(metric); + + } else if (p_msg == "error") { + DebuggerMarshalls::OutputError oe; + ERR_FAIL_COND_MSG(oe.deserialize(p_data) == false, "Failed to deserialize error message"); + + // Format time. + Array time_vals; + time_vals.push_back(oe.hr); + time_vals.push_back(oe.min); + time_vals.push_back(oe.sec); + time_vals.push_back(oe.msec); + bool e; + String time = String("%d:%02d:%02d:%04d").sprintf(time_vals, &e); + + // Rest of the error data. + bool source_is_project_file = oe.source_file.begins_with("res://"); + + // Metadata to highlight error line in scripts. + Array source_meta; + source_meta.push_back(oe.source_file); + source_meta.push_back(oe.source_line); + + // Create error tree to display above error or warning details. + TreeItem *r = error_tree->get_root(); + if (!r) { + r = error_tree->create_item(); + } + + // Also provide the relevant details as tooltip to quickly check without + // uncollapsing the tree. + String tooltip = oe.warning ? TTR("Warning:") : TTR("Error:"); + + TreeItem *error = error_tree->create_item(r); + error->set_collapsed(true); + + error->set_icon(0, get_theme_icon(oe.warning ? "Warning" : "Error", "EditorIcons")); + error->set_text(0, time); + error->set_text_align(0, TreeItem::ALIGN_LEFT); + + String error_title; + // Include method name, when given, in error title. + if (!oe.source_func.empty()) { + error_title += oe.source_func + ": "; + } + // If we have a (custom) error message, use it as title, and add a C++ Error + // item with the original error condition. + error_title += oe.error_descr.empty() ? oe.error : oe.error_descr; + error->set_text(1, error_title); + tooltip += " " + error_title + "\n"; + + if (!oe.error_descr.empty()) { + // Add item for C++ error condition. + TreeItem *cpp_cond = error_tree->create_item(error); + cpp_cond->set_text(0, "<" + TTR("C++ Error") + ">"); + cpp_cond->set_text(1, oe.error); + cpp_cond->set_text_align(0, TreeItem::ALIGN_LEFT); + tooltip += TTR("C++ Error:") + " " + oe.error + "\n"; + if (source_is_project_file) { + cpp_cond->set_metadata(0, source_meta); + } + } + Vector<uint8_t> v; + v.resize(100); + + // Source of the error. + String source_txt = (source_is_project_file ? oe.source_file.get_file() : oe.source_file) + ":" + itos(oe.source_line); + if (!oe.source_func.empty()) { + source_txt += " @ " + oe.source_func + "()"; + } + + TreeItem *cpp_source = error_tree->create_item(error); + cpp_source->set_text(0, "<" + (source_is_project_file ? TTR("Source") : TTR("C++ Source")) + ">"); + cpp_source->set_text(1, source_txt); + cpp_source->set_text_align(0, TreeItem::ALIGN_LEFT); + tooltip += (source_is_project_file ? TTR("Source:") : TTR("C++ Source:")) + " " + source_txt + "\n"; + + // Set metadata to highlight error line in scripts. + if (source_is_project_file) { + error->set_metadata(0, source_meta); + cpp_source->set_metadata(0, source_meta); + } + + error->set_tooltip(0, tooltip); + error->set_tooltip(1, tooltip); + + // Format stack trace. + // stack_items_count is the number of elements to parse, with 3 items per frame + // of the stack trace (script, method, line). + const ScriptLanguage::StackInfo *infos = oe.callstack.ptr(); + for (unsigned int i = 0; i < (unsigned int)oe.callstack.size(); i++) { + TreeItem *stack_trace = error_tree->create_item(error); + + Array meta; + meta.push_back(infos[i].file); + meta.push_back(infos[i].line); + stack_trace->set_metadata(0, meta); + + if (i == 0) { + stack_trace->set_text(0, "<" + TTR("Stack Trace") + ">"); + stack_trace->set_text_align(0, TreeItem::ALIGN_LEFT); + error->set_metadata(0, meta); + } + stack_trace->set_text(1, infos[i].file.get_file() + ":" + itos(infos[i].line) + " @ " + infos[i].func + "()"); + } + + if (oe.warning) { + warning_count++; + } else { + error_count++; + } + + } else if (p_msg == "servers:function_signature") { + // Cache a profiler signature. + DebuggerMarshalls::ScriptFunctionSignature sig; + sig.deserialize(p_data); + profiler_signature[sig.id] = sig.name; + + } else if (p_msg == "servers:profile_frame" || p_msg == "servers:profile_total") { + EditorProfiler::Metric metric; + DebuggerMarshalls::ServersProfilerFrame frame; + frame.deserialize(p_data); + metric.valid = true; + metric.frame_number = frame.frame_number; + metric.frame_time = frame.frame_time; + metric.idle_time = frame.idle_time; + metric.physics_time = frame.physics_time; + metric.physics_frame_time = frame.physics_frame_time; + + if (frame.servers.size()) { + EditorProfiler::Metric::Category frame_time; + frame_time.signature = "category_frame_time"; + frame_time.name = "Frame Time"; + frame_time.total_time = metric.frame_time; + + EditorProfiler::Metric::Category::Item item; + item.calls = 1; + item.line = 0; + + item.name = "Physics Time"; + item.total = metric.physics_time; + item.self = item.total; + item.signature = "physics_time"; + + frame_time.items.push_back(item); + + item.name = "Idle Time"; + item.total = metric.idle_time; + item.self = item.total; + item.signature = "idle_time"; + + frame_time.items.push_back(item); + + item.name = "Physics Frame Time"; + item.total = metric.physics_frame_time; + item.self = item.total; + item.signature = "physics_frame_time"; + + frame_time.items.push_back(item); + + metric.categories.push_back(frame_time); + } + + for (int i = 0; i < frame.servers.size(); i++) { + const DebuggerMarshalls::ServerInfo &srv = frame.servers[i]; + EditorProfiler::Metric::Category c; + const String name = srv.name; + c.name = name.capitalize(); + c.items.resize(srv.functions.size()); + c.total_time = 0; + c.signature = "categ::" + name; + for (int j = 0; j < srv.functions.size(); j++) { + EditorProfiler::Metric::Category::Item item; + item.calls = 1; + item.line = 0; + item.name = srv.functions[j].name; + item.self = srv.functions[j].time; + item.total = item.self; + item.signature = "categ::" + name + "::" + item.name; + item.name = item.name.capitalize(); + c.total_time += item.total; + c.items.write[j] = item; + } + metric.categories.push_back(c); + } + + EditorProfiler::Metric::Category funcs; + funcs.total_time = frame.script_time; + funcs.items.resize(frame.script_functions.size()); + funcs.name = "Script Functions"; + funcs.signature = "script_functions"; + for (int i = 0; i < frame.script_functions.size(); i++) { + int signature = frame.script_functions[i].sig_id; + int calls = frame.script_functions[i].call_count; + float total = frame.script_functions[i].total_time; + float self = frame.script_functions[i].self_time; + + EditorProfiler::Metric::Category::Item item; + if (profiler_signature.has(signature)) { + item.signature = profiler_signature[signature]; + + String name = profiler_signature[signature]; + Vector<String> strings = name.split("::"); + if (strings.size() == 3) { + item.name = strings[2]; + item.script = strings[0]; + item.line = strings[1].to_int(); + } else if (strings.size() == 4) { //Built-in scripts have an :: in their name + item.name = strings[3]; + item.script = strings[0] + "::" + strings[1]; + item.line = strings[2].to_int(); + } + + } else { + item.name = "SigErr " + itos(signature); + } + + item.calls = calls; + item.self = self; + item.total = total; + funcs.items.write[i] = item; + } + + metric.categories.push_back(funcs); + + if (p_msg == "servers:profile_frame") { + profiler->add_frame_metric(metric, false); + } else { + profiler->add_frame_metric(metric, true); + } + + } else if (p_msg == "network:profile_frame") { + DebuggerMarshalls::NetworkProfilerFrame frame; + frame.deserialize(p_data); + for (int i = 0; i < frame.infos.size(); i++) { + network_profiler->add_node_frame_data(frame.infos[i]); + } + + } else if (p_msg == "network:bandwidth") { + ERR_FAIL_COND(p_data.size() < 2); + network_profiler->set_bandwidth(p_data[0], p_data[1]); + + } else if (p_msg == "request_quit") { + emit_signal("stop_requested"); + _stop_and_notify(); + + } else if (p_msg == "performance:profile_names") { + Vector<StringName> monitors; + monitors.resize(p_data.size()); + for (int i = 0; i < p_data.size(); i++) { + ERR_FAIL_COND(p_data[i].get_type() != Variant::STRING_NAME); + monitors.set(i, p_data[i]); + } + performance_profiler->update_monitors(monitors); + + } else { + int colon_index = p_msg.find_char(':'); + ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received"); + + bool parsed = false; + const String cap = p_msg.substr(0, colon_index); + Map<StringName, Callable>::Element *element = captures.find(cap); + if (element) { + Callable &c = element->value(); + ERR_FAIL_COND_MSG(c.is_null(), "Invalid callable registered: " + cap); + Variant cmd = p_msg.substr(colon_index + 1), data = p_data; + const Variant *args[2] = { &cmd, &data }; + Variant retval; + Callable::CallError err; + c.call(args, 2, retval, err); + ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'capture' to callable: " + Variant::get_callable_error_text(c, args, 2, err)); + ERR_FAIL_COND_MSG(retval.get_type() != Variant::BOOL, "Error calling 'capture' to callable: " + String(c) + ". Return type is not bool."); + parsed = retval; + } + + if (!parsed) { + WARN_PRINT("unknown message " + p_msg); + } + } +} + +void ScriptEditorDebugger::_set_reason_text(const String &p_reason, MessageType p_type) { + switch (p_type) { + case MESSAGE_ERROR: + reason->add_theme_color_override("font_color", get_theme_color("error_color", "Editor")); + break; + case MESSAGE_WARNING: + reason->add_theme_color_override("font_color", get_theme_color("warning_color", "Editor")); + break; + default: + reason->add_theme_color_override("font_color", get_theme_color("success_color", "Editor")); + } + reason->set_text(p_reason); + reason->set_tooltip(p_reason.word_wrap(80)); +} + +void ScriptEditorDebugger::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + skip_breakpoints->set_icon(get_theme_icon("DebugSkipBreakpointsOff", "EditorIcons")); + copy->set_icon(get_theme_icon("ActionCopy", "EditorIcons")); + + step->set_icon(get_theme_icon("DebugStep", "EditorIcons")); + next->set_icon(get_theme_icon("DebugNext", "EditorIcons")); + dobreak->set_icon(get_theme_icon("Pause", "EditorIcons")); + docontinue->set_icon(get_theme_icon("DebugContinue", "EditorIcons")); + le_set->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_live_edit_set)); + le_clear->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_live_edit_clear)); + error_tree->connect("item_selected", callable_mp(this, &ScriptEditorDebugger::_error_selected)); + error_tree->connect("item_activated", callable_mp(this, &ScriptEditorDebugger::_error_activated)); + vmem_refresh->set_icon(get_theme_icon("Reload", "EditorIcons")); + vmem_export->set_icon(get_theme_icon("Save", "EditorIcons")); + + reason->add_theme_color_override("font_color", get_theme_color("error_color", "Editor")); + + } break; + case NOTIFICATION_PROCESS: { + if (is_session_active()) { + peer->poll(); + + if (camera_override == CameraOverride::OVERRIDE_2D) { + CanvasItemEditor *editor = CanvasItemEditor::get_singleton(); + + Dictionary state = editor->get_state(); + float zoom = state["zoom"]; + Point2 offset = state["ofs"]; + Transform2D transform; + + transform.scale_basis(Size2(zoom, zoom)); + transform.elements[2] = -offset * zoom; + + Array msg; + msg.push_back(transform); + _put_msg("scene:override_camera_2D:transform", msg); + + } else if (camera_override >= CameraOverride::OVERRIDE_3D_1) { + int viewport_idx = camera_override - CameraOverride::OVERRIDE_3D_1; + Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(viewport_idx); + Camera3D *const cam = viewport->get_camera(); + + Array msg; + msg.push_back(cam->get_camera_transform()); + if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) { + msg.push_back(false); + msg.push_back(cam->get_size()); + } else { + msg.push_back(true); + msg.push_back(cam->get_fov()); + } + msg.push_back(cam->get_znear()); + msg.push_back(cam->get_zfar()); + _put_msg("scene:override_camera_3D:transform", msg); + } + } + + const uint64_t until = OS::get_singleton()->get_ticks_msec() + 20; + + while (peer.is_valid() && peer->has_message()) { + Array arr = peer->get_message(); + if (arr.size() != 2 || arr[0].get_type() != Variant::STRING || arr[1].get_type() != Variant::ARRAY) { + _stop_and_notify(); + ERR_FAIL_MSG("Invalid message format received from peer"); + } + _parse_message(arr[0], arr[1]); + + if (OS::get_singleton()->get_ticks_msec() > until) { + break; + } + } + if (!is_session_active()) { + _stop_and_notify(); + break; + }; + } break; + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (tabs->has_theme_stylebox_override("panel")) { + tabs->add_theme_style_override("panel", editor->get_gui_base()->get_theme_stylebox("DebuggerPanel", "EditorStyles")); + } + + copy->set_icon(get_theme_icon("ActionCopy", "EditorIcons")); + step->set_icon(get_theme_icon("DebugStep", "EditorIcons")); + next->set_icon(get_theme_icon("DebugNext", "EditorIcons")); + dobreak->set_icon(get_theme_icon("Pause", "EditorIcons")); + docontinue->set_icon(get_theme_icon("DebugContinue", "EditorIcons")); + vmem_refresh->set_icon(get_theme_icon("Reload", "EditorIcons")); + vmem_export->set_icon(get_theme_icon("Save", "EditorIcons")); + } break; + } +} + +void ScriptEditorDebugger::_clear_execution() { + TreeItem *ti = stack_dump->get_selected(); + if (!ti) { + return; + } + + Dictionary d = ti->get_metadata(0); + + stack_script = ResourceLoader::load(d["file"]); + emit_signal("clear_execution", stack_script); + stack_script.unref(); + stack_dump->clear(); + inspector->clear_stack_variables(); +} + +void ScriptEditorDebugger::start(Ref<RemoteDebuggerPeer> p_peer) { + error_count = 0; + warning_count = 0; + stop(); + + peer = p_peer; + ERR_FAIL_COND(p_peer.is_null()); + + performance_profiler->reset(); + + set_process(true); + breaked = false; + can_debug = true; + camera_override = CameraOverride::OVERRIDE_NONE; + + tabs->set_current_tab(0); + _set_reason_text(TTR("Debug session started."), MESSAGE_SUCCESS); + _update_buttons_state(); + emit_signal("started"); +} + +void ScriptEditorDebugger::_update_buttons_state() { + const bool active = is_session_active(); + const bool has_editor_tree = active && editor_remote_tree && editor_remote_tree->get_selected(); + vmem_refresh->set_disabled(!active); + step->set_disabled(!active || !breaked || !can_debug); + next->set_disabled(!active || !breaked || !can_debug); + copy->set_disabled(!active || !breaked); + docontinue->set_disabled(!active || !breaked); + dobreak->set_disabled(!active || breaked); + le_clear->set_disabled(!active); + le_set->set_disabled(!has_editor_tree); +} + +void ScriptEditorDebugger::_stop_and_notify() { + stop(); + emit_signal("stopped"); + _set_reason_text(TTR("Debug session closed."), MESSAGE_WARNING); +} + +void ScriptEditorDebugger::stop() { + set_process(false); + breaked = false; + can_debug = false; + remote_pid = 0; + _clear_execution(); + + inspector->clear_cache(); + + if (peer.is_valid()) { + peer->close(); + peer.unref(); + reason->set_text(""); + reason->set_tooltip(""); + } + + node_path_cache.clear(); + res_path_cache.clear(); + profiler_signature.clear(); + + inspector->edit(nullptr); + _update_buttons_state(); +} + +void ScriptEditorDebugger::_profiler_activate(bool p_enable, int p_type) { + Array data; + data.push_back(p_enable); + switch (p_type) { + case PROFILER_NETWORK: + _put_msg("profiler:network", data); + break; + case PROFILER_VISUAL: + _put_msg("profiler:visual", data); + break; + case PROFILER_SCRIPTS_SERVERS: + if (p_enable) { + // Clear old script signatures. (should we move all this into the profiler?) + profiler_signature.clear(); + // Add max funcs options to request. + Array opts; + int max_funcs = EditorSettings::get_singleton()->get("debugger/profiler_frame_max_functions"); + opts.push_back(CLAMP(max_funcs, 16, 512)); + data.push_back(opts); + } + _put_msg("profiler:servers", data); + break; + default: + ERR_FAIL_MSG("Invalid profiler type"); + } +} + +void ScriptEditorDebugger::_profiler_seeked() { + if (breaked) { + return; + } + debug_break(); +} + +void ScriptEditorDebugger::_stack_dump_frame_selected() { + emit_signal("stack_frame_selected"); + + int frame = get_stack_script_frame(); + + if (is_session_active() && frame >= 0) { + Array msg; + msg.push_back(frame); + _put_msg("get_stack_frame_vars", msg); + } else { + inspector->edit(nullptr); + } +} + +void ScriptEditorDebugger::_export_csv() { + file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + file_dialog_purpose = SAVE_MONITORS_CSV; + file_dialog->popup_file_dialog(); +} + +String ScriptEditorDebugger::get_var_value(const String &p_var) const { + if (!breaked) { + return String(); + } + return inspector->get_stack_variable(p_var); +} + +int ScriptEditorDebugger::_get_node_path_cache(const NodePath &p_path) { + const int *r = node_path_cache.getptr(p_path); + if (r) { + return *r; + } + + last_path_id++; + + node_path_cache[p_path] = last_path_id; + Array msg; + msg.push_back(p_path); + msg.push_back(last_path_id); + _put_msg("scene:live_node_path", msg); + + return last_path_id; +} + +int ScriptEditorDebugger::_get_res_path_cache(const String &p_path) { + Map<String, int>::Element *E = res_path_cache.find(p_path); + + if (E) { + return E->get(); + } + + last_path_id++; + + res_path_cache[p_path] = last_path_id; + Array msg; + msg.push_back(p_path); + msg.push_back(last_path_id); + _put_msg("scene:live_res_path", msg); + + return last_path_id; +} + +void ScriptEditorDebugger::_method_changed(Object *p_base, const StringName &p_name, VARIANT_ARG_DECLARE) { + if (!p_base || !live_debug || !is_session_active() || !editor->get_edited_scene()) { + return; + } + + Node *node = Object::cast_to<Node>(p_base); + + VARIANT_ARGPTRS + + for (int i = 0; i < VARIANT_ARG_MAX; i++) { + //no pointers, sorry + if (argptr[i] && (argptr[i]->get_type() == Variant::OBJECT || argptr[i]->get_type() == Variant::_RID)) { + return; + } + } + + if (node) { + NodePath path = editor->get_edited_scene()->get_path_to(node); + int pathid = _get_node_path_cache(path); + + Array msg; + msg.push_back(pathid); + msg.push_back(p_name); + for (int i = 0; i < VARIANT_ARG_MAX; i++) { + //no pointers, sorry + msg.push_back(*argptr[i]); + } + _put_msg("scene:live_node_call", msg); + + return; + } + + Resource *res = Object::cast_to<Resource>(p_base); + + if (res && res->get_path() != String()) { + String respath = res->get_path(); + int pathid = _get_res_path_cache(respath); + + Array msg; + msg.push_back(pathid); + msg.push_back(p_name); + for (int i = 0; i < VARIANT_ARG_MAX; i++) { + //no pointers, sorry + msg.push_back(*argptr[i]); + } + _put_msg("scene:live_res_call", msg); + + return; + } +} + +void ScriptEditorDebugger::_property_changed(Object *p_base, const StringName &p_property, const Variant &p_value) { + if (!p_base || !live_debug || !editor->get_edited_scene()) { + return; + } + + Node *node = Object::cast_to<Node>(p_base); + + if (node) { + NodePath path = editor->get_edited_scene()->get_path_to(node); + int pathid = _get_node_path_cache(path); + + if (p_value.is_ref()) { + Ref<Resource> res = p_value; + if (res.is_valid() && res->get_path() != String()) { + Array msg; + msg.push_back(pathid); + msg.push_back(p_property); + msg.push_back(res->get_path()); + _put_msg("scene:live_node_prop_res", msg); + } + } else { + Array msg; + msg.push_back(pathid); + msg.push_back(p_property); + msg.push_back(p_value); + _put_msg("scene:live_node_prop", msg); + } + + return; + } + + Resource *res = Object::cast_to<Resource>(p_base); + + if (res && res->get_path() != String()) { + String respath = res->get_path(); + int pathid = _get_res_path_cache(respath); + + if (p_value.is_ref()) { + Ref<Resource> res2 = p_value; + if (res2.is_valid() && res2->get_path() != String()) { + Array msg; + msg.push_back(pathid); + msg.push_back(p_property); + msg.push_back(res2->get_path()); + _put_msg("scene:live_res_prop_res", msg); + } + } else { + Array msg; + msg.push_back(pathid); + msg.push_back(p_property); + msg.push_back(p_value); + _put_msg("scene:live_res_prop", msg); + } + + return; + } +} + +String ScriptEditorDebugger::get_stack_script_file() const { + TreeItem *ti = stack_dump->get_selected(); + if (!ti) { + return ""; + } + Dictionary d = ti->get_metadata(0); + return d["file"]; +} + +int ScriptEditorDebugger::get_stack_script_line() const { + TreeItem *ti = stack_dump->get_selected(); + if (!ti) { + return -1; + } + Dictionary d = ti->get_metadata(0); + return d["line"]; +} + +int ScriptEditorDebugger::get_stack_script_frame() const { + TreeItem *ti = stack_dump->get_selected(); + if (!ti) { + return -1; + } + Dictionary d = ti->get_metadata(0); + return d["frame"]; +} + +void ScriptEditorDebugger::set_live_debugging(bool p_enable) { + live_debug = p_enable; +} + +void ScriptEditorDebugger::_live_edit_set() { + if (!is_session_active() || !editor_remote_tree) { + return; + } + + TreeItem *ti = editor_remote_tree->get_selected(); + if (!ti) { + return; + } + + String path; + + while (ti) { + String lp = ti->get_text(0); + path = "/" + lp + path; + ti = ti->get_parent(); + } + + NodePath np = path; + + editor->get_editor_data().set_edited_scene_live_edit_root(np); + + update_live_edit_root(); +} + +void ScriptEditorDebugger::_live_edit_clear() { + NodePath np = NodePath("/root"); + editor->get_editor_data().set_edited_scene_live_edit_root(np); + + update_live_edit_root(); +} + +void ScriptEditorDebugger::update_live_edit_root() { + NodePath np = editor->get_editor_data().get_edited_scene_live_edit_root(); + + Array msg; + msg.push_back(np); + if (editor->get_edited_scene()) { + msg.push_back(editor->get_edited_scene()->get_filename()); + } else { + msg.push_back(""); + } + _put_msg("scene:live_set_root", msg); + live_edit_root->set_text(np); +} + +void ScriptEditorDebugger::live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name) { + if (live_debug) { + Array msg; + msg.push_back(p_parent); + msg.push_back(p_type); + msg.push_back(p_name); + _put_msg("scene:live_create_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_instance_node(const NodePath &p_parent, const String &p_path, const String &p_name) { + if (live_debug) { + Array msg; + msg.push_back(p_parent); + msg.push_back(p_path); + msg.push_back(p_name); + _put_msg("scene:live_instance_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_remove_node(const NodePath &p_at) { + if (live_debug) { + Array msg; + msg.push_back(p_at); + _put_msg("scene:live_remove_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id) { + if (live_debug) { + Array msg; + msg.push_back(p_at); + msg.push_back(p_keep_id); + _put_msg("scene:live_remove_and_keep_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos) { + if (live_debug) { + Array msg; + msg.push_back(p_id); + msg.push_back(p_at); + msg.push_back(p_at_pos); + _put_msg("scene:live_restore_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name) { + if (live_debug) { + Array msg; + msg.push_back(p_at); + msg.push_back(p_new_name); + _put_msg("scene:live_duplicate_node", msg); + } +} + +void ScriptEditorDebugger::live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos) { + if (live_debug) { + Array msg; + msg.push_back(p_at); + msg.push_back(p_new_place); + msg.push_back(p_new_name); + msg.push_back(p_at_pos); + _put_msg("scene:live_reparent_node", msg); + } +} + +CameraOverride ScriptEditorDebugger::get_camera_override() const { + return camera_override; +} + +void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) { + if (p_override == CameraOverride::OVERRIDE_2D && camera_override != CameraOverride::OVERRIDE_2D) { + Array msg; + msg.push_back(true); + _put_msg("scene:override_camera_2D:set", msg); + } else if (p_override != CameraOverride::OVERRIDE_2D && camera_override == CameraOverride::OVERRIDE_2D) { + Array msg; + msg.push_back(false); + _put_msg("scene:override_camera_2D:set", msg); + } else if (p_override >= CameraOverride::OVERRIDE_3D_1 && camera_override < CameraOverride::OVERRIDE_3D_1) { + Array msg; + msg.push_back(true); + _put_msg("scene:override_camera_3D:set", msg); + } else if (p_override < CameraOverride::OVERRIDE_3D_1 && camera_override >= CameraOverride::OVERRIDE_3D_1) { + Array msg; + msg.push_back(false); + _put_msg("scene:override_camera_3D:set", msg); + } + + camera_override = p_override; +} + +void ScriptEditorDebugger::set_breakpoint(const String &p_path, int p_line, bool p_enabled) { + Array msg; + msg.push_back(p_path); + msg.push_back(p_line); + msg.push_back(p_enabled); + _put_msg("breakpoint", msg); +} + +void ScriptEditorDebugger::reload_scripts() { + _put_msg("reload_scripts", Array()); +} + +bool ScriptEditorDebugger::is_skip_breakpoints() { + return skip_breakpoints_value; +} + +void ScriptEditorDebugger::_error_activated() { + TreeItem *selected = error_tree->get_selected(); + + TreeItem *ci = selected->get_children(); + if (ci) { + selected->set_collapsed(!selected->is_collapsed()); + } +} + +void ScriptEditorDebugger::_error_selected() { + TreeItem *selected = error_tree->get_selected(); + Array meta = selected->get_metadata(0); + if (meta.size() == 0) { + return; + } + + emit_signal("error_selected", String(meta[0]), int(meta[1])); +} + +void ScriptEditorDebugger::_expand_errors_list() { + TreeItem *root = error_tree->get_root(); + if (!root) { + return; + } + + TreeItem *item = root->get_children(); + while (item) { + item->set_collapsed(false); + item = item->get_next(); + } +} + +void ScriptEditorDebugger::_collapse_errors_list() { + TreeItem *root = error_tree->get_root(); + if (!root) { + return; + } + + TreeItem *item = root->get_children(); + while (item) { + item->set_collapsed(true); + item = item->get_next(); + } +} + +void ScriptEditorDebugger::_clear_errors_list() { + error_tree->clear(); + error_count = 0; + warning_count = 0; +} + +// Right click on specific file(s) or folder(s). +void ScriptEditorDebugger::_error_tree_item_rmb_selected(const Vector2 &p_pos) { + item_menu->clear(); + item_menu->set_size(Size2(1, 1)); + + if (error_tree->is_anything_selected()) { + item_menu->add_icon_item(get_theme_icon("ActionCopy", "EditorIcons"), TTR("Copy Error"), 0); + } + + if (item_menu->get_item_count() > 0) { + item_menu->set_position(error_tree->get_global_position() + p_pos); + item_menu->popup(); + } +} + +void ScriptEditorDebugger::_item_menu_id_pressed(int p_option) { + TreeItem *ti = error_tree->get_selected(); + while (ti->get_parent() != error_tree->get_root()) { + ti = ti->get_parent(); + } + + String type; + + if (ti->get_icon(0) == get_theme_icon("Warning", "EditorIcons")) { + type = "W "; + } else if (ti->get_icon(0) == get_theme_icon("Error", "EditorIcons")) { + type = "E "; + } + + String text = ti->get_text(0) + " "; + int rpad_len = text.length(); + + text = type + text + ti->get_text(1) + "\n"; + TreeItem *ci = ti->get_children(); + while (ci) { + text += " " + ci->get_text(0).rpad(rpad_len) + ci->get_text(1) + "\n"; + ci = ci->get_next(); + } + + DisplayServer::get_singleton()->clipboard_set(text); +} + +void ScriptEditorDebugger::_tab_changed(int p_tab) { + if (tabs->get_tab_title(p_tab) == TTR("Video RAM")) { + // "Video RAM" tab was clicked, refresh the data it's displaying when entering the tab. + _video_mem_request(); + } +} + +void ScriptEditorDebugger::_bind_methods() { + ClassDB::bind_method(D_METHOD("live_debug_create_node"), &ScriptEditorDebugger::live_debug_create_node); + ClassDB::bind_method(D_METHOD("live_debug_instance_node"), &ScriptEditorDebugger::live_debug_instance_node); + ClassDB::bind_method(D_METHOD("live_debug_remove_node"), &ScriptEditorDebugger::live_debug_remove_node); + ClassDB::bind_method(D_METHOD("live_debug_remove_and_keep_node"), &ScriptEditorDebugger::live_debug_remove_and_keep_node); + ClassDB::bind_method(D_METHOD("live_debug_restore_node"), &ScriptEditorDebugger::live_debug_restore_node); + ClassDB::bind_method(D_METHOD("live_debug_duplicate_node"), &ScriptEditorDebugger::live_debug_duplicate_node); + ClassDB::bind_method(D_METHOD("live_debug_reparent_node"), &ScriptEditorDebugger::live_debug_reparent_node); + ClassDB::bind_method(D_METHOD("request_remote_object", "id"), &ScriptEditorDebugger::request_remote_object); + ClassDB::bind_method(D_METHOD("update_remote_object", "id", "property", "value"), &ScriptEditorDebugger::update_remote_object); + + ADD_SIGNAL(MethodInfo("started")); + ADD_SIGNAL(MethodInfo("stopped")); + ADD_SIGNAL(MethodInfo("stop_requested")); + ADD_SIGNAL(MethodInfo("stack_frame_selected", PropertyInfo(Variant::INT, "frame"))); + ADD_SIGNAL(MethodInfo("error_selected", PropertyInfo(Variant::INT, "error"))); + ADD_SIGNAL(MethodInfo("set_execution", PropertyInfo("script"), PropertyInfo(Variant::INT, "line"))); + ADD_SIGNAL(MethodInfo("clear_execution", PropertyInfo("script"))); + ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug"))); + ADD_SIGNAL(MethodInfo("remote_object_requested", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"))); + ADD_SIGNAL(MethodInfo("remote_tree_updated")); +} + +void ScriptEditorDebugger::add_debugger_plugin(const Ref<Script> &p_script) { + if (!debugger_plugins.has(p_script)) { + EditorDebuggerPlugin *plugin = memnew(EditorDebuggerPlugin()); + plugin->attach_debugger(this); + plugin->set_script(p_script); + tabs->add_child(plugin); + debugger_plugins.insert(p_script, plugin); + } +} + +void ScriptEditorDebugger::remove_debugger_plugin(const Ref<Script> &p_script) { + if (debugger_plugins.has(p_script)) { + tabs->remove_child(debugger_plugins[p_script]); + debugger_plugins[p_script]->detach_debugger(false); + memdelete(debugger_plugins[p_script]); + debugger_plugins.erase(p_script); + } +} + +void ScriptEditorDebugger::send_message(const String &p_message, const Array &p_args) { + _put_msg(p_message, p_args); +} + +void ScriptEditorDebugger::register_message_capture(const StringName &p_name, const Callable &p_callable) { + ERR_FAIL_COND_MSG(has_capture(p_name), "Capture already registered: " + p_name); + captures.insert(p_name, p_callable); +} + +void ScriptEditorDebugger::unregister_message_capture(const StringName &p_name) { + ERR_FAIL_COND_MSG(!has_capture(p_name), "Capture not registered: " + p_name); + captures.erase(p_name); +} + +bool ScriptEditorDebugger::has_capture(const StringName &p_name) { + return captures.has(p_name); +} + +ScriptEditorDebugger::ScriptEditorDebugger(EditorNode *p_editor) { + editor = p_editor; + + tabs = memnew(TabContainer); + tabs->set_tab_align(TabContainer::ALIGN_LEFT); + tabs->add_theme_style_override("panel", editor->get_gui_base()->get_theme_stylebox("DebuggerPanel", "EditorStyles")); + tabs->connect("tab_changed", callable_mp(this, &ScriptEditorDebugger::_tab_changed)); + + add_child(tabs); + + { //debugger + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->set_name(TTR("Debugger")); + Control *dbg = vbc; + + HBoxContainer *hbc = memnew(HBoxContainer); + vbc->add_child(hbc); + + reason = memnew(Label); + reason->set_text(""); + hbc->add_child(reason); + reason->set_h_size_flags(SIZE_EXPAND_FILL); + reason->set_autowrap(true); + reason->set_max_lines_visible(3); + reason->set_mouse_filter(Control::MOUSE_FILTER_PASS); + + hbc->add_child(memnew(VSeparator)); + + skip_breakpoints = memnew(Button); + skip_breakpoints->set_flat(true); + hbc->add_child(skip_breakpoints); + skip_breakpoints->set_tooltip(TTR("Skip Breakpoints")); + skip_breakpoints->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_skip_breakpoints)); + + hbc->add_child(memnew(VSeparator)); + + copy = memnew(Button); + copy->set_flat(true); + hbc->add_child(copy); + copy->set_tooltip(TTR("Copy Error")); + copy->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_copy)); + + hbc->add_child(memnew(VSeparator)); + + step = memnew(Button); + step->set_flat(true); + hbc->add_child(step); + step->set_tooltip(TTR("Step Into")); + step->set_shortcut(ED_GET_SHORTCUT("debugger/step_into")); + step->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_step)); + + next = memnew(Button); + next->set_flat(true); + hbc->add_child(next); + next->set_tooltip(TTR("Step Over")); + next->set_shortcut(ED_GET_SHORTCUT("debugger/step_over")); + next->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_next)); + + hbc->add_child(memnew(VSeparator)); + + dobreak = memnew(Button); + dobreak->set_flat(true); + hbc->add_child(dobreak); + dobreak->set_tooltip(TTR("Break")); + dobreak->set_shortcut(ED_GET_SHORTCUT("debugger/break")); + dobreak->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_break)); + + docontinue = memnew(Button); + docontinue->set_flat(true); + hbc->add_child(docontinue); + docontinue->set_tooltip(TTR("Continue")); + docontinue->set_shortcut(ED_GET_SHORTCUT("debugger/continue")); + docontinue->connect("pressed", callable_mp(this, &ScriptEditorDebugger::debug_continue)); + + HSplitContainer *sc = memnew(HSplitContainer); + vbc->add_child(sc); + sc->set_v_size_flags(SIZE_EXPAND_FILL); + + stack_dump = memnew(Tree); + stack_dump->set_allow_reselect(true); + stack_dump->set_columns(1); + stack_dump->set_column_titles_visible(true); + stack_dump->set_column_title(0, TTR("Stack Frames")); + stack_dump->set_h_size_flags(SIZE_EXPAND_FILL); + stack_dump->set_hide_root(true); + stack_dump->connect("cell_selected", callable_mp(this, &ScriptEditorDebugger::_stack_dump_frame_selected)); + sc->add_child(stack_dump); + + inspector = memnew(EditorDebuggerInspector); + inspector->set_h_size_flags(SIZE_EXPAND_FILL); + inspector->set_enable_capitalize_paths(false); + inspector->set_read_only(true); + inspector->connect("object_selected", callable_mp(this, &ScriptEditorDebugger::_remote_object_selected)); + inspector->connect("object_edited", callable_mp(this, &ScriptEditorDebugger::_remote_object_edited)); + inspector->connect("object_property_updated", callable_mp(this, &ScriptEditorDebugger::_remote_object_property_updated)); + sc->add_child(inspector); + tabs->add_child(dbg); + } + + { //errors + errors_tab = memnew(VBoxContainer); + errors_tab->set_name(TTR("Errors")); + + HBoxContainer *errhb = memnew(HBoxContainer); + errors_tab->add_child(errhb); + + Button *expand_all = memnew(Button); + expand_all->set_text(TTR("Expand All")); + expand_all->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_expand_errors_list)); + errhb->add_child(expand_all); + + Button *collapse_all = memnew(Button); + collapse_all->set_text(TTR("Collapse All")); + collapse_all->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_collapse_errors_list)); + errhb->add_child(collapse_all); + + Control *space = memnew(Control); + space->set_h_size_flags(SIZE_EXPAND_FILL); + errhb->add_child(space); + + clearbutton = memnew(Button); + clearbutton->set_text(TTR("Clear")); + clearbutton->set_h_size_flags(0); + clearbutton->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_clear_errors_list)); + errhb->add_child(clearbutton); + + error_tree = memnew(Tree); + error_tree->set_columns(2); + + error_tree->set_column_expand(0, false); + error_tree->set_column_min_width(0, 140); + + error_tree->set_column_expand(1, true); + + error_tree->set_select_mode(Tree::SELECT_ROW); + error_tree->set_hide_root(true); + error_tree->set_v_size_flags(SIZE_EXPAND_FILL); + error_tree->set_allow_rmb_select(true); + error_tree->connect("item_rmb_selected", callable_mp(this, &ScriptEditorDebugger::_error_tree_item_rmb_selected)); + errors_tab->add_child(error_tree); + + item_menu = memnew(PopupMenu); + item_menu->connect("id_pressed", callable_mp(this, &ScriptEditorDebugger::_item_menu_id_pressed)); + error_tree->add_child(item_menu); + + tabs->add_child(errors_tab); + } + + { // File dialog + file_dialog = memnew(EditorFileDialog); + file_dialog->connect("file_selected", callable_mp(this, &ScriptEditorDebugger::_file_selected)); + add_child(file_dialog); + } + + { //profiler + profiler = memnew(EditorProfiler); + profiler->set_name(TTR("Profiler")); + tabs->add_child(profiler); + profiler->connect("enable_profiling", callable_mp(this, &ScriptEditorDebugger::_profiler_activate), varray(PROFILER_SCRIPTS_SERVERS)); + profiler->connect("break_request", callable_mp(this, &ScriptEditorDebugger::_profiler_seeked)); + } + + { //frame profiler + visual_profiler = memnew(EditorVisualProfiler); + visual_profiler->set_name(TTR("Visual Profiler")); + tabs->add_child(visual_profiler); + visual_profiler->connect("enable_profiling", callable_mp(this, &ScriptEditorDebugger::_profiler_activate), varray(PROFILER_VISUAL)); + } + + { //network profiler + network_profiler = memnew(EditorNetworkProfiler); + network_profiler->set_name(TTR("Network Profiler")); + tabs->add_child(network_profiler); + network_profiler->connect("enable_profiling", callable_mp(this, &ScriptEditorDebugger::_profiler_activate), varray(PROFILER_NETWORK)); + } + + { //monitors + performance_profiler = memnew(EditorPerformanceProfiler); + tabs->add_child(performance_profiler); + } + + { //vmem inspect + VBoxContainer *vmem_vb = memnew(VBoxContainer); + HBoxContainer *vmem_hb = memnew(HBoxContainer); + Label *vmlb = memnew(Label(TTR("List of Video Memory Usage by Resource:") + " ")); + vmlb->set_h_size_flags(SIZE_EXPAND_FILL); + vmem_hb->add_child(vmlb); + vmem_hb->add_child(memnew(Label(TTR("Total:") + " "))); + vmem_total = memnew(LineEdit); + vmem_total->set_editable(false); + vmem_total->set_custom_minimum_size(Size2(100, 0) * EDSCALE); + vmem_hb->add_child(vmem_total); + vmem_refresh = memnew(Button); + vmem_refresh->set_flat(true); + vmem_hb->add_child(vmem_refresh); + vmem_export = memnew(Button); + vmem_export->set_flat(true); + vmem_export->set_tooltip(TTR("Export list to a CSV file")); + vmem_hb->add_child(vmem_export); + vmem_vb->add_child(vmem_hb); + vmem_refresh->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_video_mem_request)); + vmem_export->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_video_mem_export)); + + VBoxContainer *vmmc = memnew(VBoxContainer); + vmem_tree = memnew(Tree); + vmem_tree->set_v_size_flags(SIZE_EXPAND_FILL); + vmem_tree->set_h_size_flags(SIZE_EXPAND_FILL); + vmmc->add_child(vmem_tree); + vmmc->set_v_size_flags(SIZE_EXPAND_FILL); + vmem_vb->add_child(vmmc); + + vmem_vb->set_name(TTR("Video RAM")); + vmem_tree->set_columns(4); + vmem_tree->set_column_titles_visible(true); + vmem_tree->set_column_title(0, TTR("Resource Path")); + vmem_tree->set_column_expand(0, true); + vmem_tree->set_column_expand(1, false); + vmem_tree->set_column_title(1, TTR("Type")); + vmem_tree->set_column_min_width(1, 100 * EDSCALE); + vmem_tree->set_column_expand(2, false); + vmem_tree->set_column_title(2, TTR("Format")); + vmem_tree->set_column_min_width(2, 150 * EDSCALE); + vmem_tree->set_column_expand(3, false); + vmem_tree->set_column_title(3, TTR("Usage")); + vmem_tree->set_column_min_width(3, 80 * EDSCALE); + vmem_tree->set_hide_root(true); + + tabs->add_child(vmem_vb); + } + + { // misc + VBoxContainer *misc = memnew(VBoxContainer); + misc->set_name(TTR("Misc")); + tabs->add_child(misc); + + GridContainer *info_left = memnew(GridContainer); + info_left->set_columns(2); + misc->add_child(info_left); + clicked_ctrl = memnew(LineEdit); + clicked_ctrl->set_h_size_flags(SIZE_EXPAND_FILL); + info_left->add_child(memnew(Label(TTR("Clicked Control:")))); + info_left->add_child(clicked_ctrl); + clicked_ctrl_type = memnew(LineEdit); + info_left->add_child(memnew(Label(TTR("Clicked Control Type:")))); + info_left->add_child(clicked_ctrl_type); + + scene_tree = memnew(SceneDebuggerTree); + live_edit_root = memnew(LineEdit); + live_edit_root->set_h_size_flags(SIZE_EXPAND_FILL); + + { + HBoxContainer *lehb = memnew(HBoxContainer); + Label *l = memnew(Label(TTR("Live Edit Root:"))); + info_left->add_child(l); + lehb->add_child(live_edit_root); + le_set = memnew(Button(TTR("Set From Tree"))); + lehb->add_child(le_set); + le_clear = memnew(Button(TTR("Clear"))); + lehb->add_child(le_clear); + info_left->add_child(lehb); + } + + misc->add_child(memnew(VSeparator)); + + HBoxContainer *buttons = memnew(HBoxContainer); + + export_csv = memnew(Button(TTR("Export measures as CSV"))); + export_csv->connect("pressed", callable_mp(this, &ScriptEditorDebugger::_export_csv)); + buttons->add_child(export_csv); + + misc->add_child(buttons); + } + + msgdialog = memnew(AcceptDialog); + add_child(msgdialog); + + live_debug = true; + camera_override = CameraOverride::OVERRIDE_NONE; + last_path_id = false; + error_count = 0; + warning_count = 0; + _update_buttons_state(); +} + +ScriptEditorDebugger::~ScriptEditorDebugger() { + if (peer.is_valid()) { + peer->close(); + peer.unref(); + } + memdelete(scene_tree); +} diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h new file mode 100644 index 0000000000..56b34e8e8c --- /dev/null +++ b/editor/debugger/script_editor_debugger.h @@ -0,0 +1,275 @@ +/*************************************************************************/ +/* script_editor_debugger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef SCRIPT_EDITOR_DEBUGGER_H +#define SCRIPT_EDITOR_DEBUGGER_H + +#include "core/os/os.h" +#include "editor/debugger/editor_debugger_inspector.h" +#include "editor/debugger/editor_debugger_node.h" +#include "editor/debugger/editor_debugger_server.h" +#include "editor/editor_file_dialog.h" +#include "scene/gui/button.h" +#include "scene/gui/margin_container.h" + +class Tree; +class EditorNode; +class LineEdit; +class TabContainer; +class RichTextLabel; +class TextureButton; +class AcceptDialog; +class TreeItem; +class HSplitContainer; +class ItemList; +class EditorProfiler; +class EditorVisualProfiler; +class EditorNetworkProfiler; +class EditorPerformanceProfiler; +class SceneDebuggerTree; +class EditorDebuggerPlugin; + +class ScriptEditorDebugger : public MarginContainer { + GDCLASS(ScriptEditorDebugger, MarginContainer); + + friend class EditorDebuggerNode; + +private: + enum MessageType { + MESSAGE_ERROR, + MESSAGE_WARNING, + MESSAGE_SUCCESS, + }; + + enum ProfilerType { + PROFILER_NETWORK, + PROFILER_VISUAL, + PROFILER_SCRIPTS_SERVERS + }; + + AcceptDialog *msgdialog; + + LineEdit *clicked_ctrl; + LineEdit *clicked_ctrl_type; + LineEdit *live_edit_root; + Button *le_set; + Button *le_clear; + Button *export_csv; + + VBoxContainer *errors_tab; + Tree *error_tree; + Button *clearbutton; + PopupMenu *item_menu; + + EditorFileDialog *file_dialog; + enum FileDialogPurpose { + SAVE_MONITORS_CSV, + SAVE_VRAM_CSV, + }; + FileDialogPurpose file_dialog_purpose; + + int error_count; + int warning_count; + + bool skip_breakpoints_value = false; + Ref<Script> stack_script; + + TabContainer *tabs; + + Label *reason; + + Button *skip_breakpoints; + Button *copy; + Button *step; + Button *next; + Button *dobreak; + Button *docontinue; + // Reference to "Remote" tab in scene tree. Needed by _live_edit_set and buttons state. + // Each debugger should have it's tree in the future I guess. + const Tree *editor_remote_tree = nullptr; + + Map<int, String> profiler_signature; + + Tree *vmem_tree; + Button *vmem_refresh; + Button *vmem_export; + LineEdit *vmem_total; + + Tree *stack_dump; + EditorDebuggerInspector *inspector; + SceneDebuggerTree *scene_tree; + + Ref<RemoteDebuggerPeer> peer; + + HashMap<NodePath, int> node_path_cache; + int last_path_id; + Map<String, int> res_path_cache; + + EditorProfiler *profiler; + EditorVisualProfiler *visual_profiler; + EditorNetworkProfiler *network_profiler; + EditorPerformanceProfiler *performance_profiler; + + EditorNode *editor; + + OS::ProcessID remote_pid = 0; + bool breaked = false; + bool can_debug = false; + + bool live_debug; + + EditorDebuggerNode::CameraOverride camera_override; + + Map<Ref<Script>, EditorDebuggerPlugin *> debugger_plugins; + + Map<StringName, Callable> captures; + + void _stack_dump_frame_selected(); + + void _file_selected(const String &p_file); + void _parse_message(const String &p_msg, const Array &p_data); + void _set_reason_text(const String &p_reason, MessageType p_type); + void _update_buttons_state(); + void _remote_object_selected(ObjectID p_object); + void _remote_object_edited(ObjectID, const String &p_prop, const Variant &p_value); + void _remote_object_property_updated(ObjectID p_id, const String &p_property); + + void _video_mem_request(); + void _video_mem_export(); + + int _get_node_path_cache(const NodePath &p_path); + + int _get_res_path_cache(const String &p_path); + + void _live_edit_set(); + void _live_edit_clear(); + + void _method_changed(Object *p_base, const StringName &p_name, VARIANT_ARG_DECLARE); + void _property_changed(Object *p_base, const StringName &p_property, const Variant &p_value); + + void _error_activated(); + void _error_selected(); + + void _expand_errors_list(); + void _collapse_errors_list(); + + void _profiler_activate(bool p_enable, int p_profiler); + void _profiler_seeked(); + + void _clear_errors_list(); + + void _error_tree_item_rmb_selected(const Vector2 &p_pos); + void _item_menu_id_pressed(int p_option); + void _tab_changed(int p_tab); + + void _put_msg(String p_message, Array p_data); + void _export_csv(); + + void _clear_execution(); + void _stop_and_notify(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void request_remote_object(ObjectID p_obj_id); + void update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value); + Object *get_remote_object(ObjectID p_id); + + // Needed by _live_edit_set, buttons state. + void set_editor_remote_tree(const Tree *p_tree) { editor_remote_tree = p_tree; } + + void request_remote_tree(); + const SceneDebuggerTree *get_remote_tree(); + + void start(Ref<RemoteDebuggerPeer> p_peer); + void stop(); + + void debug_skip_breakpoints(); + void debug_copy(); + + void debug_next(); + void debug_step(); + void debug_break(); + void debug_continue(); + bool is_breaked() const { return breaked; } + bool is_debuggable() const { return can_debug; } + bool is_session_active() { return peer.is_valid() && peer->is_peer_connected(); }; + int get_remote_pid() const { return remote_pid; } + + int get_error_count() const { return error_count; } + int get_warning_count() const { return warning_count; } + String get_stack_script_file() const; + int get_stack_script_line() const; + int get_stack_script_frame() const; + + void update_tabs(); + void clear_style(); + String get_var_value(const String &p_var) const; + + void save_node(ObjectID p_id, const String &p_file); + void set_live_debugging(bool p_enable); + + void live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name); + void live_debug_instance_node(const NodePath &p_parent, const String &p_path, const String &p_name); + void live_debug_remove_node(const NodePath &p_at); + void live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id); + void live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos); + void live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name); + void live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos); + + EditorDebuggerNode::CameraOverride get_camera_override() const; + void set_camera_override(EditorDebuggerNode::CameraOverride p_override); + + void set_breakpoint(const String &p_path, int p_line, bool p_enabled); + + void update_live_edit_root(); + + void reload_scripts(); + + bool is_skip_breakpoints(); + + virtual Size2 get_minimum_size() const override; + + void add_debugger_plugin(const Ref<Script> &p_script); + void remove_debugger_plugin(const Ref<Script> &p_script); + + void send_message(const String &p_message, const Array &p_args); + + void register_message_capture(const StringName &p_name, const Callable &p_callable); + void unregister_message_capture(const StringName &p_name); + bool has_capture(const StringName &p_name); + + ScriptEditorDebugger(EditorNode *p_editor = nullptr); + ~ScriptEditorDebugger(); +}; + +#endif // SCRIPT_EDITOR_DEBUGGER_H |