feat: godot-engine-source-4.3-stable

This commit is contained in:
Jan van der Weide 2025-01-17 16:36:38 +01:00
parent c59a7dcade
commit 7125d019b5
11149 changed files with 5070401 additions and 0 deletions

View file

@ -0,0 +1,7 @@
#!/usr/bin/env python
Import("env")
env.add_source_files(env.editor_sources, "*.cpp")
SConscript("debug_adapter/SCsub")

View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
Import("env")
env.add_source_files(env.editor_sources, "*.cpp")

View file

@ -0,0 +1,636 @@
/**************************************************************************/
/* debug_adapter_parser.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "debug_adapter_parser.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/export/editor_export_platform.h"
#include "editor/gui/editor_run_bar.h"
#include "editor/plugins/script_editor_plugin.h"
void DebugAdapterParser::_bind_methods() {
// Requests
ClassDB::bind_method(D_METHOD("req_initialize", "params"), &DebugAdapterParser::req_initialize);
ClassDB::bind_method(D_METHOD("req_disconnect", "params"), &DebugAdapterParser::req_disconnect);
ClassDB::bind_method(D_METHOD("req_launch", "params"), &DebugAdapterParser::req_launch);
ClassDB::bind_method(D_METHOD("req_attach", "params"), &DebugAdapterParser::req_attach);
ClassDB::bind_method(D_METHOD("req_restart", "params"), &DebugAdapterParser::req_restart);
ClassDB::bind_method(D_METHOD("req_terminate", "params"), &DebugAdapterParser::req_terminate);
ClassDB::bind_method(D_METHOD("req_configurationDone", "params"), &DebugAdapterParser::req_configurationDone);
ClassDB::bind_method(D_METHOD("req_pause", "params"), &DebugAdapterParser::req_pause);
ClassDB::bind_method(D_METHOD("req_continue", "params"), &DebugAdapterParser::req_continue);
ClassDB::bind_method(D_METHOD("req_threads", "params"), &DebugAdapterParser::req_threads);
ClassDB::bind_method(D_METHOD("req_stackTrace", "params"), &DebugAdapterParser::req_stackTrace);
ClassDB::bind_method(D_METHOD("req_setBreakpoints", "params"), &DebugAdapterParser::req_setBreakpoints);
ClassDB::bind_method(D_METHOD("req_breakpointLocations", "params"), &DebugAdapterParser::req_breakpointLocations);
ClassDB::bind_method(D_METHOD("req_scopes", "params"), &DebugAdapterParser::req_scopes);
ClassDB::bind_method(D_METHOD("req_variables", "params"), &DebugAdapterParser::req_variables);
ClassDB::bind_method(D_METHOD("req_next", "params"), &DebugAdapterParser::req_next);
ClassDB::bind_method(D_METHOD("req_stepIn", "params"), &DebugAdapterParser::req_stepIn);
ClassDB::bind_method(D_METHOD("req_evaluate", "params"), &DebugAdapterParser::req_evaluate);
ClassDB::bind_method(D_METHOD("req_godot/put_msg", "params"), &DebugAdapterParser::req_godot_put_msg);
}
Dictionary DebugAdapterParser::prepare_base_event() const {
Dictionary event;
event["type"] = "event";
return event;
}
Dictionary DebugAdapterParser::prepare_success_response(const Dictionary &p_params) const {
Dictionary response;
response["type"] = "response";
response["request_seq"] = p_params["seq"];
response["command"] = p_params["command"];
response["success"] = true;
return response;
}
Dictionary DebugAdapterParser::prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables) const {
Dictionary response, body;
response["type"] = "response";
response["request_seq"] = p_params["seq"];
response["command"] = p_params["command"];
response["success"] = false;
response["body"] = body;
DAP::Message message;
String error, error_desc;
switch (err_type) {
case DAP::ErrorType::WRONG_PATH:
error = "wrong_path";
error_desc = "The editor and client are working on different paths; the client is on \"{clientPath}\", but the editor is on \"{editorPath}\"";
break;
case DAP::ErrorType::NOT_RUNNING:
error = "not_running";
error_desc = "Can't attach to a running session since there isn't one.";
break;
case DAP::ErrorType::TIMEOUT:
error = "timeout";
error_desc = "Timeout reached while processing a request.";
break;
case DAP::ErrorType::UNKNOWN_PLATFORM:
error = "unknown_platform";
error_desc = "The specified platform is unknown.";
break;
case DAP::ErrorType::MISSING_DEVICE:
error = "missing_device";
error_desc = "There's no connected device with specified id.";
break;
case DAP::ErrorType::UNKNOWN:
default:
error = "unknown";
error_desc = "An unknown error has occurred when processing the request.";
break;
}
message.id = err_type;
message.format = error_desc;
message.variables = variables;
response["message"] = error;
body["error"] = message.to_json();
return response;
}
Dictionary DebugAdapterParser::req_initialize(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params);
Dictionary args = p_params["arguments"];
Ref<DAPeer> peer = DebugAdapterProtocol::get_singleton()->get_current_peer();
peer->linesStartAt1 = args.get("linesStartAt1", false);
peer->columnsStartAt1 = args.get("columnsStartAt1", false);
peer->supportsVariableType = args.get("supportsVariableType", false);
peer->supportsInvalidatedEvent = args.get("supportsInvalidatedEvent", false);
DAP::Capabilities caps;
response["body"] = caps.to_json();
DebugAdapterProtocol::get_singleton()->notify_initialized();
if (DebugAdapterProtocol::get_singleton()->_sync_breakpoints) {
// Send all current breakpoints
List<String> breakpoints;
ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
for (List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
String breakpoint = E->get();
String path = breakpoint.left(breakpoint.find(":", 6)); // Skip initial part of path, aka "res://"
int line = breakpoint.substr(path.size()).to_int();
DebugAdapterProtocol::get_singleton()->on_debug_breakpoint_toggled(path, line, true);
}
} else {
// Remove all current breakpoints
EditorDebuggerNode::get_singleton()->get_default_debugger()->_clear_breakpoints();
}
return response;
}
Dictionary DebugAdapterParser::req_disconnect(const Dictionary &p_params) const {
if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->attached) {
EditorRunBar::get_singleton()->stop_playing();
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_launch(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
if (args.has("project") && !is_valid_path(args["project"])) {
Dictionary variables;
variables["clientPath"] = args["project"];
variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path();
return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables);
}
if (args.has("godot/custom_data")) {
DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsCustomData = args["godot/custom_data"];
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->pending_launch = p_params;
return Dictionary();
}
Dictionary DebugAdapterParser::_launch_process(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
ScriptEditorDebugger *dbg = EditorDebuggerNode::get_singleton()->get_default_debugger();
if ((bool)args["noDebug"] != dbg->is_skip_breakpoints()) {
dbg->debug_skip_breakpoints();
}
String platform_string = args.get("platform", "host");
if (platform_string == "host") {
EditorRunBar::get_singleton()->play_main_scene();
} else {
int device = args.get("device", -1);
int idx = -1;
if (platform_string == "android") {
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
if (EditorExport::get_singleton()->get_export_platform(i)->get_name() == "Android") {
idx = i;
break;
}
}
} else if (platform_string == "web") {
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
if (EditorExport::get_singleton()->get_export_platform(i)->get_name() == "Web") {
idx = i;
break;
}
}
}
if (idx == -1) {
return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN_PLATFORM);
}
EditorRunBar *run_bar = EditorRunBar::get_singleton();
Error err = platform_string == "android" ? run_bar->start_native_device(device * 10000 + idx) : run_bar->start_native_device(idx);
if (err) {
if (err == ERR_INVALID_PARAMETER && platform_string == "android") {
return prepare_error_response(p_params, DAP::ErrorType::MISSING_DEVICE);
} else {
return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN);
}
}
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->attached = false;
DebugAdapterProtocol::get_singleton()->notify_process();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_attach(const Dictionary &p_params) const {
ScriptEditorDebugger *dbg = EditorDebuggerNode::get_singleton()->get_default_debugger();
if (!dbg->is_session_active()) {
return prepare_error_response(p_params, DAP::ErrorType::NOT_RUNNING);
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->attached = true;
DebugAdapterProtocol::get_singleton()->notify_process();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_restart(const Dictionary &p_params) const {
// Extract embedded "arguments" so it can be given to req_launch/req_attach
Dictionary params = p_params, args;
args = params["arguments"];
args = args["arguments"];
params["arguments"] = args;
Dictionary response = DebugAdapterProtocol::get_singleton()->get_current_peer()->attached ? req_attach(params) : _launch_process(params);
if (!response["success"]) {
response["command"] = p_params["command"];
return response;
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_terminate(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->stop_playing();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_configurationDone(const Dictionary &p_params) const {
Ref<DAPeer> peer = DebugAdapterProtocol::get_singleton()->get_current_peer();
if (!peer->pending_launch.is_empty()) {
peer->res_queue.push_back(_launch_process(peer->pending_launch));
peer->pending_launch.clear();
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_pause(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(true);
EditorDebuggerNode::get_singleton()->_paused();
DebugAdapterProtocol::get_singleton()->notify_stopped_paused();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_continue(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(false);
EditorDebuggerNode::get_singleton()->_paused();
DebugAdapterProtocol::get_singleton()->notify_continued();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_threads(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Array arr;
DAP::Thread thread;
thread.id = 1; // Hardcoded because Godot only supports debugging one thread at the moment
thread.name = "Main";
arr.push_back(thread.to_json());
body["threads"] = arr;
return response;
}
Dictionary DebugAdapterParser::req_stackTrace(const Dictionary &p_params) const {
if (DebugAdapterProtocol::get_singleton()->_processing_stackdump) {
return Dictionary();
}
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1;
bool columns_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->columnsStartAt1;
Array arr;
DebugAdapterProtocol *dap = DebugAdapterProtocol::get_singleton();
for (const KeyValue<DAP::StackFrame, List<int>> &E : dap->stackframe_list) {
DAP::StackFrame sf = E.key;
if (!lines_at_one) {
sf.line--;
}
if (!columns_at_one) {
sf.column--;
}
arr.push_back(sf.to_json());
}
body["stackFrames"] = arr;
return response;
}
Dictionary DebugAdapterParser::req_setBreakpoints(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
DAP::Source source;
source.from_json(args["source"]);
bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1;
if (!is_valid_path(source.path)) {
Dictionary variables;
variables["clientPath"] = source.path;
variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path();
return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables);
}
// If path contains \, it's a Windows path, so we need to convert it to /, and make the drive letter uppercase
if (source.path.contains("\\")) {
source.path = source.path.replace("\\", "/");
source.path = source.path.substr(0, 1).to_upper() + source.path.substr(1);
}
Array breakpoints = args["breakpoints"], lines;
for (int i = 0; i < breakpoints.size(); i++) {
DAP::SourceBreakpoint breakpoint;
breakpoint.from_json(breakpoints[i]);
lines.push_back(breakpoint.line + !lines_at_one);
}
Array updated_breakpoints = DebugAdapterProtocol::get_singleton()->update_breakpoints(source.path, lines);
body["breakpoints"] = updated_breakpoints;
return response;
}
Dictionary DebugAdapterParser::req_breakpointLocations(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
Array locations;
DAP::BreakpointLocation location;
location.line = args["line"];
if (args.has("endLine")) {
location.endLine = args["endLine"];
}
locations.push_back(location.to_json());
body["breakpoints"] = locations;
return response;
}
Dictionary DebugAdapterParser::req_scopes(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
int frame_id = args["frameId"];
Array scope_list;
DAP::StackFrame frame;
frame.id = frame_id;
HashMap<DAP::StackFrame, List<int>, DAP::StackFrame>::Iterator E = DebugAdapterProtocol::get_singleton()->stackframe_list.find(frame);
if (E) {
ERR_FAIL_COND_V(E->value.size() != 3, prepare_error_response(p_params, DAP::ErrorType::UNKNOWN));
List<int>::ConstIterator itr = E->value.begin();
for (int i = 0; i < 3; ++itr, ++i) {
DAP::Scope scope;
scope.variablesReference = *itr;
switch (i) {
case 0:
scope.name = "Locals";
scope.presentationHint = "locals";
break;
case 1:
scope.name = "Members";
scope.presentationHint = "members";
break;
case 2:
scope.name = "Globals";
scope.presentationHint = "globals";
}
scope_list.push_back(scope.to_json());
}
}
EditorDebuggerNode::get_singleton()->get_default_debugger()->request_stack_dump(frame_id);
DebugAdapterProtocol::get_singleton()->_current_frame = frame_id;
body["scopes"] = scope_list;
return response;
}
Dictionary DebugAdapterParser::req_variables(const Dictionary &p_params) const {
// If _remaining_vars > 0, the debuggee is still sending a stack dump to the editor.
if (DebugAdapterProtocol::get_singleton()->_remaining_vars > 0) {
return Dictionary();
}
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
int variable_id = args["variablesReference"];
HashMap<int, Array>::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id);
if (E) {
if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsVariableType) {
for (int i = 0; i < E->value.size(); i++) {
Dictionary variable = E->value[i];
variable.erase("type");
}
}
body["variables"] = E ? E->value : Array();
return response;
} else {
return Dictionary();
}
}
Dictionary DebugAdapterParser::req_next(const Dictionary &p_params) const {
EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_next();
DebugAdapterProtocol::get_singleton()->_stepping = true;
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_stepIn(const Dictionary &p_params) const {
EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_step();
DebugAdapterProtocol::get_singleton()->_stepping = true;
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_evaluate(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
String value = EditorDebuggerNode::get_singleton()->get_var_value(args["expression"]);
body["result"] = value;
body["variablesReference"] = 0;
return response;
}
Dictionary DebugAdapterParser::req_godot_put_msg(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
String msg = args["message"];
Array data = args["data"];
EditorDebuggerNode::get_singleton()->get_default_debugger()->_put_msg(msg, data);
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::ev_initialized() const {
Dictionary event = prepare_base_event();
event["event"] = "initialized";
return event;
}
Dictionary DebugAdapterParser::ev_process(const String &p_command) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "process";
event["body"] = body;
body["name"] = OS::get_singleton()->get_executable_path();
body["startMethod"] = p_command;
return event;
}
Dictionary DebugAdapterParser::ev_terminated() const {
Dictionary event = prepare_base_event();
event["event"] = "terminated";
return event;
}
Dictionary DebugAdapterParser::ev_exited(const int &p_exitcode) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "exited";
event["body"] = body;
body["exitCode"] = p_exitcode;
return event;
}
Dictionary DebugAdapterParser::ev_stopped() const {
Dictionary event = prepare_base_event(), body;
event["event"] = "stopped";
event["body"] = body;
body["threadId"] = 1;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_paused() const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "paused";
body["description"] = "Paused";
return event;
}
Dictionary DebugAdapterParser::ev_stopped_exception(const String &p_error) const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "exception";
body["description"] = "Exception";
body["text"] = p_error;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_breakpoint(const int &p_id) const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "breakpoint";
body["description"] = "Breakpoint";
Array breakpoints;
breakpoints.push_back(p_id);
body["hitBreakpointIds"] = breakpoints;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_step() const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "step";
body["description"] = "Breakpoint";
return event;
}
Dictionary DebugAdapterParser::ev_continued() const {
Dictionary event = prepare_base_event(), body;
event["event"] = "continued";
event["body"] = body;
body["threadId"] = 1;
return event;
}
Dictionary DebugAdapterParser::ev_output(const String &p_message, RemoteDebugger::MessageType p_type) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "output";
event["body"] = body;
body["category"] = (p_type == RemoteDebugger::MessageType::MESSAGE_TYPE_ERROR) ? "stderr" : "stdout";
body["output"] = p_message + "\r\n";
return event;
}
Dictionary DebugAdapterParser::ev_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "breakpoint";
event["body"] = body;
body["reason"] = p_enabled ? "new" : "removed";
body["breakpoint"] = p_breakpoint.to_json();
return event;
}
Dictionary DebugAdapterParser::ev_custom_data(const String &p_msg, const Array &p_data) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "godot/custom_data";
event["body"] = body;
body["message"] = p_msg;
body["data"] = p_data;
return event;
}

View file

@ -0,0 +1,107 @@
/**************************************************************************/
/* debug_adapter_parser.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 DEBUG_ADAPTER_PARSER_H
#define DEBUG_ADAPTER_PARSER_H
#include "core/config/project_settings.h"
#include "core/debugger/remote_debugger.h"
#include "debug_adapter_protocol.h"
#include "debug_adapter_types.h"
struct DAPeer;
class DebugAdapterProtocol;
class DebugAdapterParser : public Object {
GDCLASS(DebugAdapterParser, Object);
private:
friend DebugAdapterProtocol;
_FORCE_INLINE_ bool is_valid_path(const String &p_path) const {
// If path contains \, it's a Windows path, so we need to convert it to /, and check as case-insensitive.
if (p_path.contains("\\")) {
String project_path = ProjectSettings::get_singleton()->get_resource_path();
String path = p_path.replace("\\", "/");
return path.containsn(project_path);
}
return p_path.begins_with(ProjectSettings::get_singleton()->get_resource_path());
}
protected:
static void _bind_methods();
Dictionary prepare_base_event() const;
Dictionary prepare_success_response(const Dictionary &p_params) const;
Dictionary prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables = Dictionary()) const;
Dictionary ev_stopped() const;
public:
// Requests
Dictionary req_initialize(const Dictionary &p_params) const;
Dictionary req_launch(const Dictionary &p_params) const;
Dictionary req_disconnect(const Dictionary &p_params) const;
Dictionary req_attach(const Dictionary &p_params) const;
Dictionary req_restart(const Dictionary &p_params) const;
Dictionary req_terminate(const Dictionary &p_params) const;
Dictionary req_configurationDone(const Dictionary &p_params) const;
Dictionary req_pause(const Dictionary &p_params) const;
Dictionary req_continue(const Dictionary &p_params) const;
Dictionary req_threads(const Dictionary &p_params) const;
Dictionary req_stackTrace(const Dictionary &p_params) const;
Dictionary req_setBreakpoints(const Dictionary &p_params) const;
Dictionary req_breakpointLocations(const Dictionary &p_params) const;
Dictionary req_scopes(const Dictionary &p_params) const;
Dictionary req_variables(const Dictionary &p_params) const;
Dictionary req_next(const Dictionary &p_params) const;
Dictionary req_stepIn(const Dictionary &p_params) const;
Dictionary req_evaluate(const Dictionary &p_params) const;
Dictionary req_godot_put_msg(const Dictionary &p_params) const;
// Internal requests
Dictionary _launch_process(const Dictionary &p_params) const;
// Events
Dictionary ev_initialized() const;
Dictionary ev_process(const String &p_command) const;
Dictionary ev_terminated() const;
Dictionary ev_exited(const int &p_exitcode) const;
Dictionary ev_stopped_paused() const;
Dictionary ev_stopped_exception(const String &p_error) const;
Dictionary ev_stopped_breakpoint(const int &p_id) const;
Dictionary ev_stopped_step() const;
Dictionary ev_continued() const;
Dictionary ev_output(const String &p_message, RemoteDebugger::MessageType p_type) const;
Dictionary ev_custom_data(const String &p_msg, const Array &p_data) const;
Dictionary ev_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled) const;
};
#endif // DEBUG_ADAPTER_PARSER_H

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,156 @@
/**************************************************************************/
/* debug_adapter_protocol.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 DEBUG_ADAPTER_PROTOCOL_H
#define DEBUG_ADAPTER_PROTOCOL_H
#include "core/io/stream_peer.h"
#include "core/io/stream_peer_tcp.h"
#include "core/io/tcp_server.h"
#include "debug_adapter_parser.h"
#include "debug_adapter_types.h"
#define DAP_MAX_BUFFER_SIZE 4194304 // 4MB
#define DAP_MAX_CLIENTS 8
class DebugAdapterParser;
struct DAPeer : RefCounted {
Ref<StreamPeerTCP> connection;
uint8_t req_buf[DAP_MAX_BUFFER_SIZE];
int req_pos = 0;
bool has_header = false;
int content_length = 0;
List<Dictionary> res_queue;
int seq = 0;
uint64_t timestamp = 0;
// Client specific info
bool linesStartAt1 = false;
bool columnsStartAt1 = false;
bool supportsVariableType = false;
bool supportsInvalidatedEvent = false;
bool supportsCustomData = false;
// Internal client info
bool attached = false;
Dictionary pending_launch;
Error handle_data();
Error send_data();
String format_output(const Dictionary &p_params) const;
};
class DebugAdapterProtocol : public Object {
GDCLASS(DebugAdapterProtocol, Object)
friend class DebugAdapterParser;
private:
static DebugAdapterProtocol *singleton;
DebugAdapterParser *parser = nullptr;
List<Ref<DAPeer>> clients;
Ref<TCPServer> server;
Error on_client_connected();
void on_client_disconnected(const Ref<DAPeer> &p_peer);
void on_debug_paused();
void on_debug_stopped();
void on_debug_output(const String &p_message, int p_type);
void on_debug_breaked(const bool &p_reallydid, const bool &p_can_debug, const String &p_reason, const bool &p_has_stackdump);
void on_debug_breakpoint_toggled(const String &p_path, const int &p_line, const bool &p_enabled);
void on_debug_stack_dump(const Array &p_stack_dump);
void on_debug_stack_frame_vars(const int &p_size);
void on_debug_stack_frame_var(const Array &p_data);
void on_debug_data(const String &p_msg, const Array &p_data);
void reset_current_info();
void reset_ids();
void reset_stack_info();
int parse_variant(const Variant &p_var);
bool _initialized = false;
bool _processing_breakpoint = false;
bool _stepping = false;
bool _processing_stackdump = false;
int _remaining_vars = 0;
int _current_frame = 0;
uint64_t _request_timeout = 1000;
bool _sync_breakpoints = false;
String _current_request;
Ref<DAPeer> _current_peer;
int breakpoint_id = 0;
int stackframe_id = 0;
int variable_id = 0;
List<DAP::Breakpoint> breakpoint_list;
HashMap<DAP::StackFrame, List<int>, DAP::StackFrame> stackframe_list;
HashMap<int, Array> variable_list;
public:
friend class DebugAdapterServer;
_FORCE_INLINE_ static DebugAdapterProtocol *get_singleton() { return singleton; }
_FORCE_INLINE_ bool is_active() const { return _initialized && clients.size() > 0; }
bool process_message(const String &p_text);
String get_current_request() const { return _current_request; }
Ref<DAPeer> get_current_peer() const { return _current_peer; }
void notify_initialized();
void notify_process();
void notify_terminated();
void notify_exited(const int &p_exitcode = 0);
void notify_stopped_paused();
void notify_stopped_exception(const String &p_error);
void notify_stopped_breakpoint(const int &p_id);
void notify_stopped_step();
void notify_continued();
void notify_output(const String &p_message, RemoteDebugger::MessageType p_type);
void notify_custom_data(const String &p_msg, const Array &p_data);
void notify_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled);
Array update_breakpoints(const String &p_path, const Array &p_lines);
void poll();
Error start(int p_port, const IPAddress &p_bind_ip);
void stop();
DebugAdapterProtocol();
~DebugAdapterProtocol();
};
#endif // DEBUG_ADAPTER_PROTOCOL_H

View file

@ -0,0 +1,94 @@
/**************************************************************************/
/* debug_adapter_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "debug_adapter_server.h"
#include "core/os/os.h"
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/editor_settings.h"
int DebugAdapterServer::port_override = -1;
DebugAdapterServer::DebugAdapterServer() {
_EDITOR_DEF("network/debug_adapter/remote_port", remote_port);
_EDITOR_DEF("network/debug_adapter/request_timeout", protocol._request_timeout);
_EDITOR_DEF("network/debug_adapter/sync_breakpoints", protocol._sync_breakpoints);
}
void DebugAdapterServer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
start();
} break;
case NOTIFICATION_EXIT_TREE: {
stop();
} break;
case NOTIFICATION_INTERNAL_PROCESS: {
// The main loop can be run again during request processing, which modifies internal state of the protocol.
// Thus, "polling" is needed to prevent it from parsing other requests while the current one isn't finished.
if (started && !polling) {
polling = true;
protocol.poll();
polling = false;
}
} break;
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (!EditorSettings::get_singleton()->check_changed_settings_in_group("network/debug_adapter")) {
break;
}
protocol._request_timeout = EDITOR_GET("network/debug_adapter/request_timeout");
protocol._sync_breakpoints = EDITOR_GET("network/debug_adapter/sync_breakpoints");
int port = (DebugAdapterServer::port_override > -1) ? DebugAdapterServer::port_override : (int)_EDITOR_GET("network/debug_adapter/remote_port");
if (port != remote_port) {
stop();
start();
}
} break;
}
}
void DebugAdapterServer::start() {
remote_port = (DebugAdapterServer::port_override > -1) ? DebugAdapterServer::port_override : (int)_EDITOR_GET("network/debug_adapter/remote_port");
if (protocol.start(remote_port, IPAddress("127.0.0.1")) == OK) {
EditorNode::get_log()->add_message("--- Debug adapter server started on port " + itos(remote_port) + " ---", EditorLog::MSG_TYPE_EDITOR);
set_process_internal(true);
started = true;
}
}
void DebugAdapterServer::stop() {
protocol.stop();
started = false;
EditorNode::get_log()->add_message("--- Debug adapter server stopped ---", EditorLog::MSG_TYPE_EDITOR);
}

View file

@ -0,0 +1,58 @@
/**************************************************************************/
/* debug_adapter_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 DEBUG_ADAPTER_SERVER_H
#define DEBUG_ADAPTER_SERVER_H
#include "debug_adapter_protocol.h"
#include "editor/plugins/editor_plugin.h"
class DebugAdapterServer : public EditorPlugin {
GDCLASS(DebugAdapterServer, EditorPlugin);
DebugAdapterProtocol protocol;
int remote_port = 6006;
bool thread_running = false;
bool started = false;
bool polling = false;
static void thread_func(void *p_userdata);
private:
void _notification(int p_what);
public:
static int port_override;
DebugAdapterServer();
void start();
void stop();
};
#endif // DEBUG_ADAPTER_SERVER_H

View file

@ -0,0 +1,281 @@
/**************************************************************************/
/* debug_adapter_types.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 DEBUG_ADAPTER_TYPES_H
#define DEBUG_ADAPTER_TYPES_H
#include "core/io/json.h"
#include "core/variant/dictionary.h"
namespace DAP {
enum ErrorType {
UNKNOWN,
WRONG_PATH,
NOT_RUNNING,
TIMEOUT,
UNKNOWN_PLATFORM,
MISSING_DEVICE
};
struct Checksum {
String algorithm;
String checksum;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["algorithm"] = algorithm;
dict["checksum"] = checksum;
return dict;
}
};
struct Source {
private:
Array _checksums;
public:
String name;
String path;
void compute_checksums() {
ERR_FAIL_COND(path.is_empty());
// MD5
Checksum md5;
md5.algorithm = "MD5";
md5.checksum = FileAccess::get_md5(path);
// SHA-256
Checksum sha256;
sha256.algorithm = "SHA256";
sha256.checksum = FileAccess::get_sha256(path);
_checksums.push_back(md5.to_json());
_checksums.push_back(sha256.to_json());
}
_FORCE_INLINE_ void from_json(const Dictionary &p_params) {
name = p_params["name"];
path = p_params["path"];
_checksums = p_params["checksums"];
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["path"] = path;
dict["checksums"] = _checksums;
return dict;
}
};
struct Breakpoint {
int id = 0;
bool verified = false;
Source source;
int line = 0;
bool operator==(const Breakpoint &p_other) const {
return source.path == p_other.source.path && line == p_other.line;
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["verified"] = verified;
dict["source"] = source.to_json();
dict["line"] = line;
return dict;
}
};
struct BreakpointLocation {
int line = 0;
int endLine = -1;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["line"] = line;
if (endLine >= 0) {
dict["endLine"] = endLine;
}
return dict;
}
};
struct Capabilities {
bool supportsConfigurationDoneRequest = true;
bool supportsEvaluateForHovers = true;
bool supportsSetVariable = true;
String supportedChecksumAlgorithms[2] = { "MD5", "SHA256" };
bool supportsRestartRequest = true;
bool supportsValueFormattingOptions = true;
bool supportTerminateDebuggee = true;
bool supportSuspendDebuggee = true;
bool supportsTerminateRequest = true;
bool supportsBreakpointLocationsRequest = true;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["supportsConfigurationDoneRequest"] = supportsConfigurationDoneRequest;
dict["supportsEvaluateForHovers"] = supportsEvaluateForHovers;
dict["supportsSetVariable"] = supportsSetVariable;
dict["supportsRestartRequest"] = supportsRestartRequest;
dict["supportsValueFormattingOptions"] = supportsValueFormattingOptions;
dict["supportTerminateDebuggee"] = supportTerminateDebuggee;
dict["supportSuspendDebuggee"] = supportSuspendDebuggee;
dict["supportsTerminateRequest"] = supportsTerminateRequest;
dict["supportsBreakpointLocationsRequest"] = supportsBreakpointLocationsRequest;
Array arr;
arr.push_back(supportedChecksumAlgorithms[0]);
arr.push_back(supportedChecksumAlgorithms[1]);
dict["supportedChecksumAlgorithms"] = arr;
return dict;
}
};
struct Message {
int id = 0;
String format;
bool sendTelemetry = false; // Just in case :)
bool showUser = false;
Dictionary variables;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["format"] = format;
dict["sendTelemetry"] = sendTelemetry;
dict["showUser"] = showUser;
dict["variables"] = variables;
return dict;
}
};
struct Scope {
String name;
String presentationHint;
int variablesReference = 0;
bool expensive = false;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["presentationHint"] = presentationHint;
dict["variablesReference"] = variablesReference;
dict["expensive"] = expensive;
return dict;
}
};
struct SourceBreakpoint {
int line = 0;
_FORCE_INLINE_ void from_json(const Dictionary &p_params) {
line = p_params["line"];
}
};
struct StackFrame {
int id = 0;
String name;
Source source;
int line = 0;
int column = 0;
static uint32_t hash(const StackFrame &p_frame) {
return hash_murmur3_one_32(p_frame.id);
}
bool operator==(const StackFrame &p_other) const {
return id == p_other.id;
}
_FORCE_INLINE_ void from_json(const Dictionary &p_params) {
id = p_params["id"];
name = p_params["name"];
source.from_json(p_params["source"]);
line = p_params["line"];
column = p_params["column"];
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["name"] = name;
dict["source"] = source.to_json();
dict["line"] = line;
dict["column"] = column;
return dict;
}
};
struct Thread {
int id = 0;
String name;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["name"] = name;
return dict;
}
};
struct Variable {
String name;
String value;
String type;
int variablesReference = 0;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["value"] = value;
dict["type"] = type;
dict["variablesReference"] = variablesReference;
return dict;
}
};
} // namespace DAP
#endif // DEBUG_ADAPTER_TYPES_H

View file

@ -0,0 +1,287 @@
/**************************************************************************/
/* editor_debugger_inspector.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 (!prop_values.has(p_name) || String(p_name).begins_with("Constants/")) {
return false;
}
prop_values[p_name] = p_value;
emit_signal(SNAME("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 PropertyInfo &prop : prop_list) {
if (prop.name == "script") {
// Skip the script property, it's always added by the non-virtual method.
continue;
}
p_list->push_back(prop);
}
}
String EditorDebuggerRemoteObject::get_title() {
if (remote_object_id.is_valid()) {
return vformat(TTR("Remote %s:"), 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);
}
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;
}
}
void EditorDebuggerInspector::_object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value) {
emit_signal(SNAME("object_edited"), p_id, p_prop, p_value);
}
void EditorDebuggerInspector::_object_selected(ObjectID p_object) {
emit_signal(SNAME("object_selected"), p_object);
}
ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) {
EditorDebuggerRemoteObject *debug_obj = nullptr;
SceneDebuggerObject obj;
obj.deserialize(p_arr);
ERR_FAIL_COND_V(obj.id.is_null(), ObjectID());
if (remote_objects.has(obj.id)) {
debug_obj = remote_objects[obj.id];
} else {
debug_obj = memnew(EditorDebuggerRemoteObject);
debug_obj->remote_object_id = obj.id;
debug_obj->type_name = obj.class_name;
remote_objects[obj.id] = debug_obj;
debug_obj->connect("value_edited", callable_mp(this, &EditorDebuggerInspector::_object_edited));
}
int old_prop_size = debug_obj->prop_list.size();
debug_obj->prop_list.clear();
int new_props_added = 0;
HashSet<String> changed;
for (SceneDebuggerObject::SceneDebuggerProperty &property : obj.properties) {
PropertyInfo &pinfo = property.first;
Variant &var = property.second;
if (pinfo.type == Variant::OBJECT) {
if (var.get_type() == Variant::STRING) {
String path = var;
if (path.contains("::")) {
// built-in resource
String base_path = path.get_slice("::", 0);
Ref<Resource> dependency = ResourceLoader::load(base_path);
if (dependency.is_valid()) {
remote_dependencies.insert(dependency);
}
}
var = ResourceLoader::load(path);
if (pinfo.hint_string == "Script") {
if (debug_obj->get_script() != var) {
debug_obj->set_script(Ref<RefCounted>());
Ref<Script> scr(var);
if (!scr.is_null()) {
ScriptInstance *scr_instance = scr->placeholder_instance_create(debug_obj);
if (scr_instance) {
debug_obj->set_script_and_instance(var, scr_instance);
}
}
}
}
}
}
//always add the property, since props may have been added or removed
debug_obj->prop_list.push_back(pinfo);
if (!debug_obj->prop_values.has(pinfo.name)) {
new_props_added++;
debug_obj->prop_values[pinfo.name] = var;
} else {
if (bool(Variant::evaluate(Variant::OP_NOT_EQUAL, debug_obj->prop_values[pinfo.name], var))) {
debug_obj->prop_values[pinfo.name] = var;
changed.insert(pinfo.name);
}
}
}
if (old_prop_size == debug_obj->prop_list.size() && new_props_added == 0) {
//only some may have changed, if so, then update those, if exist
for (const String &E : changed) {
emit_signal(SNAME("object_property_updated"), debug_obj->remote_object_id, E);
}
} else {
//full update, because props were added or removed
debug_obj->update();
}
return obj.id;
}
void EditorDebuggerInspector::clear_cache() {
for (const KeyValue<ObjectID, EditorDebuggerRemoteObject *> &E : remote_objects) {
EditorNode *editor = EditorNode::get_singleton();
if (editor->get_editor_selection_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;
if (var.var_type == Variant::OBJECT && v) {
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);
// To prevent constantly resizing when using filtering.
int size_x = get_size().x;
if (size_x > get_custom_minimum_size().x) {
set_custom_minimum_size(Size2(size_x, 0));
}
}
void EditorDebuggerInspector::clear_stack_variables() {
variables->clear();
variables->update();
set_custom_minimum_size(Size2(0, 0));
}
String EditorDebuggerInspector::get_stack_variable(const String &p_var) {
for (KeyValue<StringName, Variant> &E : variables->prop_values) {
String v = E.key.operator String();
if (v.get_slice("/", 1) == p_var) {
return variables->get_variant(v);
}
}
return String();
}

View file

@ -0,0 +1,97 @@
/**************************************************************************/
/* editor_debugger_inspector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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:
ObjectID remote_object_id;
String type_name;
List<PropertyInfo> prop_list;
HashMap<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() { notify_property_list_changed(); }
EditorDebuggerRemoteObject() {}
};
class EditorDebuggerInspector : public EditorInspector {
GDCLASS(EditorDebuggerInspector, EditorInspector);
private:
ObjectID inspected_object_id;
HashMap<ObjectID, EditorDebuggerRemoteObject *> remote_objects;
HashSet<Ref<Resource>> remote_dependencies;
EditorDebuggerRemoteObject *variables = nullptr;
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

View file

@ -0,0 +1,839 @@
/**************************************************************************/
/* editor_debugger_node.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "core/object/undo_redo.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/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_bottom_panel.h"
#include "editor/gui/editor_run_bar.h"
#include "editor/inspector_dock.h"
#include "editor/plugins/editor_debugger_plugin.h"
#include "editor/plugins/script_editor_plugin.h"
#include "editor/scene_tree_dock.h"
#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/menu_button.h"
#include "scene/gui/tab_container.h"
#include "scene/resources/packed_scene.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_NULL(dbg);
p_func(dbg);
}
}
EditorDebuggerNode *EditorDebuggerNode::singleton = nullptr;
EditorDebuggerNode::EditorDebuggerNode() {
if (!singleton) {
singleton = this;
}
add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_LEFT));
add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_RIGHT));
tabs = memnew(TabContainer);
tabs->set_tabs_visible(false);
tabs->connect("tab_changed", callable_mp(this, &EditorDebuggerNode::_debugger_changed));
add_child(tabs);
Ref<StyleBoxEmpty> empty;
empty.instantiate();
tabs->add_theme_style_override(SceneStringName(panel), empty);
auto_switch_remote_scene_tree = EDITOR_GET("debugger/auto_switch_to_remote_scene_tree");
_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));
remote_scene_tree->connect("button_clicked", callable_mp(this, &EditorDebuggerNode::_remote_tree_button_pressed));
SceneTreeDock::get_singleton()->add_remote_tree_editor(remote_scene_tree);
SceneTreeDock::get_singleton()->connect("remote_tree_selected", callable_mp(this, &EditorDebuggerNode::request_remote_tree));
remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval");
inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
EditorRunBar::get_singleton()->get_pause_button()->connect(SceneStringName(pressed), callable_mp(this, &EditorDebuggerNode::_paused));
}
ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() {
ScriptEditorDebugger *node = memnew(ScriptEditorDebugger);
int id = tabs->get_tab_count();
node->connect("stop_requested", callable_mp(this, &EditorDebuggerNode::_debugger_wants_stop).bind(id));
node->connect("stopped", callable_mp(this, &EditorDebuggerNode::_debugger_stopped).bind(id));
node->connect("stack_frame_selected", callable_mp(this, &EditorDebuggerNode::_stack_frame_selected).bind(id));
node->connect("error_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
node->connect("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id));
node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated).bind(id));
node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id));
node->connect("remote_object_requested", callable_mp(this, &EditorDebuggerNode::_remote_object_requested).bind(id));
node->connect("set_breakpoint", callable_mp(this, &EditorDebuggerNode::_breakpoint_set_in_tree).bind(id));
node->connect("clear_breakpoints", callable_mp(this, &EditorDebuggerNode::_breakpoints_cleared_in_tree).bind(id));
node->connect("errors_cleared", callable_mp(this, &EditorDebuggerNode::_update_errors));
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(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("DebuggerPanel"), EditorStringName(EditorStyles)));
}
if (!debugger_plugins.is_empty()) {
for (Ref<EditorDebuggerPlugin> plugin : debugger_plugins) {
plugin->create_session(node);
}
}
return node;
}
void EditorDebuggerNode::_stack_frame_selected(int p_debugger) {
const ScriptEditorDebugger *dbg = get_debugger(p_debugger);
ERR_FAIL_NULL(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(SNAME("goto_script_line"), s, p_line - 1);
}
void EditorDebuggerNode::_text_editor_stack_goto(const ScriptEditorDebugger *p_debugger) {
String file = p_debugger->get_stack_script_file();
if (file.is_empty()) {
return;
}
if (file.is_resource_file()) {
stack_script = ResourceLoader::load(file);
} else {
// If the script is built-in, it can be opened only if the scene is loaded in memory.
int i = file.find("::");
int j = file.rfind("(", i);
if (j > -1) { // If the script is named, the string is "name (file)", so we need to extract the path.
file = file.substr(j + 1, file.find(")", i) - j - 1);
}
Ref<PackedScene> ps = ResourceLoader::load(file.get_slice("::", 0));
stack_script = ResourceLoader::load(file);
}
const int line = p_debugger->get_stack_script_line() - 1;
emit_signal(SNAME("goto_script_line"), stack_script, line);
emit_signal(SNAME("set_execution"), stack_script, line);
stack_script.unref(); // Why?!?
}
void EditorDebuggerNode::_text_editor_stack_clear(const ScriptEditorDebugger *p_debugger) {
String file = p_debugger->get_stack_script_file();
if (file.is_empty()) {
return;
}
if (file.is_resource_file()) {
stack_script = ResourceLoader::load(file);
} else {
// If the script is built-in, it can be opened only if the scene is loaded in memory.
int i = file.find("::");
int j = file.rfind("(", i);
if (j > -1) { // If the script is named, the string is "name (file)", so we need to extract the path.
file = file.substr(j + 1, file.find(")", i) - j - 1);
}
Ref<PackedScene> ps = ResourceLoader::load(file.get_slice("::", 0));
stack_script = ResourceLoader::load(file);
}
emit_signal(SNAME("clear_execution"), stack_script);
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_instantiate_node", &EditorDebuggerNode::live_debug_instantiate_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")));
ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::BOOL, "enabled")));
ADD_SIGNAL(MethodInfo("breakpoint_set_in_tree", PropertyInfo("script"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::BOOL, "enabled"), PropertyInfo(Variant::INT, "debugger")));
ADD_SIGNAL(MethodInfo("breakpoints_cleared_in_tree", PropertyInfo(Variant::INT, "debugger")));
}
void EditorDebuggerNode::register_undo_redo(UndoRedo *p_undo_redo) {
p_undo_redo->set_method_notify_callback(_method_changeds, this);
p_undo_redo->set_property_notify_callback(_property_changeds, this);
}
EditorDebuggerRemoteObject *EditorDebuggerNode::get_inspected_remote_object() {
return Object::cast_to<EditorDebuggerRemoteObject>(ObjectDB::get_instance(EditorNode::get_singleton()->get_editor_selection_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_previous_debugger() const {
return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(tabs->get_previous_tab()));
}
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));
}
String EditorDebuggerNode::get_server_uri() const {
ERR_FAIL_COND_V(server.is_null(), "");
return server->get_uri();
}
void EditorDebuggerNode::set_keep_open(bool p_keep_open) {
keep_open = p_keep_open;
if (keep_open) {
if (server.is_null() || !server->is_active()) {
start();
}
} else {
bool found = false;
_for_all(tabs, [&](ScriptEditorDebugger *p_debugger) {
if (p_debugger->is_session_active()) {
found = true;
}
});
if (!found) {
stop();
}
}
}
Error EditorDebuggerNode::start(const String &p_uri) {
ERR_FAIL_COND_V(!p_uri.contains("://"), ERR_INVALID_PARAMETER);
if (keep_open && current_uri == p_uri && server.is_valid()) {
return OK;
}
stop(true);
current_uri = p_uri;
server = Ref<EditorDebuggerServer>(EditorDebuggerServer::create(p_uri.substr(0, p_uri.find("://") + 3)));
const Error err = server->start(p_uri);
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(bool p_force) {
if (keep_open && !p_force) {
return;
}
current_uri.clear();
if (server.is_valid()) {
server->stop();
EditorNode::get_log()->add_message("--- Debugging process stopped ---", EditorLog::MSG_TYPE_EDITOR);
if (EditorRunBar::get_singleton()->is_movie_maker_enabled()) {
// Request attention in case the user was doing something else when movie recording is finished.
DisplayServer::get_singleton()->window_request_attention();
}
server.unref();
}
// Also close all debugging sessions.
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (dbg->is_session_active()) {
dbg->_stop_and_notify();
}
});
_break_state_changed();
breakpoints.clear();
EditorUndoRedoManager::get_singleton()->clear_history(false, EditorUndoRedoManager::REMOTE_HISTORY);
set_process(false);
}
void EditorDebuggerNode::_notification(int p_what) {
switch (p_what) {
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (!EditorThemeManager::is_generated_theme_outdated()) {
return;
}
if (tabs->get_tab_count() > 1) {
add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_LEFT));
add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_RIGHT));
tabs->add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("DebuggerPanel"), EditorStringName(EditorStyles)));
}
remote_scene_tree->update_icon_max_width();
} break;
case NOTIFICATION_READY: {
_update_debug_options();
initializing = false;
} break;
case NOTIFICATION_PROCESS: {
if (!server.is_valid()) {
return;
}
if (!server->is_active()) {
stop();
return;
}
server->poll();
_update_errors();
// Remote scene tree update
remote_scene_tree_timeout -= get_process_delta_time();
if (remote_scene_tree_timeout < 0) {
remote_scene_tree_timeout = EDITOR_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 = EDITOR_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.
}
}
EditorRunBar::get_singleton()->get_pause_button()->set_disabled(false);
// Switch to remote tree view if so desired.
auto_switch_remote_scene_tree = (bool)EDITOR_GET("debugger/auto_switch_to_remote_scene_tree");
if (auto_switch_remote_scene_tree) {
SceneTreeDock::get_singleton()->show_remote_tree();
}
// Good to go.
SceneTreeDock::get_singleton()->show_tab_buttons();
debugger->set_editor_remote_tree(remote_scene_tree);
debugger->start(server->take_connection());
// Send breakpoints.
for (const KeyValue<Breakpoint, bool> &E : breakpoints) {
const Breakpoint &bp = E.key;
debugger->set_breakpoint(bp.source, bp.line, E.value);
} // Will arrive too late, how does the regular run work?
debugger->update_live_edit_root();
}
} break;
}
}
void EditorDebuggerNode::_update_errors() {
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->remove_theme_color_override(SceneStringName(font_color));
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_editor_theme_icon(SNAME("ErrorWarning")));
// Use error color to represent the highest level of severity reported.
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
} else if (error_count >= 1) {
debugger_button->set_icon(get_editor_theme_icon(SNAME("Error")));
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
} else {
debugger_button->set_icon(get_editor_theme_icon(SNAME("Warning")));
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
}
}
last_error_count = error_count;
last_warning_count = warning_count;
}
}
void EditorDebuggerNode::_debugger_stopped(int p_id) {
ScriptEditorDebugger *dbg = get_debugger(p_id);
ERR_FAIL_NULL(dbg);
bool found = false;
_for_all(tabs, [&](ScriptEditorDebugger *p_debugger) {
if (p_debugger->is_session_active()) {
found = true;
}
});
if (!found) {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(false);
EditorRunBar::get_singleton()->get_pause_button()->set_disabled(true);
SceneTreeDock::get_singleton()->hide_remote_tree();
SceneTreeDock::get_singleton()->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) {
callable_mp(EditorNode::get_singleton(), &EditorNode::stop_child_process).call_deferred(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 (get_previous_debugger()) {
_text_editor_stack_clear(get_previous_debugger());
}
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());
}
_break_state_changed();
}
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/debug_with_external_editor"), DEBUG_WITH_EXTERNAL_EDITOR);
p->connect(SceneStringName(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_bottom_panel()->make_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_WITH_EXTERNAL_EDITOR: {
bool ischecked = script_menu->get_popup()->is_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR));
debug_with_external_editor = !ischecked;
script_menu->get_popup()->set_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "debug_with_external_editor", !ischecked);
}
} break;
}
}
void EditorDebuggerNode::_update_debug_options() {
if (EditorSettings::get_singleton()->get_project_metadata("debug_options", "debug_with_external_editor", false).operator bool()) {
_menu_option(DEBUG_WITH_EXTERNAL_EDITOR);
}
}
void EditorDebuggerNode::_paused() {
const bool paused = EditorRunBar::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, const String &p_message, bool p_has_stackdump, int p_debugger) {
if (get_current_debugger() != get_debugger(p_debugger)) {
if (!p_breaked) {
return;
}
tabs->set_current_tab(p_debugger);
}
_break_state_changed();
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(p_breaked);
emit_signal(SNAME("breaked"), p_breaked, p_can_debug);
}
bool EditorDebuggerNode::is_skip_breakpoints() const {
return get_current_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);
});
emit_signal(SNAME("breakpoint_toggled"), p_path, p_line, p_enabled);
}
void EditorDebuggerNode::set_breakpoints(const String &p_path, const Array &p_lines) {
for (int i = 0; i < p_lines.size(); i++) {
set_breakpoint(p_path, p_lines[i], true);
}
for (const KeyValue<Breakpoint, bool> &E : breakpoints) {
Breakpoint b = E.key;
if (b.source == p_path && !p_lines.has(b.line)) {
set_breakpoint(p_path, b.line, false);
}
}
}
void EditorDebuggerNode::reload_all_scripts() {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->reload_all_scripts();
});
}
void EditorDebuggerNode::reload_scripts(const Vector<String> &p_script_paths) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->reload_scripts(p_script_paths);
});
}
void EditorDebuggerNode::debug_next() {
get_current_debugger()->debug_next();
}
void EditorDebuggerNode::debug_step() {
get_current_debugger()->debug_step();
}
void EditorDebuggerNode::debug_break() {
get_current_debugger()->debug_break();
}
void EditorDebuggerNode::debug_continue() {
get_current_debugger()->debug_continue();
}
String EditorDebuggerNode::get_var_value(const String &p_var) const {
return get_current_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_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
if (p_button != MouseButton::LEFT) {
return;
}
TreeItem *item = Object::cast_to<TreeItem>(p_item);
ERR_FAIL_NULL(item);
if (p_id == EditorDebuggerTree::BUTTON_SUBSCENE) {
remote_scene_tree->emit_signal(SNAME("open"), item->get_meta("scene_file_path"));
} else if (p_id == EditorDebuggerTree::BUTTON_VISIBILITY) {
ObjectID obj_id = item->get_metadata(0);
ERR_FAIL_COND(obj_id.is_null());
get_current_debugger()->update_remote_object(obj_id, "visible", !item->get_meta("visible"));
get_current_debugger()->request_remote_tree();
}
}
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;
}
InspectorDock::get_inspector_singleton()->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);
}
void EditorDebuggerNode::_breakpoint_set_in_tree(Ref<RefCounted> p_script, int p_line, bool p_enabled, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
emit_signal(SNAME("breakpoint_set_in_tree"), p_script, p_line, p_enabled);
}
void EditorDebuggerNode::_breakpoints_cleared_in_tree(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
emit_signal(SNAME("breakpoints_cleared_in_tree"));
}
// Remote inspector/edit.
void EditorDebuggerNode::_method_changeds(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount) {
if (!singleton) {
return;
}
_for_all(singleton->tabs, [&](ScriptEditorDebugger *dbg) {
dbg->_method_changed(p_base, p_name, p_args, p_argcount);
});
}
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_instantiate_node(const NodePath &p_parent, const String &p_path, const String &p_name) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_instantiate_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::set_camera_override(CameraOverride p_override) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->set_camera_override(p_override);
});
camera_override = p_override;
}
EditorDebuggerNode::CameraOverride EditorDebuggerNode::get_camera_override() {
return camera_override;
}
void EditorDebuggerNode::add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
ERR_FAIL_COND_MSG(p_plugin.is_null(), "Debugger plugin is null.");
ERR_FAIL_COND_MSG(debugger_plugins.has(p_plugin), "Debugger plugin already exists.");
debugger_plugins.insert(p_plugin);
Ref<EditorDebuggerPlugin> plugin = p_plugin;
for (int i = 0; get_debugger(i); i++) {
plugin->create_session(get_debugger(i));
}
}
void EditorDebuggerNode::remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
ERR_FAIL_COND_MSG(p_plugin.is_null(), "Debugger plugin is null.");
ERR_FAIL_COND_MSG(!debugger_plugins.has(p_plugin), "Debugger plugin doesn't exists.");
debugger_plugins.erase(p_plugin);
Ref<EditorDebuggerPlugin>(p_plugin)->clear();
}
bool EditorDebuggerNode::plugins_capture(ScriptEditorDebugger *p_debugger, const String &p_message, const Array &p_data) {
int session_index = tabs->get_tab_idx_from_control(p_debugger);
ERR_FAIL_COND_V(session_index < 0, false);
int colon_index = p_message.find_char(':');
ERR_FAIL_COND_V_MSG(colon_index < 1, false, "Invalid message received.");
const String cap = p_message.substr(0, colon_index);
bool parsed = false;
for (Ref<EditorDebuggerPlugin> plugin : debugger_plugins) {
if (plugin->has_capture(cap)) {
parsed |= plugin->capture(p_message, p_data, session_index);
}
}
return parsed;
}

View file

@ -0,0 +1,224 @@
/**************************************************************************/
/* editor_debugger_node.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "core/object/script_language.h"
#include "editor/debugger/editor_debugger_server.h"
#include "scene/gui/margin_container.h"
class Button;
class DebugAdapterParser;
class EditorDebuggerPlugin;
class EditorDebuggerTree;
class EditorDebuggerRemoteObject;
class MenuButton;
class ScriptEditorDebugger;
class TabContainer;
class UndoRedo;
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_WITH_EXTERNAL_EDITOR,
};
class Breakpoint {
public:
String source;
int line = 0;
static uint32_t hash(const Breakpoint &p_val) {
uint32_t h = HashMapHasherDefault::hash(p_val.source);
return hash_murmur3_one_32(p_val.line, h);
}
bool operator==(const Breakpoint &p_b) const {
return (line == p_b.line && source == p_b.source);
}
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?!?
bool initializing = true;
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 keep_open = false;
String current_uri;
CameraOverride camera_override = OVERRIDE_NONE;
HashMap<Breakpoint, bool, Breakpoint> breakpoints;
HashSet<Ref<EditorDebuggerPlugin>> debugger_plugins;
ScriptEditorDebugger *_add_debugger();
EditorDebuggerRemoteObject *get_inspected_remote_object();
void _update_errors();
friend class DebuggerEditorPlugin;
friend class DebugAdapterParser;
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_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
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 _breakpoint_set_in_tree(Ref<RefCounted> p_script, int p_line, bool p_enabled, int p_debugger);
void _breakpoints_cleared_in_tree(int p_debugger);
void _clear_execution(Ref<RefCounted> p_script) {
emit_signal(SNAME("clear_execution"), p_script);
}
void _text_editor_stack_goto(const ScriptEditorDebugger *p_debugger);
void _text_editor_stack_clear(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, const String &p_message, bool p_has_stackdump, int p_debugger);
void _paused();
void _break_state_changed();
void _menu_option(int p_id);
void _update_debug_options();
protected:
void _notification(int p_what);
static void _bind_methods();
public:
static EditorDebuggerNode *get_singleton() { return singleton; }
void register_undo_redo(UndoRedo *p_undo_redo);
ScriptEditorDebugger *get_previous_debugger() const;
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 set_breakpoints(const String &p_path, const Array &p_lines);
void reload_all_scripts();
void reload_scripts(const Vector<String> &p_script_paths);
// Remote inspector/edit.
void request_remote_tree();
static void _method_changeds(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount);
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_instantiate_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);
void set_camera_override(CameraOverride p_override);
CameraOverride get_camera_override();
String get_server_uri() const;
void set_keep_open(bool p_keep_open);
Error start(const String &p_uri = "tcp://");
void stop(bool p_force = false);
bool plugins_capture(ScriptEditorDebugger *p_debugger, const String &p_message, const Array &p_data);
void add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
void remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
};
#endif // EDITOR_DEBUGGER_NODE_H

View file

@ -0,0 +1,143 @@
/**************************************************************************/
/* editor_debugger_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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<TCPServer> server;
String endpoint;
public:
static EditorDebuggerServer *create(const String &p_protocol);
virtual void poll() override {}
virtual String get_uri() const override;
virtual Error start(const String &p_uri) override;
virtual void stop() override;
virtual bool is_active() const override;
virtual bool is_connection_available() const override;
virtual Ref<RemoteDebuggerPeer> take_connection() override;
EditorDebuggerServerTCP();
};
EditorDebuggerServer *EditorDebuggerServerTCP::create(const String &p_protocol) {
ERR_FAIL_COND_V(p_protocol != "tcp://", nullptr);
return memnew(EditorDebuggerServerTCP);
}
EditorDebuggerServerTCP::EditorDebuggerServerTCP() {
server.instantiate();
}
String EditorDebuggerServerTCP::get_uri() const {
return endpoint;
}
Error EditorDebuggerServerTCP::start(const String &p_uri) {
// Default host and port
String bind_host = (String)EDITOR_GET("network/debug/remote_host");
int bind_port = (int)EDITOR_GET("network/debug/remote_port");
// Optionally override
if (!p_uri.is_empty() && p_uri != "tcp://") {
String scheme, path;
Error err = p_uri.parse_url(scheme, bind_host, bind_port, path);
ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER);
}
// Try listening on ports
const int max_attempts = 5;
for (int attempt = 1;; ++attempt) {
const Error err = server->listen(bind_port, bind_host);
if (err == OK) {
break;
}
if (attempt >= max_attempts) {
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, remote debugging unavailable.", bind_port), EditorLog::MSG_TYPE_ERROR);
return err;
}
int last_port = bind_port++;
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, trying %d instead.", last_port, bind_port), EditorLog::MSG_TYPE_WARNING);
}
// Endpoint that the client should connect to
endpoint = vformat("tcp://%s:%d", bind_host, bind_port);
return OK;
}
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
HashMap<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();
}

View file

@ -0,0 +1,60 @@
/**************************************************************************/
/* editor_debugger_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_SERVER_H
#define EDITOR_DEBUGGER_SERVER_H
#include "core/debugger/remote_debugger_peer.h"
#include "core/object/ref_counted.h"
class EditorDebuggerServer : public RefCounted {
public:
typedef EditorDebuggerServer *(*CreateServerFunc)(const String &p_uri);
private:
static HashMap<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 String get_uri() const = 0;
virtual void poll() = 0;
virtual Error start(const String &p_uri) = 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_SERVER_H

View file

@ -0,0 +1,370 @@
/**************************************************************************/
/* editor_debugger_tree.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "editor/editor_string_names.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/scene_tree_dock.h"
#include "scene/debugger/scene_debugger.h"
#include "scene/gui/texture_rect.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(SceneStringName(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) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
connect("cell_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selected));
connect("item_collapsed", callable_mp(this, &EditorDebuggerTree::_scene_tree_folded));
connect("item_mouse_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_rmb_selected));
} break;
case NOTIFICATION_ENTER_TREE: {
update_icon_max_width();
} break;
}
}
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")));
ADD_SIGNAL(MethodInfo("open"));
}
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(SNAME("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, MouseButton p_button) {
if (p_button != MouseButton::RIGHT) {
return;
}
TreeItem *item = get_item_at_position(p_position);
if (!item) {
return;
}
item->select(0);
item_menu->clear();
item_menu->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTR("Save Branch as Scene"), ITEM_MENU_SAVE_REMOTE_NODE);
item_menu->add_icon_item(get_editor_theme_icon(SNAME("CopyNodePath")), TTR("Copy Node Path"), ITEM_MENU_COPY_NODE_PATH);
item_menu->set_position(get_screen_position() + get_local_mouse_position());
item_menu->reset_size();
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 = SceneTreeDock::get_singleton()->get_filter();
bool filter_changed = filter != last_filter;
TreeItem *scroll_item = nullptr;
// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
List<Pair<TreeItem *, int>> parents;
for (const SceneDebuggerTree::RemoteNode &node : p_tree->nodes) {
TreeItem *parent = nullptr;
if (parents.size()) { // Find last parent.
Pair<TreeItem *, int> &p = parents.front()->get();
parent = p.first;
if (!(--p.second)) { // If no child left, remove it.
parents.pop_front();
}
}
// Add this node.
TreeItem *item = create_item(parent);
item->set_text(0, node.name);
if (node.scene_file_path.is_empty()) {
item->set_tooltip_text(0, node.name + "\n" + TTR("Type:") + " " + node.type_name);
} else {
item->set_tooltip_text(0, node.name + "\n" + TTR("Instance:") + " " + node.scene_file_path + "\n" + 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);
if (filter_changed) {
scroll_item = item;
}
}
} else { // Must use path
if (last_path == _get_path(item)) {
updating_scene_tree = false; // Force emission of new selection.
item->select(0);
if (filter_changed) {
scroll_item = item;
}
updating_scene_tree = true;
}
}
// Add buttons.
const Color remote_button_color = Color(1, 1, 1, 0.8);
if (!node.scene_file_path.is_empty()) {
String node_scene_file_path = node.scene_file_path;
Ref<Texture2D> button_icon = get_editor_theme_icon(SNAME("InstanceOptions"));
String tooltip = vformat(TTR("This node has been instantiated from a PackedScene file:\n%s\nClick to open the original file in the Editor."), node_scene_file_path);
item->set_meta("scene_file_path", node_scene_file_path);
item->add_button(0, button_icon, BUTTON_SUBSCENE, false, tooltip);
item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color);
}
if (node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_HAS_VISIBLE_METHOD) {
bool node_visible = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE;
bool node_visible_in_tree = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE_IN_TREE;
Ref<Texture2D> button_icon = get_editor_theme_icon(node_visible ? SNAME("GuiVisibilityVisible") : SNAME("GuiVisibilityHidden"));
String tooltip = TTR("Toggle Visibility");
item->set_meta("visible", node_visible);
item->add_button(0, button_icon, BUTTON_VISIBILITY, false, tooltip);
if (ClassDB::is_parent_class(node.type_name, "CanvasItem") || ClassDB::is_parent_class(node.type_name, "Node3D")) {
item->set_button_color(0, item->get_button_count(0) - 1, node_visible_in_tree ? remote_button_color : Color(1, 1, 1, 0.6));
} else {
item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color);
}
}
// 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_ofn(item->get_text(0))) {
break; // Filter matches, must survive.
}
parent->remove_child(item);
memdelete(item);
if (scroll_item == item) {
scroll_item = nullptr;
}
if (had_siblings) {
break; // Parent must survive.
}
item = parent;
parent = item->get_parent();
// Check if parent expects more children.
for (const Pair<TreeItem *, int> &pair : parents) {
if (pair.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
if (scroll_item) {
callable_mp((Tree *)this, &Tree::scroll_to_item).call_deferred(scroll_item, false);
}
last_filter = filter;
updating_scene_tree = false;
}
Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) {
if (get_button_id_at_position(p_point) != -1) {
return Variant();
}
TreeItem *selected = get_selected();
if (!selected) {
return Variant();
}
String path = selected->get_text(0);
HBoxContainer *hb = memnew(HBoxContainer);
TextureRect *tf = memnew(TextureRect);
tf->set_texture(selected->get_icon(0));
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
hb->add_child(tf);
Label *label = memnew(Label(path));
hb->add_child(label);
set_drag_preview(hb);
if (!selected->get_parent() || !selected->get_parent()->get_parent()) {
path = ".";
} else {
while (selected->get_parent()->get_parent() != get_root()) {
selected = selected->get_parent();
path = selected->get_text(0) + "/" + path;
}
}
return vformat("\"%s\"", path);
}
void EditorDebuggerTree::update_icon_max_width() {
add_theme_constant_override("icon_max_width", get_theme_constant("class_icon_size", EditorStringName(Editor)));
}
String EditorDebuggerTree::get_selected_path() {
if (!get_selected()) {
return "";
}
return _get_path(get_selected());
}
String EditorDebuggerTree::_get_path(TreeItem *p_item) {
ERR_FAIL_NULL_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 (const String &extension : extensions) {
file_dialog->add_filter("*." + extension, extension.to_upper());
}
String filename = get_selected_path().get_file() + "." + extensions.front()->get().to_lower();
file_dialog->set_current_path(filename);
file_dialog->popup_file_dialog();
} break;
case ITEM_MENU_COPY_NODE_PATH: {
String text = get_selected_path();
if (text.is_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(SNAME("save_node"), inspected_object_id, p_file, debugger_id);
}

View file

@ -0,0 +1,83 @@
/**************************************************************************/
/* editor_debugger_tree.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_TREE_H
#define EDITOR_DEBUGGER_TREE_H
#include "scene/gui/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;
HashSet<ObjectID> unfold_cache;
PopupMenu *item_menu = nullptr;
EditorFileDialog *file_dialog = nullptr;
String last_filter;
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, MouseButton p_button);
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:
enum Button {
BUTTON_SUBSCENE = 0,
BUTTON_VISIBILITY = 1,
};
virtual Variant get_drag_data(const Point2 &p_point) override;
void update_icon_max_width();
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

View file

@ -0,0 +1,281 @@
/**************************************************************************/
/* editor_file_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_file_server.h"
#include "../editor_settings.h"
#include "core/io/marshalls.h"
#include "editor/editor_node.h"
#include "editor/export/editor_export_platform.h"
#define FILESYSTEM_PROTOCOL_VERSION 1
#define PASSWORD_LENGTH 32
#define MAX_FILE_BUFFER_SIZE 100 * 1024 * 1024 // 100mb max file buffer size (description of files to update, compressed).
static void _add_file(String f, const uint64_t &p_modified_time, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
f = f.replace_first("res://", ""); // remove res://
const uint64_t *cached_mt = cached_files.getptr(f);
if (cached_mt && *cached_mt == p_modified_time) {
// File is good, skip it.
cached_files.erase(f); // Erase to mark this file as existing. Remaining files not added to files_to_send will be considered erased here, so they need to be erased in the client too.
return;
}
files_to_send.insert(f, p_modified_time);
}
void EditorFileServer::_scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
for (int i = 0; i < efd->get_file_count(); i++) {
String f = efd->get_file_path(i);
if (FileAccess::exists(f + ".import")) {
// is imported, determine what to do
// Todo the modified times of remapped files should most likely be kept in EditorFileSystem to speed this up in the future.
Ref<ConfigFile> cf;
cf.instantiate();
Error err = cf->load(f + ".import");
ERR_CONTINUE(err != OK);
{
uint64_t mt = FileAccess::get_modified_time(f + ".import");
_add_file(f + ".import", mt, files_to_send, cached_files);
}
if (!cf->has_section("remap")) {
continue;
}
List<String> remaps;
cf->get_section_keys("remap", &remaps);
for (const String &remap : remaps) {
if (remap == "path") {
String remapped_path = cf->get_value("remap", remap);
uint64_t mt = FileAccess::get_modified_time(remapped_path);
_add_file(remapped_path, mt, files_to_send, cached_files);
} else if (remap.begins_with("path.")) {
String feature = remap.get_slice(".", 1);
if (p_tags.has(feature)) {
String remapped_path = cf->get_value("remap", remap);
uint64_t mt = FileAccess::get_modified_time(remapped_path);
_add_file(remapped_path, mt, files_to_send, cached_files);
}
}
}
} else {
uint64_t mt = efd->get_file_modified_time(i);
_add_file(f, mt, files_to_send, cached_files);
}
}
for (int i = 0; i < efd->get_subdir_count(); i++) {
_scan_files_changed(efd->get_subdir(i), p_tags, files_to_send, cached_files);
}
}
static void _add_custom_file(const String &f, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
if (!FileAccess::exists(f)) {
return;
}
_add_file(f, FileAccess::get_modified_time(f), files_to_send, cached_files);
}
void EditorFileServer::poll() {
if (!active) {
return;
}
if (!server->is_connection_available()) {
return;
}
Ref<StreamPeerTCP> tcp_peer = server->take_connection();
ERR_FAIL_COND(tcp_peer.is_null());
// Got a connection!
EditorProgress pr("updating_remote_file_system", TTR("Updating assets on target device:"), 105);
pr.step(TTR("Syncing headers"), 0, true);
print_verbose("EFS: Connecting taken!");
char header[4];
Error err = tcp_peer->get_data((uint8_t *)&header, 4);
ERR_FAIL_COND(err != OK);
ERR_FAIL_COND(header[0] != 'G');
ERR_FAIL_COND(header[1] != 'R');
ERR_FAIL_COND(header[2] != 'F');
ERR_FAIL_COND(header[3] != 'S');
uint32_t protocol_version = tcp_peer->get_u32();
ERR_FAIL_COND(protocol_version != FILESYSTEM_PROTOCOL_VERSION);
char cpassword[PASSWORD_LENGTH + 1];
err = tcp_peer->get_data((uint8_t *)cpassword, PASSWORD_LENGTH);
cpassword[PASSWORD_LENGTH] = 0;
ERR_FAIL_COND(err != OK);
print_verbose("EFS: Got password: " + String(cpassword));
ERR_FAIL_COND_MSG(password != cpassword, "Client disconnected because password mismatch.");
uint32_t tag_count = tcp_peer->get_u32();
print_verbose("EFS: Getting tags: " + itos(tag_count));
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
Vector<String> tags;
for (uint32_t i = 0; i < tag_count; i++) {
String tag = tcp_peer->get_utf8_string();
print_verbose("EFS: tag #" + itos(i) + ": " + tag);
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
tags.push_back(tag);
}
uint32_t file_buffer_decompressed_size = tcp_peer->get_32();
HashMap<String, uint64_t> cached_files;
if (file_buffer_decompressed_size > 0) {
pr.step(TTR("Getting remote file system"), 1, true);
// Got files cached by client.
uint32_t file_buffer_size = tcp_peer->get_32();
print_verbose("EFS: Getting file buffer: compressed - " + String::humanize_size(file_buffer_size) + " decompressed: " + String::humanize_size(file_buffer_decompressed_size));
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
ERR_FAIL_COND(file_buffer_size > MAX_FILE_BUFFER_SIZE);
LocalVector<uint8_t> file_buffer;
file_buffer.resize(file_buffer_size);
LocalVector<uint8_t> file_buffer_decompressed;
file_buffer_decompressed.resize(file_buffer_decompressed_size);
err = tcp_peer->get_data(file_buffer.ptr(), file_buffer_size);
pr.step(TTR("Decompressing remote file system"), 2, true);
ERR_FAIL_COND(err != OK);
// Decompress the text with all the files
Compression::decompress(file_buffer_decompressed.ptr(), file_buffer_decompressed.size(), file_buffer.ptr(), file_buffer.size(), Compression::MODE_ZSTD);
String files_text = String::utf8((const char *)file_buffer_decompressed.ptr(), file_buffer_decompressed.size());
Vector<String> files = files_text.split("\n");
print_verbose("EFS: Total cached files received: " + itos(files.size()));
for (int i = 0; i < files.size(); i++) {
if (files[i].get_slice_count("::") != 2) {
continue;
}
String file = files[i].get_slice("::", 0);
uint64_t modified_time = files[i].get_slice("::", 1).to_int();
cached_files.insert(file, modified_time);
}
} else {
// Client does not have any files stored.
}
pr.step(TTR("Scanning for local changes"), 3, true);
print_verbose("EFS: Scanning changes:");
HashMap<String, uint64_t> files_to_send;
// Scan files to send.
_scan_files_changed(EditorFileSystem::get_singleton()->get_filesystem(), tags, files_to_send, cached_files);
// Add forced export files
Vector<String> forced_export = EditorExportPlatform::get_forced_export_files();
for (int i = 0; i < forced_export.size(); i++) {
_add_custom_file(forced_export[i], files_to_send, cached_files);
}
_add_custom_file("res://project.godot", files_to_send, cached_files);
// Check which files were removed and also add them
for (KeyValue<String, uint64_t> K : cached_files) {
if (!files_to_send.has(K.key)) {
files_to_send.insert(K.key, 0); //0 means removed
}
}
tcp_peer->put_32(files_to_send.size());
print_verbose("EFS: Sending list of changed files.");
pr.step(TTR("Sending list of changed files:"), 4, true);
// Send list of changed files first, to ensure that if connecting breaks, the client is not found in a broken state.
for (KeyValue<String, uint64_t> K : files_to_send) {
tcp_peer->put_utf8_string(K.key);
tcp_peer->put_64(K.value);
}
print_verbose("EFS: Sending " + itos(files_to_send.size()) + " files.");
int idx = 0;
for (KeyValue<String, uint64_t> K : files_to_send) {
pr.step(TTR("Sending file:") + " " + K.key.get_file(), 5 + idx * 100 / files_to_send.size(), false);
idx++;
if (K.value == 0 || !FileAccess::exists("res://" + K.key)) { // File was removed
continue;
}
Vector<uint8_t> array = FileAccess::_get_file_as_bytes("res://" + K.key);
tcp_peer->put_64(array.size());
tcp_peer->put_data(array.ptr(), array.size());
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
}
tcp_peer->put_data((const uint8_t *)"GEND", 4); // End marker.
print_verbose("EFS: Done.");
}
void EditorFileServer::start() {
if (active) {
stop();
}
port = EDITOR_GET("filesystem/file_server/port");
password = EDITOR_GET("filesystem/file_server/password");
Error err = server->listen(port);
ERR_FAIL_COND_MSG(err != OK, "EditorFileServer: Unable to listen on port " + itos(port));
active = true;
}
bool EditorFileServer::is_active() const {
return active;
}
void EditorFileServer::stop() {
if (active) {
server->stop();
active = false;
}
}
EditorFileServer::EditorFileServer() {
server.instantiate();
EDITOR_DEF("filesystem/file_server/port", 6010);
EDITOR_DEF("filesystem/file_server/password", "");
}
EditorFileServer::~EditorFileServer() {
stop();
}

View file

@ -0,0 +1,61 @@
/**************************************************************************/
/* editor_file_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_FILE_SERVER_H
#define EDITOR_FILE_SERVER_H
#include "core/io/packet_peer.h"
#include "core/io/tcp_server.h"
#include "core/object/class_db.h"
#include "core/os/thread.h"
#include "editor/editor_file_system.h"
class EditorFileServer : public Object {
GDCLASS(EditorFileServer, Object);
Ref<TCPServer> server;
String password;
int port = 0;
bool active = false;
void _scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files);
public:
void poll();
void start();
void stop();
bool is_active() const;
EditorFileServer();
~EditorFileServer();
};
#endif // EDITOR_FILE_SERVER_H

View file

@ -0,0 +1,430 @@
/**************************************************************************/
/* editor_performance_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_property_name_processor.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
#include "main/performance.h"
EditorPerformanceProfiler::Monitor::Monitor() {}
EditorPerformanceProfiler::Monitor::Monitor(const String &p_name, const 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_NULL(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_text(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_text(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 TS->format_number(rtos(p_value * 1000).pad_decimals(2)) + " " + TTR("ms");
}
default: {
return TS->format_number(rtos(p_value));
}
}
}
void EditorPerformanceProfiler::_monitor_select() {
monitor_draw->queue_redraw();
}
void EditorPerformanceProfiler::_monitor_draw() {
Vector<StringName> active;
for (const KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item->is_checked(0)) {
active.push_back(E.key);
}
}
if (active.is_empty()) {
info_message->show();
return;
}
info_message->hide();
Ref<StyleBox> graph_style_box = get_theme_stylebox(CoreStringName(normal), SNAME("TextEdit"));
Ref<Font> graph_font = get_theme_font(SceneStringName(font), SNAME("TextEdit"));
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("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 = EditorThemeManager::is_dark_theme() ? 1.4f : 0.55f;
float hue_shift = 1.0f / float(monitors.size());
for (int i = 0; i < active.size(); i++) {
Monitor &current = 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(SNAME("accent_color"), EditorStringName(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(font_size)), current.item->get_text(0), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, draw_color);
draw_color.a = 0.9f;
float value_position = rect.size.width - graph_font->get_string_size(current.item->get_text(1), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width;
if (value_position < 0) {
value_position = 0;
}
monitor_draw->draw_string(graph_font, rect.position + Point2(value_position, graph_font->get_ascent(font_size)), current.item->get_text(1), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, draw_color);
rect.position.y += graph_font->get_height(font_size);
rect.size.height -= graph_font->get_height(font_size);
int line_count = rect.size.height / (graph_font->get_height(font_size) * 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(font_size)), _create_label(current.max, current.type), HORIZONTAL_ALIGNMENT_LEFT, rect.size.width, font_size, horizontal_line_color);
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(font_size)) + y_offset, _create_label(current.max * float(j) / float(line_count), current.type), HORIZONTAL_ALIGNMENT_LEFT, rect.size.width, font_size, horizontal_line_color);
}
}
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, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size);
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(font_size)), label, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, line_color);
}
prev = h2;
e = e->next();
from -= spacing;
count++;
}
}
}
void EditorPerformanceProfiler::_build_monitor_tree() {
HashSet<StringName> monitor_checked;
for (KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item && E.value.item->is_checked(0)) {
monitor_checked.insert(E.key);
}
}
base_map.clear();
monitor_tree->get_root()->clear_children();
for (KeyValue<StringName, Monitor> &E : monitors) {
TreeItem *base = _get_monitor_base(E.value.base);
TreeItem *item = _create_monitor_item(E.value.name, base);
item->set_checked(0, monitor_checked.has(E.key));
E.value.item = item;
if (!E.value.history.is_empty()) {
E.value.update_value(E.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, EditorPropertyNameProcessor::get_singleton()->process_name(p_base_name, EditorPropertyNameProcessor::get_settings_style()));
base->set_editable(0, false);
base->set_selectable(0, false);
base->set_expand_right(0, true);
if (is_inside_tree()) {
base->set_custom_font(0, get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
}
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, EditorPropertyNameProcessor::get_singleton()->process_name(p_monitor_name, EditorPropertyNameProcessor::get_settings_style()));
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() == MouseButton::LEFT) {
Vector<StringName> active;
for (KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item->is_checked(0)) {
active.push_back(E.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(CoreStringName(normal), SNAME("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->queue_redraw();
return;
}
}
marker_key = "";
monitor_draw->queue_redraw();
}
}
void EditorPerformanceProfiler::reset() {
HashMap<StringName, Monitor>::Iterator E = monitors.begin();
while (E != monitors.end()) {
HashMap<StringName, Monitor>::Iterator N = E;
++N;
if (String(E->key).begins_with("custom:")) {
monitors.remove(E);
} else {
E->value.reset();
}
E = N;
}
_build_monitor_tree();
marker_key = "";
marker_frame = 0;
monitor_draw->queue_redraw();
}
void EditorPerformanceProfiler::update_monitors(const Vector<StringName> &p_names) {
HashMap<StringName, int> names;
for (int i = 0; i < p_names.size(); i++) {
names.insert("custom:" + p_names[i], Performance::MONITOR_MAX + i);
}
{
HashMap<StringName, Monitor>::Iterator E = monitors.begin();
while (E != monitors.end()) {
HashMap<StringName, Monitor>::Iterator N = E;
++N;
if (String(E->key).begins_with("custom:")) {
if (!names.has(E->key)) {
monitors.remove(E);
} else {
E->value.frame_index = names[E->key];
names.erase(E->key);
}
}
E = N;
}
}
for (const KeyValue<StringName, int> &E : names) {
String name = String(E.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(E.key, Monitor(name, base, E.value, Performance::MONITOR_TYPE_QUANTITY, nullptr));
}
_build_monitor_tree();
}
void EditorPerformanceProfiler::add_profile_frame(const Vector<float> &p_values) {
for (KeyValue<StringName, Monitor> &E : monitors) {
float value = 0.0f;
if (E.value.frame_index >= 0 && E.value.frame_index < p_values.size()) {
value = p_values[E.value.frame_index];
}
E.value.history.push_front(value);
E.value.update_value(value);
}
marker_frame++;
monitor_draw->queue_redraw();
}
List<float> *EditorPerformanceProfiler::get_monitor_data(const StringName &p_name) {
if (monitors.has(p_name)) {
return &monitors[p_name].history;
}
return nullptr;
}
void EditorPerformanceProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
for (KeyValue<StringName, TreeItem *> &E : base_map) {
E.value->set_custom_font(0, get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
}
} break;
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/localize_settings")) {
_build_monitor_tree();
}
} break;
}
}
EditorPerformanceProfiler::EditorPerformanceProfiler() {
set_name(TTR("Monitors"));
set_split_offset(340 * EDSCALE);
monitor_tree = memnew(Tree);
monitor_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
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(SceneStringName(draw), callable_mp(this, &EditorPerformanceProfiler::_monitor_draw));
monitor_draw->connect(SceneStringName(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_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
info_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
info_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
info_message->set_anchors_and_offsets_preset(PRESET_FULL_RECT, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);
monitor_draw->add_child(info_message);
for (int i = 0; i < Performance::MONITOR_MAX; i++) {
const Performance::Monitor monitor = Performance::Monitor(i);
const String path = Performance::get_singleton()->get_monitor_name(monitor);
const String base = path.get_slicec('/', 0);
const String name = path.get_slicec('/', 1);
monitors.insert(path, Monitor(name, base, i, Performance::get_singleton()->get_monitor_type(monitor), nullptr));
}
_build_monitor_tree();
}

View file

@ -0,0 +1,93 @@
/**************************************************************************/
/* editor_performance_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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/templates/hash_map.h"
#include "core/templates/rb_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(const String &p_name, const String &p_base, int p_frame_index, Performance::MonitorType p_type, TreeItem *p_item);
void update_value(float p_value);
void reset();
};
HashMap<StringName, Monitor> monitors;
HashMap<StringName, TreeItem *> base_map;
Tree *monitor_tree = nullptr;
Control *monitor_draw = nullptr;
Label *info_message = nullptr;
StringName marker_key;
int marker_frame = 0;
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);
protected:
void _notification(int p_what);
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

View file

@ -0,0 +1,731 @@
/**************************************************************************/
/* editor_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_settings.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
#include "scene/resources/image_texture.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];
}
}
}
EditorProfiler::Metric EditorProfiler::_get_frame_metric(int index) {
return frame_metrics[(frame_metrics.size() + last_metric - (total_metrics - 1) + index) % frame_metrics.size()];
}
void EditorProfiler::add_frame_metric(const Metric &p_metric, bool p_final) {
++last_metric;
if (last_metric >= frame_metrics.size()) {
last_metric = 0;
}
total_metrics++;
if (total_metrics > frame_metrics.size()) {
total_metrics = frame_metrics.size();
}
frame_metrics.write[last_metric] = p_metric;
_make_metric_ptrs(frame_metrics.write[last_metric]);
updating_frame = true;
clear_button->set_disabled(false);
cursor_metric_edit->set_editable(true);
cursor_metric_edit->set_max(p_metric.frame_number);
cursor_metric_edit->set_min(_get_frame_metric(0).frame_number);
if (!seeking) {
cursor_metric_edit->set_value(p_metric.frame_number);
}
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 = EDITOR_GET("debugger/profiler_frame_history_size");
metric_size = CLAMP(metric_size, 60, 10000);
frame_metrics.clear();
frame_metrics.resize(metric_size);
total_metrics = 0;
last_metric = -1;
variables->clear();
plot_sigs.clear();
plot_sigs.insert("physics_frame_time");
plot_sigs.insert("category_frame_time");
display_internal_profiles->set_visible(EDITOR_GET("debugger/profile_native_calls"));
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);
cursor_metric_edit->set_editable(false);
updating_frame = false;
hover_metric = -1;
seeking = false;
// Ensure button text (start, stop) is correct
_update_button_text();
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
static String _get_percent_txt(float p_value, float p_total) {
if (p_total == 0) {
p_total = 0.00001;
}
return TS->format_number(String::num((p_value / p_total) * 100, 1)) + TS->percent_sign();
}
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 TS->format_number(rtos(p_time * 1000).pad_decimals(2)) + " " + TTR("ms");
} else if (dmode == DISPLAY_AVERAGE_TIME) {
if (p_calls == 0) {
return TS->format_number("0.00") + " " + TTR("ms");
} else {
return TS->format_number(rtos((p_time / p_calls) * 1000).pad_decimals(2)) + " " + TTR("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(SNAME("error_color"), EditorStringName(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(SNAME("base_color"), EditorStringName(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(SNAME("dark_color_2"), EditorStringName(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 < total_metrics; i++) {
const Metric &m = _get_frame_metric(i);
for (const StringName &E : plot_sigs) {
HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E);
if (F) {
highest = MAX(F->value->total_time, highest);
}
HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E);
if (G) {
if (use_self) {
highest = MAX(G->value->self, highest);
} else {
highest = MAX(G->value->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();
HashMap<StringName, int> prev_plots;
for (int i = 0; i < total_metrics * w / frame_metrics.size() - 1; i++) {
for (int j = 0; j < h * 4; j++) {
column[j] = 0;
}
int current = i * frame_metrics.size() / w;
for (const StringName &E : plot_sigs) {
const Metric &m = _get_frame_metric(current);
float value = 0;
HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E);
if (F) {
value = F->value->total_time;
}
HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E);
if (G) {
if (use_self) {
value = G->value->self;
} else {
value = G->value->total;
}
}
int plot_pos = CLAMP(int(value * h / highest), 0, h - 1);
int prev_plot = plot_pos;
HashMap<StringName, int>::Iterator H = prev_plots.find(E);
if (H) {
prev_plot = H->value;
H->value = plot_pos;
} else {
prev_plots[E] = 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);
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 = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, graph_image);
if (reset_texture) {
if (graph_texture.is_null()) {
graph_texture.instantiate();
}
graph_texture->set_image(img);
}
graph_texture->update(img);
graph->set_texture(graph_texture);
graph->queue_redraw();
}
void EditorProfiler::_update_frame() {
int cursor_metric = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number;
updating_frame = true;
variables->clear();
TreeItem *root = variables->create_item();
const Metric &m = _get_frame_metric(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 = 0; j < m.categories[i].items.size(); j++) {
const Metric::Category::Item &it = m.categories[i].items[j];
if (it.internal == it.total && !display_internal_profiles->is_pressed() && m.categories[i].name == "Script Functions") {
continue;
}
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_alignment(2, HORIZONTAL_ALIGNMENT_RIGHT);
item->set_tooltip_text(0, it.name + "\n" + it.script + ":" + itos(it.line));
float time = dtime == DISPLAY_SELF_TIME ? it.self : it.total;
if (dtime == DISPLAY_SELF_TIME && !display_internal_profiles->is_pressed()) {
time += it.internal;
}
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::_update_button_text() {
if (activate->is_pressed()) {
activate->set_icon(get_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
} else {
activate->set_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
}
void EditorProfiler::_activate_pressed() {
_update_button_text();
if (activate->is_pressed()) {
_clear_pressed();
}
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
void EditorProfiler::_clear_pressed() {
clear_button->set_disabled(true);
clear();
_update_plot();
}
void EditorProfiler::_internal_profiles_pressed() {
_combo_changed(0);
}
void EditorProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
activate->set_icon(get_editor_theme_icon(SNAME("Play")));
clear_button->set_icon(get_editor_theme_icon(SNAME("Clear")));
theme_cache.seek_line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
theme_cache.seek_line_color.a = 0.8;
theme_cache.seek_line_hover_color = theme_cache.seek_line_color;
theme_cache.seek_line_hover_color.a = 0.4;
if (total_metrics > 0) {
_update_plot();
}
} break;
}
}
void EditorProfiler::_graph_tex_draw() {
if (total_metrics == 0) {
return;
}
if (seeking) {
int frame = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number;
int cur_x = (2 * frame + 1) * graph->get_size().x / (2 * frame_metrics.size()) + 1;
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), theme_cache.seek_line_color);
}
if (hover_metric > -1 && hover_metric < total_metrics) {
int cur_x = (2 * hover_metric + 1) * graph->get_size().x / (2 * frame_metrics.size()) + 1;
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), theme_cache.seek_line_hover_color);
}
}
void EditorProfiler::_graph_tex_mouse_exit() {
hover_metric = -1;
graph->queue_redraw();
}
void EditorProfiler::_cursor_metric_changed(double) {
if (updating_frame) {
return;
}
graph->queue_redraw();
_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() == MouseButton::LEFT && mb->is_pressed()) ||
(mm.is_valid())) {
int x = me->get_position().x - 1;
x = x * frame_metrics.size() / graph->get_size().width;
hover_metric = x;
if (x < 0) {
x = 0;
}
if (x >= frame_metrics.size()) {
x = frame_metrics.size() - 1;
}
if (mb.is_valid() || (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) {
updating_frame = true;
if (x < total_metrics) {
cursor_metric_edit->set_value(_get_frame_metric(x).frame_number);
}
updating_frame = false;
if (activate->is_pressed()) {
if (!seeking) {
emit_signal(SNAME("break_request"));
}
}
seeking = true;
if (!frame_delay->is_processing()) {
frame_delay->set_wait_time(0.1);
frame_delay->start();
}
}
graph->queue_redraw();
}
}
void EditorProfiler::disable_seeking() {
seeking = false;
graph->queue_redraw();
}
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, bool p_clear) {
activate->set_disabled(!p_enable);
if (p_clear) {
clear();
}
}
void EditorProfiler::set_pressed(bool p_pressed) {
activate->set_pressed(p_pressed);
_update_button_text();
}
bool EditorProfiler::is_profiling() {
return activate->is_pressed();
}
Vector<Vector<String>> EditorProfiler::get_data_as_csv() const {
Vector<Vector<String>> res;
if (frame_metrics.is_empty()) {
return res;
}
// Different metrics may contain different number of categories.
HashSet<StringName> possible_signatures;
for (int i = 0; i < frame_metrics.size(); i++) {
const Metric &m = frame_metrics[i];
if (!m.valid) {
continue;
}
for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) {
possible_signatures.insert(E.key);
}
for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) {
possible_signatures.insert(E.key);
}
}
// Generate CSV header and cache indices.
HashMap<StringName, int> sig_map;
Vector<String> signatures;
signatures.resize(possible_signatures.size());
int sig_index = 0;
for (const StringName &E : possible_signatures) {
signatures.write[sig_index] = E;
sig_map[E] = sig_index;
sig_index++;
}
res.push_back(signatures);
// values
Vector<String> values;
int index = last_metric;
for (int i = 0; i < frame_metrics.size(); i++) {
++index;
if (index >= frame_metrics.size()) {
index = 0;
}
const Metric &m = frame_metrics[index];
if (!m.valid) {
continue;
}
// Don't keep old values since there may be empty cells.
values.clear();
values.resize(possible_signatures.size());
for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) {
values.write[sig_map[E.key]] = String::num_real(E.value->total_time);
}
for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) {
values.write[sig_map[E.key]] = String::num_real(E.value->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_disabled(true);
activate->set_text(TTR("Start"));
activate->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_activate_pressed));
hb->add_child(activate);
clear_button = memnew(Button);
clear_button->set_text(TTR("Clear"));
clear_button->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_clear_pressed));
clear_button->set_disabled(true);
hb->add_child(clear_button);
hb->add_child(memnew(Label(TTR("Measure:"))));
display_mode = memnew(OptionButton);
display_mode->add_item(TTR("Frame Time (ms)"));
display_mode->add_item(TTR("Average Time (ms)"));
display_mode->add_item(TTR("Frame %"));
display_mode->add_item(TTR("Physics Frame %"));
display_mode->connect(SceneStringName(item_selected), callable_mp(this, &EditorProfiler::_combo_changed));
hb->add_child(display_mode);
hb->add_child(memnew(Label(TTR("Time:"))));
display_time = memnew(OptionButton);
// TRANSLATORS: This is an option in the profiler to display the time spent in a function, including the time spent in other functions called by that function.
display_time->add_item(TTR("Inclusive"));
// TRANSLATORS: This is an option in the profiler to display the time spent in a function, exincluding the time spent in other functions called by that function.
display_time->add_item(TTR("Self"));
display_time->set_tooltip_text(TTR("Inclusive: Includes time from other functions called by this function.\nUse this to spot bottlenecks.\n\nSelf: Only count the time spent in the function itself, not in other functions called by that function.\nUse this to find individual functions to optimize."));
display_time->connect(SceneStringName(item_selected), callable_mp(this, &EditorProfiler::_combo_changed));
hb->add_child(display_time);
display_internal_profiles = memnew(CheckButton(TTR("Display internal functions")));
display_internal_profiles->set_visible(EDITOR_GET("debugger/profile_native_calls"));
display_internal_profiles->set_pressed(false);
display_internal_profiles->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_internal_profiles_pressed));
hb->add_child(display_internal_profiles);
hb->add_spacer();
hb->add_child(memnew(Label(TTR("Frame #:"))));
cursor_metric_edit = memnew(SpinBox);
cursor_metric_edit->set_h_size_flags(SIZE_FILL);
cursor_metric_edit->set_value(0);
cursor_metric_edit->set_editable(false);
hb->add_child(cursor_metric_edit);
cursor_metric_edit->connect(SceneStringName(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_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
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_clip_content(0, true);
variables->set_column_custom_minimum_width(0, 60);
variables->set_column_title(1, TTR("Time"));
variables->set_column_expand(1, false);
variables->set_column_clip_content(1, true);
variables->set_column_custom_minimum_width(1, 75 * EDSCALE);
variables->set_column_title(2, TTR("Calls"));
variables->set_column_expand(2, false);
variables->set_column_clip_content(2, true);
variables->set_column_custom_minimum_width(2, 50 * EDSCALE);
variables->connect("item_edited", callable_mp(this, &EditorProfiler::_item_edited));
graph = memnew(TextureRect);
graph->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
graph->set_mouse_filter(MOUSE_FILTER_STOP);
graph->connect(SceneStringName(draw), callable_mp(this, &EditorProfiler::_graph_tex_draw));
graph->connect(SceneStringName(gui_input), callable_mp(this, &EditorProfiler::_graph_tex_input));
graph->connect(SceneStringName(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_GET("debugger/profiler_frame_history_size")), 60, 10000);
frame_metrics.resize(metric_size);
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");
}

View file

@ -0,0 +1,183 @@
/**************************************************************************/
/* editor_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_PROFILER_H
#define EDITOR_PROFILER_H
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/check_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 ImageTexture;
class EditorProfiler : public VBoxContainer {
GDCLASS(EditorProfiler, VBoxContainer);
public:
struct Metric {
bool valid = false;
int frame_number = 0;
float frame_time = 0;
float process_time = 0;
float physics_time = 0;
float physics_frame_time = 0;
struct Category {
StringName signature;
String name;
float total_time = 0; //total for category
struct Item {
StringName signature;
String name;
String script;
int line = 0;
float self = 0;
float total = 0;
float internal = 0;
int calls = 0;
};
Vector<Item> items;
};
Vector<Category> categories;
HashMap<StringName, Category *> category_ptrs;
HashMap<StringName, Category::Item *> item_ptrs;
};
enum DisplayMode {
DISPLAY_FRAME_TIME,
DISPLAY_AVERAGE_TIME,
DISPLAY_FRAME_PERCENT,
DISPLAY_PHYSICS_FRAME_PERCENT,
};
enum DisplayTime {
DISPLAY_TOTAL_TIME,
DISPLAY_SELF_TIME,
};
private:
struct ThemeCache {
Color seek_line_color;
Color seek_line_hover_color;
} theme_cache;
Button *activate = nullptr;
Button *clear_button = nullptr;
TextureRect *graph = nullptr;
Ref<ImageTexture> graph_texture;
Vector<uint8_t> graph_image;
Tree *variables = nullptr;
HSplitContainer *h_split = nullptr;
HashSet<StringName> plot_sigs;
OptionButton *display_mode = nullptr;
OptionButton *display_time = nullptr;
CheckButton *display_internal_profiles = nullptr;
SpinBox *cursor_metric_edit = nullptr;
Vector<Metric> frame_metrics;
int total_metrics = 0;
int last_metric = -1;
int max_functions = 0;
bool updating_frame = false;
int hover_metric = -1;
float graph_height = 1.0f;
bool seeking = false;
Timer *frame_delay = nullptr;
Timer *plot_delay = nullptr;
void _update_button_text();
void _update_frame();
void _activate_pressed();
void _clear_pressed();
void _internal_profiles_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);
Color _get_color_from_signature(const StringName &p_signature) const;
void _cursor_metric_changed(double);
void _combo_changed(int);
Metric _get_frame_metric(int index);
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 p_clear = true);
void set_pressed(bool p_pressed);
bool is_profiling();
bool is_seeking() { return seeking; }
void disable_seeking();
void clear();
Vector<Vector<String>> get_data_as_csv() const;
EditorProfiler();
};
#endif // EDITOR_PROFILER_H

View file

@ -0,0 +1,826 @@
/**************************************************************************/
/* editor_visual_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_settings.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "scene/resources/image_texture.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;
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;
clear_button->set_disabled(false);
cursor_metric_edit->set_max(frame_metrics[last_metric].frame_number);
cursor_metric_edit->set_min(MAX(int64_t(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 = EDITOR_GET("debugger/profiler_frame_history_size");
metric_size = CLAMP(metric_size, 60, 10000);
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 TS->format_number(String::num(p_time, 2)) + " " + TTR("ms");
} else if (dmode == DISPLAY_FRAME_PERCENT) {
return TS->format_number(String::num(p_time * 100 / graph_limit, 2)) + " " + TS->percent_sign();
}
return "err";
}
Color EditorVisualProfiler::_get_color_from_signature(const StringName &p_signature) const {
Color bc = get_theme_color(SNAME("error_color"), EditorStringName(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(SNAME("base_color"), EditorStringName(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() {
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", EditorStringName(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
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 = Math::fast_ftoi(background_color.r * 255);
g = Math::fast_ftoi(background_color.g * 255);
b = Math::fast_ftoi(background_color.b * 255);
} 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 = Math::fast_ftoi(background_color.r * 255);
g = Math::fast_ftoi(background_color.g * 255);
b = Math::fast_ftoi(background_color.b * 255);
} 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 = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, graph_image);
if (reset_texture) {
if (graph_texture.is_null()) {
graph_texture.instantiate();
}
graph_texture->set_image(img);
}
graph_texture->update(img);
graph->set_texture(graph_texture);
graph->queue_redraw();
}
void EditorVisualProfiler::_update_frame(bool p_focus_selected) {
int cursor_metric = _get_cursor_index();
Ref<Texture> track_icon = get_editor_theme_icon(SNAME("TrackColor"));
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 (TreeItem *E : stack) {
float total_cpu = E->get_metadata(1);
float total_gpu = E->get_metadata(2);
total_cpu += cpu_time;
total_gpu += gpu_time;
E->set_metadata(1, total_cpu);
E->set_metadata(2, total_gpu);
}
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 (TreeItem *E : categories) {
float total_cpu = E->get_metadata(1);
float total_gpu = E->get_metadata(2);
E->set_text(1, _get_time_as_text(total_cpu));
E->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_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
_clear_pressed(); //always clear on start
clear_button->set_disabled(false);
} else {
activate->set_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
void EditorVisualProfiler::_clear_pressed() {
clear_button->set_disabled(true);
clear();
_update_plot();
}
void EditorVisualProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
if (is_layout_rtl()) {
activate->set_icon(get_editor_theme_icon(SNAME("PlayBackwards")));
} else {
activate->set_icon(get_editor_theme_icon(SNAME("Play")));
}
clear_button->set_icon(get_editor_theme_icon(SNAME("Clear")));
} break;
}
}
void EditorVisualProfiler::_graph_tex_draw() {
if (last_metric < 0) {
return;
}
Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));
const Color color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
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;
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), color * Color(1, 1, 1));
graph->draw_line(Vector2(cur_x + half_width, 0), Vector2(cur_x + half_width, graph->get_size().y), color * Color(1, 1, 1));
}
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 * Color(1, 1, 1, 0.5));
const String limit_str = String::num(graph_limit, 2) + " ms";
graph->draw_string(font, Vector2(half_width - font->get_string_size(limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 2, frame_y - 2), limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
}
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 * Color(1, 1, 1, 0.5));
const String limit_str = String::num(graph_limit, 2) + " ms";
graph->draw_string(font, Vector2(half_width * 2 - font->get_string_size(limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 2, frame_y - 2), limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
}
graph->draw_string(font, Vector2(font->get_string_size("X", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x, font->get_ascent(font_size) + 2), "CPU:", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1));
graph->draw_string(font, Vector2(font->get_string_size("X", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + graph->get_size().width / 2, font->get_ascent(font_size) + 2), "GPU:", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1));
}
void EditorVisualProfiler::_graph_tex_mouse_exit() {
hover_metric = -1;
graph->queue_redraw();
}
void EditorVisualProfiler::_cursor_metric_changed(double) {
if (updating_frame) {
return;
}
graph->queue_redraw();
_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() == MouseButton::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().has_flag(MouseButtonMask::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->queue_redraw();
}
}
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->queue_redraw();
}
void EditorVisualProfiler::_combo_changed(int) {
_update_frame();
_update_plot();
}
void EditorVisualProfiler::_bind_methods() {
ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable")));
}
void EditorVisualProfiler::_update_button_text() {
if (activate->is_pressed()) {
activate->set_icon(get_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
} else {
activate->set_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
}
void EditorVisualProfiler::set_enabled(bool p_enable) {
activate->set_disabled(!p_enable);
}
void EditorVisualProfiler::set_pressed(bool p_pressed) {
activate->set_pressed(p_pressed);
_update_button_text();
}
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.is_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_disabled(true);
activate->set_text(TTR("Start"));
activate->connect(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_activate_pressed));
hb->add_child(activate);
clear_button = memnew(Button);
clear_button->set_text(TTR("Clear"));
clear_button->set_disabled(true);
clear_button->connect(SceneStringName(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 (ms)"));
display_mode->add_item(TTR("Frame %"));
display_mode->connect(SceneStringName(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(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_update_plot));
linked = memnew(CheckBox(TTR("Linked")));
linked->set_pressed(true);
hb->add_child(linked);
linked->connect(SceneStringName(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(SceneStringName(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_clip_content(0, true);
variables->set_column_custom_minimum_width(0, 60);
variables->set_column_title(1, TTR("CPU"));
variables->set_column_expand(1, false);
variables->set_column_clip_content(1, true);
variables->set_column_custom_minimum_width(1, 75 * EDSCALE);
variables->set_column_title(2, TTR("GPU"));
variables->set_column_expand(2, false);
variables->set_column_clip_content(2, true);
variables->set_column_custom_minimum_width(2, 75 * EDSCALE);
variables->connect("cell_selected", callable_mp(this, &EditorVisualProfiler::_item_selected));
graph = memnew(TextureRect);
graph->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
graph->set_mouse_filter(MOUSE_FILTER_STOP);
graph->connect(SceneStringName(draw), callable_mp(this, &EditorVisualProfiler::_graph_tex_draw));
graph->connect(SceneStringName(gui_input), callable_mp(this, &EditorVisualProfiler::_graph_tex_input));
graph->connect(SceneStringName(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_GET("debugger/profiler_frame_history_size")), 60, 10000);
frame_metrics.resize(metric_size);
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).bind(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));
}

View file

@ -0,0 +1,152 @@
/**************************************************************************/
/* editor_visual_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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_VISUAL_PROFILER_H
#define EDITOR_VISUAL_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 ImageTexture;
class EditorVisualProfiler : public VBoxContainer {
GDCLASS(EditorVisualProfiler, VBoxContainer);
public:
struct Metric {
bool valid = false;
uint64_t frame_number = 0;
struct Area {
String name;
Color color_cache;
StringName fullpath_cache;
float cpu_time = 0;
float gpu_time = 0;
};
Vector<Area> areas;
};
enum DisplayTimeMode {
DISPLAY_FRAME_TIME,
DISPLAY_FRAME_PERCENT,
};
private:
Button *activate = nullptr;
Button *clear_button = nullptr;
TextureRect *graph = nullptr;
Ref<ImageTexture> graph_texture;
Vector<uint8_t> graph_image;
Tree *variables = nullptr;
HSplitContainer *h_split = nullptr;
CheckBox *frame_relative = nullptr;
CheckBox *linked = nullptr;
OptionButton *display_mode = nullptr;
SpinBox *cursor_metric_edit = nullptr;
Vector<Metric> frame_metrics;
int last_metric = -1;
int hover_metric = -1;
StringName selected_area;
bool updating_frame = false;
float graph_height_cpu = 1.0f;
float graph_height_gpu = 1.0f;
float graph_limit = 1000.0f / 60;
bool seeking = false;
Timer *frame_delay = nullptr;
Timer *plot_delay = nullptr;
void _update_button_text();
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);
void set_pressed(bool p_pressed);
bool is_profiling();
bool is_seeking() { return seeking; }
void disable_seeking();
void clear();
Vector<Vector<String>> get_data_as_csv() const;
EditorVisualProfiler();
};
#endif // EDITOR_VISUAL_PROFILER_H

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,322 @@
/**************************************************************************/
/* script_editor_debugger.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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/object/script_language.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 "scene/gui/button.h"
#include "scene/gui/margin_container.h"
class Tree;
class LineEdit;
class TabContainer;
class RichTextLabel;
class TextureButton;
class AcceptDialog;
class TreeItem;
class HSplitContainer;
class ItemList;
class EditorProfiler;
class EditorFileDialog;
class EditorVisualProfiler;
class EditorPerformanceProfiler;
class SceneDebuggerTree;
class EditorDebuggerPlugin;
class DebugAdapterProtocol;
class DebugAdapterParser;
class ScriptEditorDebugger : public MarginContainer {
GDCLASS(ScriptEditorDebugger, MarginContainer);
friend class EditorDebuggerNode;
friend class DebugAdapterProtocol;
friend class DebugAdapterParser;
private:
enum MessageType {
MESSAGE_ERROR,
MESSAGE_WARNING,
MESSAGE_SUCCESS,
};
enum ProfilerType {
PROFILER_VISUAL,
PROFILER_SCRIPTS_SERVERS
};
enum Actions {
ACTION_COPY_ERROR,
ACTION_OPEN_SOURCE,
ACTION_DELETE_BREAKPOINT,
ACTION_DELETE_BREAKPOINTS_IN_FILE,
ACTION_DELETE_ALL_BREAKPOINTS,
};
AcceptDialog *msgdialog = nullptr;
LineEdit *clicked_ctrl = nullptr;
LineEdit *clicked_ctrl_type = nullptr;
LineEdit *live_edit_root = nullptr;
Button *le_set = nullptr;
Button *le_clear = nullptr;
Button *export_csv = nullptr;
VBoxContainer *errors_tab = nullptr;
Tree *error_tree = nullptr;
Button *expand_all_button = nullptr;
Button *collapse_all_button = nullptr;
Button *clear_button = nullptr;
PopupMenu *item_menu = nullptr;
Tree *breakpoints_tree = nullptr;
PopupMenu *breakpoints_menu = nullptr;
EditorFileDialog *file_dialog = nullptr;
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 = nullptr;
Label *reason = nullptr;
Button *skip_breakpoints = nullptr;
Button *copy = nullptr;
Button *step = nullptr;
Button *next = nullptr;
Button *dobreak = nullptr;
Button *docontinue = nullptr;
// 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;
HashMap<int, String> profiler_signature;
Tree *vmem_tree = nullptr;
Button *vmem_refresh = nullptr;
Button *vmem_export = nullptr;
LineEdit *vmem_total = nullptr;
Tree *stack_dump = nullptr;
LineEdit *search = nullptr;
OptionButton *threads = nullptr;
EditorDebuggerInspector *inspector = nullptr;
SceneDebuggerTree *scene_tree = nullptr;
Ref<RemoteDebuggerPeer> peer;
HashMap<NodePath, int> node_path_cache;
int last_path_id;
HashMap<String, int> res_path_cache;
EditorProfiler *profiler = nullptr;
EditorVisualProfiler *visual_profiler = nullptr;
EditorPerformanceProfiler *performance_profiler = nullptr;
OS::ProcessID remote_pid = 0;
bool move_to_foreground = true;
bool can_request_idle_draw = false;
bool live_debug;
uint64_t debugging_thread_id = Thread::UNASSIGNED_ID;
struct ThreadDebugged {
String name;
String error;
bool can_debug = false;
bool has_stackdump = false;
uint32_t debug_order = 0;
uint64_t thread_id = Thread::UNASSIGNED_ID; // for order
};
struct ThreadSort {
bool operator()(const ThreadDebugged *a, const ThreadDebugged *b) const {
return a->debug_order < b->debug_order;
}
};
HashMap<uint64_t, ThreadDebugged> threads_debugged;
bool thread_list_updating = false;
void _select_thread(int p_index);
EditorDebuggerNode::CameraOverride camera_override;
void _stack_dump_frame_selected();
void _file_selected(const String &p_file);
void _parse_message(const String &p_msg, uint64_t p_thread_id, 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, const Variant **p_args, int p_argcount);
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 _breakpoints_item_rmb_selected(const Vector2 &p_pos, MouseButton p_button);
void _error_tree_item_rmb_selected(const Vector2 &p_pos, MouseButton p_button);
void _item_menu_id_pressed(int p_option);
void _tab_changed(int p_tab);
void _put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID);
void _export_csv();
void _clear_execution();
void _stop_and_notify();
void _set_breakpoint(const String &p_path, const int &p_line, const bool &p_enabled);
void _clear_breakpoints();
void _breakpoint_tree_clicked();
String _format_frame_text(const ScriptLanguage::StackInfo *info);
void _thread_debug_enter(uint64_t p_thread_id);
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 threads_debugged.size() > 0; }
bool is_debuggable() const { return threads_debugged.size() > 0 && threads_debugged[debugging_thread_id].can_debug; }
bool is_session_active() { return peer.is_valid() && peer->is_peer_connected(); }
int get_remote_pid() const { return remote_pid; }
bool is_move_to_foreground() const;
void set_move_to_foreground(const bool &p_move_to_foreground);
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;
bool request_stack_dump(const int &p_frame);
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_instantiate_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_all_scripts();
void reload_scripts(const Vector<String> &p_script_paths);
bool is_skip_breakpoints();
virtual Size2 get_minimum_size() const override;
void add_debugger_tab(Control *p_control);
void remove_debugger_tab(Control *p_control);
int get_current_debugger_tab() const;
void switch_to_debugger(int p_debugger_tab_idx);
void send_message(const String &p_message, const Array &p_args);
void toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data);
ScriptEditorDebugger();
~ScriptEditorDebugger();
};
#endif // SCRIPT_EDITOR_DEBUGGER_H