From ab644a991340f5bd2baf02f371c8ff67409db879 Mon Sep 17 00:00:00 2001
From: BimDav <67792882+BimDav@users.noreply.github.com>
Date: Tue, 19 Jul 2022 10:56:47 +0200
Subject: [PATCH 01/10] Add option in VisibilityEnabler2D to hide the parent
for better performance
---
doc/classes/VisibilityEnabler2D.xml | 7 ++++++-
scene/2d/visibility_notifier_2d.cpp | 24 ++++++++++++++++++++++++
scene/2d/visibility_notifier_2d.h | 1 +
3 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/doc/classes/VisibilityEnabler2D.xml b/doc/classes/VisibilityEnabler2D.xml
index 115f7a2429c8..a25f665a4f8c 100644
--- a/doc/classes/VisibilityEnabler2D.xml
+++ b/doc/classes/VisibilityEnabler2D.xml
@@ -47,6 +47,9 @@
If [code]true[/code], the parent's [method Node._process] will be stopped.
+
+ If [code]true[/code] and the parent is a [CanvasItem], the parent will be hidden.
+
@@ -67,7 +70,9 @@
This enabler will stop [AnimatedSprite] nodes animations.
-
+
+
+
Represents the size of the [enum Enabler] enum.
diff --git a/scene/2d/visibility_notifier_2d.cpp b/scene/2d/visibility_notifier_2d.cpp
index 5d414204809b..4d9ff910821a 100644
--- a/scene/2d/visibility_notifier_2d.cpp
+++ b/scene/2d/visibility_notifier_2d.cpp
@@ -153,6 +153,13 @@ void VisibilityEnabler2D::_screen_enter() {
if (enabler[ENABLER_PARENT_PROCESS] && get_parent()) {
get_parent()->set_process(true);
}
+ if (enabler[ENABLER_PARENT_VISIBILITY] && get_parent()) {
+ CanvasItem *ci = Object::cast_to(get_parent());
+
+ if (ci) {
+ ci->set_visible(true);
+ }
+ }
visible = true;
}
@@ -168,6 +175,13 @@ void VisibilityEnabler2D::_screen_exit() {
if (enabler[ENABLER_PARENT_PROCESS] && get_parent()) {
get_parent()->set_process(false);
}
+ if (enabler[ENABLER_PARENT_VISIBILITY] && get_parent()) {
+ CanvasItem *ci = Object::cast_to(get_parent());
+
+ if (ci) {
+ ci->set_visible(false);
+ }
+ }
visible = false;
}
@@ -248,6 +262,14 @@ void VisibilityEnabler2D::_notification(int p_what) {
get_parent()->connect(SceneStringNames::get_singleton()->ready,
get_parent(), "set_process", varray(false), CONNECT_REFERENCE_COUNTED);
}
+ if (enabler[ENABLER_PARENT_VISIBILITY] && get_parent()) {
+ CanvasItem *ci = Object::cast_to(get_parent());
+
+ if (ci) {
+ ci->connect(SceneStringNames::get_singleton()->ready,
+ ci, "set_visible", varray(false), CONNECT_REFERENCE_COUNTED);
+ }
+ }
}
if (p_what == NOTIFICATION_EXIT_TREE) {
@@ -336,6 +358,7 @@ void VisibilityEnabler2D::_bind_methods() {
ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "pause_animated_sprites"), "set_enabler", "is_enabler_enabled", ENABLER_PAUSE_ANIMATED_SPRITES);
ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "process_parent"), "set_enabler", "is_enabler_enabled", ENABLER_PARENT_PROCESS);
ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "physics_process_parent"), "set_enabler", "is_enabler_enabled", ENABLER_PARENT_PHYSICS_PROCESS);
+ ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "visibility_parent"), "set_enabler", "is_enabler_enabled", ENABLER_PARENT_VISIBILITY);
BIND_ENUM_CONSTANT(ENABLER_PAUSE_ANIMATIONS);
BIND_ENUM_CONSTANT(ENABLER_FREEZE_BODIES);
@@ -343,6 +366,7 @@ void VisibilityEnabler2D::_bind_methods() {
BIND_ENUM_CONSTANT(ENABLER_PARENT_PROCESS);
BIND_ENUM_CONSTANT(ENABLER_PARENT_PHYSICS_PROCESS);
BIND_ENUM_CONSTANT(ENABLER_PAUSE_ANIMATED_SPRITES);
+ BIND_ENUM_CONSTANT(ENABLER_PARENT_VISIBILITY);
BIND_ENUM_CONSTANT(ENABLER_MAX);
}
diff --git a/scene/2d/visibility_notifier_2d.h b/scene/2d/visibility_notifier_2d.h
index c964f8a9d7f8..5a7da01a2064 100644
--- a/scene/2d/visibility_notifier_2d.h
+++ b/scene/2d/visibility_notifier_2d.h
@@ -78,6 +78,7 @@ class VisibilityEnabler2D : public VisibilityNotifier2D {
ENABLER_PARENT_PROCESS,
ENABLER_PARENT_PHYSICS_PROCESS,
ENABLER_PAUSE_ANIMATED_SPRITES,
+ ENABLER_PARENT_VISIBILITY,
ENABLER_MAX
};
From 1730fab22a5f431de448aa7e96b0666730c642a6 Mon Sep 17 00:00:00 2001
From: lawnjelly
Date: Mon, 12 Dec 2022 11:43:01 +0000
Subject: [PATCH 02/10] Consistent render ordering for CanvasLayers
Maintains scene tree ordering for CanvasLayers that share identical layer ID.
---
scene/main/canvas_layer.cpp | 50 ++++++++++++++++++++++++++++++-------
scene/main/canvas_layer.h | 3 +++
2 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/scene/main/canvas_layer.cpp b/scene/main/canvas_layer.cpp
index 237cd535e749..78c004ea0239 100644
--- a/scene/main/canvas_layer.cpp
+++ b/scene/main/canvas_layer.cpp
@@ -35,7 +35,11 @@
void CanvasLayer::set_layer(int p_xform) {
layer = p_xform;
if (viewport.is_valid()) {
- VisualServer::get_singleton()->viewport_set_canvas_stacking(viewport, canvas, layer, get_position_in_parent());
+ // For the sublayer, we will use the order in which the layer occurs in the scene tree
+ // rather than just the child_id via get_position_in_parent().
+ // We have 32 bits to play with for the sublayer (or more likely 31, as sublayer is signed)
+ // (see Viewport::CanvasKey constructor in visual_server_viewport.h).
+ VisualServer::get_singleton()->viewport_set_canvas_stacking(viewport, canvas, layer, _layer_order_in_tree);
vp->_gui_set_root_order_dirty();
}
}
@@ -59,6 +63,34 @@ void CanvasLayer::set_visible(bool p_visible) {
}
}
+// Make sure layer orders are up to date whenever moving layers in the tree.
+void CanvasLayer::_update_layer_orders() {
+ if (is_inside_tree() && get_tree()) {
+ Node *root = get_tree()->get_root();
+ if (root) {
+ uint32_t layer_order_count = 0;
+ _calculate_layer_orders_in_tree(root, layer_order_count);
+ }
+ }
+}
+
+// When rendering layers, if the layer id (set by user) and sublayer (child id) is the same
+// we need something else sensible which we can sort repeatably to determine which layers should render first,
+// so we will simply calculate the order that the layers occur throughout the scene tree.
+void CanvasLayer::_calculate_layer_orders_in_tree(Node *p_node, uint32_t &r_order) {
+ CanvasLayer *layer = Object::cast_to(p_node);
+ if (layer) {
+ layer->_layer_order_in_tree = r_order++;
+
+ // Force an update of the layer order in the VisualServer
+ set_layer(get_layer());
+ }
+
+ for (int n = 0; n < p_node->get_child_count(); n++) {
+ _calculate_layer_orders_in_tree(p_node->get_child(n), r_order);
+ }
+}
+
void CanvasLayer::show() {
set_visible(true);
}
@@ -183,25 +215,24 @@ void CanvasLayer::_notification(int p_what) {
viewport = vp->get_viewport_rid();
VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas);
- VisualServer::get_singleton()->viewport_set_canvas_stacking(viewport, canvas, layer, get_position_in_parent());
VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform);
_update_follow_viewport();
-
+ _update_layer_orders();
} break;
case NOTIFICATION_EXIT_TREE: {
ERR_FAIL_NULL_MSG(vp, "Viewport is not initialized.");
+ _update_layer_orders();
vp->_canvas_layer_remove(this);
VisualServer::get_singleton()->viewport_remove_canvas(viewport, canvas);
viewport = RID();
_update_follow_viewport(false);
-
} break;
case NOTIFICATION_MOVED_IN_PARENT: {
- if (is_inside_tree()) {
- VisualServer::get_singleton()->viewport_set_canvas_stacking(viewport, canvas, layer, get_position_in_parent());
- }
-
+ // Note: As this step requires traversing the entire scene tree, it is thus expensive
+ // to move the canvas layer multiple times. Take special care when deleting / moving
+ // multiple nodes to prevent multiple NOTIFICATION_MOVED_IN_PARENT occurring.
+ _update_layer_orders();
} break;
}
}
@@ -248,8 +279,8 @@ void CanvasLayer::set_custom_viewport(Node *p_viewport) {
viewport = vp->get_viewport_rid();
VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas);
- VisualServer::get_singleton()->viewport_set_canvas_stacking(viewport, canvas, layer, get_position_in_parent());
VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform);
+ _update_layer_orders();
}
}
@@ -379,6 +410,7 @@ CanvasLayer::CanvasLayer() {
visible = true;
follow_viewport = false;
follow_viewport_scale = 1.0;
+ _layer_order_in_tree = 0;
}
CanvasLayer::~CanvasLayer() {
diff --git a/scene/main/canvas_layer.h b/scene/main/canvas_layer.h
index 90bdcd7a78b8..47ecfebdc91f 100644
--- a/scene/main/canvas_layer.h
+++ b/scene/main/canvas_layer.h
@@ -45,6 +45,7 @@ class CanvasLayer : public Node {
int layer;
Transform2D transform;
RID canvas;
+ uint32_t _layer_order_in_tree;
ObjectID custom_viewport_id; // to check validity
Viewport *custom_viewport;
@@ -61,6 +62,8 @@ class CanvasLayer : public Node {
void _update_xform();
void _update_locrotscale();
void _update_follow_viewport(bool p_force_exit = false);
+ void _calculate_layer_orders_in_tree(Node *p_node, uint32_t &r_order);
+ void _update_layer_orders();
protected:
void _notification(int p_what);
From f3cdff46fc36267afe11f220b2b354739e3d7d20 Mon Sep 17 00:00:00 2001
From: Fredia Huya-Kouadio
Date: Sun, 22 Jan 2023 10:42:34 -0800
Subject: [PATCH 03/10] Add benchmark logic
Add benchmarking measuring methods to `OS` to allow for platform specific overrides (e.g: can be used to hook into platform specific benchmarking and tracing capabilities).
---
core/os/os.cpp | 53 ++++++++
core/os/os.h | 15 +++
core/register_core_types.cpp | 7 +
editor/editor_fonts.cpp | 4 +
editor/editor_node.cpp | 14 ++
editor/editor_themes.cpp | 8 ++
editor/project_manager.cpp | 3 +
main/main.cpp | 32 ++++-
misc/dist/shell/_godot.zsh-completion | 2 +
misc/dist/shell/godot.bash-completion | 2 +
misc/dist/shell/godot.fish | 2 +
.../org/godotengine/editor/GodotEditor.kt | 3 +
.../lib/src/org/godotengine/godot/Godot.java | 64 ++++++---
.../src/org/godotengine/godot/GodotView.java | 45 ++++++-
.../io/directory/DirectoryAccessHandler.kt | 3 +
.../godot/io/file/FileAccessHandler.kt | 8 +-
.../godotengine/godot/utils/BenchmarkUtils.kt | 122 ++++++++++++++++++
platform/android/java_godot_view_wrapper.cpp | 13 +-
platform/android/java_godot_view_wrapper.h | 5 +
platform/android/java_godot_wrapper.cpp | 30 +++++
platform/android/java_godot_wrapper.h | 6 +
platform/android/os_android.cpp | 38 +++++-
platform/android/os_android.h | 7 +
23 files changed, 460 insertions(+), 26 deletions(-)
create mode 100644 platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
diff --git a/core/os/os.cpp b/core/os/os.cpp
index 6e5dab9a8612..d450407c9056 100644
--- a/core/os/os.cpp
+++ b/core/os/os.cpp
@@ -30,6 +30,7 @@
#include "os.h"
+#include "core/io/json.h"
#include "core/os/dir_access.h"
#include "core/os/file_access.h"
#include "core/os/input.h"
@@ -931,6 +932,58 @@ void OS::add_frame_delay(bool p_can_draw) {
}
}
+void OS::set_use_benchmark(bool p_use_benchmark) {
+ use_benchmark = p_use_benchmark;
+}
+
+bool OS::is_use_benchmark_set() {
+ return use_benchmark;
+}
+
+void OS::set_benchmark_file(const String &p_benchmark_file) {
+ benchmark_file = p_benchmark_file;
+}
+
+String OS::get_benchmark_file() {
+ return benchmark_file;
+}
+
+void OS::benchmark_begin_measure(const String &p_what) {
+#ifdef TOOLS_ENABLED
+ start_benchmark_from[p_what] = OS::get_singleton()->get_ticks_usec();
+#endif
+}
+void OS::benchmark_end_measure(const String &p_what) {
+#ifdef TOOLS_ENABLED
+ uint64_t total = OS::get_singleton()->get_ticks_usec() - start_benchmark_from[p_what];
+ double total_f = double(total) / double(1000000);
+
+ startup_benchmark_json[p_what] = total_f;
+#endif
+}
+
+void OS::benchmark_dump() {
+#ifdef TOOLS_ENABLED
+ if (!use_benchmark) {
+ return;
+ }
+ if (!benchmark_file.empty()) {
+ FileAccess *f = FileAccess::open(benchmark_file, FileAccess::WRITE);
+ if (f) {
+ f->store_string(JSON::print(startup_benchmark_json, "\t", false));
+ }
+ } else {
+ List keys;
+ startup_benchmark_json.get_key_list(&keys);
+ print_line("BENCHMARK:");
+ for (List::Element *E = keys.front(); E; E = E->next()) {
+ Variant &K = E->get();
+ print_line("\t-" + K.operator String() + ": " + startup_benchmark_json[K] + " sec.");
+ }
+ }
+#endif
+}
+
OS::OS() {
void *volatile stack_bottom;
diff --git a/core/os/os.h b/core/os/os.h
index 85270d3e1ed3..d67e9d61250e 100644
--- a/core/os/os.h
+++ b/core/os/os.h
@@ -75,6 +75,12 @@ class OS {
bool restart_on_exit;
List restart_commandline;
+ // For tracking benchmark data
+ bool use_benchmark = false;
+ String benchmark_file;
+ HashMap start_benchmark_from;
+ Dictionary startup_benchmark_json;
+
protected:
bool _update_pending;
@@ -663,6 +669,15 @@ class OS {
virtual bool request_permissions() { return true; }
virtual Vector get_granted_permissions() const { return Vector(); }
+ // For recording / measuring benchmark data. Only enabled with tools
+ void set_use_benchmark(bool p_use_benchmark);
+ bool is_use_benchmark_set();
+ void set_benchmark_file(const String &p_benchmark_file);
+ String get_benchmark_file();
+ virtual void benchmark_begin_measure(const String &p_what);
+ virtual void benchmark_end_measure(const String &p_what);
+ virtual void benchmark_dump();
+
virtual void process_and_drop_events() {}
OS();
virtual ~OS();
diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp
index 3f8855ee8ff5..6f969e1cddcb 100644
--- a/core/register_core_types.cpp
+++ b/core/register_core_types.cpp
@@ -101,6 +101,7 @@ extern void register_variant_methods();
extern void unregister_variant_methods();
void register_core_types() {
+ OS::get_singleton()->benchmark_begin_measure("register_core_types");
MemoryPool::setup();
StringName::setup();
@@ -225,6 +226,8 @@ void register_core_types() {
_classdb = memnew(_ClassDB);
_marshalls = memnew(_Marshalls);
_json = memnew(_JSON);
+
+ OS::get_singleton()->benchmark_end_measure("register_core_types");
}
void register_core_settings() {
@@ -272,6 +275,8 @@ void register_core_singletons() {
}
void unregister_core_types() {
+ OS::get_singleton()->benchmark_begin_measure("unregister_core_types");
+
memdelete(_resource_loader);
memdelete(_resource_saver);
memdelete(_os);
@@ -320,4 +325,6 @@ void unregister_core_types() {
StringName::cleanup();
MemoryPool::cleanup();
+
+ OS::get_singleton()->benchmark_end_measure("unregister_core_types");
}
diff --git a/editor/editor_fonts.cpp b/editor/editor_fonts.cpp
index 8f068db14d93..1d73a5a62b89 100644
--- a/editor/editor_fonts.cpp
+++ b/editor/editor_fonts.cpp
@@ -32,6 +32,7 @@
#include "builtin_fonts.gen.h"
#include "core/os/dir_access.h"
+#include "core/os/os.h"
#include "editor_scale.h"
#include "editor_settings.h"
#include "scene/resources/default_theme/default_theme.h"
@@ -100,6 +101,7 @@
MAKE_FALLBACKS(m_name);
void editor_register_fonts(Ref p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("editor_register_fonts");
DirAccess *dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
/* Custom font */
@@ -285,4 +287,6 @@ void editor_register_fonts(Ref p_theme) {
MAKE_SOURCE_FONT(df_text_editor_status_code, default_font_size);
p_theme->set_font("status_source", "EditorFonts", df_text_editor_status_code);
+
+ OS::get_singleton()->benchmark_end_measure("editor_register_fonts");
}
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 0f4f8cc890ef..32a0a451048a 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -866,8 +866,11 @@ void EditorNode::_sources_changed(bool p_exist) {
_load_docks();
if (defer_load_scene != "") {
+ OS::get_singleton()->benchmark_begin_measure("editor_load_scene");
load_scene(defer_load_scene);
defer_load_scene = "";
+ OS::get_singleton()->benchmark_end_measure("editor_load_scene");
+ OS::get_singleton()->benchmark_dump();
}
}
}
@@ -3887,6 +3890,8 @@ bool EditorNode::is_scene_in_use(const String &p_path) {
}
void EditorNode::register_editor_types() {
+ OS::get_singleton()->benchmark_begin_measure("register_editor_types");
+
ResourceLoader::set_timestamp_on_load(true);
ResourceSaver::set_timestamp_on_save(true);
@@ -3922,12 +3927,18 @@ void EditorNode::register_editor_types() {
// FIXME: Is this stuff obsolete, or should it be ported to new APIs?
ClassDB::register_class();
//ClassDB::register_type();
+
+ OS::get_singleton()->benchmark_end_measure("register_editor_types");
}
void EditorNode::unregister_editor_types() {
+ OS::get_singleton()->benchmark_begin_measure("unregister_editor_types");
+
_init_callbacks.clear();
EditorResourcePicker::clear_caches();
+
+ OS::get_singleton()->benchmark_end_measure("unregister_editor_types");
}
void EditorNode::stop_child_process() {
@@ -5839,6 +5850,7 @@ int EditorNode::execute_and_show_output(const String &p_title, const String &p_p
}
EditorNode::EditorNode() {
+ OS::get_singleton()->benchmark_begin_measure("editor");
EditorPropertyNameProcessor *epnp = memnew(EditorPropertyNameProcessor);
add_child(epnp);
@@ -7226,6 +7238,8 @@ EditorNode::EditorNode() {
String exec = OS::get_singleton()->get_executable_path();
EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "executable_path", exec); // Save editor executable path for third-party tools
+
+ OS::get_singleton()->benchmark_end_measure("editor");
}
EditorNode::~EditorNode() {
diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp
index 4d4f14b62d1c..b1206f96cef3 100644
--- a/editor/editor_themes.cpp
+++ b/editor/editor_themes.cpp
@@ -31,6 +31,7 @@
#include "editor_themes.h"
#include "core/io/resource_loader.h"
+#include "core/os/os.h"
#include "editor_fonts.h"
#include "editor_icons.gen.h"
#include "editor_scale.h"
@@ -134,6 +135,7 @@ static Ref editor_generate_icon(int p_index, bool p_convert_color,
#endif
void editor_register_and_generate_icons(Ref p_theme, bool p_dark_theme = true, int p_thumb_size = 32, bool p_only_thumbs = false) {
+ OS::get_singleton()->benchmark_begin_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all")));
#ifdef MODULE_SVG_ENABLED
// The default icon theme is designed to be used for a dark theme.
// This dictionary stores color codes to convert to other colors
@@ -290,9 +292,11 @@ void editor_register_and_generate_icons(Ref p_theme, bool p_dark_theme =
#else
WARN_PRINT("SVG support disabled, editor icons won't be rendered.");
#endif
+ OS::get_singleton()->benchmark_end_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all")));
}
Ref create_editor_theme(const Ref p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("create_editor_theme");
Ref theme = Ref(memnew(Theme));
const float default_contrast = 0.25;
@@ -1419,10 +1423,13 @@ Ref create_editor_theme(const Ref p_theme) {
setting->load_text_editor_theme();
}
+ OS::get_singleton()->benchmark_end_measure("create_editor_theme");
+
return theme;
}
Ref create_custom_theme(const Ref p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("create_custom_theme");
Ref theme = create_editor_theme(p_theme);
const String custom_theme_path = EditorSettings::get_singleton()->get("interface/theme/custom_theme");
@@ -1433,6 +1440,7 @@ Ref create_custom_theme(const Ref p_theme) {
}
}
+ OS::get_singleton()->benchmark_end_measure("create_custom_theme");
return theme;
}
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index 461ac438f522..97e2617fb4af 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -2391,6 +2391,7 @@ void ProjectManager::_version_button_pressed() {
}
ProjectManager::ProjectManager() {
+ OS::get_singleton()->benchmark_begin_measure("project_manager");
// load settings
if (!EditorSettings::get_singleton()) {
EditorSettings::create();
@@ -2791,6 +2792,8 @@ ProjectManager::ProjectManager() {
about = memnew(EditorAbout);
add_child(about);
+
+ OS::get_singleton()->benchmark_end_measure("project_manager");
}
ProjectManager::~ProjectManager() {
diff --git a/main/main.cpp b/main/main.cpp
index 960baf17ff08..9bcb1d3d035f 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -349,6 +349,8 @@ void Main::print_help(const char *p_binary) {
OS::get_singleton()->print(" --doctool [] Dump the engine API reference to the given (defaults to current dir) in XML format, merging if existing files are found.\n");
OS::get_singleton()->print(" --no-docbase Disallow dumping the base types (used with --doctool).\n");
OS::get_singleton()->print(" --build-solutions Build the scripting solutions (e.g. for C# projects). Implies --editor and requires a valid project to edit.\n");
+ OS::get_singleton()->print(" --benchmark Benchmark the run time and print it to console.\n");
+ OS::get_singleton()->print(" --benchmark-file Benchmark the run time and save it to a given file in JSON format. The path should be absolute.\n");
#ifdef DEBUG_METHODS_ENABLED
OS::get_singleton()->print(" --gdnative-generate-json-api Generate JSON dump of the Godot API for GDNative bindings.\n");
#endif
@@ -399,9 +401,14 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
OS::get_singleton()->initialize_core();
+ // Benchmark tracking must be done after `OS::get_singleton()->initialize_core()` as on some
+ // platforms, it's used to set up the time utilities.
+ OS::get_singleton()->benchmark_begin_measure("startup_begin");
+
engine = memnew(Engine);
MAIN_PRINT("Main: Initialize CORE");
+ OS::get_singleton()->benchmark_begin_measure("core");
register_core_types();
register_core_driver_types();
@@ -921,6 +928,20 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
OS::get_singleton()->disable_crash_handler();
} else if (I->get() == "--skip-breakpoints") {
skip_breakpoints = true;
+ } else if (I->get() == "--benchmark") {
+ OS::get_singleton()->set_use_benchmark(true);
+ } else if (I->get() == "--benchmark-file") {
+ if (I->next()) {
+ OS::get_singleton()->set_use_benchmark(true);
+ String benchmark_file = I->next()->get();
+ OS::get_singleton()->set_benchmark_file(benchmark_file);
+ N = I->next()->next();
+ } else {
+ OS::get_singleton()->print("Missing argument for --startup-benchmark-file .\n");
+ OS::get_singleton()->print("Missing argument for --benchmark-file .\n");
+ goto error;
+ }
+
} else {
main_args.push_back(I->get());
}
@@ -1299,7 +1320,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
if (p_second_phase) {
return setup2();
}
-
+ OS::get_singleton()->benchmark_end_measure("core");
return OK;
error:
@@ -1352,6 +1373,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
if (message_queue) {
memdelete(message_queue);
}
+
+ OS::get_singleton()->benchmark_end_measure("core");
+
OS::get_singleton()->finalize_core();
locale = String();
@@ -2197,6 +2221,8 @@ bool Main::start() {
}
}
+ OS::get_singleton()->benchmark_end_measure("startup_begin");
+ OS::get_singleton()->benchmark_dump();
return true;
}
@@ -2456,6 +2482,7 @@ void Main::force_redraw() {
* The order matters as some of those steps are linked with each other.
*/
void Main::cleanup(bool p_force) {
+ OS::get_singleton()->benchmark_begin_measure("Main::cleanup");
if (!p_force) {
ERR_FAIL_COND(!_start_success);
}
@@ -2575,6 +2602,9 @@ void Main::cleanup(bool p_force) {
unregister_core_driver_types();
unregister_core_types();
+ OS::get_singleton()->benchmark_end_measure("Main::cleanup");
+ OS::get_singleton()->benchmark_dump();
+
OS::get_singleton()->finalize_core();
#ifdef RID_HANDLES_ENABLED
diff --git a/misc/dist/shell/_godot.zsh-completion b/misc/dist/shell/_godot.zsh-completion
index 0a147dcf9d24..9da65794b5c4 100644
--- a/misc/dist/shell/_godot.zsh-completion
+++ b/misc/dist/shell/_godot.zsh-completion
@@ -74,4 +74,6 @@ _arguments \
'--no-docbase[disallow dumping the base types (used with --doctool)]' \
'--build-solutions[build the scripting solutions (e.g. for C# projects)]' \
'--gdnative-generate-json-api[generate JSON dump of the Godot API for GDNative bindings]' \
+ '--benchmark[benchmark the run time and print it to console]' \
+ '--benchmark-file[benchmark the run time and save it to a given file in JSON format]:path to output JSON file' \
'--test[run a unit test]:unit test name'
diff --git a/misc/dist/shell/godot.bash-completion b/misc/dist/shell/godot.bash-completion
index d0638c0b3927..af6b4a44178a 100644
--- a/misc/dist/shell/godot.bash-completion
+++ b/misc/dist/shell/godot.bash-completion
@@ -77,6 +77,8 @@ _complete_godot_options() {
--no-docbase
--build-solutions
--gdnative-generate-json-api
+--benchmark
+--benchmark-file
--test
" -- "$1"))
}
diff --git a/misc/dist/shell/godot.fish b/misc/dist/shell/godot.fish
index 57fa90e053bb..338c24796a0c 100644
--- a/misc/dist/shell/godot.fish
+++ b/misc/dist/shell/godot.fish
@@ -88,4 +88,6 @@ complete -c godot -l doctool -d "Dump the engine API reference to the given path
complete -c godot -l no-docbase -d "Disallow dumping the base types (used with --doctool)"
complete -c godot -l build-solutions -d "Build the scripting solutions (e.g. for C# projects)"
complete -c godot -l gdnative-generate-json-api -d "Generate JSON dump of the Godot API for GDNative bindings"
+complete -c godot -l benchmark -d "Benchmark the run time and print it to console"
+complete -c godot -l benchmark-file -d "Benchmark the run time and save it to a given file in JSON format" -x
complete -c godot -l test -d "Run a unit test" -x
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 71385315aeef..239047f2c83c 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -104,6 +104,9 @@ open class GodotEditor : FullScreenGodotApp() {
if (args != null && args.isNotEmpty()) {
commandLineParams.addAll(listOf(*args))
}
+ if (BuildConfig.BUILD_TYPE == "dev") {
+ commandLineParams.add("--benchmark")
+ }
}
override fun getCommandLine() = commandLineParams
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
index a55fe567e075..f679e169ae7f 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
@@ -39,6 +39,7 @@
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
import org.godotengine.godot.tts.GodotTTS;
+import org.godotengine.godot.utils.BenchmarkUtils;
import org.godotengine.godot.utils.GodotNetUtils;
import org.godotengine.godot.utils.PermissionsUtil;
import org.godotengine.godot.xr.XRMode;
@@ -268,6 +269,8 @@ protected void instanceSingleton(SingletonBase s) {
public GodotIO io;
public GodotNetUtils netUtils;
public GodotTTS tts;
+ private DirectoryAccessHandler directoryAccessHandler;
+ private FileAccessHandler fileAccessHandler;
static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS];
static int singleton_count = 0;
@@ -601,7 +604,7 @@ private String[] parseCommandLine() {
}
return cmdline;
} catch (Exception e) {
- e.printStackTrace();
+ // The _cl_ file can be missing with no adverse effect
return new String[0];
}
}
@@ -662,8 +665,8 @@ private void initializeGodot() {
netUtils = new GodotNetUtils(activity);
tts = new GodotTTS(activity);
Context context = getContext();
- DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
- FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
+ directoryAccessHandler = new DirectoryAccessHandler(context);
+ fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
@@ -685,6 +688,7 @@ public void onServiceConnected(Messenger m) {
@Override
public void onCreate(Bundle icicle) {
+ BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate");
super.onCreate(icicle);
final Activity activity = getActivity();
@@ -736,6 +740,18 @@ public void onCreate(Bundle icicle) {
editor.putString("store_public_key", main_pack_key);
editor.apply();
+ i++;
+ } else if (command_line[i].equals("--benchmark")) {
+ BenchmarkUtils.setUseBenchmark(true);
+ new_args.add(command_line[i]);
+ } else if (has_extra && command_line[i].equals("--benchmark-file")) {
+ BenchmarkUtils.setUseBenchmark(true);
+ new_args.add(command_line[i]);
+
+ // Retrieve the filepath
+ BenchmarkUtils.setBenchmarkFile(command_line[i + 1]);
+ new_args.add(command_line[i + 1]);
+
i++;
} else if (command_line[i].trim().length() != 0) {
new_args.add(command_line[i]);
@@ -807,6 +823,7 @@ public void onCreate(Bundle icicle) {
mCurrentIntent = activity.getIntent();
initializeGodot();
+ BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate");
}
@Override
@@ -1021,22 +1038,6 @@ public final void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do something here if sensor accuracy changes.
}
- /*
- @Override public boolean dispatchKeyEvent(KeyEvent event) {
-
- if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) {
-
- System.out.printf("** BACK REQUEST!\n");
-
- GodotLib.quit();
- return true;
- }
- System.out.printf("** OTHER KEY!\n");
-
- return false;
- }
- */
-
public void onBackPressed() {
boolean shouldQuit = true;
@@ -1242,6 +1243,16 @@ public void initInputDevices() {
mView.initInputDevices();
}
+ @Keep
+ public DirectoryAccessHandler getDirectoryAccessHandler() {
+ return directoryAccessHandler;
+ }
+
+ @Keep
+ public FileAccessHandler getFileAccessHandler() {
+ return fileAccessHandler;
+ }
+
@Keep
private int createNewGodotInstance(String[] args) {
if (godotHost != null) {
@@ -1249,4 +1260,19 @@ private int createNewGodotInstance(String[] args) {
}
return 0;
}
+
+ @Keep
+ private void beginBenchmarkMeasure(String label) {
+ BenchmarkUtils.beginBenchmarkMeasure(label);
+ }
+
+ @Keep
+ private void endBenchmarkMeasure(String label) {
+ BenchmarkUtils.endBenchmarkMeasure(label);
+ }
+
+ @Keep
+ private void dumpBenchmark(String benchmarkFile) {
+ BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile);
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java
index 98bc45f58a5f..07430a9beae2 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java
@@ -43,14 +43,21 @@
import android.annotation.SuppressLint;
import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.os.Build;
+import android.text.TextUtils;
+import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import androidx.annotation.Keep;
+import java.io.InputStream;
+
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
@@ -79,6 +86,7 @@ public class GodotView extends GLSurfaceView {
private final Godot godot;
private final GodotInputHandler inputHandler;
private final GodotRenderer godotRenderer;
+ private final SparseArray customPointerIcons = new SparseArray<>();
private EGLConfigChooser eglConfigChooser;
private EGLContextFactory eglContextFactory;
@@ -149,13 +157,48 @@ public void releasePointerCapture() {
inputHandler.onPointerCaptureChange(false);
}
+ /**
+ * Used to configure the PointerIcon for the given type.
+ *
+ * Called from JNI
+ */
+ @Keep
+ public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
+ try {
+ Bitmap bitmap = null;
+ if (!TextUtils.isEmpty(imagePath)) {
+ if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) {
+ // Try to load the bitmap from the file system
+ bitmap = BitmapFactory.decodeFile(imagePath);
+ } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) {
+ // Try to load the bitmap from the assets directory
+ AssetManager am = getContext().getAssets();
+ InputStream imageInputStream = am.open(imagePath);
+ bitmap = BitmapFactory.decodeStream(imageInputStream);
+ }
+ }
+
+ PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY);
+ customPointerIcons.put(pointerType, customPointerIcon);
+ } catch (Exception e) {
+ // Reset the custom pointer icon
+ customPointerIcons.delete(pointerType);
+ }
+ }
+ }
+
/**
* Called from JNI to change the pointer icon
*/
@Keep
private void setPointerIcon(int pointerType) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType));
+ PointerIcon pointerIcon = customPointerIcons.get(pointerType);
+ if (pointerIcon == null) {
+ pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType);
+ }
+ setPointerIcon(pointerIcon);
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
index 2d19167817f8..dd6d5180c553 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
@@ -79,6 +79,9 @@ class DirectoryAccessHandler(context: Context) {
private val assetsDirAccess = AssetsDirectoryAccess(context)
private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
+ fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
+ fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)
+
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
index 357008ca666c..984bf607d000 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -46,7 +46,7 @@ class FileAccessHandler(val context: Context) {
private val TAG = FileAccessHandler::class.java.simpleName
private const val FILE_NOT_FOUND_ERROR_ID = -1
- private const val INVALID_FILE_ID = 0
+ internal const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
@@ -96,13 +96,17 @@ class FileAccessHandler(val context: Context) {
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
fun fileOpen(path: String?, modeFlags: Int): Int {
+ val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
+ return fileOpen(path, accessFlag)
+ }
+
+ internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int {
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
if (storageScope == StorageScope.UNKNOWN) {
return INVALID_FILE_ID
}
try {
- val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
files.put(++lastFileId, dataAccess)
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
new file mode 100644
index 000000000000..e78e310ef1c0
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
@@ -0,0 +1,122 @@
+/**************************************************************************/
+/* BenchmarkUtils.kt */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+@file:JvmName("BenchmarkUtils")
+
+package org.godotengine.godot.utils
+
+import android.os.Build
+import android.os.SystemClock
+import android.os.Trace
+import android.util.Log
+import org.godotengine.godot.BuildConfig
+import org.godotengine.godot.io.file.FileAccessFlags
+import org.godotengine.godot.io.file.FileAccessHandler
+import org.json.JSONObject
+import java.nio.ByteBuffer
+import java.util.concurrent.ConcurrentSkipListMap
+
+/**
+ * Contains benchmark related utilities methods
+ */
+private const val TAG = "GodotBenchmark"
+
+var useBenchmark = false
+var benchmarkFile = ""
+
+private val startBenchmarkFrom = ConcurrentSkipListMap()
+private val benchmarkTracker = ConcurrentSkipListMap()
+
+/**
+ * Start measuring and tracing the execution of a given section of code using the given label.
+ *
+ * Must be followed by a call to [endBenchmarkMeasure].
+ *
+ * Note: Only enabled on 'editorDev' build variant.
+ */
+fun beginBenchmarkMeasure(label: String) {
+ if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
+ return
+ }
+ startBenchmarkFrom[label] = SystemClock.elapsedRealtime()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Trace.beginAsyncSection(label, 0)
+ }
+}
+
+/**
+ * End measuring and tracing of the section of code with the given label.
+ *
+ * Must be preceded by a call [beginBenchmarkMeasure]
+ *
+ * Note: Only enabled on 'editorDev' build variant.
+ */
+fun endBenchmarkMeasure(label: String) {
+ if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
+ return
+ }
+ val startTime = startBenchmarkFrom[label] ?: return
+ val total = SystemClock.elapsedRealtime() - startTime
+ benchmarkTracker[label] = total / 1000.0
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Trace.endAsyncSection(label, 0)
+ }
+}
+
+/**
+ * Dump the benchmark measurements.
+ * If [filepath] is valid, the data is also written in json format to the specified file.
+ *
+ * Note: Only enabled on 'editorDev' build variant.
+ */
+@JvmOverloads
+fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) {
+ if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
+ return
+ }
+ if (!useBenchmark) {
+ return
+ }
+
+ val printOut =
+ benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n")
+ Log.i(TAG, "BENCHMARK:\n$printOut")
+
+ if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
+ val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
+ if (fileId != FileAccessHandler.INVALID_FILE_ID) {
+ val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4)
+ fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray()))
+ fileAccessHandler.fileClose(fileId)
+ }
+ }
+}
diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp
index 249a82bf5b65..c25171b5840d 100644
--- a/platform/android/java_godot_view_wrapper.cpp
+++ b/platform/android/java_godot_view_wrapper.cpp
@@ -40,6 +40,7 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) {
int android_device_api_level = android_get_device_api_level();
if (android_device_api_level >= __ANDROID_API_N__) {
+ _configure_pointer_icon = env->GetMethodID(_cls, "configurePointerIcon", "(ILjava/lang/String;FF)V");
_set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V");
}
if (android_device_api_level >= __ANDROID_API_O__) {
@@ -49,7 +50,7 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) {
}
bool GodotJavaViewWrapper::can_update_pointer_icon() const {
- return _set_pointer_icon != nullptr;
+ return _configure_pointer_icon != nullptr && _set_pointer_icon != nullptr;
}
bool GodotJavaViewWrapper::can_capture_pointer() const {
@@ -74,6 +75,16 @@ void GodotJavaViewWrapper::release_pointer_capture() {
}
}
+void GodotJavaViewWrapper::configure_pointer_icon(int pointer_type, const String &image_path, const Vector2 &p_hotspot) {
+ if (_configure_pointer_icon != nullptr) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ jstring jImagePath = env->NewStringUTF(image_path.utf8().get_data());
+ env->CallVoidMethod(_godot_view, _configure_pointer_icon, pointer_type, jImagePath, p_hotspot.x, p_hotspot.y);
+ }
+}
+
void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) {
if (_set_pointer_icon != nullptr) {
JNIEnv *env = get_jni_env();
diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h
index de71eddaeb89..e6bacdf9159a 100644
--- a/platform/android/java_godot_view_wrapper.h
+++ b/platform/android/java_godot_view_wrapper.h
@@ -31,6 +31,7 @@
#ifndef JAVA_GODOT_VIEW_WRAPPER_H
#define JAVA_GODOT_VIEW_WRAPPER_H
+#include "core/math/vector2.h"
#include
#include
@@ -44,6 +45,8 @@ class GodotJavaViewWrapper {
jmethodID _request_pointer_capture = 0;
jmethodID _release_pointer_capture = 0;
+
+ jmethodID _configure_pointer_icon = 0;
jmethodID _set_pointer_icon = 0;
public:
@@ -54,6 +57,8 @@ class GodotJavaViewWrapper {
void request_pointer_capture();
void release_pointer_capture();
+
+ void configure_pointer_icon(int pointer_type, const String &image_path, const Vector2 &p_hotspot);
void set_pointer_icon(int pointer_type);
~GodotJavaViewWrapper();
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 9d845835c407..c04dc6785add 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -82,6 +82,9 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotView;");
+ _begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V");
+ _end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V");
+ _dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V");
// get some Activity method pointers...
_get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
@@ -386,3 +389,30 @@ int GodotJavaWrapper::create_new_godot_instance(List args) {
return 0;
}
}
+
+void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) {
+ if (_begin_benchmark_measure) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
+ env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label);
+ }
+}
+
+void GodotJavaWrapper::end_benchmark_measure(const String &p_label) {
+ if (_end_benchmark_measure) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
+ env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label);
+ }
+}
+
+void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) {
+ if (_dump_benchmark) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data());
+ env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file);
+ }
+}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 5cdde5c12501..5e49074f7627 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -76,6 +76,9 @@ class GodotJavaWrapper {
jmethodID _get_class_loader = nullptr;
jmethodID _create_new_godot_instance = nullptr;
jmethodID _get_render_view = nullptr;
+ jmethodID _begin_benchmark_measure = nullptr;
+ jmethodID _end_benchmark_measure = nullptr;
+ jmethodID _dump_benchmark = nullptr;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -113,6 +116,9 @@ class GodotJavaWrapper {
void vibrate(int p_duration_ms);
String get_input_fallback_mapping();
int create_new_godot_instance(List args);
+ void begin_benchmark_measure(const String &p_label);
+ void end_benchmark_measure(const String &p_label);
+ void dump_benchmark(const String &benchmark_file);
};
#endif // JAVA_GODOT_WRAPPER_H
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 5b1ed9dc284f..223cdcd020d6 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -302,11 +302,11 @@ OS::MouseMode OS_Android::get_mouse_mode() const {
return mouse_mode;
}
-void OS_Android::set_cursor_shape(CursorShape p_shape) {
+void OS_Android::_set_cursor_shape_helper(CursorShape p_shape, bool force) {
if (!godot_java->get_godot_view()->can_update_pointer_icon()) {
return;
}
- if (cursor_shape == p_shape) {
+ if (cursor_shape == p_shape && !force) {
return;
}
@@ -316,6 +316,19 @@ void OS_Android::set_cursor_shape(CursorShape p_shape) {
}
}
+void OS_Android::set_cursor_shape(CursorShape p_shape) {
+ _set_cursor_shape_helper(p_shape);
+}
+
+void OS_Android::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {
+ String cursor_path = p_cursor.is_valid() ? p_cursor->get_path() : "";
+ if (!cursor_path.empty()) {
+ cursor_path = ProjectSettings::get_singleton()->globalize_path(cursor_path);
+ }
+ godot_java->get_godot_view()->configure_pointer_icon(android_cursors[cursor_shape], cursor_path, p_hotspot);
+ _set_cursor_shape_helper(p_shape, true);
+}
+
OS::CursorShape OS_Android::get_cursor_shape() const {
return cursor_shape;
}
@@ -669,6 +682,27 @@ String OS_Android::get_config_path() const {
return get_user_data_dir().plus_file("config");
}
+void OS_Android::benchmark_begin_measure(const String &p_what) {
+#ifdef TOOLS_ENABLED
+ godot_java->begin_benchmark_measure(p_what);
+#endif
+}
+
+void OS_Android::benchmark_end_measure(const String &p_what) {
+#ifdef TOOLS_ENABLED
+ godot_java->end_benchmark_measure(p_what);
+#endif
+}
+
+void OS_Android::benchmark_dump() {
+#ifdef TOOLS_ENABLED
+ if (!is_use_benchmark_set()) {
+ return;
+ }
+ godot_java->dump_benchmark(get_benchmark_file());
+#endif
+}
+
bool OS_Android::_check_internal_feature_support(const String &p_feature) {
if (p_feature == "mobile") {
//TODO support etc2 only if GLES3 driver is selected
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index 41bb956eed1c..5bb2a5571092 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -163,8 +163,11 @@ class OS_Android : public OS_Unix {
virtual void hide_virtual_keyboard();
virtual int get_virtual_keyboard_height() const;
+ void _set_cursor_shape_helper(CursorShape p_shape, bool force = false);
virtual void set_cursor_shape(CursorShape p_shape);
virtual CursorShape get_cursor_shape() const;
+ virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot);
+
virtual void set_mouse_mode(MouseMode p_mode);
virtual MouseMode get_mouse_mode() const;
@@ -217,6 +220,10 @@ class OS_Android : public OS_Unix {
virtual Error execute(const String &p_path, const List &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false);
virtual Error kill(const ProcessID &p_pid);
+ virtual void benchmark_begin_measure(const String &p_what);
+ virtual void benchmark_end_measure(const String &p_what);
+ virtual void benchmark_dump();
+
virtual bool _check_internal_feature_support(const String &p_feature);
OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);
~OS_Android();
From dd6c213dac4d029fb04e4d92cd025b274a452419 Mon Sep 17 00:00:00 2001
From: lawnjelly
Date: Mon, 10 Apr 2023 16:19:32 +0100
Subject: [PATCH 04/10] Fix Polygon2D skinned bounds (for culling)
The bound Rect2 was previously incorrect because bone transforms need to be applied to verts in bone space, rather than local space. This was previously resulting in skinned Polygon2Ds being incorrectly culled.
---
doc/classes/VisualServer.xml | 8 ++
scene/2d/polygon_2d.cpp | 10 ++
servers/visual/rasterizer.cpp | 170 ++++++++++++++++++++++++
servers/visual/rasterizer.h | 87 +++++-------
servers/visual/visual_server_canvas.cpp | 32 +++++
servers/visual/visual_server_canvas.h | 2 +
servers/visual/visual_server_raster.h | 2 +
servers/visual/visual_server_wrap_mt.h | 2 +
servers/visual_server.cpp | 1 +
servers/visual_server.h | 10 ++
10 files changed, 269 insertions(+), 55 deletions(-)
diff --git a/doc/classes/VisualServer.xml b/doc/classes/VisualServer.xml
index abe44ca3b902..cd0460b974ae 100644
--- a/doc/classes/VisualServer.xml
+++ b/doc/classes/VisualServer.xml
@@ -728,6 +728,14 @@
Modulates all colors in the given canvas.
+
+
+
+
+ Returns the bounding rectangle for a canvas item in local space, as calculated by the renderer. This bound is used internally for culling.
+ [b]Warning:[/b] This function is intended for debugging in the editor, and will pass through and return a zero [Rect2] in exported projects.
+
+
diff --git a/scene/2d/polygon_2d.cpp b/scene/2d/polygon_2d.cpp
index 7d68bb56a5c2..669e93fc0f69 100644
--- a/scene/2d/polygon_2d.cpp
+++ b/scene/2d/polygon_2d.cpp
@@ -111,6 +111,16 @@ void Polygon2D::_notification(int p_what) {
if (skeleton_node) {
VS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), skeleton_node->get_skeleton());
new_skeleton_id = skeleton_node->get_instance_id();
+
+ // Sync the offset transform between the Polygon2D and the skeleton.
+ // This is needed for accurate culling in VisualServer.
+ Transform2D global_xform_skel = skeleton_node->get_global_transform();
+ Transform2D global_xform_poly = get_global_transform();
+
+ // find the difference
+ Transform2D global_xform_offset = global_xform_skel.affine_inverse() * global_xform_poly;
+ VS::get_singleton()->canvas_item_set_skeleton_relative_xform(get_canvas_item(), global_xform_offset);
+
} else {
VS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), RID());
}
diff --git a/servers/visual/rasterizer.cpp b/servers/visual/rasterizer.cpp
index cc10eb9dc6d6..696366537d01 100644
--- a/servers/visual/rasterizer.cpp
+++ b/servers/visual/rasterizer.cpp
@@ -561,3 +561,173 @@ int RasterizerStorage::multimesh_get_visible_instances(RID p_multimesh) const {
AABB RasterizerStorage::multimesh_get_aabb(RID p_multimesh) const {
return _multimesh_get_aabb(p_multimesh);
}
+
+// The bone bounds are determined by rigging,
+// as such they can be calculated as a one off operation,
+// rather than each call to get_rect().
+void RasterizerCanvas::Item::precalculate_polygon_bone_bounds(const Item::CommandPolygon &p_polygon) const {
+ p_polygon.skinning_data->dirty = false;
+ p_polygon.skinning_data->untransformed_bound = Rect2(Vector2(), Vector2(-1, -1)); // negative means unused.
+
+ int num_points = p_polygon.points.size();
+ const Point2 *pp = &p_polygon.points[0];
+
+ // Calculate bone AABBs.
+ int bone_count = RasterizerStorage::base_singleton->skeleton_get_bone_count(skeleton);
+
+ // Get some local aliases
+ LocalVector &active_bounds = p_polygon.skinning_data->active_bounds;
+ LocalVector &active_bone_ids = p_polygon.skinning_data->active_bone_ids;
+ active_bounds.clear();
+ active_bone_ids.clear();
+
+ // Uses dynamic allocation, but shouldn't happen very often.
+ // If happens more often, use alloca.
+ LocalVector bone_to_active_bone_mapping;
+ bone_to_active_bone_mapping.resize(bone_count);
+
+ for (int n = 0; n < bone_count; n++) {
+ bone_to_active_bone_mapping[n] = -1;
+ }
+
+ const Transform2D &item_transform = skinning_data->skeleton_relative_xform;
+
+ bool some_were_untransformed = false;
+
+ for (int n = 0; n < num_points; n++) {
+ Point2 p = pp[n];
+ bool bone_space = false;
+ float total_weight = 0;
+
+ for (int k = 0; k < 4; k++) {
+ int bone_id = p_polygon.bones[n * 4 + k];
+ float w = p_polygon.weights[n * 4 + k];
+ if (w == 0) {
+ continue;
+ }
+ total_weight += w;
+
+ // Ensure the point is in "bone space" / rigged space.
+ if (!bone_space) {
+ bone_space = true;
+ p = item_transform.xform(p);
+ }
+
+ // get the active bone, or create a new active bone
+ DEV_ASSERT(bone_id < bone_count);
+ int32_t &active_bone = bone_to_active_bone_mapping[bone_id];
+ if (active_bone != -1) {
+ active_bounds[active_bone].expand_to(p);
+ } else {
+ // Increment the number of active bones stored.
+ active_bone = active_bounds.size();
+ active_bounds.resize(active_bone + 1);
+ active_bone_ids.resize(active_bone + 1);
+
+ // First point for the bone
+ DEV_ASSERT(bone_id <= UINT16_MAX);
+ active_bone_ids[active_bone] = bone_id;
+ active_bounds[active_bone] = Rect2(p, Vector2(0.00001, 0.00001));
+ }
+ }
+
+ // If some points were not rigged,
+ // we want to add them directly to an "untransformed bound",
+ // and merge this with the skinned bound later.
+ // Also do this if a point is not FULLY weighted,
+ // because the untransformed position is still having an influence.
+ if (!bone_space || (total_weight < 0.99f)) {
+ if (some_were_untransformed) {
+ p_polygon.skinning_data->untransformed_bound.expand_to(pp[n]);
+ } else {
+ // First point
+ some_were_untransformed = true;
+ p_polygon.skinning_data->untransformed_bound = Rect2(pp[n], Vector2());
+ }
+ }
+ }
+}
+
+Rect2 RasterizerCanvas::Item::calculate_polygon_bounds(const Item::CommandPolygon &p_polygon) const {
+ int num_points = p_polygon.points.size();
+
+ // If there is no skeleton, or the bones data is invalid...
+ // Note : Can we check the second more efficiently? by checking if polygon.skinning_data is set perhaps?
+ if (skeleton == RID() || !(num_points && p_polygon.bones.size() == num_points * 4 && p_polygon.weights.size() == p_polygon.bones.size())) {
+ // With no skeleton, all points are untransformed.
+ Rect2 r;
+ const Point2 *pp = &p_polygon.points[0];
+ r.position = pp[0];
+
+ for (int n = 1; n < num_points; n++) {
+ r.expand_to(pp[n]);
+ }
+
+ return r;
+ }
+
+ // Skinned skeleton is present.
+ ERR_FAIL_COND_V_MSG(!skinning_data, Rect2(), "Skinned Polygon2D must have skeleton_relative_xform set for correct culling.");
+
+ // Ensure the polygon skinning data is created...
+ // (This isn't stored on every polygon to save memory).
+ if (!p_polygon.skinning_data) {
+ p_polygon.skinning_data = memnew(Item::CommandPolygon::SkinningData);
+ }
+
+ Item::CommandPolygon::SkinningData &pdata = *p_polygon.skinning_data;
+
+ // This should only occur when rigging has changed.
+ // Usually a one off in games.
+ if (pdata.dirty) {
+ precalculate_polygon_bone_bounds(p_polygon);
+ }
+
+ // We only deal with the precalculated ACTIVE bone AABBs using the skeleton.
+ // (No need to bother with bones that are unused for this poly.)
+ int num_active_bones = pdata.active_bounds.size();
+ if (!num_active_bones) {
+ return pdata.untransformed_bound;
+ }
+
+ // No need to make a dynamic allocation here in 99% of cases.
+ Rect2 *bptr = nullptr;
+ LocalVector bone_aabbs;
+ if (num_active_bones <= 1024) {
+ bptr = (Rect2 *)alloca(sizeof(Rect2) * num_active_bones);
+ } else {
+ bone_aabbs.resize(num_active_bones);
+ bptr = bone_aabbs.ptr();
+ }
+
+ // Copy across the precalculated bone bounds.
+ memcpy(bptr, pdata.active_bounds.ptr(), sizeof(Rect2) * num_active_bones);
+
+ const Transform2D &item_transform_inv = skinning_data->skeleton_relative_xform_inv;
+
+ Rect2 aabb;
+ bool first_bone = true;
+
+ for (int n = 0; n < num_active_bones; n++) {
+ int bone_id = pdata.active_bone_ids[n];
+ const Transform2D &mtx = RasterizerStorage::base_singleton->skeleton_bone_get_transform_2d(skeleton, bone_id);
+ Rect2 baabb = mtx.xform(bptr[n]);
+
+ if (first_bone) {
+ aabb = baabb;
+ first_bone = false;
+ } else {
+ aabb = aabb.merge(baabb);
+ }
+ }
+
+ // Transform the polygon AABB back into local space from bone space.
+ aabb = item_transform_inv.xform(aabb);
+
+ // If some were untransformed...
+ if (pdata.untransformed_bound.size.x >= 0) {
+ return pdata.untransformed_bound.merge(aabb);
+ }
+
+ return aabb;
+}
diff --git a/servers/visual/rasterizer.h b/servers/visual/rasterizer.h
index f58b22128111..b2471f922a16 100644
--- a/servers/visual/rasterizer.h
+++ b/servers/visual/rasterizer.h
@@ -892,10 +892,24 @@ class RasterizerCanvas {
bool antialiased;
bool antialiasing_use_indices;
+ struct SkinningData {
+ bool dirty = true;
+ LocalVector active_bounds;
+ LocalVector active_bone_ids;
+ Rect2 untransformed_bound;
+ };
+ mutable SkinningData *skinning_data = nullptr;
+
CommandPolygon() {
type = TYPE_POLYGON;
count = 0;
}
+ virtual ~CommandPolygon() {
+ if (skinning_data) {
+ memdelete(skinning_data);
+ skinning_data = nullptr;
+ }
+ }
};
struct CommandMesh : public Command {
@@ -968,6 +982,12 @@ class RasterizerCanvas {
Item *next;
+ struct SkinningData {
+ Transform2D skeleton_relative_xform;
+ Transform2D skeleton_relative_xform_inv;
+ };
+ SkinningData *skinning_data = nullptr;
+
struct CopyBackBuffer {
Rect2 rect;
Rect2 screen_rect;
@@ -984,6 +1004,11 @@ class RasterizerCanvas {
Rect2 global_rect_cache;
+ private:
+ Rect2 calculate_polygon_bounds(const Item::CommandPolygon &p_polygon) const;
+ void precalculate_polygon_bone_bounds(const Item::CommandPolygon &p_polygon) const;
+
+ public:
const Rect2 &get_rect() const {
if (custom_rect) {
return rect;
@@ -1068,61 +1093,8 @@ class RasterizerCanvas {
} break;
case Item::Command::TYPE_POLYGON: {
const Item::CommandPolygon *polygon = static_cast(c);
- int l = polygon->points.size();
- const Point2 *pp = &polygon->points[0];
- r.position = pp[0];
- for (int j = 1; j < l; j++) {
- r.expand_to(pp[j]);
- }
-
- if (skeleton != RID()) {
- // calculate bone AABBs
- int bone_count = RasterizerStorage::base_singleton->skeleton_get_bone_count(skeleton);
-
- Vector bone_aabbs;
- bone_aabbs.resize(bone_count);
- Rect2 *bptr = bone_aabbs.ptrw();
-
- for (int j = 0; j < bone_count; j++) {
- bptr[j].size = Vector2(-1, -1); //negative means unused
- }
- if (l && polygon->bones.size() == l * 4 && polygon->weights.size() == polygon->bones.size()) {
- for (int j = 0; j < l; j++) {
- Point2 p = pp[j];
- for (int k = 0; k < 4; k++) {
- int idx = polygon->bones[j * 4 + k];
- float w = polygon->weights[j * 4 + k];
- if (w == 0) {
- continue;
- }
-
- if (bptr[idx].size.x < 0) {
- //first
- bptr[idx] = Rect2(p, Vector2(0.00001, 0.00001));
- } else {
- bptr[idx].expand_to(p);
- }
- }
- }
-
- Rect2 aabb;
- bool first_bone = true;
- for (int j = 0; j < bone_count; j++) {
- Transform2D mtx = RasterizerStorage::base_singleton->skeleton_bone_get_transform_2d(skeleton, j);
- Rect2 baabb = mtx.xform(bone_aabbs[j]);
-
- if (first_bone) {
- aabb = baabb;
- first_bone = false;
- } else {
- aabb = aabb.merge(baabb);
- }
- }
-
- r = r.merge(aabb);
- }
- }
-
+ DEV_ASSERT(polygon);
+ r = calculate_polygon_bounds(*polygon);
} break;
case Item::Command::TYPE_MESH: {
const Item::CommandMesh *mesh = static_cast(c);
@@ -1188,6 +1160,11 @@ class RasterizerCanvas {
final_clip_owner = nullptr;
material_owner = nullptr;
light_masked = false;
+
+ if (skinning_data) {
+ memdelete(skinning_data);
+ skinning_data = nullptr;
+ }
}
Item() {
light_mask = 1;
diff --git a/servers/visual/visual_server_canvas.cpp b/servers/visual/visual_server_canvas.cpp
index 7cf80e136d5b..ba013ab5c518 100644
--- a/servers/visual/visual_server_canvas.cpp
+++ b/servers/visual/visual_server_canvas.cpp
@@ -917,6 +917,38 @@ void VisualServerCanvas::canvas_item_set_z_as_relative_to_parent(RID p_item, boo
canvas_item->z_relative = p_enable;
}
+Rect2 VisualServerCanvas::_debug_canvas_item_get_rect(RID p_item) {
+ Item *canvas_item = canvas_item_owner.getornull(p_item);
+ ERR_FAIL_COND_V(!canvas_item, Rect2());
+ return canvas_item->get_rect();
+}
+
+void VisualServerCanvas::canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform) {
+ Item *canvas_item = canvas_item_owner.getornull(p_item);
+ ERR_FAIL_COND(!canvas_item);
+
+ if (!canvas_item->skinning_data) {
+ canvas_item->skinning_data = memnew(Item::SkinningData);
+ }
+ canvas_item->skinning_data->skeleton_relative_xform = p_relative_xform;
+ canvas_item->skinning_data->skeleton_relative_xform_inv = p_relative_xform.affine_inverse();
+
+ // Set any Polygon2Ds pre-calced bone bounds to dirty.
+ for (int n = 0; n < canvas_item->commands.size(); n++) {
+ Item::Command *c = canvas_item->commands[n];
+ if (c->type == Item::Command::TYPE_POLYGON) {
+ Item::CommandPolygon *polygon = static_cast(c);
+
+ // Make sure skinning data is present.
+ if (!polygon->skinning_data) {
+ polygon->skinning_data = memnew(Item::CommandPolygon::SkinningData);
+ }
+
+ polygon->skinning_data->dirty = true;
+ }
+ }
+}
+
void VisualServerCanvas::canvas_item_attach_skeleton(RID p_item, RID p_skeleton) {
Item *canvas_item = canvas_item_owner.getornull(p_item);
ERR_FAIL_COND(!canvas_item);
diff --git a/servers/visual/visual_server_canvas.h b/servers/visual/visual_server_canvas.h
index 774c8fdd7efe..7e25dc55a15a 100644
--- a/servers/visual/visual_server_canvas.h
+++ b/servers/visual/visual_server_canvas.h
@@ -208,6 +208,8 @@ class VisualServerCanvas {
void canvas_item_set_z_as_relative_to_parent(RID p_item, bool p_enable);
void canvas_item_set_copy_to_backbuffer(RID p_item, bool p_enable, const Rect2 &p_rect);
void canvas_item_attach_skeleton(RID p_item, RID p_skeleton);
+ void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform);
+ Rect2 _debug_canvas_item_get_rect(RID p_item);
void canvas_item_clear(RID p_item);
void canvas_item_set_draw_index(RID p_item, int p_index);
diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h
index c43501e7f4f2..4f05d1faf772 100644
--- a/servers/visual/visual_server_raster.h
+++ b/servers/visual/visual_server_raster.h
@@ -718,6 +718,8 @@ class VisualServerRaster : public VisualServer {
BIND2(canvas_item_set_z_as_relative_to_parent, RID, bool)
BIND3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &)
BIND2(canvas_item_attach_skeleton, RID, RID)
+ BIND2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
+ BIND1R(Rect2, _debug_canvas_item_get_rect, RID)
BIND1(canvas_item_clear, RID)
BIND2(canvas_item_set_draw_index, RID, int)
diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h
index 3d814c2db399..2cb6adfa3af2 100644
--- a/servers/visual/visual_server_wrap_mt.h
+++ b/servers/visual/visual_server_wrap_mt.h
@@ -619,6 +619,8 @@ class VisualServerWrapMT : public VisualServer {
FUNC2(canvas_item_set_z_as_relative_to_parent, RID, bool)
FUNC3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &)
FUNC2(canvas_item_attach_skeleton, RID, RID)
+ FUNC2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
+ FUNC1R(Rect2, _debug_canvas_item_get_rect, RID)
FUNC1(canvas_item_clear, RID)
FUNC2(canvas_item_set_draw_index, RID, int)
diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp
index 9cd7ef3bc2c6..d3d2b078497a 100644
--- a/servers/visual_server.cpp
+++ b/servers/visual_server.cpp
@@ -2202,6 +2202,7 @@ void VisualServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("canvas_item_set_draw_index", "item", "index"), &VisualServer::canvas_item_set_draw_index);
ClassDB::bind_method(D_METHOD("canvas_item_set_material", "item", "material"), &VisualServer::canvas_item_set_material);
ClassDB::bind_method(D_METHOD("canvas_item_set_use_parent_material", "item", "enabled"), &VisualServer::canvas_item_set_use_parent_material);
+ ClassDB::bind_method(D_METHOD("debug_canvas_item_get_rect", "item"), &VisualServer::debug_canvas_item_get_rect);
ClassDB::bind_method(D_METHOD("canvas_light_create"), &VisualServer::canvas_light_create);
ClassDB::bind_method(D_METHOD("canvas_light_attach_to_canvas", "light", "canvas"), &VisualServer::canvas_light_attach_to_canvas);
ClassDB::bind_method(D_METHOD("canvas_light_set_enabled", "light", "enabled"), &VisualServer::canvas_light_set_enabled);
diff --git a/servers/visual_server.h b/servers/visual_server.h
index ba1e05345159..07a2fb553cc0 100644
--- a/servers/visual_server.h
+++ b/servers/visual_server.h
@@ -1052,6 +1052,16 @@ class VisualServer : public Object {
virtual void canvas_item_set_copy_to_backbuffer(RID p_item, bool p_enable, const Rect2 &p_rect) = 0;
virtual void canvas_item_attach_skeleton(RID p_item, RID p_skeleton) = 0;
+ virtual void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform) = 0;
+
+ Rect2 debug_canvas_item_get_rect(RID p_item) {
+#ifdef TOOLS_ENABLED
+ return _debug_canvas_item_get_rect(p_item);
+#else
+ return Rect2();
+#endif
+ }
+ virtual Rect2 _debug_canvas_item_get_rect(RID p_item) = 0;
virtual void canvas_item_clear(RID p_item) = 0;
virtual void canvas_item_set_draw_index(RID p_item, int p_index) = 0;
From efb4a0a6d523f09c7a847c5c6f7a94b5aa9219b9 Mon Sep 17 00:00:00 2001
From: belzecue <1931303+belzecue@users.noreply.github.com>
Date: Wed, 19 Apr 2023 01:13:58 +0800
Subject: [PATCH 05/10] Backport from Godot 4 - Fix RigidDynamicBody gaining
momentum with bounce #55313
---
servers/physics/body_pair_sw.cpp | 18 +++---------------
servers/physics/body_sw.cpp | 3 +++
servers/physics/body_sw.h | 6 ++++++
servers/physics_2d/body_2d_sw.cpp | 3 +++
servers/physics_2d/body_2d_sw.h | 6 ++++++
servers/physics_2d/body_pair_2d_sw.cpp | 7 +++----
6 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/servers/physics/body_pair_sw.cpp b/servers/physics/body_pair_sw.cpp
index f0cc8ae68118..cd07a783667d 100644
--- a/servers/physics/body_pair_sw.cpp
+++ b/servers/physics/body_pair_sw.cpp
@@ -34,18 +34,6 @@
#include "core/os/os.h"
#include "space_sw.h"
-/*
-#define NO_ACCUMULATE_IMPULSES
-#define NO_SPLIT_IMPULSES
-
-#define NO_FRICTION
-*/
-
-#define NO_TANGENTIALS
-/* BODY PAIR */
-
-//#define ALLOWED_PENETRATION 0.01
-#define RELAXATION_TIMESTEPS 3
#define MIN_VELOCITY 0.0001
#define MAX_BIAS_ROTATION (Math_PI / 8)
@@ -335,9 +323,9 @@ bool BodyPairSW::setup(real_t p_step) {
c.bounce = combine_bounce(A, B);
if (c.bounce) {
- Vector3 crA = A->get_angular_velocity().cross(c.rA);
- Vector3 crB = B->get_angular_velocity().cross(c.rB);
- Vector3 dv = B->get_linear_velocity() + crB - A->get_linear_velocity() - crA;
+ Vector3 crA = A->get_prev_angular_velocity().cross(c.rA);
+ Vector3 crB = B->get_prev_angular_velocity().cross(c.rB);
+ Vector3 dv = B->get_prev_linear_velocity() + crB - A->get_prev_linear_velocity() - crA;
//normal impule
c.bounce = c.bounce * dv.dot(c.normal);
}
diff --git a/servers/physics/body_sw.cpp b/servers/physics/body_sw.cpp
index e52cd13998c7..601f6fdd73fe 100644
--- a/servers/physics/body_sw.cpp
+++ b/servers/physics/body_sw.cpp
@@ -509,6 +509,9 @@ void BodySW::integrate_forces(real_t p_step) {
area_linear_damp=damp_area->get_linear_damp();
*/
+ prev_linear_velocity = linear_velocity;
+ prev_angular_velocity = angular_velocity;
+
Vector3 motion;
bool do_motion = false;
diff --git a/servers/physics/body_sw.h b/servers/physics/body_sw.h
index 42eb7e263e25..a3ba2849efaf 100644
--- a/servers/physics/body_sw.h
+++ b/servers/physics/body_sw.h
@@ -44,6 +44,9 @@ class BodySW : public CollisionObjectSW {
Vector3 linear_velocity;
Vector3 angular_velocity;
+ Vector3 prev_linear_velocity;
+ Vector3 prev_angular_velocity;
+
Vector3 biased_linear_velocity;
Vector3 biased_angular_velocity;
real_t mass;
@@ -215,6 +218,9 @@ class BodySW : public CollisionObjectSW {
_FORCE_INLINE_ void set_angular_velocity(const Vector3 &p_velocity) { angular_velocity = p_velocity; }
_FORCE_INLINE_ Vector3 get_angular_velocity() const { return angular_velocity; }
+ _FORCE_INLINE_ Vector3 get_prev_linear_velocity() const { return prev_linear_velocity; }
+ _FORCE_INLINE_ Vector3 get_prev_angular_velocity() const { return prev_angular_velocity; }
+
_FORCE_INLINE_ const Vector3 &get_biased_linear_velocity() const { return biased_linear_velocity; }
_FORCE_INLINE_ const Vector3 &get_biased_angular_velocity() const { return biased_angular_velocity; }
diff --git a/servers/physics_2d/body_2d_sw.cpp b/servers/physics_2d/body_2d_sw.cpp
index 28de7927189d..556ba9c2a8cf 100644
--- a/servers/physics_2d/body_2d_sw.cpp
+++ b/servers/physics_2d/body_2d_sw.cpp
@@ -461,6 +461,9 @@ void Body2DSW::integrate_forces(real_t p_step) {
area_linear_damp=damp_area->get_linear_damp();
*/
+ prev_linear_velocity = linear_velocity;
+ prev_angular_velocity = angular_velocity;
+
Vector2 motion;
bool do_motion = false;
diff --git a/servers/physics_2d/body_2d_sw.h b/servers/physics_2d/body_2d_sw.h
index 7bd301936ffc..e0c4e31d5c74 100644
--- a/servers/physics_2d/body_2d_sw.h
+++ b/servers/physics_2d/body_2d_sw.h
@@ -47,6 +47,9 @@ class Body2DSW : public CollisionObject2DSW {
Vector2 linear_velocity;
real_t angular_velocity;
+ Vector2 prev_linear_velocity;
+ real_t prev_angular_velocity = 0.0;
+
real_t linear_damp;
real_t angular_damp;
real_t gravity_scale;
@@ -195,6 +198,9 @@ class Body2DSW : public CollisionObject2DSW {
_FORCE_INLINE_ void set_angular_velocity(real_t p_velocity) { angular_velocity = p_velocity; }
_FORCE_INLINE_ real_t get_angular_velocity() const { return angular_velocity; }
+ _FORCE_INLINE_ Vector2 get_prev_linear_velocity() const { return prev_linear_velocity; }
+ _FORCE_INLINE_ real_t get_prev_angular_velocity() const { return prev_angular_velocity; }
+
_FORCE_INLINE_ void set_biased_linear_velocity(const Vector2 &p_velocity) { biased_linear_velocity = p_velocity; }
_FORCE_INLINE_ Vector2 get_biased_linear_velocity() const { return biased_linear_velocity; }
diff --git a/servers/physics_2d/body_pair_2d_sw.cpp b/servers/physics_2d/body_pair_2d_sw.cpp
index 61488e924380..cebb87aa6b07 100644
--- a/servers/physics_2d/body_pair_2d_sw.cpp
+++ b/servers/physics_2d/body_pair_2d_sw.cpp
@@ -32,7 +32,6 @@
#include "collision_solver_2d_sw.h"
#include "space_2d_sw.h"
-#define POSITION_CORRECTION
#define ACCUMULATE_IMPULSES
void BodyPair2DSW::_add_contact(const Vector2 &p_point_A, const Vector2 &p_point_B, void *p_self) {
@@ -423,9 +422,9 @@ bool BodyPair2DSW::setup(real_t p_step) {
c.bounce = combine_bounce(A, B);
if (c.bounce) {
- Vector2 crA(-A->get_angular_velocity() * c.rA.y, A->get_angular_velocity() * c.rA.x);
- Vector2 crB(-B->get_angular_velocity() * c.rB.y, B->get_angular_velocity() * c.rB.x);
- Vector2 dv = B->get_linear_velocity() + crB - A->get_linear_velocity() - crA;
+ Vector2 crA(-A->get_prev_angular_velocity() * c.rA.y, A->get_prev_angular_velocity() * c.rA.x);
+ Vector2 crB(-B->get_prev_angular_velocity() * c.rB.y, B->get_prev_angular_velocity() * c.rB.x);
+ Vector2 dv = B->get_prev_linear_velocity() + crB - A->get_prev_linear_velocity() - crA;
c.bounce = c.bounce * dv.dot(c.normal);
}
From 837db0b051dc8bbd5e6d8b754bdbeb07ef9f64b8 Mon Sep 17 00:00:00 2001
From: Fredia Huya-Kouadio
Date: Fri, 21 Apr 2023 13:19:45 -0700
Subject: [PATCH 06/10] Downgrade android gradle plugin to version 7.2.1.
Version 7.3.0 changes the build layout which causes updates to the generated shared libraries to not be picked up.
---
platform/android/java/app/config.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 72f253e62336..1710887b2ff8 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -1,5 +1,5 @@
ext.versions = [
- androidGradlePlugin: '7.3.0',
+ androidGradlePlugin: '7.2.1',
compileSdk : 33,
minSdk : 19, // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
targetSdk : 33, // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
From de20011cbe151ee7ab4427d22a2a6b8bc6d6d2a1 Mon Sep 17 00:00:00 2001
From: Fredia Huya-Kouadio
Date: Tue, 4 Apr 2023 18:05:01 -0700
Subject: [PATCH 07/10] Make tab's close button responsive to touch taps
---
scene/gui/tabs.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/scene/gui/tabs.cpp b/scene/gui/tabs.cpp
index 3ab4b33387e8..b32f25325694 100644
--- a/scene/gui/tabs.cpp
+++ b/scene/gui/tabs.cpp
@@ -193,12 +193,14 @@ void Tabs::_gui_input(const Ref &p_event) {
for (int i = offset; i <= max_drawn_tab; i++) {
if (tabs[i].rb_rect.has_point(pos)) {
rb_pressing = true;
+ _update_hover();
update();
return;
}
if (tabs[i].cb_rect.has_point(pos) && (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current))) {
cb_pressing = true;
+ _update_hover();
update();
return;
}
From e31b1a76f83537d996040df2fe912bfd5a33d9ad Mon Sep 17 00:00:00 2001
From: Fredia Huya-Kouadio
Date: Wed, 5 Apr 2023 15:59:29 -0700
Subject: [PATCH 08/10] Make `EditorPropertyLayersGrid` responsive to touch
taps
---
editor/editor_properties.cpp | 100 ++++++++++++++++++++---------------
1 file changed, 56 insertions(+), 44 deletions(-)
diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp
index 320e56cb80b9..6172f2a1999d 100644
--- a/editor/editor_properties.cpp
+++ b/editor/editor_properties.cpp
@@ -796,53 +796,72 @@ class EditorPropertyLayersGrid : public Control {
return String();
}
- void _gui_input(const Ref &p_ev) {
- const Ref mm = p_ev;
- if (mm.is_valid()) {
- bool expand_was_hovered = expand_hovered;
- expand_hovered = expand_rect.has_point(mm->get_position());
- if (expand_hovered != expand_was_hovered) {
- update();
- }
+ void _update_hovered(const Vector2 &p_position) {
+ bool expand_was_hovered = expand_hovered;
+ expand_hovered = expand_rect.has_point(p_position);
+ if (expand_hovered != expand_was_hovered) {
+ update();
+ }
- if (!expand_hovered) {
- for (int i = 0; i < flag_rects.size(); i++) {
- if (flag_rects[i].has_point(mm->get_position())) {
- // Used to highlight the hovered flag in the layers grid.
- hovered_index = i;
- update();
- return;
- }
+ if (!expand_hovered) {
+ for (int i = 0; i < flag_rects.size(); i++) {
+ if (flag_rects[i].has_point(p_position)) {
+ // Used to highlight the hovered flag in the layers grid.
+ hovered_index = i;
+ update();
+ return;
}
}
+ }
+
+ // Remove highlight when no square is hovered.
+ if (hovered_index != -1) {
+ hovered_index = -1;
+ update();
+ }
+ }
+
+ void _on_hover_exit() {
+ if (expand_hovered) {
+ expand_hovered = false;
+ update();
+ }
+ if (hovered_index != -1) {
+ hovered_index = -1;
+ update();
+ }
+ }
- // Remove highlight when no square is hovered.
- if (hovered_index != -1) {
- hovered_index = -1;
- update();
+ void _update_flag() {
+ if (hovered_index >= 0) {
+ // Toggle the flag.
+ // We base our choice on the hovered flag, so that it always matches the hovered flag.
+ if (value & (1 << hovered_index)) {
+ value &= ~(1 << hovered_index);
+ } else {
+ value |= (1 << hovered_index);
}
+ emit_signal("flag_changed", value);
+ update();
+ } else if (expand_hovered) {
+ expanded = !expanded;
+ minimum_size_changed();
+ update();
+ }
+ }
+
+ void _gui_input(const Ref &p_ev) {
+ const Ref mm = p_ev;
+ if (mm.is_valid()) {
+ _update_hovered(mm->get_position());
return;
}
const Ref mb = p_ev;
if (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && mb->is_pressed()) {
- if (hovered_index >= 0) {
- // Toggle the flag.
- // We base our choice on the hovered flag, so that it always matches the hovered flag.
- if (value & (1 << hovered_index)) {
- value &= ~(1 << hovered_index);
- } else {
- value |= (1 << hovered_index);
- }
-
- emit_signal("flag_changed", value);
- update();
- } else if (expand_hovered) {
- expanded = !expanded;
- minimum_size_changed();
- update();
- }
+ _update_hovered(mb->get_position());
+ _update_flag();
}
}
@@ -974,14 +993,7 @@ class EditorPropertyLayersGrid : public Control {
} break;
case NOTIFICATION_MOUSE_EXIT: {
- if (expand_hovered) {
- expand_hovered = false;
- update();
- }
- if (hovered_index != -1) {
- hovered_index = -1;
- update();
- }
+ _on_hover_exit();
} break;
default:
From 392f65fe099646740c9dd84b0e8d5ed14cb7b7e7 Mon Sep 17 00:00:00 2001
From: Haoyu Qiu
Date: Tue, 25 Apr 2023 09:27:26 +0800
Subject: [PATCH 09/10] Fix inconsistent file dialog settings usage
---
editor/editor_node.cpp | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 32a0a451048a..965adf426918 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -557,6 +557,10 @@ void EditorNode::_notification(int p_what) {
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
scene_tabs->set_tab_close_display_policy((bool(EDITOR_GET("interface/scene_tabs/always_show_close_button")) ? Tabs::CLOSE_BUTTON_SHOW_ALWAYS : Tabs::CLOSE_BUTTON_SHOW_ACTIVE_ONLY));
+ FileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files"));
+ EditorFileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files"));
+ EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EditorSettings::get_singleton()->get("filesystem/file_dialog/display_mode").operator int());
+
theme = create_custom_theme(theme_base->get_theme());
theme_base->set_theme(theme);
From d6672d8fe4e5d179eca9fad59e5865b487e71567 Mon Sep 17 00:00:00 2001
From: Haoyu Qiu
Date: Tue, 25 Apr 2023 10:15:07 +0800
Subject: [PATCH 10/10] Make create folder popup support nested folders
---
editor/directory_create_dialog.cpp | 178 +++++++++++++++++++++++++++++
editor/directory_create_dialog.h | 68 +++++++++++
editor/filesystem_dock.cpp | 55 ++-------
editor/filesystem_dock.h | 5 +-
4 files changed, 257 insertions(+), 49 deletions(-)
create mode 100644 editor/directory_create_dialog.cpp
create mode 100644 editor/directory_create_dialog.h
diff --git a/editor/directory_create_dialog.cpp b/editor/directory_create_dialog.cpp
new file mode 100644
index 000000000000..7b918ea5530a
--- /dev/null
+++ b/editor/directory_create_dialog.cpp
@@ -0,0 +1,178 @@
+/**************************************************************************/
+/* directory_create_dialog.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 "directory_create_dialog.h"
+
+#include "core/os/dir_access.h"
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "scene/gui/box_container.h"
+#include "scene/gui/label.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/panel_container.h"
+
+static String sanitize_input(const String &p_path) {
+ String path = p_path.strip_edges();
+ if (path.ends_with("/")) {
+ path = path.left(path.length() - 1);
+ }
+ return path;
+}
+
+String DirectoryCreateDialog::_validate_path(const String &p_path) const {
+ if (p_path.empty()) {
+ return TTR("Folder name cannot be empty.");
+ }
+
+ const Vector parts = p_path.split("/");
+ for (int i = 0; i < parts.size(); i++) {
+ const String part = parts[i];
+ if (part.empty()) {
+ return TTR("Folder name cannot be empty.");
+ }
+ if (p_path.find("\\") != -1 || p_path.find(":") != -1 || p_path.find("*") != -1 ||
+ p_path.find("|") != -1 || p_path.find(">") != -1 || p_path.ends_with(".") || p_path.ends_with(" ")) {
+ return TTR("Folder name contains invalid characters.");
+ }
+ }
+
+ DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ da->change_dir(base_dir);
+ if (da->file_exists(p_path)) {
+ return TTR("File with that name already exists.");
+ }
+ if (da->dir_exists(p_path)) {
+ return TTR("Folder with that name already exists.");
+ }
+
+ return String();
+}
+
+void DirectoryCreateDialog::_on_dir_path_changed(const String &p_text) {
+ const String path = sanitize_input(p_text);
+ const String error = _validate_path(path);
+
+ if (error.empty()) {
+ status_label->add_color_override("font_color", get_color("success_color", "Editor"));
+
+ if (path.find("/") != -1) {
+ status_label->set_text(TTR("Using slashes in folder names will create subfolders recursively."));
+ } else {
+ status_label->set_text(TTR("Folder name is valid."));
+ }
+ } else {
+ status_label->add_color_override("font_color", get_color("error_color", "Editor"));
+ status_label->set_text(error);
+ }
+
+ get_ok()->set_disabled(!error.empty());
+}
+
+void DirectoryCreateDialog::ok_pressed() {
+ const String path = sanitize_input(dir_path->get_text());
+
+ // The OK button should be disabled if the path is invalid, but just in case.
+ const String error = _validate_path(path);
+ ERR_FAIL_COND_MSG(!error.empty(), error);
+
+ Error err;
+ DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+
+ err = da->change_dir(base_dir);
+ ERR_FAIL_COND_MSG(err != OK, "Cannot open directory '" + base_dir + "'.");
+
+ print_verbose("Making folder " + path + " in " + base_dir);
+ err = da->make_dir_recursive(path);
+
+ if (err == OK) {
+ emit_signal("dir_created");
+ } else {
+ EditorNode::get_singleton()->show_warning(TTR("Could not create folder."));
+ }
+ hide();
+}
+
+void DirectoryCreateDialog::_post_popup() {
+ ConfirmationDialog::_post_popup();
+ label->set_text(vformat(TTR("Create new folder in %s:"), base_dir));
+ minimum_size_changed();
+ dir_path->grab_focus();
+}
+
+void DirectoryCreateDialog::config(const String &p_base_dir) {
+ base_dir = p_base_dir;
+ label->set_text(""); // Set the correct text later in _post_popup to avoid GH-47005.
+ dir_path->set_text("new folder");
+ dir_path->select_all();
+ _on_dir_path_changed(dir_path->get_text());
+}
+
+void DirectoryCreateDialog::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_on_dir_path_changed"), &DirectoryCreateDialog::_on_dir_path_changed);
+
+ ADD_SIGNAL(MethodInfo("dir_created"));
+}
+
+void DirectoryCreateDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ status_panel->add_style_override("panel", get_stylebox("bg", "Tree"));
+ } break;
+ }
+}
+
+DirectoryCreateDialog::DirectoryCreateDialog() {
+ set_title(TTR("Create Folder"));
+ set_custom_minimum_size(Size2i(480, 0) * EDSCALE);
+
+ VBoxContainer *vb = memnew(VBoxContainer);
+ add_child(vb);
+
+ label = memnew(Label);
+ label->set_autowrap(true);
+ vb->add_child(label);
+
+ dir_path = memnew(LineEdit);
+ dir_path->connect("text_changed", this, "_on_dir_path_changed");
+ vb->add_child(dir_path);
+ register_text_enter(dir_path);
+
+ Control *spacing = memnew(Control);
+ spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
+ vb->add_child(spacing);
+
+ status_panel = memnew(PanelContainer);
+ status_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ vb->add_child(status_panel);
+
+ status_label = memnew(Label);
+ status_label->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ status_panel->add_child(status_label);
+}
diff --git a/editor/directory_create_dialog.h b/editor/directory_create_dialog.h
new file mode 100644
index 000000000000..dc4ff2d934b8
--- /dev/null
+++ b/editor/directory_create_dialog.h
@@ -0,0 +1,68 @@
+/**************************************************************************/
+/* directory_create_dialog.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 DIRECTORY_CREATE_DIALOG_H
+#define DIRECTORY_CREATE_DIALOG_H
+
+#include "scene/gui/dialogs.h"
+
+class Label;
+class LineEdit;
+class PanelContainer;
+
+class DirectoryCreateDialog : public ConfirmationDialog {
+ GDCLASS(DirectoryCreateDialog, ConfirmationDialog);
+
+ String base_dir;
+
+ Label *label = nullptr;
+ LineEdit *dir_path = nullptr;
+
+ PanelContainer *status_panel = nullptr;
+ Label *status_label = nullptr;
+
+ String _validate_path(const String &p_path) const;
+
+ void _on_dir_path_changed(const String &p_text);
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_what);
+
+ virtual void ok_pressed();
+ virtual void _post_popup();
+
+public:
+ void config(const String &p_base_dir);
+
+ DirectoryCreateDialog();
+};
+
+#endif // DIRECTORY_CREATE_DIALOG_H
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 8866559a4c48..566206f83c0c 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -37,6 +37,7 @@
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "core/project_settings.h"
+#include "editor/directory_create_dialog.h"
#include "editor/scene_create_dialog.h"
#include "editor_feature_profile.h"
#include "editor_node.h"
@@ -1344,38 +1345,6 @@ void FileSystemDock::_save_scenes_after_move(const Map &p_rename
editor->save_scene_list(new_filenames);
}
-void FileSystemDock::_make_dir_confirm() {
- String dir_name = make_dir_dialog_text->get_text().strip_edges();
-
- if (dir_name.length() == 0) {
- EditorNode::get_singleton()->show_warning(TTR("No name provided."));
- return;
- } else if (dir_name.find("/") != -1 || dir_name.find("\\") != -1 || dir_name.find(":") != -1 || dir_name.find("*") != -1 ||
- dir_name.find("|") != -1 || dir_name.find(">") != -1 || dir_name.ends_with(".") || dir_name.ends_with(" ")) {
- EditorNode::get_singleton()->show_warning(TTR("Provided name contains invalid characters."));
- return;
- }
-
- String directory = path;
- if (!directory.ends_with("/")) {
- directory = directory.get_base_dir();
- }
- print_verbose("Making folder " + dir_name + " in " + directory);
- DirAccess *da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
- Error err = da->change_dir(directory);
- if (err == OK) {
- err = da->make_dir(dir_name);
- }
- memdelete(da);
-
- if (err == OK) {
- print_verbose("FileSystem: calling rescan.");
- _rescan();
- } else {
- EditorNode::get_singleton()->show_warning(TTR("Could not create folder."));
- }
-}
-
void FileSystemDock::_make_scene_confirm() {
const String scene_path = make_scene_dialog->get_scene_path();
@@ -1877,10 +1846,12 @@ void FileSystemDock::_file_option(int p_option, const Vector &p_selected
} break;
case FILE_NEW_FOLDER: {
- make_dir_dialog_text->set_text("new folder");
- make_dir_dialog_text->select_all();
- make_dir_dialog->popup_centered_minsize(Size2(250, 80) * EDSCALE);
- make_dir_dialog_text->grab_focus();
+ String directory = path;
+ if (!directory.ends_with("/")) {
+ directory = directory.get_base_dir();
+ }
+ make_dir_dialog->config(directory);
+ make_dir_dialog->popup_centered(Size2(1, 1));
} break;
case FILE_NEW_SCENE: {
@@ -2744,7 +2715,6 @@ void FileSystemDock::_bind_methods() {
ClassDB::bind_method(D_METHOD("_bw_history"), &FileSystemDock::_bw_history);
ClassDB::bind_method(D_METHOD("_fs_changed"), &FileSystemDock::_fs_changed);
ClassDB::bind_method(D_METHOD("_tree_multi_selected"), &FileSystemDock::_tree_multi_selected);
- ClassDB::bind_method(D_METHOD("_make_dir_confirm"), &FileSystemDock::_make_dir_confirm);
ClassDB::bind_method(D_METHOD("_make_scene_confirm"), &FileSystemDock::_make_scene_confirm);
ClassDB::bind_method(D_METHOD("_resource_created"), &FileSystemDock::_resource_created);
ClassDB::bind_method(D_METHOD("_move_operation_confirm", "to_path", "overwrite"), &FileSystemDock::_move_operation_confirm, DEFVAL(false));
@@ -2959,16 +2929,9 @@ FileSystemDock::FileSystemDock(EditorNode *p_editor) {
duplicate_dialog->register_text_enter(duplicate_dialog_text);
duplicate_dialog->connect("confirmed", this, "_duplicate_operation_confirm");
- make_dir_dialog = memnew(ConfirmationDialog);
- make_dir_dialog->set_title(TTR("Create Folder"));
- VBoxContainer *make_folder_dialog_vb = memnew(VBoxContainer);
- make_dir_dialog->add_child(make_folder_dialog_vb);
-
- make_dir_dialog_text = memnew(LineEdit);
- make_folder_dialog_vb->add_margin_child(TTR("Name:"), make_dir_dialog_text);
+ make_dir_dialog = memnew(DirectoryCreateDialog);
add_child(make_dir_dialog);
- make_dir_dialog->register_text_enter(make_dir_dialog_text);
- make_dir_dialog->connect("confirmed", this, "_make_dir_confirm");
+ make_dir_dialog->connect("dir_created", this, "_rescan");
make_scene_dialog = memnew(SceneCreateDialog);
add_child(make_scene_dialog);
diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h
index e1ad11e84a2f..0031a531d2d0 100644
--- a/editor/filesystem_dock.h
+++ b/editor/filesystem_dock.h
@@ -56,6 +56,7 @@
class EditorNode;
class SceneCreateDialog;
+class DirectoryCreateDialog;
class FileSystemDock : public VBoxContainer {
GDCLASS(FileSystemDock, VBoxContainer);
@@ -153,8 +154,7 @@ class FileSystemDock : public VBoxContainer {
LineEdit *rename_dialog_text;
ConfirmationDialog *duplicate_dialog;
LineEdit *duplicate_dialog_text;
- ConfirmationDialog *make_dir_dialog;
- LineEdit *make_dir_dialog_text;
+ DirectoryCreateDialog *make_dir_dialog = nullptr;
ConfirmationDialog *overwrite_dialog;
SceneCreateDialog *make_scene_dialog = nullptr;
ScriptCreateDialog *make_script_dialog;
@@ -229,7 +229,6 @@ class FileSystemDock : public VBoxContainer {
void _folder_removed(String p_folder);
void _resource_created() const;
- void _make_dir_confirm();
void _make_scene_confirm();
void _rename_operation_confirm();
void _duplicate_operation_confirm();