feat: godot-engine-source-4.3-stable
This commit is contained in:
parent
c59a7dcade
commit
7125d019b5
11149 changed files with 5070401 additions and 0 deletions
7
engine/editor/debugger/SCsub
Normal file
7
engine/editor/debugger/SCsub
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
Import("env")
|
||||
|
||||
env.add_source_files(env.editor_sources, "*.cpp")
|
||||
|
||||
SConscript("debug_adapter/SCsub")
|
||||
5
engine/editor/debugger/debug_adapter/SCsub
Normal file
5
engine/editor/debugger/debug_adapter/SCsub
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
Import("env")
|
||||
|
||||
env.add_source_files(env.editor_sources, "*.cpp")
|
||||
636
engine/editor/debugger/debug_adapter/debug_adapter_parser.cpp
Normal file
636
engine/editor/debugger/debug_adapter/debug_adapter_parser.cpp
Normal 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;
|
||||
}
|
||||
107
engine/editor/debugger/debug_adapter/debug_adapter_parser.h
Normal file
107
engine/editor/debugger/debug_adapter/debug_adapter_parser.h
Normal 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
|
||||
1062
engine/editor/debugger/debug_adapter/debug_adapter_protocol.cpp
Normal file
1062
engine/editor/debugger/debug_adapter/debug_adapter_protocol.cpp
Normal file
File diff suppressed because it is too large
Load diff
156
engine/editor/debugger/debug_adapter/debug_adapter_protocol.h
Normal file
156
engine/editor/debugger/debug_adapter/debug_adapter_protocol.h
Normal 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
|
||||
|
|
@ -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);
|
||||
}
|
||||
58
engine/editor/debugger/debug_adapter/debug_adapter_server.h
Normal file
58
engine/editor/debugger/debug_adapter/debug_adapter_server.h
Normal 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
|
||||
281
engine/editor/debugger/debug_adapter/debug_adapter_types.h
Normal file
281
engine/editor/debugger/debug_adapter/debug_adapter_types.h
Normal 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
|
||||
287
engine/editor/debugger/editor_debugger_inspector.cpp
Normal file
287
engine/editor/debugger/editor_debugger_inspector.cpp
Normal 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();
|
||||
}
|
||||
97
engine/editor/debugger/editor_debugger_inspector.h
Normal file
97
engine/editor/debugger/editor_debugger_inspector.h
Normal 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
|
||||
839
engine/editor/debugger/editor_debugger_node.cpp
Normal file
839
engine/editor/debugger/editor_debugger_node.cpp
Normal 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;
|
||||
}
|
||||
224
engine/editor/debugger/editor_debugger_node.h
Normal file
224
engine/editor/debugger/editor_debugger_node.h
Normal 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
|
||||
143
engine/editor/debugger/editor_debugger_server.cpp
Normal file
143
engine/editor/debugger/editor_debugger_server.cpp
Normal 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();
|
||||
}
|
||||
60
engine/editor/debugger/editor_debugger_server.h
Normal file
60
engine/editor/debugger/editor_debugger_server.h
Normal 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
|
||||
370
engine/editor/debugger/editor_debugger_tree.cpp
Normal file
370
engine/editor/debugger/editor_debugger_tree.cpp
Normal 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);
|
||||
}
|
||||
83
engine/editor/debugger/editor_debugger_tree.h
Normal file
83
engine/editor/debugger/editor_debugger_tree.h
Normal 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
|
||||
281
engine/editor/debugger/editor_file_server.cpp
Normal file
281
engine/editor/debugger/editor_file_server.cpp
Normal 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();
|
||||
}
|
||||
61
engine/editor/debugger/editor_file_server.h
Normal file
61
engine/editor/debugger/editor_file_server.h
Normal 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
|
||||
430
engine/editor/debugger/editor_performance_profiler.cpp
Normal file
430
engine/editor/debugger/editor_performance_profiler.cpp
Normal 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 ¤t = monitors[active[i]];
|
||||
Rect2i rect(Point2i(i % columns, i / columns) * cell_size + Point2i(MARGIN, MARGIN), cell_size - Point2i(MARGIN, MARGIN) * 2);
|
||||
monitor_draw->draw_style_box(graph_style_box, rect);
|
||||
|
||||
rect.position += graph_style_box->get_offset();
|
||||
rect.size -= graph_style_box->get_minimum_size();
|
||||
Color draw_color = get_theme_color(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();
|
||||
}
|
||||
93
engine/editor/debugger/editor_performance_profiler.h
Normal file
93
engine/editor/debugger/editor_performance_profiler.h
Normal 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
|
||||
731
engine/editor/debugger/editor_profiler.cpp
Normal file
731
engine/editor/debugger/editor_profiler.cpp
Normal 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");
|
||||
}
|
||||
183
engine/editor/debugger/editor_profiler.h
Normal file
183
engine/editor/debugger/editor_profiler.h
Normal 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
|
||||
826
engine/editor/debugger/editor_visual_profiler.cpp
Normal file
826
engine/editor/debugger/editor_visual_profiler.cpp
Normal 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));
|
||||
}
|
||||
152
engine/editor/debugger/editor_visual_profiler.h
Normal file
152
engine/editor/debugger/editor_visual_profiler.h
Normal 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
|
||||
2139
engine/editor/debugger/script_editor_debugger.cpp
Normal file
2139
engine/editor/debugger/script_editor_debugger.cpp
Normal file
File diff suppressed because it is too large
Load diff
322
engine/editor/debugger/script_editor_debugger.h
Normal file
322
engine/editor/debugger/script_editor_debugger.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue