summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/linux_builds.yml6
-rw-r--r--core/config/engine.cpp39
-rw-r--r--core/config/engine.h10
-rw-r--r--core/io/resource_loader.cpp2
-rw-r--r--core/math/basis.cpp7
-rw-r--r--core/math/basis.h2
-rw-r--r--core/math/transform_3d.cpp8
-rw-r--r--core/math/transform_3d.h4
-rw-r--r--core/os/os.cpp54
-rw-r--r--core/os/os.h15
-rw-r--r--core/os/thread_safe.cpp46
-rw-r--r--core/os/thread_safe.h3
-rw-r--r--core/register_core_types.cpp7
-rw-r--r--core/variant/variant_call.cpp11
-rw-r--r--doc/classes/Basis.xml2
-rw-r--r--doc/classes/Control.xml2
-rw-r--r--doc/classes/Node3D.xml5
-rw-r--r--doc/classes/PathFollow2D.xml3
-rw-r--r--doc/classes/PathFollow3D.xml3
-rw-r--r--doc/classes/Transform3D.xml2
-rw-r--r--doc/classes/Vector3.xml20
-rw-r--r--drivers/gles3/shaders/sky.glsl2
-rw-r--r--drivers/gles3/storage/material_storage.cpp4
-rw-r--r--drivers/vulkan/rendering_device_vulkan.cpp8
-rw-r--r--editor/editor_fonts.cpp3
-rw-r--r--editor/editor_help.cpp2
-rw-r--r--editor/editor_node.cpp18
-rw-r--r--editor/editor_node.h3
-rw-r--r--editor/editor_themes.cpp7
-rw-r--r--editor/filesystem_dock.cpp4
-rw-r--r--editor/icons/BoxOccluder3D.svg2
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp12
-rw-r--r--editor/plugins/path_3d_editor_plugin.cpp4
-rw-r--r--editor/plugins/tiles/atlas_merging_dialog.cpp3
-rw-r--r--editor/plugins/tiles/tile_map_editor.cpp1
-rw-r--r--editor/plugins/tiles/tile_set_editor.cpp1
-rw-r--r--editor/plugins/tiles/tiles_editor_plugin.cpp1
-rw-r--r--editor/project_manager.cpp3
-rw-r--r--editor/register_editor_types.cpp8
-rw-r--r--main/main.cpp75
-rw-r--r--misc/dist/shell/_godot.zsh-completion4
-rw-r--r--misc/dist/shell/godot.bash-completion4
-rw-r--r--misc/dist/shell/godot.fish4
-rw-r--r--misc/extension_api_validation/4.0-stable.expected219
-rwxr-xr-xmisc/scripts/validate_extension_api.sh60
-rw-r--r--modules/gltf/structures/gltf_skin.cpp2
-rw-r--r--modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml13
-rw-r--r--modules/multiplayer/doc_classes/SceneMultiplayer.xml6
-rw-r--r--modules/multiplayer/doc_classes/SceneReplicationConfig.xml16
-rw-r--r--modules/multiplayer/editor/replication_editor.cpp104
-rw-r--r--modules/multiplayer/editor/replication_editor.h2
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.cpp113
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.h22
-rw-r--r--modules/multiplayer/scene_multiplayer.cpp23
-rw-r--r--modules/multiplayer/scene_multiplayer.h6
-rw-r--r--modules/multiplayer/scene_replication_config.cpp35
-rw-r--r--modules/multiplayer/scene_replication_config.h6
-rw-r--r--modules/multiplayer/scene_replication_interface.cpp182
-rw-r--r--modules/multiplayer/scene_replication_interface.h15
-rw-r--r--platform/android/SCsub5
-rw-r--r--platform/android/detect.py3
-rw-r--r--platform/android/java/app/config.gradle2
-rw-r--r--platform/android/java/build.gradle58
-rw-r--r--platform/android/java/editor/build.gradle92
-rw-r--r--platform/android/java/editor/src/debug/res/values/strings.xml4
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt3
-rw-r--r--platform/android/java/lib/build.gradle23
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java61
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt122
-rw-r--r--platform/android/java_godot_wrapper.cpp30
-rw-r--r--platform/android/java_godot_wrapper.h6
-rw-r--r--platform/android/os_android.cpp21
-rw-r--r--platform/android/os_android.h4
-rw-r--r--platform/windows/vulkan_context_win.cpp4
-rw-r--r--scene/2d/path_2d.cpp32
-rw-r--r--scene/2d/path_2d.h12
-rw-r--r--scene/3d/node_3d.cpp15
-rw-r--r--scene/3d/node_3d.h4
-rw-r--r--scene/3d/path_3d.cpp29
-rw-r--r--scene/3d/path_3d.h11
-rw-r--r--scene/gui/control.h2
-rw-r--r--scene/gui/rich_text_label.cpp60
-rw-r--r--scene/gui/rich_text_label.h1
-rw-r--r--scene/gui/tree.cpp19
-rw-r--r--scene/main/node.h8
-rw-r--r--scene/resources/curve.cpp8
-rw-r--r--servers/rendering/renderer_canvas_cull.cpp244
-rw-r--r--servers/rendering/renderer_rd/renderer_scene_render_rd.cpp7
-rw-r--r--servers/rendering/renderer_rd/shaders/environment/sky.glsl2
-rw-r--r--tests/scene/test_curve_3d.h6
93 files changed, 1633 insertions, 529 deletions
diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml
index dc313359ab..aa30e5a55e 100644
--- a/.github/workflows/linux_builds.yml
+++ b/.github/workflows/linux_builds.yml
@@ -32,6 +32,7 @@ jobs:
build-mono: true
proj-conv: true
artifact: true
+ compat: true
- name: Editor with doubles and GCC sanitizers (target=editor, tests=yes, dev_build=yes, precision=double, use_asan=yes, use_ubsan=yes, linker=gold)
cache-name: linux-editor-double-sanitizers
@@ -202,6 +203,11 @@ jobs:
scons target=template_debug dev_build=yes
cd ../..
+ - name: Check for GDExtension compatibility
+ if: ${{ matrix.compat }}
+ run: |
+ ./misc/scripts/validate_extension_api.sh "${{ matrix.bin }}" || true # don't fail the CI for now
+
- name: Prepare artifact
if: ${{ matrix.artifact }}
run: |
diff --git a/core/config/engine.cpp b/core/config/engine.cpp
index 814ad3d076..7fdea7d1aa 100644
--- a/core/config/engine.cpp
+++ b/core/config/engine.cpp
@@ -33,9 +33,7 @@
#include "core/authors.gen.h"
#include "core/config/project_settings.h"
#include "core/donors.gen.h"
-#include "core/io/json.h"
#include "core/license.gen.h"
-#include "core/os/os.h"
#include "core/variant/typed_array.h"
#include "core/version.h"
@@ -319,43 +317,6 @@ Engine::Engine() {
singleton = this;
}
-void Engine::startup_begin() {
- startup_benchmark_total_from = OS::get_singleton()->get_ticks_usec();
-}
-
-void Engine::startup_benchmark_begin_measure(const String &p_what) {
- startup_benchmark_section = p_what;
- startup_benchmark_from = OS::get_singleton()->get_ticks_usec();
-}
-void Engine::startup_benchmark_end_measure() {
- uint64_t total = OS::get_singleton()->get_ticks_usec() - startup_benchmark_from;
- double total_f = double(total) / double(1000000);
-
- startup_benchmark_json[startup_benchmark_section] = total_f;
-}
-
-void Engine::startup_dump(const String &p_to_file) {
- uint64_t total = OS::get_singleton()->get_ticks_usec() - startup_benchmark_total_from;
- double total_f = double(total) / double(1000000);
- startup_benchmark_json["total_time"] = total_f;
-
- if (!p_to_file.is_empty()) {
- Ref<FileAccess> f = FileAccess::open(p_to_file, FileAccess::WRITE);
- if (f.is_valid()) {
- Ref<JSON> json;
- json.instantiate();
- f->store_string(json->stringify(startup_benchmark_json, "\t", false, true));
- }
- } else {
- List<Variant> keys;
- startup_benchmark_json.get_key_list(&keys);
- print_line("STARTUP BENCHMARK:");
- for (const Variant &K : keys) {
- print_line("\t-", K, ": ", startup_benchmark_json[K], +" sec.");
- }
- }
-}
-
Engine::Singleton::Singleton(const StringName &p_name, Object *p_ptr, const StringName &p_class_name) :
name(p_name),
ptr(p_ptr),
diff --git a/core/config/engine.h b/core/config/engine.h
index 52408f4be1..5ea653ba6c 100644
--- a/core/config/engine.h
+++ b/core/config/engine.h
@@ -83,11 +83,6 @@ private:
String write_movie_path;
String shader_cache_path;
- Dictionary startup_benchmark_json;
- String startup_benchmark_section;
- uint64_t startup_benchmark_from = 0;
- uint64_t startup_benchmark_total_from = 0;
-
public:
static Engine *get_singleton();
@@ -163,11 +158,6 @@ public:
bool is_validation_layers_enabled() const;
int32_t get_gpu_index() const;
- void startup_begin();
- void startup_benchmark_begin_measure(const String &p_what);
- void startup_benchmark_end_measure();
- void startup_dump(const String &p_to_file);
-
Engine();
virtual ~Engine() {}
};
diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp
index e9f812ab1c..ac1870fe88 100644
--- a/core/io/resource_loader.cpp
+++ b/core/io/resource_loader.cpp
@@ -302,6 +302,7 @@ void ResourceLoader::_thread_load_function(void *p_userdata) {
if (!Thread::is_main_thread()) {
mq_override = memnew(CallQueue);
MessageQueue::set_thread_singleton_override(mq_override);
+ set_current_thread_safe_for_nodes(true);
}
} else {
DEV_ASSERT(load_task.dependent_path.is_empty());
@@ -357,6 +358,7 @@ void ResourceLoader::_thread_load_function(void *p_userdata) {
if (load_nesting == 0 && mq_override) {
memdelete(mq_override);
+ set_current_thread_safe_for_nodes(false);
}
}
diff --git a/core/math/basis.cpp b/core/math/basis.cpp
index bfd902c7e2..1481dbc32e 100644
--- a/core/math/basis.cpp
+++ b/core/math/basis.cpp
@@ -1016,12 +1016,15 @@ void Basis::rotate_sh(real_t *p_values) {
p_values[8] = d4 * s_scale_dst4;
}
-Basis Basis::looking_at(const Vector3 &p_target, const Vector3 &p_up) {
+Basis Basis::looking_at(const Vector3 &p_target, const Vector3 &p_up, bool p_use_model_front) {
#ifdef MATH_CHECKS
ERR_FAIL_COND_V_MSG(p_target.is_zero_approx(), Basis(), "The target vector can't be zero.");
ERR_FAIL_COND_V_MSG(p_up.is_zero_approx(), Basis(), "The up vector can't be zero.");
#endif
- Vector3 v_z = -p_target.normalized();
+ Vector3 v_z = p_target.normalized();
+ if (!p_use_model_front) {
+ v_z = -v_z;
+ }
Vector3 v_x = p_up.cross(v_z);
#ifdef MATH_CHECKS
ERR_FAIL_COND_V_MSG(v_x.is_zero_approx(), Basis(), "The target vector and up vector can't be parallel to each other.");
diff --git a/core/math/basis.h b/core/math/basis.h
index bbc1d40469..1a68bee686 100644
--- a/core/math/basis.h
+++ b/core/math/basis.h
@@ -217,7 +217,7 @@ struct _NO_DISCARD_ Basis {
operator Quaternion() const { return get_quaternion(); }
- static Basis looking_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0));
+ static Basis looking_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0), bool p_use_model_front = false);
Basis(const Quaternion &p_quaternion) { set_quaternion(p_quaternion); };
Basis(const Quaternion &p_quaternion, const Vector3 &p_scale) { set_quaternion_scale(p_quaternion, p_scale); }
diff --git a/core/math/transform_3d.cpp b/core/math/transform_3d.cpp
index 8d497209f1..cdc94676c9 100644
--- a/core/math/transform_3d.cpp
+++ b/core/math/transform_3d.cpp
@@ -77,20 +77,20 @@ void Transform3D::rotate_basis(const Vector3 &p_axis, real_t p_angle) {
basis.rotate(p_axis, p_angle);
}
-Transform3D Transform3D::looking_at(const Vector3 &p_target, const Vector3 &p_up) const {
+Transform3D Transform3D::looking_at(const Vector3 &p_target, const Vector3 &p_up, bool p_use_model_front) const {
#ifdef MATH_CHECKS
ERR_FAIL_COND_V_MSG(origin.is_equal_approx(p_target), Transform3D(), "The transform's origin and target can't be equal.");
#endif
Transform3D t = *this;
- t.basis = Basis::looking_at(p_target - origin, p_up);
+ t.basis = Basis::looking_at(p_target - origin, p_up, p_use_model_front);
return t;
}
-void Transform3D::set_look_at(const Vector3 &p_eye, const Vector3 &p_target, const Vector3 &p_up) {
+void Transform3D::set_look_at(const Vector3 &p_eye, const Vector3 &p_target, const Vector3 &p_up, bool p_use_model_front) {
#ifdef MATH_CHECKS
ERR_FAIL_COND_MSG(p_eye.is_equal_approx(p_target), "The eye and target vectors can't be equal.");
#endif
- basis = Basis::looking_at(p_target - p_eye, p_up);
+ basis = Basis::looking_at(p_target - p_eye, p_up, p_use_model_front);
origin = p_eye;
}
diff --git a/core/math/transform_3d.h b/core/math/transform_3d.h
index bf1b4cdb63..70141a3dbe 100644
--- a/core/math/transform_3d.h
+++ b/core/math/transform_3d.h
@@ -52,8 +52,8 @@ struct _NO_DISCARD_ Transform3D {
void rotate(const Vector3 &p_axis, real_t p_angle);
void rotate_basis(const Vector3 &p_axis, real_t p_angle);
- void set_look_at(const Vector3 &p_eye, const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0));
- Transform3D looking_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0)) const;
+ void set_look_at(const Vector3 &p_eye, const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0), bool p_use_model_front = false);
+ Transform3D looking_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0), bool p_use_model_front = false) const;
void scale(const Vector3 &p_scale);
Transform3D scaled(const Vector3 &p_scale) const;
diff --git a/core/os/os.cpp b/core/os/os.cpp
index 025dcfe982..5704ef7a40 100644
--- a/core/os/os.cpp
+++ b/core/os/os.cpp
@@ -34,6 +34,7 @@
#include "core/input/input.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
+#include "core/io/json.h"
#include "core/os/midi_driver.h"
#include "core/version_generated.gen.h"
@@ -589,6 +590,59 @@ OS::PreferredTextureFormat OS::get_preferred_texture_format() const {
#endif
}
+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.is_empty()) {
+ Ref<FileAccess> f = FileAccess::open(benchmark_file, FileAccess::WRITE);
+ if (f.is_valid()) {
+ Ref<JSON> json;
+ json.instantiate();
+ f->store_string(json->stringify(startup_benchmark_json, "\t", false, true));
+ }
+ } else {
+ List<Variant> keys;
+ startup_benchmark_json.get_key_list(&keys);
+ print_line("BENCHMARK:");
+ for (const Variant &K : keys) {
+ print_line("\t-", K, ": ", startup_benchmark_json[K], +" sec.");
+ }
+ }
+#endif
+}
+
OS::OS() {
singleton = this;
diff --git a/core/os/os.h b/core/os/os.h
index 09ed31b9ce..f2787d6381 100644
--- a/core/os/os.h
+++ b/core/os/os.h
@@ -76,6 +76,12 @@ class OS {
RemoteFilesystemClient default_rfs;
+ // For tracking benchmark data
+ bool use_benchmark = false;
+ String benchmark_file;
+ HashMap<String, uint64_t> start_benchmark_from;
+ Dictionary startup_benchmark_json;
+
protected:
void _set_logger(CompositeLogger *p_logger);
@@ -299,6 +305,15 @@ public:
virtual bool request_permissions() { return true; }
virtual Vector<String> get_granted_permissions() const { return Vector<String>(); }
+ // 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() {}
virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path);
diff --git a/core/os/thread_safe.cpp b/core/os/thread_safe.cpp
new file mode 100644
index 0000000000..96b7de8ed2
--- /dev/null
+++ b/core/os/thread_safe.cpp
@@ -0,0 +1,46 @@
+/**************************************************************************/
+/* thread_safe.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. */
+/**************************************************************************/
+
+#ifndef THREAD_SAFE_CPP
+#define THREAD_SAFE_CPP
+
+#include "thread_safe.h"
+
+static thread_local bool current_thread_safe_for_nodes = false;
+
+bool is_current_thread_safe_for_nodes() {
+ return current_thread_safe_for_nodes;
+}
+
+void set_current_thread_safe_for_nodes(bool p_safe) {
+ current_thread_safe_for_nodes = p_safe;
+}
+
+#endif // THREAD_SAFE_CPP
diff --git a/core/os/thread_safe.h b/core/os/thread_safe.h
index ac8734b6c1..042a0b7d98 100644
--- a/core/os/thread_safe.h
+++ b/core/os/thread_safe.h
@@ -38,4 +38,7 @@
#define _THREAD_SAFE_LOCK_ _thread_safe_.lock();
#define _THREAD_SAFE_UNLOCK_ _thread_safe_.unlock();
+bool is_current_thread_safe_for_nodes();
+void set_current_thread_safe_for_nodes(bool p_safe);
+
#endif // THREAD_SAFE_H
diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp
index b8b8119618..e3f69fa9e1 100644
--- a/core/register_core_types.cpp
+++ b/core/register_core_types.cpp
@@ -120,6 +120,7 @@ static ResourceUID *resource_uid = nullptr;
static bool _is_core_extensions_registered = false;
void register_core_types() {
+ OS::get_singleton()->benchmark_begin_measure("register_core_types");
//consistency check
static_assert(sizeof(Callable) <= 16);
@@ -294,6 +295,8 @@ void register_core_types() {
GDREGISTER_NATIVE_STRUCT(ScriptLanguageExtensionProfilingInfo, "StringName signature;uint64_t call_count;uint64_t total_time;uint64_t self_time");
worker_thread_pool = memnew(WorkerThreadPool);
+
+ OS::get_singleton()->benchmark_end_measure("register_core_types");
}
void register_core_settings() {
@@ -360,6 +363,8 @@ void unregister_core_extensions() {
}
void unregister_core_types() {
+ OS::get_singleton()->benchmark_begin_measure("unregister_core_types");
+
memdelete(gdextension_manager);
memdelete(resource_uid);
@@ -425,4 +430,6 @@ void unregister_core_types() {
ResourceCache::clear();
CoreStringNames::free();
StringName::cleanup();
+
+ OS::get_singleton()->benchmark_end_measure("unregister_core_types");
}
diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp
index 0a836c125a..dad9183216 100644
--- a/core/variant/variant_call.cpp
+++ b/core/variant/variant_call.cpp
@@ -2101,7 +2101,7 @@ static void _register_variant_builtin_methods() {
bind_method(Basis, is_equal_approx, sarray("b"), varray());
bind_method(Basis, is_finite, sarray(), varray());
bind_method(Basis, get_rotation_quaternion, sarray(), varray());
- bind_static_method(Basis, looking_at, sarray("target", "up"), varray(Vector3(0, 1, 0)));
+ bind_static_method(Basis, looking_at, sarray("target", "up", "use_model_front"), varray(Vector3(0, 1, 0), false));
bind_static_method(Basis, from_scale, sarray("scale"), varray());
bind_static_method(Basis, from_euler, sarray("euler", "order"), varray((int64_t)EulerOrder::YXZ));
@@ -2144,7 +2144,7 @@ static void _register_variant_builtin_methods() {
bind_method(Transform3D, scaled_local, sarray("scale"), varray());
bind_method(Transform3D, translated, sarray("offset"), varray());
bind_method(Transform3D, translated_local, sarray("offset"), varray());
- bind_method(Transform3D, looking_at, sarray("target", "up"), varray(Vector3(0, 1, 0)));
+ bind_method(Transform3D, looking_at, sarray("target", "up", "use_model_front"), varray(Vector3(0, 1, 0), false));
bind_method(Transform3D, interpolate_with, sarray("xform", "weight"), varray());
bind_method(Transform3D, is_equal_approx, sarray("xform"), varray());
bind_method(Transform3D, is_finite, sarray(), varray());
@@ -2532,6 +2532,13 @@ static void _register_variant_builtin_methods() {
_VariantCall::add_variant_constant(Variant::VECTOR3, "FORWARD", Vector3(0, 0, -1));
_VariantCall::add_variant_constant(Variant::VECTOR3, "BACK", Vector3(0, 0, 1));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_LEFT", Vector3(1, 0, 0));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_RIGHT", Vector3(-1, 0, 0));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_TOP", Vector3(0, 1, 0));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_BOTTOM", Vector3(0, -1, 0));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_FRONT", Vector3(0, 0, 1));
+ _VariantCall::add_variant_constant(Variant::VECTOR3, "MODEL_REAR", Vector3(0, 0, -1));
+
_VariantCall::add_constant(Variant::VECTOR4, "AXIS_X", Vector4::AXIS_X);
_VariantCall::add_constant(Variant::VECTOR4, "AXIS_Y", Vector4::AXIS_Y);
_VariantCall::add_constant(Variant::VECTOR4, "AXIS_Z", Vector4::AXIS_Z);
diff --git a/doc/classes/Basis.xml b/doc/classes/Basis.xml
index 53dde5a286..a769cfeabf 100644
--- a/doc/classes/Basis.xml
+++ b/doc/classes/Basis.xml
@@ -123,9 +123,11 @@
<return type="Basis" />
<param index="0" name="target" type="Vector3" />
<param index="1" name="up" type="Vector3" default="Vector3(0, 1, 0)" />
+ <param index="2" name="use_model_front" type="bool" default="false" />
<description>
Creates a Basis with a rotation such that the forward axis (-Z) points towards the [param target] position.
The up axis (+Y) points as close to the [param up] vector as possible while staying perpendicular to the forward axis. The resulting Basis is orthonormalized. The [param target] and [param up] vectors cannot be zero, and cannot be parallel to each other.
+ If [param use_model_front] is [code]true[/code], the +Z axis (asset front) is treated as forward (implies +X is left) and points toward the [param target] position. By default, the -Z axis (camera forward) is treated as forward (implies +X is right).
</description>
</method>
<method name="orthonormalized" qualifiers="const">
diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml
index 9a34bc4f99..36074639dc 100644
--- a/doc/classes/Control.xml
+++ b/doc/classes/Control.xml
@@ -75,7 +75,7 @@
[/codeblocks]
</description>
</method>
- <method name="_get_drag_data" qualifiers="virtual const">
+ <method name="_get_drag_data" qualifiers="virtual">
<return type="Variant" />
<param index="0" name="at_position" type="Vector2" />
<description>
diff --git a/doc/classes/Node3D.xml b/doc/classes/Node3D.xml
index b4857bacde..8247911a34 100644
--- a/doc/classes/Node3D.xml
+++ b/doc/classes/Node3D.xml
@@ -113,11 +113,13 @@
<return type="void" />
<param index="0" name="target" type="Vector3" />
<param index="1" name="up" type="Vector3" default="Vector3(0, 1, 0)" />
+ <param index="2" name="use_model_front" type="bool" default="false" />
<description>
- Rotates the node so that the local forward axis (-Z) points toward the [param target] position.
+ Rotates the node so that the local forward axis (-Z, [constant Vector3.FORWARD]) points toward the [param target] position.
The local up axis (+Y) points as close to the [param up] vector as possible while staying perpendicular to the local forward axis. The resulting transform is orthogonal, and the scale is preserved. Non-uniform scaling may not work correctly.
The [param target] position cannot be the same as the node's position, the [param up] vector cannot be zero, and the direction from the node's position to the [param target] vector cannot be parallel to the [param up] vector.
Operations take place in global space, which means that the node must be in the scene tree.
+ If [param use_model_front] is [code]true[/code], the +Z axis (asset front) is treated as forward (implies +X is left) and points toward the [param target] position. By default, the -Z axis (camera forward) is treated as forward (implies +X is right).
</description>
</method>
<method name="look_at_from_position">
@@ -125,6 +127,7 @@
<param index="0" name="position" type="Vector3" />
<param index="1" name="target" type="Vector3" />
<param index="2" name="up" type="Vector3" default="Vector3(0, 1, 0)" />
+ <param index="3" name="use_model_front" type="bool" default="false" />
<description>
Moves the node to the specified [param position], and then rotates the node to point toward the [param target] as per [method look_at]. Operations take place in global space.
</description>
diff --git a/doc/classes/PathFollow2D.xml b/doc/classes/PathFollow2D.xml
index 60118675b4..d958d4c14f 100644
--- a/doc/classes/PathFollow2D.xml
+++ b/doc/classes/PathFollow2D.xml
@@ -18,9 +18,6 @@
<member name="h_offset" type="float" setter="set_h_offset" getter="get_h_offset" default="0.0">
The node's offset along the curve.
</member>
- <member name="lookahead" type="float" setter="set_lookahead" getter="get_lookahead" default="4.0">
- How far to look ahead of the curve to calculate the tangent if the node is rotating. E.g. shorter lookaheads will lead to faster rotations.
- </member>
<member name="loop" type="bool" setter="set_loop" getter="has_loop" default="true">
If [code]true[/code], any offset outside the path's length will wrap around, instead of stopping at the ends. Use it for cyclic paths.
</member>
diff --git a/doc/classes/PathFollow3D.xml b/doc/classes/PathFollow3D.xml
index 41727a7bd8..8d4101df0b 100644
--- a/doc/classes/PathFollow3D.xml
+++ b/doc/classes/PathFollow3D.xml
@@ -43,6 +43,9 @@
<member name="tilt_enabled" type="bool" setter="set_tilt_enabled" getter="is_tilt_enabled" default="true">
If [code]true[/code], the tilt property of [Curve3D] takes effect.
</member>
+ <member name="use_model_front" type="bool" setter="set_use_model_front" getter="is_using_model_front" default="false">
+ If [code]true[/code], the node moves on the travel path with orienting the +Z axis as forward. See also [constant Vector3.FORWARD] and [constant Vector3.MODEL_FRONT].
+ </member>
<member name="v_offset" type="float" setter="set_v_offset" getter="get_v_offset" default="0.0">
The node's offset perpendicular to the curve.
</member>
diff --git a/doc/classes/Transform3D.xml b/doc/classes/Transform3D.xml
index fb5c8559b6..c01268779a 100644
--- a/doc/classes/Transform3D.xml
+++ b/doc/classes/Transform3D.xml
@@ -93,9 +93,11 @@
<return type="Transform3D" />
<param index="0" name="target" type="Vector3" />
<param index="1" name="up" type="Vector3" default="Vector3(0, 1, 0)" />
+ <param index="2" name="use_model_front" type="bool" default="false" />
<description>
Returns a copy of the transform rotated such that the forward axis (-Z) points towards the [param target] position.
The up axis (+Y) points as close to the [param up] vector as possible while staying perpendicular to the forward axis. The resulting transform is orthonormalized. The existing rotation, scale, and skew information from the original transform is discarded. The [param target] and [param up] vectors cannot be zero, cannot be parallel to each other, and are defined in global/parent space.
+ If [param use_model_front] is [code]true[/code], the +Z axis (asset front) is treated as forward (implies +X is left) and points toward the [param target] position. By default, the -Z axis (camera forward) is treated as forward (implies +X is right).
</description>
</method>
<method name="orthonormalized" qualifiers="const">
diff --git a/doc/classes/Vector3.xml b/doc/classes/Vector3.xml
index d55e31f7b7..511d84d24d 100644
--- a/doc/classes/Vector3.xml
+++ b/doc/classes/Vector3.xml
@@ -404,11 +404,29 @@
Down unit vector.
</constant>
<constant name="FORWARD" value="Vector3(0, 0, -1)">
- Forward unit vector. Represents the local direction of forward, and the global direction of north.
+ Forward unit vector. Represents the local direction of forward, and the global direction of north. Keep in mind that the forward direction for lights, cameras, etc is different from 3D assets like characters, which face towards the camera by convention. Use [constant Vector3.MODEL_FRONT] and similar constants when working in 3D asset space.
</constant>
<constant name="BACK" value="Vector3(0, 0, 1)">
Back unit vector. Represents the local direction of back, and the global direction of south.
</constant>
+ <constant name="MODEL_LEFT" value="Vector3(1, 0, 0)">
+ Unit vector pointing towards the left side of imported 3D assets.
+ </constant>
+ <constant name="MODEL_RIGHT" value="Vector3(-1, 0, 0)">
+ Unit vector pointing towards the right side of imported 3D assets.
+ </constant>
+ <constant name="MODEL_TOP" value="Vector3(0, 1, 0)">
+ Unit vector pointing towards the top side (up) of imported 3D assets.
+ </constant>
+ <constant name="MODEL_BOTTOM" value="Vector3(0, -1, 0)">
+ Unit vector pointing towards the bottom side (down) of imported 3D assets.
+ </constant>
+ <constant name="MODEL_FRONT" value="Vector3(0, 0, 1)">
+ Unit vector pointing towards the front side (facing forward) of imported 3D assets.
+ </constant>
+ <constant name="MODEL_REAR" value="Vector3(0, 0, -1)">
+ Unit vector pointing towards the rear side (back) of imported 3D assets.
+ </constant>
</constants>
<operators>
<operator name="operator !=">
diff --git a/drivers/gles3/shaders/sky.glsl b/drivers/gles3/shaders/sky.glsl
index 2455ffb8e2..191d873269 100644
--- a/drivers/gles3/shaders/sky.glsl
+++ b/drivers/gles3/shaders/sky.glsl
@@ -215,6 +215,6 @@ void main() {
frag_color.a = alpha;
#ifdef USE_DEBANDING
- frag_color.rgb += interleaved_gradient_noise(gl_FragCoord.xy);
+ frag_color.rgb += interleaved_gradient_noise(gl_FragCoord.xy) * luminance_multiplier;
#endif
}
diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp
index c7b2a715be..db74708214 100644
--- a/drivers/gles3/storage/material_storage.cpp
+++ b/drivers/gles3/storage/material_storage.cpp
@@ -3452,6 +3452,10 @@ void SceneShaderData::set_code(const String &p_code) {
blend_mode = BLEND_MODE_ALPHA_TO_COVERAGE;
}
+ if (blend_mode == BLEND_MODE_ADD || blend_mode == BLEND_MODE_SUB || blend_mode == BLEND_MODE_MUL) {
+ uses_blend_alpha = true; // Force alpha used because of blend.
+ }
+
valid = true;
}
diff --git a/drivers/vulkan/rendering_device_vulkan.cpp b/drivers/vulkan/rendering_device_vulkan.cpp
index 69d9baf910..a72252b3e1 100644
--- a/drivers/vulkan/rendering_device_vulkan.cpp
+++ b/drivers/vulkan/rendering_device_vulkan.cpp
@@ -3655,7 +3655,7 @@ VkRenderPass RenderingDeviceVulkan::_render_pass_create(const Vector<AttachmentF
} else {
description.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
- description.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Don't care what is there.
+ description.finalLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Don't care what is there.
// TODO: What does this mean about the next usage (and thus appropriate dependency masks.
}
} break;
@@ -5939,10 +5939,10 @@ Vector<uint8_t> RenderingDeviceVulkan::buffer_get_data(RID p_buffer, uint32_t p_
ERR_FAIL_V_MSG(Vector<uint8_t>(), "Buffer is either invalid or this type of buffer can't be retrieved. Only Index and Vertex buffers allow retrieving.");
}
- // Make sure no one is using the buffer -- the "false" gets us to the same command buffer as below.
- _buffer_memory_barrier(buffer->buffer, 0, buffer->size, src_stage_mask, VK_PIPELINE_STAGE_TRANSFER_BIT, src_access_mask, VK_ACCESS_TRANSFER_READ_BIT, false);
+ // Make sure no one is using the buffer -- the "true" gets us to the same command buffer as below.
+ _buffer_memory_barrier(buffer->buffer, 0, buffer->size, src_stage_mask, VK_PIPELINE_STAGE_TRANSFER_BIT, src_access_mask, VK_ACCESS_TRANSFER_READ_BIT, true);
- VkCommandBuffer command_buffer = frames[frame].setup_command_buffer;
+ VkCommandBuffer command_buffer = frames[frame].draw_command_buffer;
// Size of buffer to retrieve.
if (!p_size) {
diff --git a/editor/editor_fonts.cpp b/editor/editor_fonts.cpp
index dfcb083ef9..74616bc0ce 100644
--- a/editor/editor_fonts.cpp
+++ b/editor/editor_fonts.cpp
@@ -107,6 +107,7 @@ Ref<FontVariation> make_bold_font(const Ref<Font> &p_font, double p_embolden, Ty
}
void editor_register_fonts(Ref<Theme> p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("editor_register_fonts");
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
TextServer::FontAntialiasing font_antialiasing = (TextServer::FontAntialiasing)(int)EDITOR_GET("interface/editor/font_antialiasing");
@@ -443,4 +444,6 @@ void editor_register_fonts(Ref<Theme> p_theme) {
p_theme->set_font_size("status_source_size", "EditorFonts", default_font_size);
p_theme->set_font("status_source", "EditorFonts", mono_other_fc);
+
+ OS::get_singleton()->benchmark_end_measure("editor_register_fonts");
}
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 2ddde4e507..2b8cace2f1 100644
--- a/editor/editor_help.cpp
+++ b/editor/editor_help.cpp
@@ -2297,6 +2297,7 @@ void EditorHelp::_gen_doc_thread(void *p_udata) {
static bool doc_gen_use_threads = true;
void EditorHelp::generate_doc(bool p_use_cache) {
+ OS::get_singleton()->benchmark_begin_measure("EditorHelp::generate_doc");
if (doc_gen_use_threads) {
// In case not the first attempt.
_wait_for_thread();
@@ -2327,6 +2328,7 @@ void EditorHelp::generate_doc(bool p_use_cache) {
_gen_doc_thread(nullptr);
}
}
+ OS::get_singleton()->benchmark_end_measure("EditorHelp::generate_doc");
}
void EditorHelp::_toggle_scripts_pressed() {
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index c9f356b82c..ce7702d5b0 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -1041,7 +1041,7 @@ void EditorNode::_sources_changed(bool p_exist) {
if (waiting_for_first_scan) {
waiting_for_first_scan = false;
- Engine::get_singleton()->startup_benchmark_end_measure(); // editor_scan_and_reimport
+ OS::get_singleton()->benchmark_end_measure("editor_scan_and_import");
// Reload the global shader variables, but this time
// loading textures, as they are now properly imported.
@@ -1055,16 +1055,12 @@ void EditorNode::_sources_changed(bool p_exist) {
_load_editor_layout();
if (!defer_load_scene.is_empty()) {
- Engine::get_singleton()->startup_benchmark_begin_measure("editor_load_scene");
+ OS::get_singleton()->benchmark_begin_measure("editor_load_scene");
load_scene(defer_load_scene);
defer_load_scene = "";
- Engine::get_singleton()->startup_benchmark_end_measure();
+ OS::get_singleton()->benchmark_end_measure("editor_load_scene");
- if (use_startup_benchmark) {
- Engine::get_singleton()->startup_dump(startup_benchmark_file);
- startup_benchmark_file = String();
- use_startup_benchmark = false;
- }
+ OS::get_singleton()->benchmark_dump();
}
}
}
@@ -4392,13 +4388,9 @@ void EditorNode::_editor_file_dialog_unregister(EditorFileDialog *p_dialog) {
Vector<EditorNodeInitCallback> EditorNode::_init_callbacks;
void EditorNode::_begin_first_scan() {
- Engine::get_singleton()->startup_benchmark_begin_measure("editor_scan_and_import");
+ OS::get_singleton()->benchmark_begin_measure("editor_scan_and_import");
EditorFileSystem::get_singleton()->scan();
}
-void EditorNode::set_use_startup_benchmark(bool p_use_startup_benchmark, const String &p_startup_benchmark_file) {
- use_startup_benchmark = p_use_startup_benchmark;
- startup_benchmark_file = p_startup_benchmark_file;
-}
Error EditorNode::export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only) {
export_defer.preset = p_preset;
diff --git a/editor/editor_node.h b/editor/editor_node.h
index 814899e169..66da019560 100644
--- a/editor/editor_node.h
+++ b/editor/editor_node.h
@@ -694,8 +694,6 @@ private:
void _bottom_panel_raise_toggled(bool);
void _begin_first_scan();
- bool use_startup_benchmark = false;
- String startup_benchmark_file;
protected:
friend class FileSystemDock;
@@ -871,7 +869,6 @@ public:
void _copy_warning(const String &p_str);
- void set_use_startup_benchmark(bool p_use_startup_benchmark, const String &p_startup_benchmark_file);
Error export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only);
Control *get_gui_base() { return gui_base; }
diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp
index aa98eb6103..25749a9589 100644
--- a/editor/editor_themes.cpp
+++ b/editor/editor_themes.cpp
@@ -281,6 +281,7 @@ float get_gizmo_handle_scale(const String &gizmo_handle_name = "") {
}
void editor_register_and_generate_icons(Ref<Theme> p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, bool p_only_thumbs = false) {
+ OS::get_singleton()->benchmark_begin_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all")));
// Before we register the icons, we adjust their colors and saturation.
// Most icons follow the standard rules for color conversion to follow the editor
// theme's polarity (dark/light). We also adjust the saturation for most icons,
@@ -408,9 +409,11 @@ void editor_register_and_generate_icons(Ref<Theme> p_theme, bool p_dark_theme, f
p_theme->set_icon(editor_icons_names[index], SNAME("EditorIcons"), icon);
}
}
+ OS::get_singleton()->benchmark_end_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all")));
}
Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("create_editor_theme");
Ref<Theme> theme = Ref<Theme>(memnew(Theme));
// Controls may rely on the scale for their internal drawing logic.
@@ -2093,10 +2096,13 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
theme->set_color("search_result_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_color"));
theme->set_color("search_result_border_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_border_color"));
+ OS::get_singleton()->benchmark_end_measure("create_editor_theme");
+
return theme;
}
Ref<Theme> create_custom_theme(const Ref<Theme> p_theme) {
+ OS::get_singleton()->benchmark_begin_measure("create_custom_theme");
Ref<Theme> theme = create_editor_theme(p_theme);
const String custom_theme_path = EDITOR_GET("interface/theme/custom_theme");
@@ -2107,6 +2113,7 @@ Ref<Theme> create_custom_theme(const Ref<Theme> p_theme) {
}
}
+ OS::get_singleton()->benchmark_end_measure("create_custom_theme");
return theme;
}
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 976ccab99e..1c43c29c33 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -2281,6 +2281,10 @@ void FileSystemDock::remove_resource_tooltip_plugin(const Ref<EditorResourceTool
}
Control *FileSystemDock::create_tooltip_for_path(const String &p_path) const {
+ if (p_path == "Favorites") {
+ // No tooltip for the "Favorites" group.
+ return nullptr;
+ }
if (DirAccess::exists(p_path)) {
// No tooltip for directory.
return nullptr;
diff --git a/editor/icons/BoxOccluder3D.svg b/editor/icons/BoxOccluder3D.svg
index 3cee3db532..888e9febdd 100644
--- a/editor/icons/BoxOccluder3D.svg
+++ b/editor/icons/BoxOccluder3D.svg
@@ -1 +1 @@
-<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 .88867188-.5058594.25390622a4.5 4.5 0 0 1 1.3789063 2.2988281l3.0664061 1.5332032-3.5546874 1.7753906a4.5 4.5 0 0 1 -1.6796875 1.6601562l.2949219.1464844v3.9414064l-4-2.001953v-1.7636721a4.5 4.5 0 0 1 -2-1.4179688v4.2968749l7 3.5 7-3.5v-7.2226561zm5 5.66796872v3.9394534l-4 2.001953v-3.9414064z"/><path d="m8 .88867188-.5058594.25390622a4.5 4.5 0 0 1 1.5058594 3.3574219 4.5 4.5 0 0 1 -4.5 4.5 4.5 4.5 0 0 1 -3.5-1.6855469v4.2968749l7 3.5 7-3.5v-7.2226561z" fill="#ffca5f"/></svg>
+<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 .889-.506.254A4.5 4.5 0 1 1 1 7.314v4.297l7 3.5 7-3.5V4.39z" fill="#ffca5f"/></svg>
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index 00f4af47e1..34667b006d 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -296,10 +296,10 @@ void ViewportRotationControl::_notification(int p_what) {
axis_menu_options.clear();
axis_menu_options.push_back(Node3DEditorViewport::VIEW_RIGHT);
axis_menu_options.push_back(Node3DEditorViewport::VIEW_TOP);
- axis_menu_options.push_back(Node3DEditorViewport::VIEW_REAR);
+ axis_menu_options.push_back(Node3DEditorViewport::VIEW_FRONT);
axis_menu_options.push_back(Node3DEditorViewport::VIEW_LEFT);
axis_menu_options.push_back(Node3DEditorViewport::VIEW_BOTTOM);
- axis_menu_options.push_back(Node3DEditorViewport::VIEW_FRONT);
+ axis_menu_options.push_back(Node3DEditorViewport::VIEW_REAR);
axis_colors.clear();
axis_colors.push_back(get_theme_color(SNAME("axis_x_color"), SNAME("Editor")));
@@ -370,7 +370,7 @@ void ViewportRotationControl::_get_sorted_axis(Vector<Axis2D> &r_axis) {
Vector3 axis_3d = camera_basis.get_column(i);
Vector2i axis_vector = Vector2(axis_3d.x, -axis_3d.y) * radius;
- if (Math::abs(axis_3d.z) < 1.0) {
+ if (Math::abs(axis_3d.z) <= 1.0) {
Axis2D pos_axis;
pos_axis.axis = i;
pos_axis.screen_point = center + axis_vector;
@@ -385,7 +385,7 @@ void ViewportRotationControl::_get_sorted_axis(Vector<Axis2D> &r_axis) {
} else {
// Special case when the camera is aligned with one axis
Axis2D axis;
- axis.axis = i + (axis_3d.z < 0 ? 0 : 3);
+ axis.axis = i + (axis_3d.z <= 0 ? 0 : 3);
axis.screen_point = center;
axis.z_axis = 1.0;
r_axis.push_back(axis);
@@ -3176,7 +3176,7 @@ void Node3DEditorViewport::_menu_option(int p_option) {
} break;
case VIEW_FRONT: {
cursor.x_rot = 0;
- cursor.y_rot = Math_PI;
+ cursor.y_rot = 0;
set_message(TTR("Front View."), 2);
view_type = VIEW_TYPE_FRONT;
_set_auto_orthogonal();
@@ -3185,7 +3185,7 @@ void Node3DEditorViewport::_menu_option(int p_option) {
} break;
case VIEW_REAR: {
cursor.x_rot = 0;
- cursor.y_rot = 0;
+ cursor.y_rot = Math_PI;
set_message(TTR("Rear View."), 2);
view_type = VIEW_TYPE_REAR;
_set_auto_orthogonal();
diff --git a/editor/plugins/path_3d_editor_plugin.cpp b/editor/plugins/path_3d_editor_plugin.cpp
index 75cd04bee8..d49c04445e 100644
--- a/editor/plugins/path_3d_editor_plugin.cpp
+++ b/editor/plugins/path_3d_editor_plugin.cpp
@@ -274,10 +274,10 @@ void Path3DGizmo::redraw() {
// Fish Bone.
v3p.push_back(p1);
- v3p.push_back(p1 + (side - forward + up * 0.3) * 0.06);
+ v3p.push_back(p1 + (side + forward + up * 0.3) * 0.06);
v3p.push_back(p1);
- v3p.push_back(p1 + (-side - forward + up * 0.3) * 0.06);
+ v3p.push_back(p1 + (-side + forward + up * 0.3) * 0.06);
}
add_lines(v3p, path_material);
diff --git a/editor/plugins/tiles/atlas_merging_dialog.cpp b/editor/plugins/tiles/atlas_merging_dialog.cpp
index 274d52da47..f958e83b4d 100644
--- a/editor/plugins/tiles/atlas_merging_dialog.cpp
+++ b/editor/plugins/tiles/atlas_merging_dialog.cpp
@@ -295,7 +295,7 @@ AtlasMergingDialog::AtlasMergingDialog() {
atlas_merging_atlases_list->set_fixed_icon_size(Size2(60, 60) * EDSCALE);
atlas_merging_atlases_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
atlas_merging_atlases_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
- atlas_merging_atlases_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST);
+ atlas_merging_atlases_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST_WITH_MIPMAPS);
atlas_merging_atlases_list->set_custom_minimum_size(Size2(100, 200));
atlas_merging_atlases_list->set_select_mode(ItemList::SELECT_MULTI);
atlas_merging_atlases_list->connect("multi_selected", callable_mp(this, &AtlasMergingDialog::_update_texture).unbind(2));
@@ -303,6 +303,7 @@ AtlasMergingDialog::AtlasMergingDialog() {
VBoxContainer *atlas_merging_right_panel = memnew(VBoxContainer);
atlas_merging_right_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ atlas_merging_right_panel->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST_WITH_MIPMAPS);
atlas_merging_h_split_container->add_child(atlas_merging_right_panel);
// Settings.
diff --git a/editor/plugins/tiles/tile_map_editor.cpp b/editor/plugins/tiles/tile_map_editor.cpp
index 0a8ccdba1a..c857539506 100644
--- a/editor/plugins/tiles/tile_map_editor.cpp
+++ b/editor/plugins/tiles/tile_map_editor.cpp
@@ -321,6 +321,7 @@ void TileMapEditorTilesPlugin::_update_patterns_list() {
for (int i = 0; i < tile_set->get_patterns_count(); i++) {
int id = patterns_item_list->add_item("");
patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i));
+ patterns_item_list->set_item_tooltip(id, vformat(TTR("Index: %d"), i));
TilesEditorPlugin::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileMapEditorTilesPlugin::_pattern_preview_done));
}
diff --git a/editor/plugins/tiles/tile_set_editor.cpp b/editor/plugins/tiles/tile_set_editor.cpp
index 358cc47977..52e91ad913 100644
--- a/editor/plugins/tiles/tile_set_editor.cpp
+++ b/editor/plugins/tiles/tile_set_editor.cpp
@@ -418,6 +418,7 @@ void TileSetEditor::_update_patterns_list() {
for (int i = 0; i < tile_set->get_patterns_count(); i++) {
int id = patterns_item_list->add_item("");
patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i));
+ patterns_item_list->set_item_tooltip(id, vformat(TTR("Index: %d"), i));
TilesEditorPlugin::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileSetEditor::_pattern_preview_done));
}
diff --git a/editor/plugins/tiles/tiles_editor_plugin.cpp b/editor/plugins/tiles/tiles_editor_plugin.cpp
index 719de9dc81..0e9ff18355 100644
--- a/editor/plugins/tiles/tiles_editor_plugin.cpp
+++ b/editor/plugins/tiles/tiles_editor_plugin.cpp
@@ -59,6 +59,7 @@ void TilesEditorPlugin::_pattern_preview_done() {
void TilesEditorPlugin::_thread_func(void *ud) {
TilesEditorPlugin *te = static_cast<TilesEditorPlugin *>(ud);
+ set_current_thread_safe_for_nodes(true);
te->_thread();
}
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index 52e6b478f9..da196d8de9 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -1963,6 +1963,9 @@ Ref<Texture2D> ProjectManager::_file_dialog_get_thumbnail(const String &p_path)
}
void ProjectManager::_build_icon_type_cache(Ref<Theme> p_theme) {
+ if (p_theme.is_null()) {
+ return;
+ }
List<StringName> tl;
p_theme->get_icon_list(SNAME("EditorIcons"), &tl);
for (List<StringName>::Element *E = tl.front(); E; E = E->next()) {
diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp
index 758565b266..0dd11d8948 100644
--- a/editor/register_editor_types.cpp
+++ b/editor/register_editor_types.cpp
@@ -116,6 +116,8 @@
#include "editor/register_exporters.h"
void register_editor_types() {
+ OS::get_singleton()->benchmark_begin_measure("register_editor_types");
+
ResourceLoader::set_timestamp_on_load(true);
ResourceSaver::set_timestamp_on_save(true);
@@ -245,13 +247,19 @@ void register_editor_types() {
GLOBAL_DEF("editor/version_control/autoload_on_startup", false);
EditorInterface::create();
+
+ OS::get_singleton()->benchmark_end_measure("register_editor_types");
}
void unregister_editor_types() {
+ OS::get_singleton()->benchmark_begin_measure("unregister_editor_types");
+
EditorNode::cleanup();
EditorInterface::free();
if (EditorPaths::get_singleton()) {
EditorPaths::free();
}
+
+ OS::get_singleton()->benchmark_end_measure("unregister_editor_types");
}
diff --git a/main/main.cpp b/main/main.cpp
index 17e4f69ef2..ec35b33321 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -183,8 +183,6 @@ static int converter_max_line_length = 100000;
HashMap<Main::CLIScope, Vector<String>> forwardable_cli_arguments;
#endif
static bool single_threaded_scene = false;
-bool use_startup_benchmark = false;
-String startup_benchmark_file;
// Display
@@ -498,8 +496,8 @@ void Main::print_help(const char *p_binary) {
OS::get_singleton()->print(" --dump-gdextension-interface Generate GDExtension header file 'gdextension_interface.h' in the current folder. This file is the base file required to implement a GDExtension.\n");
OS::get_singleton()->print(" --dump-extension-api Generate JSON dump of the Godot API for GDExtension bindings named 'extension_api.json' in the current folder.\n");
OS::get_singleton()->print(" --validate-extension-api <path> Validate an extension API file dumped (with the option above) from a previous version of the engine to ensure API compatibility. If incompatibilities or errors are detected, the return code will be non zero.\n");
- OS::get_singleton()->print(" --startup-benchmark Benchmark the startup time and print it to console.\n");
- OS::get_singleton()->print(" --startup-benchmark-file <path> Benchmark the startup time and save it to a given file in JSON format.\n");
+ OS::get_singleton()->print(" --benchmark Benchmark the run time and print it to console.\n");
+ OS::get_singleton()->print(" --benchmark-file <path> Benchmark the run time and save it to a given file in JSON format. The path should be absolute.\n");
#ifdef TESTS_ENABLED
OS::get_singleton()->print(" --test [--help] Run unit tests. Use --test --help for more information.\n");
#endif
@@ -512,6 +510,7 @@ void Main::print_help(const char *p_binary) {
// are initialized here. This also combines `Main::setup2()` initialization.
Error Main::test_setup() {
Thread::make_main_thread();
+ set_current_thread_safe_for_nodes(true);
OS::get_singleton()->initialize();
@@ -723,14 +722,18 @@ int Main::test_entrypoint(int argc, char *argv[], bool &tests_need_run) {
Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_phase) {
Thread::make_main_thread();
+ set_current_thread_safe_for_nodes(true);
OS::get_singleton()->initialize();
+ // Benchmark tracking must be done after `OS::get_singleton()->initialize()` 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");
- engine->startup_begin();
- engine->startup_benchmark_begin_measure("core");
+ OS::get_singleton()->benchmark_begin_measure("core");
register_core_types();
register_core_driver_types();
@@ -1438,15 +1441,16 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
goto error;
}
- } else if (I->get() == "--startup-benchmark") {
- use_startup_benchmark = true;
- } else if (I->get() == "--startup-benchmark-file") {
+ } else if (I->get() == "--benchmark") {
+ OS::get_singleton()->set_use_benchmark(true);
+ } else if (I->get() == "--benchmark-file") {
if (I->next()) {
- use_startup_benchmark = true;
- startup_benchmark_file = I->next()->get();
+ 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 <path> argument for --startup-benchmark-file <path>.\n");
+ OS::get_singleton()->print("Missing <path> argument for --benchmark-file <path>.\n");
goto error;
}
@@ -1987,14 +1991,14 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
message_queue = memnew(MessageQueue);
- engine->startup_benchmark_end_measure(); // core
-
Thread::release_main_thread(); // If setup2() is called from another thread, that one will become main thread, so preventively release this one.
+ set_current_thread_safe_for_nodes(false);
if (p_second_phase) {
return setup2();
}
+ OS::get_singleton()->benchmark_end_measure("core");
return OK;
error:
@@ -2047,6 +2051,9 @@ error:
if (message_queue) {
memdelete(message_queue);
}
+
+ OS::get_singleton()->benchmark_end_measure("core");
+
OS::get_singleton()->finalize_core();
locale = String();
@@ -2055,11 +2062,12 @@ error:
Error Main::setup2() {
Thread::make_main_thread(); // Make whatever thread call this the main thread.
+ set_current_thread_safe_for_nodes(true);
// Print engine name and version
print_line(String(VERSION_NAME) + " v" + get_full_version_string() + " - " + String(VERSION_WEBSITE));
- engine->startup_benchmark_begin_measure("servers");
+ OS::get_singleton()->benchmark_begin_measure("servers");
tsman = memnew(TextServerManager);
@@ -2450,11 +2458,11 @@ Error Main::setup2() {
ERR_FAIL_V_MSG(ERR_CANT_CREATE, "TextServer: Unable to create TextServer interface.");
}
- engine->startup_benchmark_end_measure(); // servers
+ OS::get_singleton()->benchmark_end_measure("servers");
MAIN_PRINT("Main: Load Scene Types");
- engine->startup_benchmark_begin_measure("scene");
+ OS::get_singleton()->benchmark_begin_measure("scene");
register_scene_types();
register_driver_types();
@@ -2539,7 +2547,7 @@ Error Main::setup2() {
print_verbose("EDITOR API HASH: " + uitos(ClassDB::get_api_hash(ClassDB::API_EDITOR)));
MAIN_PRINT("Main: Done");
- engine->startup_benchmark_end_measure(); // scene
+ OS::get_singleton()->benchmark_end_measure("scene");
return OK;
}
@@ -2957,7 +2965,7 @@ bool Main::start() {
if (!project_manager && !editor) { // game
if (!game_path.is_empty() || !script.is_empty()) {
//autoload
- Engine::get_singleton()->startup_benchmark_begin_measure("load_autoloads");
+ OS::get_singleton()->benchmark_begin_measure("load_autoloads");
HashMap<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list();
//first pass, add the constants so they exist before any script is loaded
@@ -3023,14 +3031,14 @@ bool Main::start() {
for (Node *E : to_add) {
sml->get_root()->add_child(E);
}
- Engine::get_singleton()->startup_benchmark_end_measure(); // load autoloads
+ OS::get_singleton()->benchmark_end_measure("load_autoloads");
}
}
#ifdef TOOLS_ENABLED
EditorNode *editor_node = nullptr;
if (editor) {
- Engine::get_singleton()->startup_benchmark_begin_measure("editor");
+ OS::get_singleton()->benchmark_begin_measure("editor");
editor_node = memnew(EditorNode);
sml->get_root()->add_child(editor_node);
@@ -3039,12 +3047,7 @@ bool Main::start() {
game_path = ""; // Do not load anything.
}
- Engine::get_singleton()->startup_benchmark_end_measure();
-
- editor_node->set_use_startup_benchmark(use_startup_benchmark, startup_benchmark_file);
- // Editor takes over
- use_startup_benchmark = false;
- startup_benchmark_file = String();
+ OS::get_singleton()->benchmark_end_measure("editor");
}
#endif
sml->set_auto_accept_quit(GLOBAL_GET("application/config/auto_accept_quit"));
@@ -3173,7 +3176,7 @@ bool Main::start() {
if (!project_manager && !editor) { // game
- Engine::get_singleton()->startup_benchmark_begin_measure("game_load");
+ OS::get_singleton()->benchmark_begin_measure("game_load");
// Load SSL Certificates from Project Settings (or builtin).
Crypto::load_default_certificates(GLOBAL_GET("network/tls/certificate_bundle_override"));
@@ -3215,19 +3218,19 @@ bool Main::start() {
}
}
- Engine::get_singleton()->startup_benchmark_end_measure(); // game_load
+ OS::get_singleton()->benchmark_end_measure("game_load");
}
#ifdef TOOLS_ENABLED
if (project_manager) {
- Engine::get_singleton()->startup_benchmark_begin_measure("project_manager");
+ OS::get_singleton()->benchmark_begin_measure("project_manager");
Engine::get_singleton()->set_editor_hint(true);
ProjectManager *pmanager = memnew(ProjectManager);
ProgressDialog *progress_dialog = memnew(ProgressDialog);
pmanager->add_child(progress_dialog);
sml->get_root()->add_child(pmanager);
DisplayServer::get_singleton()->set_context(DisplayServer::CONTEXT_PROJECTMAN);
- Engine::get_singleton()->startup_benchmark_end_measure();
+ OS::get_singleton()->benchmark_end_measure("project_manager");
}
if (project_manager || editor) {
@@ -3257,10 +3260,8 @@ bool Main::start() {
}
}
- if (use_startup_benchmark) {
- Engine::get_singleton()->startup_dump(startup_benchmark_file);
- startup_benchmark_file = String();
- }
+ OS::get_singleton()->benchmark_end_measure("startup_begin");
+ OS::get_singleton()->benchmark_dump();
return true;
}
@@ -3505,6 +3506,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);
}
@@ -3645,5 +3647,8 @@ void Main::cleanup(bool p_force) {
uninitialize_modules(MODULE_INITIALIZATION_LEVEL_CORE);
unregister_core_types();
+ OS::get_singleton()->benchmark_end_measure("Main::cleanup");
+ OS::get_singleton()->benchmark_dump();
+
OS::get_singleton()->finalize_core();
}
diff --git a/misc/dist/shell/_godot.zsh-completion b/misc/dist/shell/_godot.zsh-completion
index 61291899f3..89fe840166 100644
--- a/misc/dist/shell/_godot.zsh-completion
+++ b/misc/dist/shell/_godot.zsh-completion
@@ -88,6 +88,6 @@ _arguments \
'--build-solutions[build the scripting solutions (e.g. for C# projects)]' \
'--dump-gdextension-interface[generate GDExtension header file 'gdextension_interface.h' in the current folder. This file is the base file required to implement a GDExtension.]' \
'--dump-extension-api[generate JSON dump of the Godot API for GDExtension bindings named "extension_api.json" in the current folder]' \
- '--startup-benchmark[benchmark the startup time and print it to console]' \
- '--startup-benchmark-file[benchmark the startup time and save it to a given file in JSON format]:path to output JSON file' \
+ '--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 all unit tests; run with "--test --help" for more information]'
diff --git a/misc/dist/shell/godot.bash-completion b/misc/dist/shell/godot.bash-completion
index 79000da85d..a7ce11e524 100644
--- a/misc/dist/shell/godot.bash-completion
+++ b/misc/dist/shell/godot.bash-completion
@@ -91,8 +91,8 @@ _complete_godot_options() {
--build-solutions
--dump-gdextension-interface
--dump-extension-api
---startup-benchmark
---startup-benchmark-file
+--benchmark
+--benchmark-file
--test
" -- "$1"))
}
diff --git a/misc/dist/shell/godot.fish b/misc/dist/shell/godot.fish
index 8f521ec1a0..ed58d8dcf6 100644
--- a/misc/dist/shell/godot.fish
+++ b/misc/dist/shell/godot.fish
@@ -109,6 +109,6 @@ complete -c godot -l no-docbase -d "Disallow dumping the base types (used with -
complete -c godot -l build-solutions -d "Build the scripting solutions (e.g. for C# projects)"
complete -c godot -l dump-gdextension-interface -d "Generate GDExtension header file 'gdextension_interface.h' in the current folder. This file is the base file required to implement a GDExtension"
complete -c godot -l dump-extension-api -d "Generate JSON dump of the Godot API for GDExtension bindings named 'extension_api.json' in the current folder"
-complete -c godot -l startup-benchmark -d "Benchmark the startup time and print it to console"
-complete -c godot -l startup-benchmark-file -d "Benchmark the startup time and save it to a given file in JSON format" -x
+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 all unit tests; run with '--test --help' for more information" -x
diff --git a/misc/extension_api_validation/4.0-stable.expected b/misc/extension_api_validation/4.0-stable.expected
new file mode 100644
index 0000000000..7868fe39cf
--- /dev/null
+++ b/misc/extension_api_validation/4.0-stable.expected
@@ -0,0 +1,219 @@
+This file contains the expected output of --validate-extension-api when run against the extension_api.json of the
+4.0-stable tag (the basename of this file).
+
+Only lines that start with "Validate extension JSON:" matter, everything else is considered a comment and ignored. They
+should instead be used to justify these changes and describe how users should work around these changes.
+
+========================================================================================================================
+
+GH-74736
+--------
+Validate extension JSON: Error: Field 'classes/MenuBar/properties/start_index': type changed value in new API, from "bool" to "int".
+
+The previous type was simply wrong and the getter and setter already used int.
+
+
+GH-74671
+--------
+Validate extension JSON: Error: Field 'native_structures/PhysicsServer3DExtensionMotionCollision': format changed value in new API, from "Vector3 position;Vector3 normal;Vector3 collider_velocity;real_t depth;int local_shape;ObjectID collider_id;RID collider;int collider_shape" to "Vector3 position;Vector3 normal;Vector3 collider_velocity;Vector3 collider_angular_velocity;real_t depth;int local_shape;ObjectID collider_id;RID collider;int collider_shape".
+Validate extension JSON: Error: Field 'native_structures/PhysicsServer3DExtensionMotionResult': format changed value in new API, from "Vector3 travel;Vector3 remainder;real_t collision_safe_fraction;real_t collision_unsafe_fraction;PhysicsServer3DExtensionMotionCollision collisions[32];int collision_count" to "Vector3 travel;Vector3 remainder;real_t collision_depth;real_t collision_safe_fraction;real_t collision_unsafe_fraction;PhysicsServer3DExtensionMotionCollision collisions[32];int collision_count".
+
+The previous type was simply wrong and didn't match the actual C++ definition. Code targeting previous versions should use the updated definition as well.
+
+
+GH-74600
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimatedSprite2D/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimatedSprite3D/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Animation/methods/compress'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimationPlayer/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AudioStreamPlayer/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AudioStreamPlayer2D/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AudioStreamPlayer3D/methods/play'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/CanvasItem/methods/draw_set_transform'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve2D/methods/sample_baked'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve2D/methods/sample_baked_with_rotation'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve2D/methods/tessellate_even_length'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve3D/methods/sample_baked'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve3D/methods/sample_baked_with_rotation'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Curve3D/methods/tessellate_even_length'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/DisplayServer/methods/tts_speak'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Font/methods/find_variation'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/GridMap/methods/make_baked_meshes'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Image/methods/save_jpg_to_buffer'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Image/methods/save_webp_to_buffer'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Image/methods/bump_map_to_normal_map'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/PhysicsBody2D/methods/move_and_collide'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/PhysicsBody3D/methods/move_and_collide'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/PhysicsBody3D/methods/test_move'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RandomNumberGenerator/methods/randfn'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RenderingServer/methods/environment_set_ambient_light'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RenderingServer/methods/canvas_item_set_canvas_group_mode'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RenderingServer/methods/force_draw'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Window/methods/popup_centered_ratio'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Window/methods/popup_centered_clamped'. This means that the function has changed and no compatibility function was provided.
+
+None of these methods were actually changed, the hash changes only affects GDExtensions, no compatibility workaround exists currently.
+
+
+GH-76413
+--------
+Validate extension JSON: API was removed: classes/AnimationTrackEditPlugin
+
+This class does nothing and is not useful in any way.
+
+
+GH-69988
+--------
+Validate extension JSON: API was removed: classes/NavigationAgent2D/methods/get_time_horizon
+Validate extension JSON: API was removed: classes/NavigationAgent2D/methods/set_time_horizon
+Validate extension JSON: API was removed: classes/NavigationAgent2D/properties/time_horizon
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/get_agent_height_offset
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/get_ignore_y
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/get_time_horizon
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/set_agent_height_offset
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/set_ignore_y
+Validate extension JSON: API was removed: classes/NavigationAgent3D/methods/set_time_horizon
+Validate extension JSON: API was removed: classes/NavigationAgent3D/properties/agent_height_offset
+Validate extension JSON: API was removed: classes/NavigationAgent3D/properties/ignore_y
+Validate extension JSON: API was removed: classes/NavigationAgent3D/properties/time_horizon
+Validate extension JSON: API was removed: classes/NavigationObstacle2D/methods/get_rid
+Validate extension JSON: API was removed: classes/NavigationObstacle2D/methods/is_radius_estimated
+Validate extension JSON: API was removed: classes/NavigationObstacle2D/methods/set_estimate_radius
+Validate extension JSON: API was removed: classes/NavigationObstacle2D/properties/estimate_radius
+Validate extension JSON: API was removed: classes/NavigationObstacle3D/methods/get_rid
+Validate extension JSON: API was removed: classes/NavigationObstacle3D/methods/is_radius_estimated
+Validate extension JSON: API was removed: classes/NavigationObstacle3D/methods/set_estimate_radius
+Validate extension JSON: API was removed: classes/NavigationObstacle3D/properties/estimate_radius
+Validate extension JSON: API was removed: classes/NavigationServer2D/methods/agent_set_callback
+Validate extension JSON: API was removed: classes/NavigationServer2D/methods/agent_set_target_velocity
+Validate extension JSON: API was removed: classes/NavigationServer2D/methods/agent_set_time_horizon
+Validate extension JSON: API was removed: classes/NavigationServer3D/methods/agent_set_callback
+Validate extension JSON: API was removed: classes/NavigationServer3D/methods/agent_set_target_velocity
+Validate extension JSON: API was removed: classes/NavigationServer3D/methods/agent_set_time_horizon
+
+Navigation avoidance was reworked entirely.
+Migration: TODO
+
+
+GH-?????
+--------
+Validate extension JSON: API was removed: classes/FramebufferCacheRD
+Validate extension JSON: API was removed: classes/UniformSetCacheRD
+
+Unsure where these come from; when dumping the interface, these do actually still exist
+
+GH-76176
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_base_control'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_edited_scene_root'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_editor_main_screen'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_editor_paths'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_editor_settings'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_file_system_dock'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_resource_filesystem'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_resource_previewer'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_script_editor'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorInterface/methods/get_selection'. This means that the function has changed and no compatibility function was provided.
+
+Functions were made `const`. No adjustments should be necessary.
+
+
+GH-76026
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorScript/methods/get_editor_interface'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/EditorScript/methods/get_scene'. This means that the function has changed and no compatibility function was provided.
+
+Functions were made `const`. No adjustments should be necessary.
+
+
+GH-76418
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/Object/methods/get_meta_list'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimationNodeStateMachinePlayback/methods/get_travel_path'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RDShaderFile/methods/get_version_list'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/RenderingServer/methods/global_shader_parameter_get_list'. This means that the function has changed and no compatibility function was provided.
+
+Return types change, fixed some internal type issues and unnecessarily changed the public type in the process.
+
+
+GH-72749
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/Area2D/methods/get_priority'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Area2D/methods/set_priority'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Area3D/methods/get_priority'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Area3D/methods/set_priority'. This means that the function has changed and no compatibility function was provided.
+
+Type changed from `float` to `int`. Previously the `float` values were internally converted to `int`s anyways and the type ways inconsistent with the type of the priority property, which already was `int`.
+
+
+GH-72152
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/MeshInstance3D/methods/create_multiple_convex_collisions'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
+
+
+GH-75759
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimationNode/methods/blend_input'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/AnimationNode/methods/blend_node'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
+
+
+GH-75017
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/RichTextLabel/methods/push_list'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
+
+
+GH-76794
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/Tree/methods/edit_selected'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
+
+
+GH-75777
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/SyntaxHighlighter/methods/get_text_edit'. This means that the function has changed and no compatibility function was provided.
+
+Function was made `const`. No adjustments should be necessary.
+
+
+GH-75250
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/RichTextLabel/methods/push_paragraph'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
+
+
+GH-77143
+--------
+Validate extension JSON: Error: Hash mismatch for 'classes/WorkerThreadPool/methods/wait_for_task_completion'. This means that the function has changed and no compatibility function was provided.
+
+Changed the return value from `void` to `Error`. No adjustments should be necessary.
+
+
+GH-72842
+--------
+Validate extension JSON: API was removed: classes/PathFollow2D/methods/get_lookahead
+Validate extension JSON: API was removed: classes/PathFollow2D/methods/set_lookahead
+Validate extension JSON: API was removed: classes/PathFollow2D/properties/lookahead
+
+
+GH-77413
+--------
+Validate extension JSON: Error: Field 'classes/GLTFSkin/properties/godot_skin': type changed value in new API, from "Object" to "Skin".
+
+
+GH-76082
+--------
+Validate extension JSON: Error: Hash mismatch for 'builtin_classes/Basis/methods/looking_at'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'builtin_classes/Transform3D/methods/looking_at'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Node3D/methods/look_at'. This means that the function has changed and no compatibility function was provided.
+Validate extension JSON: Error: Hash mismatch for 'classes/Node3D/methods/look_at_from_position'. This means that the function has changed and no compatibility function was provided.
+
+Added an optional parameter with a default value. No adjustments should be necessary.
diff --git a/misc/scripts/validate_extension_api.sh b/misc/scripts/validate_extension_api.sh
new file mode 100755
index 0000000000..6cc22c9b63
--- /dev/null
+++ b/misc/scripts/validate_extension_api.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+set -uo pipefail
+
+if [ ! -f "version.py" ]; then
+ echo "Warning: This script is intended to be run from the root of the Godot repository."
+ echo "Some of the paths checks may not work as intended from a different folder."
+fi
+
+if [ $# != 1 ]; then
+ echo "Usage: @0 <path-to-godot-executable>"
+fi
+
+has_problems=0
+
+make_annotation()
+{
+ local title=$1
+ local body=$2
+ local type=$3
+ local file=$4
+ if [ ! -v GITHUB_OUTPUT ]; then
+ echo "$title"
+ echo "$body"
+ else
+ body="$(awk 1 ORS='%0A' - <<<"$body")"
+ echo "::$type file=$file,title=$title ::$body"
+ fi
+}
+
+while read -r file; do
+ reference_file="$(mktemp)"
+ validate="$(mktemp)"
+ validation_output="$(mktemp)"
+ allowed_errors="$(mktemp)"
+
+ # Download the reference extension_api.json
+ reference_tag="$(basename -s .expected "$file")"
+ wget -qcO "$reference_file" "https://raw.githubusercontent.com/godotengine/godot-cpp/godot-$reference_tag/gdextension/extension_api.json"
+ # Validate the current API against the reference
+ "$1" --headless --validate-extension-api "$reference_file" 2>&1 | tee "$validate" | awk '!/^Validate extension JSON:/' - || true
+ # Collect the expected and actual validation errors
+ awk '/^Validate extension JSON:/' - < "$validate" | sort > "$validation_output"
+ awk '/^Validate extension JSON:/' - < "$file" | sort > "$allowed_errors"
+
+ # Differences between the expected and actual errors
+ new_validation_error="$(comm "$validation_output" "$allowed_errors" -23)"
+ obsolete_validation_error="$(comm "$validation_output" "$allowed_errors" -13)"
+
+ if [ -n "$obsolete_validation_error" ]; then
+ make_annotation "The following validation errors no longer occur (compared to $reference_tag):" "$obsolete_validation_error" warning "$file"
+ fi
+ if [ -n "$new_validation_error" ]; then
+ make_annotation "Compatibility to $reference_tag is broken in the following ways:" "$new_validation_error" error README.md
+ has_problems=1
+ fi
+
+ rm -f "$reference_file" "$validate" "$validation_output" "$allowed_errors"
+done <<< "$(find "$( dirname -- "$( dirname -- "${BASH_SOURCE[0]//\.\//}" )" )/extension_api_validation/" -name "*.expected")"
+
+exit $has_problems
diff --git a/modules/gltf/structures/gltf_skin.cpp b/modules/gltf/structures/gltf_skin.cpp
index fa2e814c94..f0fb71c75d 100644
--- a/modules/gltf/structures/gltf_skin.cpp
+++ b/modules/gltf/structures/gltf_skin.cpp
@@ -65,7 +65,7 @@ void GLTFSkin::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::INT, "skeleton"), "set_skeleton", "get_skeleton"); // int
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "joint_i_to_bone_i", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "set_joint_i_to_bone_i", "get_joint_i_to_bone_i"); // RBMap<int,
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "joint_i_to_name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "set_joint_i_to_name", "get_joint_i_to_name"); // RBMap<int,
- ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "godot_skin"), "set_godot_skin", "get_godot_skin"); // Ref<Skin>
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "godot_skin", PROPERTY_HINT_RESOURCE_TYPE, "Skin"), "set_godot_skin", "get_godot_skin"); // Ref<Skin>
}
GLTFNodeIndex GLTFSkin::get_skin_root() {
diff --git a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
index 2c93539ae1..8f7e9df419 100644
--- a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
+++ b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
@@ -8,6 +8,7 @@
Visibility can be handled directly with [method set_visibility_for] or as-needed with [method add_visibility_filter] and [method update_visibility].
[MultiplayerSpawner]s will handle nodes according to visibility of synchronizers as long as the node at [member root_path] was spawned by one.
Internally, [MultiplayerSynchronizer] uses [method MultiplayerAPI.object_configuration_add] to notify synchronization start passing the [Node] at [member root_path] as the [code]object[/code] and itself as the [code]configuration[/code], and uses [method MultiplayerAPI.object_configuration_remove] to notify synchronization end in a similar way.
+ [b]Note:[/b] Synchronization is not supported for [Object] type properties, like [Resource]. Properties that are unique to each peer, like the instance IDs of [Object]s (see [method Object.get_instance_id]) or [RID]s, will also not work in synchronization.
</description>
<tutorials>
</tutorials>
@@ -51,6 +52,9 @@
</method>
</methods>
<members>
+ <member name="delta_interval" type="float" setter="set_delta_interval" getter="get_delta_interval" default="0.0">
+ Time interval between delta synchronizations. When set to [code]0.0[/code] (the default), delta synchronizations happen every network process frame.
+ </member>
<member name="public_visibility" type="bool" setter="set_visibility_public" getter="is_visibility_public" default="true">
Whether synchronization should be visible to all peers by default. See [method set_visibility_for] and [method add_visibility_filter] for ways of configuring fine-grained visibility options.
</member>
@@ -58,7 +62,7 @@
Resource containing which properties to synchronize.
</member>
<member name="replication_interval" type="float" setter="set_replication_interval" getter="get_replication_interval" default="0.0">
- Time interval between synchronizes. When set to [code]0.0[/code] (the default), synchronizes happen every network process frame.
+ Time interval between synchronizations. When set to [code]0.0[/code] (the default), synchronizations happen every network process frame.
</member>
<member name="root_path" type="NodePath" setter="set_root_path" getter="get_root_path" default="NodePath(&quot;..&quot;)">
Node path that replicated properties are relative to.
@@ -69,9 +73,14 @@
</member>
</members>
<signals>
+ <signal name="delta_synchronized">
+ <description>
+ Emitted when a new delta synchronization state is received by this synchronizer after the properties have been updated.
+ </description>
+ </signal>
<signal name="synchronized">
<description>
- Emitted when a new synchronization state is received by this synchronizer after the variables have been updated.
+ Emitted when a new synchronization state is received by this synchronizer after the properties have been updated.
</description>
</signal>
<signal name="visibility_changed">
diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
index 2445d60f48..2df2d53b4d 100644
--- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml
+++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
@@ -70,6 +70,12 @@
<member name="auth_timeout" type="float" setter="set_auth_timeout" getter="get_auth_timeout" default="3.0">
If set to a value greater than [code]0.0[/code], the maximum amount of time peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals.
</member>
+ <member name="max_delta_packet_size" type="int" setter="set_max_delta_packet_size" getter="get_max_delta_packet_size" default="65535">
+ Maximum size of each delta packet. Higher values increase the chance of receiving full updates in a single frame, but also the chance of causing networking congestion (higher latency, disconnections). See [MultiplayerSynchronizer].
+ </member>
+ <member name="max_sync_packet_size" type="int" setter="set_max_sync_packet_size" getter="get_max_sync_packet_size" default="1350">
+ Maximum size of each synchronization packet. Higher values increase the chance of receiving full updates in a single frame, but also the chance of packet loss. See [MultiplayerSynchronizer].
+ </member>
<member name="refuse_new_connections" type="bool" setter="set_refuse_new_connections" getter="is_refusing_new_connections" default="false">
If [code]true[/code], the MultiplayerAPI's [member MultiplayerAPI.multiplayer_peer] refuses new incoming connections.
</member>
diff --git a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
index 53ea1d19a1..f1f0d158a9 100644
--- a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
+++ b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
@@ -14,6 +14,7 @@
<param index="1" name="index" type="int" default="-1" />
<description>
Adds the property identified by the given [param path] to the list of the properties being synchronized, optionally passing an [param index].
+ [b]Note:[/b] For details on restrictions and limitations on property synchronization, see [MultiplayerSynchronizer].
</description>
</method>
<method name="get_properties" qualifiers="const">
@@ -50,6 +51,13 @@
Returns whether the property identified by the given [param path] is configured to be synchronized on process.
</description>
</method>
+ <method name="property_get_watch">
+ <return type="bool" />
+ <param index="0" name="path" type="NodePath" />
+ <description>
+ Returns whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+ </description>
+ </method>
<method name="property_set_spawn">
<return type="void" />
<param index="0" name="path" type="NodePath" />
@@ -66,6 +74,14 @@
Sets whether the property identified by the given [param path] is configured to be synchronized on process.
</description>
</method>
+ <method name="property_set_watch">
+ <return type="void" />
+ <param index="0" name="path" type="NodePath" />
+ <param index="1" name="enabled" type="bool" />
+ <description>
+ Sets whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+ </description>
+ </method>
<method name="remove_property">
<return type="void" />
<param index="0" name="path" type="NodePath" />
diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp
index 1f707f1192..04aa856bf9 100644
--- a/modules/multiplayer/editor/replication_editor.cpp
+++ b/modules/multiplayer/editor/replication_editor.cpp
@@ -195,6 +195,51 @@ ReplicationEditor::ReplicationEditor() {
prop_selector = memnew(PropertySelector);
add_child(prop_selector);
+ // Filter out properties that cannot be synchronized.
+ // * RIDs do not match across network.
+ // * Objects are too large for replication.
+ Vector<Variant::Type> types = {
+ Variant::BOOL,
+ Variant::INT,
+ Variant::FLOAT,
+ Variant::STRING,
+
+ Variant::VECTOR2,
+ Variant::VECTOR2I,
+ Variant::RECT2,
+ Variant::RECT2I,
+ Variant::VECTOR3,
+ Variant::VECTOR3I,
+ Variant::TRANSFORM2D,
+ Variant::VECTOR4,
+ Variant::VECTOR4I,
+ Variant::PLANE,
+ Variant::QUATERNION,
+ Variant::AABB,
+ Variant::BASIS,
+ Variant::TRANSFORM3D,
+ Variant::PROJECTION,
+
+ Variant::COLOR,
+ Variant::STRING_NAME,
+ Variant::NODE_PATH,
+ // Variant::RID,
+ // Variant::OBJECT,
+ Variant::SIGNAL,
+ Variant::DICTIONARY,
+ Variant::ARRAY,
+
+ Variant::PACKED_BYTE_ARRAY,
+ Variant::PACKED_INT32_ARRAY,
+ Variant::PACKED_INT64_ARRAY,
+ Variant::PACKED_FLOAT32_ARRAY,
+ Variant::PACKED_FLOAT64_ARRAY,
+ Variant::PACKED_STRING_ARRAY,
+ Variant::PACKED_VECTOR2_ARRAY,
+ Variant::PACKED_VECTOR3_ARRAY,
+ Variant::PACKED_COLOR_ARRAY
+ };
+ prop_selector->set_type_filter(types);
prop_selector->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_property_selected));
HBoxContainer *hb = memnew(HBoxContainer);
@@ -226,7 +271,7 @@ ReplicationEditor::ReplicationEditor() {
tree = memnew(Tree);
tree->set_hide_root(true);
- tree->set_columns(4);
+ tree->set_columns(5);
tree->set_column_titles_visible(true);
tree->set_column_title(0, TTR("Properties"));
tree->set_column_expand(0, true);
@@ -235,8 +280,11 @@ ReplicationEditor::ReplicationEditor() {
tree->set_column_custom_minimum_width(1, 100);
tree->set_column_title(2, TTR("Sync"));
tree->set_column_custom_minimum_width(2, 100);
+ tree->set_column_title(3, TTR("Watch"));
+ tree->set_column_custom_minimum_width(3, 100);
tree->set_column_expand(2, false);
tree->set_column_expand(3, false);
+ tree->set_column_expand(4, false);
tree->create_item();
tree->connect("button_clicked", callable_mp(this, &ReplicationEditor::_tree_button_pressed));
tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited));
@@ -353,17 +401,30 @@ void ReplicationEditor::_tree_item_edited() {
return;
}
int column = tree->get_edited_column();
- ERR_FAIL_COND(column < 1 || column > 2);
+ ERR_FAIL_COND(column < 1 || column > 3);
const NodePath prop = ti->get_metadata(0);
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
bool value = ti->is_checked(column);
+
+ // We have a hard limit of 64 watchable properties per synchronizer.
+ if (column == 3 && value && config->get_watch_properties().size() > 64) {
+ error_dialog->set_text(TTR("Each MultiplayerSynchronizer can have no more than 64 watched properties."));
+ error_dialog->popup_centered();
+ ti->set_checked(column, false);
+ return;
+ }
String method;
if (column == 1) {
undo_redo->create_action(TTR("Set spawn property"));
method = "property_set_spawn";
- } else {
+ } else if (column == 2) {
undo_redo->create_action(TTR("Set sync property"));
method = "property_set_sync";
+ } else if (column == 3) {
+ undo_redo->create_action(TTR("Set watch property"));
+ method = "property_set_watch";
+ } else {
+ ERR_FAIL();
}
undo_redo->add_do_method(config.ptr(), method, prop, value);
undo_redo->add_undo_method(config.ptr(), method, prop, !value);
@@ -395,12 +456,14 @@ void ReplicationEditor::_dialog_closed(bool p_confirmed) {
int idx = config->property_get_index(prop);
bool spawn = config->property_get_spawn(prop);
bool sync = config->property_get_sync(prop);
+ bool watch = config->property_get_watch(prop);
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Remove Property"));
undo_redo->add_do_method(config.ptr(), "remove_property", prop);
undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx);
undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn);
undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync);
+ undo_redo->add_undo_method(config.ptr(), "property_set_watch", prop, watch);
undo_redo->add_do_method(this, "_update_config");
undo_redo->add_undo_method(this, "_update_config");
undo_redo->commit_action();
@@ -436,7 +499,7 @@ void ReplicationEditor::_update_config() {
}
for (int i = 0; i < props.size(); i++) {
const NodePath path = props[i];
- _add_property(path, config->property_get_spawn(path), config->property_get_sync(path));
+ _add_property(path, config->property_get_spawn(path), config->property_get_sync(path), config->property_get_watch(path));
}
}
@@ -460,13 +523,32 @@ Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) {
return get_theme_icon(p_node->get_class(), "EditorIcons");
}
-void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) {
+static bool can_sync(const Variant &p_var) {
+ switch (p_var.get_type()) {
+ case Variant::RID:
+ case Variant::OBJECT:
+ return false;
+ case Variant::ARRAY: {
+ const Array &arr = p_var;
+ if (arr.is_typed()) {
+ const uint32_t type = arr.get_typed_builtin();
+ return (type != Variant::RID) && (type != Variant::OBJECT);
+ }
+ return true;
+ }
+ default:
+ return true;
+ }
+}
+
+void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync, bool p_watch) {
String prop = String(p_property);
TreeItem *item = tree->create_item();
item->set_selectable(0, false);
item->set_selectable(1, false);
item->set_selectable(2, false);
item->set_selectable(3, false);
+ item->set_selectable(4, false);
item->set_text(0, prop);
item->set_metadata(0, prop);
Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr;
@@ -480,9 +562,15 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
}
item->set_text(0, String(node->get_name()) + ":" + subpath);
icon = _get_class_icon(node);
+ bool valid = false;
+ Variant value = node->get(subpath, &valid);
+ if (valid && !can_sync(value)) {
+ item->set_icon(3, get_theme_icon(SNAME("StatusWarning"), SNAME("EditorIcons")));
+ item->set_tooltip_text(3, TTR("Property of this type not supported."));
+ }
}
item->set_icon(0, icon);
- item->add_button(3, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
+ item->add_button(4, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER);
item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK);
item->set_checked(1, p_spawn);
@@ -491,4 +579,8 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK);
item->set_checked(2, p_sync);
item->set_editable(2, true);
+ item->set_text_alignment(3, HORIZONTAL_ALIGNMENT_CENTER);
+ item->set_cell_mode(3, TreeItem::CELL_MODE_CHECK);
+ item->set_checked(3, p_watch);
+ item->set_editable(3, true);
}
diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h
index d1e0e0a541..262c10ea81 100644
--- a/modules/multiplayer/editor/replication_editor.h
+++ b/modules/multiplayer/editor/replication_editor.h
@@ -76,7 +76,7 @@ private:
void _update_checked(const NodePath &p_prop, int p_column, bool p_checked);
void _update_config();
void _dialog_closed(bool p_confirmed);
- void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true);
+ void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true, bool p_watch = false);
void _pick_node_filter_text_changed(const String &p_newtext);
void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates);
diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp
index 458b6a664a..e3c31352a7 100644
--- a/modules/multiplayer/multiplayer_synchronizer.cpp
+++ b/modules/multiplayer/multiplayer_synchronizer.cpp
@@ -105,7 +105,7 @@ Node *MultiplayerSynchronizer::get_root_node() {
void MultiplayerSynchronizer::reset() {
net_id = 0;
- last_sync_msec = 0;
+ last_sync_usec = 0;
last_inbound_sync = 0;
}
@@ -117,16 +117,17 @@ void MultiplayerSynchronizer::set_net_id(uint32_t p_net_id) {
net_id = p_net_id;
}
-bool MultiplayerSynchronizer::update_outbound_sync_time(uint64_t p_msec) {
- if (last_sync_msec == p_msec) {
- // last_sync_msec has been updated on this frame.
+bool MultiplayerSynchronizer::update_outbound_sync_time(uint64_t p_usec) {
+ if (last_sync_usec == p_usec) {
+ // last_sync_usec has been updated in this frame.
return true;
}
- if (p_msec >= last_sync_msec + interval_msec) {
- last_sync_msec = p_msec;
- return true;
+ if (p_usec < last_sync_usec + sync_interval_usec) {
+ // Too soon, should skip this synchronization frame.
+ return false;
}
- return false;
+ last_sync_usec = p_usec;
+ return true;
}
bool MultiplayerSynchronizer::update_inbound_sync_time(uint16_t p_network_time) {
@@ -243,6 +244,9 @@ void MultiplayerSynchronizer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_replication_interval", "milliseconds"), &MultiplayerSynchronizer::set_replication_interval);
ClassDB::bind_method(D_METHOD("get_replication_interval"), &MultiplayerSynchronizer::get_replication_interval);
+ ClassDB::bind_method(D_METHOD("set_delta_interval", "milliseconds"), &MultiplayerSynchronizer::set_delta_interval);
+ ClassDB::bind_method(D_METHOD("get_delta_interval"), &MultiplayerSynchronizer::get_delta_interval);
+
ClassDB::bind_method(D_METHOD("set_replication_config", "config"), &MultiplayerSynchronizer::set_replication_config);
ClassDB::bind_method(D_METHOD("get_replication_config"), &MultiplayerSynchronizer::get_replication_config);
@@ -260,6 +264,7 @@ void MultiplayerSynchronizer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_replication_interval", "get_replication_interval");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "delta_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_delta_interval", "get_delta_interval");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replication_config", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig", PROPERTY_USAGE_NO_EDITOR), "set_replication_config", "get_replication_config");
ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_update_mode", PROPERTY_HINT_ENUM, "Idle,Physics,None"), "set_visibility_update_mode", "get_visibility_update_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "public_visibility"), "set_visibility_public", "is_visibility_public");
@@ -269,6 +274,7 @@ void MultiplayerSynchronizer::_bind_methods() {
BIND_ENUM_CONSTANT(VISIBILITY_PROCESS_NONE);
ADD_SIGNAL(MethodInfo("synchronized"));
+ ADD_SIGNAL(MethodInfo("delta_synchronized"));
ADD_SIGNAL(MethodInfo("visibility_changed", PropertyInfo(Variant::INT, "for_peer")));
}
@@ -300,11 +306,20 @@ void MultiplayerSynchronizer::_notification(int p_what) {
void MultiplayerSynchronizer::set_replication_interval(double p_interval) {
ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)");
- interval_msec = uint64_t(p_interval * 1000);
+ sync_interval_usec = uint64_t(p_interval * 1000 * 1000);
}
double MultiplayerSynchronizer::get_replication_interval() const {
- return double(interval_msec) / 1000.0;
+ return double(sync_interval_usec) / 1000.0 / 1000.0;
+}
+
+void MultiplayerSynchronizer::set_delta_interval(double p_interval) {
+ ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)");
+ delta_interval_usec = uint64_t(p_interval * 1000 * 1000);
+}
+
+double MultiplayerSynchronizer::get_delta_interval() const {
+ return double(delta_interval_usec) / 1000.0 / 1000.0;
}
void MultiplayerSynchronizer::set_replication_config(Ref<SceneReplicationConfig> p_config) {
@@ -349,6 +364,84 @@ void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_re
get_multiplayer()->object_configuration_add(node, this);
}
+Error MultiplayerSynchronizer::_watch_changes(uint64_t p_usec) {
+ ERR_FAIL_COND_V(replication_config.is_null(), FAILED);
+ const List<NodePath> props = replication_config->get_watch_properties();
+ if (props.size() != watchers.size()) {
+ watchers.resize(props.size());
+ }
+ if (props.size() == 0) {
+ return OK;
+ }
+ Node *node = get_root_node();
+ ERR_FAIL_COND_V(!node, FAILED);
+ int idx = -1;
+ Watcher *ptr = watchers.ptrw();
+ for (const NodePath &prop : props) {
+ idx++;
+ bool valid = false;
+ const Object *obj = _get_prop_target(node, prop);
+ ERR_CONTINUE_MSG(!obj, vformat("Node not found for property '%s'.", prop));
+ Variant v = obj->get(prop.get_concatenated_subnames(), &valid);
+ ERR_CONTINUE_MSG(!valid, vformat("Property '%s' not found.", prop));
+ Watcher &w = ptr[idx];
+ if (w.prop != prop) {
+ w.prop = prop;
+ w.value = v.duplicate(true);
+ w.last_change_usec = p_usec;
+ } else if (!w.value.hash_compare(v)) {
+ w.value = v.duplicate(true);
+ w.last_change_usec = p_usec;
+ }
+ }
+ return OK;
+}
+
+List<Variant> MultiplayerSynchronizer::get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes) {
+ r_indexes = 0;
+ List<Variant> out;
+
+ if (last_watch_usec == p_cur_usec) {
+ // We already watched for changes in this frame.
+
+ } else if (p_cur_usec < p_last_usec + delta_interval_usec) {
+ // Too soon skip delta synchronization.
+ return out;
+
+ } else {
+ // Watch for changes.
+ Error err = _watch_changes(p_cur_usec);
+ ERR_FAIL_COND_V(err != OK, out);
+ last_watch_usec = p_cur_usec;
+ }
+
+ const Watcher *ptr = watchers.size() ? watchers.ptr() : nullptr;
+ for (int i = 0; i < watchers.size(); i++) {
+ const Watcher &w = ptr[i];
+ if (w.last_change_usec <= p_last_usec) {
+ continue;
+ }
+ out.push_back(w.value);
+ r_indexes |= 1ULL << i;
+ }
+ return out;
+}
+
+List<NodePath> MultiplayerSynchronizer::get_delta_properties(uint64_t p_indexes) {
+ List<NodePath> out;
+ ERR_FAIL_COND_V(replication_config.is_null(), out);
+ const List<NodePath> watch_props = replication_config->get_watch_properties();
+ int idx = 0;
+ for (const NodePath &prop : watch_props) {
+ if ((p_indexes & (1ULL << idx)) == 0) {
+ continue;
+ }
+ out.push_back(prop);
+ idx++;
+ }
+ return out;
+}
+
MultiplayerSynchronizer::MultiplayerSynchronizer() {
// Publicly visible by default.
peer_visibility.insert(0);
diff --git a/modules/multiplayer/multiplayer_synchronizer.h b/modules/multiplayer/multiplayer_synchronizer.h
index 11590f4156..6fb249d199 100644
--- a/modules/multiplayer/multiplayer_synchronizer.h
+++ b/modules/multiplayer/multiplayer_synchronizer.h
@@ -46,15 +46,24 @@ public:
};
private:
+ struct Watcher {
+ NodePath prop;
+ uint64_t last_change_usec = 0;
+ Variant value;
+ };
+
Ref<SceneReplicationConfig> replication_config;
NodePath root_path = NodePath(".."); // Start with parent, like with AnimationPlayer.
- uint64_t interval_msec = 0;
+ uint64_t sync_interval_usec = 0;
+ uint64_t delta_interval_usec = 0;
VisibilityUpdateMode visibility_update_mode = VISIBILITY_PROCESS_IDLE;
HashSet<Callable> visibility_filters;
HashSet<int> peer_visibility;
+ Vector<Watcher> watchers;
+ uint64_t last_watch_usec = 0;
ObjectID root_node_cache;
- uint64_t last_sync_msec = 0;
+ uint64_t last_sync_usec = 0;
uint16_t last_inbound_sync = 0;
uint32_t net_id = 0;
@@ -62,6 +71,7 @@ private:
void _start();
void _stop();
void _update_process();
+ Error _watch_changes(uint64_t p_usec);
protected:
static void _bind_methods();
@@ -77,7 +87,7 @@ public:
uint32_t get_net_id() const;
void set_net_id(uint32_t p_net_id);
- bool update_outbound_sync_time(uint64_t p_msec);
+ bool update_outbound_sync_time(uint64_t p_usec);
bool update_inbound_sync_time(uint16_t p_network_time);
PackedStringArray get_configuration_warnings() const override;
@@ -85,6 +95,9 @@ public:
void set_replication_interval(double p_interval);
double get_replication_interval() const;
+ void set_delta_interval(double p_interval);
+ double get_delta_interval() const;
+
void set_replication_config(Ref<SceneReplicationConfig> p_config);
Ref<SceneReplicationConfig> get_replication_config();
@@ -103,6 +116,9 @@ public:
void remove_visibility_filter(Callable p_callback);
VisibilityUpdateMode get_visibility_update_mode() const;
+ List<Variant> get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes);
+ List<NodePath> get_delta_properties(uint64_t p_indexes);
+
MultiplayerSynchronizer();
};
diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp
index 01fc1b5275..7a424e83f8 100644
--- a/modules/multiplayer/scene_multiplayer.cpp
+++ b/modules/multiplayer/scene_multiplayer.cpp
@@ -617,6 +617,22 @@ bool SceneMultiplayer::is_server_relay_enabled() const {
return server_relay;
}
+void SceneMultiplayer::set_max_sync_packet_size(int p_size) {
+ replicator->set_max_sync_packet_size(p_size);
+}
+
+int SceneMultiplayer::get_max_sync_packet_size() const {
+ return replicator->get_max_sync_packet_size();
+}
+
+void SceneMultiplayer::set_max_delta_packet_size(int p_size) {
+ replicator->set_max_delta_packet_size(p_size);
+}
+
+int SceneMultiplayer::get_max_delta_packet_size() const {
+ return replicator->get_max_delta_packet_size();
+}
+
void SceneMultiplayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_root_path", "path"), &SceneMultiplayer::set_root_path);
ClassDB::bind_method(D_METHOD("get_root_path"), &SceneMultiplayer::get_root_path);
@@ -641,12 +657,19 @@ void SceneMultiplayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_server_relay_enabled"), &SceneMultiplayer::is_server_relay_enabled);
ClassDB::bind_method(D_METHOD("send_bytes", "bytes", "id", "mode", "channel"), &SceneMultiplayer::send_bytes, DEFVAL(MultiplayerPeer::TARGET_PEER_BROADCAST), DEFVAL(MultiplayerPeer::TRANSFER_MODE_RELIABLE), DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("get_max_sync_packet_size"), &SceneMultiplayer::get_max_sync_packet_size);
+ ClassDB::bind_method(D_METHOD("set_max_sync_packet_size", "size"), &SceneMultiplayer::set_max_sync_packet_size);
+ ClassDB::bind_method(D_METHOD("get_max_delta_packet_size"), &SceneMultiplayer::get_max_delta_packet_size);
+ ClassDB::bind_method(D_METHOD("set_max_delta_packet_size", "size"), &SceneMultiplayer::set_max_delta_packet_size);
+
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
ADD_PROPERTY(PropertyInfo(Variant::CALLABLE, "auth_callback"), "set_auth_callback", "get_auth_callback");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "auth_timeout", PROPERTY_HINT_RANGE, "0,30,0.1,or_greater,suffix:s"), "set_auth_timeout", "get_auth_timeout");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_connections"), "set_refuse_new_connections", "is_refusing_new_connections");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "server_relay"), "set_server_relay_enabled", "is_server_relay_enabled");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "max_sync_packet_size"), "set_max_sync_packet_size", "get_max_sync_packet_size");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "max_delta_packet_size"), "set_max_delta_packet_size", "get_max_delta_packet_size");
ADD_PROPERTY_DEFAULT("refuse_new_connections", false);
diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h
index 1dbbf07853..678ae932f1 100644
--- a/modules/multiplayer/scene_multiplayer.h
+++ b/modules/multiplayer/scene_multiplayer.h
@@ -195,6 +195,12 @@ public:
void set_server_relay_enabled(bool p_enabled);
bool is_server_relay_enabled() const;
+ void set_max_sync_packet_size(int p_size);
+ int get_max_sync_packet_size() const;
+
+ void set_max_delta_packet_size(int p_size);
+ int get_max_delta_packet_size() const;
+
Ref<SceneCacheInterface> get_path_cache() { return cache; }
Ref<SceneReplicationInterface> get_replicator() { return replicator; }
diff --git a/modules/multiplayer/scene_replication_config.cpp b/modules/multiplayer/scene_replication_config.cpp
index b91c755c62..af6af35219 100644
--- a/modules/multiplayer/scene_replication_config.cpp
+++ b/modules/multiplayer/scene_replication_config.cpp
@@ -72,6 +72,14 @@ bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_val
spawn_props.erase(prop.name);
}
return true;
+ } else if (what == "watch") {
+ prop.watch = p_value;
+ if (prop.watch) {
+ watch_props.push_back(prop.name);
+ } else {
+ watch_props.erase(prop.name);
+ }
+ return true;
}
}
return false;
@@ -94,6 +102,9 @@ bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) cons
} else if (what == "spawn") {
r_ret = prop.spawn;
return true;
+ } else if (what == "watch") {
+ r_ret = prop.watch;
+ return true;
}
}
return false;
@@ -104,6 +115,7 @@ void SceneReplicationConfig::_get_property_list(List<PropertyInfo> *p_list) cons
p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/spawn", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
+ p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/watch", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
}
}
@@ -212,6 +224,27 @@ void SceneReplicationConfig::property_set_sync(const NodePath &p_path, bool p_en
}
}
+bool SceneReplicationConfig::property_get_watch(const NodePath &p_path) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND_V(!E, false);
+ return E->get().watch;
+}
+
+void SceneReplicationConfig::property_set_watch(const NodePath &p_path, bool p_enabled) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND(!E);
+ if (E->get().watch == p_enabled) {
+ return;
+ }
+ E->get().watch = p_enabled;
+ watch_props.clear();
+ for (const ReplicationProperty &prop : properties) {
+ if (prop.watch) {
+ watch_props.push_back(p_path);
+ }
+ }
+}
+
void SceneReplicationConfig::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_properties"), &SceneReplicationConfig::get_properties);
ClassDB::bind_method(D_METHOD("add_property", "path", "index"), &SceneReplicationConfig::add_property, DEFVAL(-1));
@@ -222,4 +255,6 @@ void SceneReplicationConfig::_bind_methods() {
ClassDB::bind_method(D_METHOD("property_set_spawn", "path", "enabled"), &SceneReplicationConfig::property_set_spawn);
ClassDB::bind_method(D_METHOD("property_get_sync", "path"), &SceneReplicationConfig::property_get_sync);
ClassDB::bind_method(D_METHOD("property_set_sync", "path", "enabled"), &SceneReplicationConfig::property_set_sync);
+ ClassDB::bind_method(D_METHOD("property_get_watch", "path"), &SceneReplicationConfig::property_get_watch);
+ ClassDB::bind_method(D_METHOD("property_set_watch", "path", "enabled"), &SceneReplicationConfig::property_set_watch);
}
diff --git a/modules/multiplayer/scene_replication_config.h b/modules/multiplayer/scene_replication_config.h
index addfec4da3..d4b0a611bc 100644
--- a/modules/multiplayer/scene_replication_config.h
+++ b/modules/multiplayer/scene_replication_config.h
@@ -45,6 +45,7 @@ private:
NodePath name;
bool spawn = true;
bool sync = true;
+ bool watch = false;
bool operator==(const ReplicationProperty &p_to) {
return name == p_to.name;
@@ -60,6 +61,7 @@ private:
List<ReplicationProperty> properties;
List<NodePath> spawn_props;
List<NodePath> sync_props;
+ List<NodePath> watch_props;
protected:
static void _bind_methods();
@@ -82,8 +84,12 @@ public:
bool property_get_sync(const NodePath &p_path);
void property_set_sync(const NodePath &p_path, bool p_enabled);
+ bool property_get_watch(const NodePath &p_path);
+ void property_set_watch(const NodePath &p_path, bool p_enabled);
+
const List<NodePath> &get_spawn_properties() { return spawn_props; }
const List<NodePath> &get_sync_properties() { return sync_props; }
+ const List<NodePath> &get_watch_properties() { return watch_props; }
SceneReplicationConfig() {}
};
diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp
index 5889b8f5f9..b058bf7a52 100644
--- a/modules/multiplayer/scene_replication_interface.cpp
+++ b/modules/multiplayer/scene_replication_interface.cpp
@@ -138,15 +138,16 @@ void SceneReplicationInterface::on_network_process() {
spawn_queue.clear();
}
- // Process timed syncs.
- uint64_t msec = OS::get_singleton()->get_ticks_msec();
+ // Process syncs.
+ uint64_t usec = OS::get_singleton()->get_ticks_usec();
for (KeyValue<int, PeerInfo> &E : peers_info) {
const HashSet<ObjectID> to_sync = E.value.sync_nodes;
if (to_sync.is_empty()) {
continue; // Nothing to sync
}
uint16_t sync_net_time = ++E.value.last_sent_sync;
- _send_sync(E.key, to_sync, sync_net_time, msec);
+ _send_sync(E.key, to_sync, sync_net_time, usec);
+ _send_delta(E.key, to_sync, usec, E.value.last_watch_usecs);
}
}
@@ -280,6 +281,7 @@ Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_co
sync_nodes.erase(sid);
for (KeyValue<int, PeerInfo> &E : peers_info) {
E.value.sync_nodes.erase(sid);
+ E.value.last_watch_usecs.erase(sid);
if (sync->get_net_id()) {
E.value.recv_sync_ids.erase(sync->get_net_id());
}
@@ -357,6 +359,7 @@ Error SceneReplicationInterface::_update_sync_visibility(int p_peer, Multiplayer
E.value.sync_nodes.insert(sid);
} else {
E.value.sync_nodes.erase(sid);
+ E.value.last_watch_usecs.erase(sid);
}
}
return OK;
@@ -369,6 +372,7 @@ Error SceneReplicationInterface::_update_sync_visibility(int p_peer, Multiplayer
peers_info[p_peer].sync_nodes.insert(sid);
} else {
peers_info[p_peer].sync_nodes.erase(sid);
+ peers_info[p_peer].last_watch_usecs.erase(sid);
}
return OK;
}
@@ -670,8 +674,126 @@ Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p
return OK;
}
-void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_msec) {
- MAKE_ROOM(sync_mtu);
+bool SceneReplicationInterface::_verify_synchronizer(int p_peer, MultiplayerSynchronizer *p_sync, uint32_t &r_net_id) {
+ r_net_id = p_sync->get_net_id();
+ if (r_net_id == 0 || (r_net_id & 0x80000000)) {
+ int path_id = 0;
+ bool verified = multiplayer->get_path_cache()->send_object_cache(p_sync, p_peer, path_id);
+ ERR_FAIL_COND_V_MSG(path_id < 0, false, "This should never happen!");
+ if (r_net_id == 0) {
+ // First time path based ID.
+ r_net_id = path_id | 0x80000000;
+ p_sync->set_net_id(r_net_id | 0x80000000);
+ }
+ return verified;
+ }
+ return true;
+}
+
+MultiplayerSynchronizer *SceneReplicationInterface::_find_synchronizer(int p_peer, uint32_t p_net_id) {
+ MultiplayerSynchronizer *sync = nullptr;
+ if (p_net_id & 0x80000000) {
+ sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_path_cache()->get_cached_object(p_peer, p_net_id & 0x7FFFFFFF));
+ } else if (peers_info[p_peer].recv_sync_ids.has(p_net_id)) {
+ const ObjectID &sid = peers_info[p_peer].recv_sync_ids[p_net_id];
+ sync = get_id_as<MultiplayerSynchronizer>(sid);
+ }
+ return sync;
+}
+
+void SceneReplicationInterface::_send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs) {
+ MAKE_ROOM(/* header */ 1 + /* element */ 4 + 8 + 4 + delta_mtu);
+ uint8_t *ptr = packet_cache.ptrw();
+ ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC | (1 << SceneMultiplayer::CMD_FLAG_0_SHIFT);
+ int ofs = 1;
+ for (const ObjectID &oid : p_synchronizers) {
+ MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(oid);
+ ERR_CONTINUE(!sync || !sync->get_replication_config().is_valid() || !sync->is_multiplayer_authority());
+ uint32_t net_id;
+ if (!_verify_synchronizer(p_peer, sync, net_id)) {
+ continue;
+ }
+ uint64_t last_usec = p_last_watch_usecs.has(oid) ? p_last_watch_usecs[oid] : 0;
+ uint64_t indexes;
+ List<Variant> delta = sync->get_delta_state(p_usec, last_usec, indexes);
+
+ if (!delta.size()) {
+ continue; // Nothing to update.
+ }
+
+ Vector<const Variant *> varp;
+ varp.resize(delta.size());
+ const Variant **vptr = varp.ptrw();
+ int i = 0;
+ for (const Variant &v : delta) {
+ vptr[i] = &v;
+ }
+ int size;
+ Error err = MultiplayerAPI::encode_and_compress_variants(vptr, varp.size(), nullptr, size);
+ ERR_CONTINUE_MSG(err != OK, "Unable to encode delta state.");
+
+ ERR_CONTINUE_MSG(size > delta_mtu, vformat("Synchronizer delta bigger than MTU will not be sent (%d > %d): %s", size, delta_mtu, sync->get_path()));
+
+ if (ofs + 4 + 8 + 4 + size > delta_mtu) {
+ // Send what we got, and reset write.
+ _send_raw(packet_cache.ptr(), ofs, p_peer, true);
+ ofs = 1;
+ }
+ if (size) {
+ ofs += encode_uint32(sync->get_net_id(), &ptr[ofs]);
+ ofs += encode_uint64(indexes, &ptr[ofs]);
+ ofs += encode_uint32(size, &ptr[ofs]);
+ MultiplayerAPI::encode_and_compress_variants(vptr, varp.size(), &ptr[ofs], size);
+ ofs += size;
+ }
+#ifdef DEBUG_ENABLED
+ _profile_node_data("delta_out", oid, size);
+#endif
+ peers_info[p_peer].last_watch_usecs[oid] = p_usec;
+ }
+ if (ofs > 1) {
+ // Got some left over to send.
+ _send_raw(packet_cache.ptr(), ofs, p_peer, true);
+ }
+}
+
+Error SceneReplicationInterface::on_delta_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
+ int ofs = 1;
+ while (ofs + 4 + 8 + 4 < p_buffer_len) {
+ uint32_t net_id = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ uint64_t indexes = decode_uint64(&p_buffer[ofs]);
+ ofs += 8;
+ uint32_t size = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA);
+ MultiplayerSynchronizer *sync = _find_synchronizer(p_from, net_id);
+ Node *node = sync ? sync->get_root_node() : nullptr;
+ if (!sync || sync->get_multiplayer_authority() != p_from || !node) {
+ ofs += size;
+ ERR_CONTINUE_MSG(true, "Ignoring delta for non-authority or invalid synchronizer.");
+ }
+ List<NodePath> props = sync->get_delta_properties(indexes);
+ ERR_FAIL_COND_V(props.size() == 0, ERR_INVALID_DATA);
+ Vector<Variant> vars;
+ vars.resize(props.size());
+ int consumed = 0;
+ Error err = MultiplayerAPI::decode_and_decompress_variants(vars, p_buffer + ofs, size, consumed);
+ ERR_FAIL_COND_V(err != OK, err);
+ ERR_FAIL_COND_V(uint32_t(consumed) != size, ERR_INVALID_DATA);
+ err = MultiplayerSynchronizer::set_state(props, node, vars);
+ ERR_FAIL_COND_V(err != OK, err);
+ ofs += size;
+ sync->emit_signal(SNAME("delta_synchronized"));
+#ifdef DEBUG_ENABLED
+ _profile_node_data("delta_in", sync->get_instance_id(), size);
+#endif
+ }
+ return OK;
+}
+
+void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec) {
+ MAKE_ROOM(/* header */ 3 + /* element */ 4 + 4 + sync_mtu);
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC;
int ofs = 1;
@@ -681,26 +803,16 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
for (const ObjectID &oid : p_synchronizers) {
MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(oid);
ERR_CONTINUE(!sync || !sync->get_replication_config().is_valid() || !sync->is_multiplayer_authority());
- if (!sync->update_outbound_sync_time(p_msec)) {
+ if (!sync->update_outbound_sync_time(p_usec)) {
continue; // nothing to sync.
}
Node *node = sync->get_root_node();
ERR_CONTINUE(!node);
uint32_t net_id = sync->get_net_id();
- if (net_id == 0 || (net_id & 0x80000000)) {
- int path_id = 0;
- bool verified = multiplayer->get_path_cache()->send_object_cache(sync, p_peer, path_id);
- ERR_CONTINUE_MSG(path_id < 0, "This should never happen!");
- if (net_id == 0) {
- // First time path based ID.
- net_id = path_id | 0x80000000;
- sync->set_net_id(net_id | 0x80000000);
- }
- if (!verified) {
- // The path based sync is not yet confirmed, skipping.
- continue;
- }
+ if (!_verify_synchronizer(p_peer, sync, net_id)) {
+ // The path based sync is not yet confirmed, skipping.
+ continue;
}
int size;
Vector<Variant> vars;
@@ -711,7 +823,7 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
err = MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), nullptr, size);
ERR_CONTINUE_MSG(err != OK, "Unable to encode sync state.");
// TODO Handle single state above MTU.
- ERR_CONTINUE_MSG(size > 3 + 4 + 4 + sync_mtu, vformat("Node states bigger then MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path()));
+ ERR_CONTINUE_MSG(size > sync_mtu, vformat("Node states bigger than MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path()));
if (ofs + 4 + 4 + size > sync_mtu) {
// Send what we got, and reset write.
_send_raw(packet_cache.ptr(), ofs, p_peer, false);
@@ -735,6 +847,10 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
ERR_FAIL_COND_V_MSG(p_buffer_len < 11, ERR_INVALID_DATA, "Invalid sync packet received");
+ bool is_delta = (p_buffer[0] & (1 << SceneMultiplayer::CMD_FLAG_0_SHIFT)) != 0;
+ if (is_delta) {
+ return on_delta_receive(p_from, p_buffer, p_buffer_len);
+ }
uint16_t time = decode_uint16(&p_buffer[1]);
int ofs = 3;
while (ofs + 8 < p_buffer_len) {
@@ -743,13 +859,7 @@ Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_bu
uint32_t size = decode_uint32(&p_buffer[ofs]);
ofs += 4;
ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA);
- MultiplayerSynchronizer *sync = nullptr;
- if (net_id & 0x80000000) {
- sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_path_cache()->get_cached_object(p_from, net_id & 0x7FFFFFFF));
- } else if (peers_info[p_from].recv_sync_ids.has(net_id)) {
- const ObjectID &sid = peers_info[p_from].recv_sync_ids[net_id];
- sync = get_id_as<MultiplayerSynchronizer>(sid);
- }
+ MultiplayerSynchronizer *sync = _find_synchronizer(p_from, net_id);
if (!sync) {
// Not received yet.
ofs += size;
@@ -782,3 +892,21 @@ Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_bu
}
return OK;
}
+
+void SceneReplicationInterface::set_max_sync_packet_size(int p_size) {
+ ERR_FAIL_COND_MSG(p_size < 128, "Sync maximum packet size must be at least 128 bytes.");
+ sync_mtu = p_size;
+}
+
+int SceneReplicationInterface::get_max_sync_packet_size() const {
+ return sync_mtu;
+}
+
+void SceneReplicationInterface::set_max_delta_packet_size(int p_size) {
+ ERR_FAIL_COND_MSG(p_size < 128, "Sync maximum packet size must be at least 128 bytes.");
+ delta_mtu = p_size;
+}
+
+int SceneReplicationInterface::get_max_delta_packet_size() const {
+ return delta_mtu;
+}
diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h
index cf45db2138..0af45c16b4 100644
--- a/modules/multiplayer/scene_replication_interface.h
+++ b/modules/multiplayer/scene_replication_interface.h
@@ -62,6 +62,7 @@ private:
struct PeerInfo {
HashSet<ObjectID> sync_nodes;
HashSet<ObjectID> spawn_nodes;
+ HashMap<ObjectID, uint64_t> last_watch_usecs;
HashMap<uint32_t, ObjectID> recv_sync_ids;
HashMap<uint32_t, ObjectID> recv_nodes;
uint16_t last_sent_sync = 0;
@@ -88,12 +89,17 @@ private:
SceneMultiplayer *multiplayer = nullptr;
PackedByteArray packet_cache;
int sync_mtu = 1350; // Highly dependent on underlying protocol.
+ int delta_mtu = 65535;
TrackedNode &_track(const ObjectID &p_id);
void _untrack(const ObjectID &p_id);
void _node_ready(const ObjectID &p_oid);
- void _send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_msec);
+ bool _verify_synchronizer(int p_peer, MultiplayerSynchronizer *p_sync, uint32_t &r_net_id);
+ MultiplayerSynchronizer *_find_synchronizer(int p_peer, uint32_t p_net_ida);
+
+ void _send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec);
+ void _send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs);
Error _make_spawn_packet(Node *p_node, MultiplayerSpawner *p_spawner, int &r_len);
Error _make_despawn_packet(Node *p_node, int &r_len);
Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable);
@@ -127,9 +133,16 @@ public:
Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
+ Error on_delta_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
bool is_rpc_visible(const ObjectID &p_oid, int p_peer) const;
+ void set_max_sync_packet_size(int p_size);
+ int get_max_sync_packet_size() const;
+
+ void set_max_delta_packet_size(int p_size);
+ int get_max_delta_packet_size() const;
+
SceneReplicationInterface(SceneMultiplayer *p_multiplayer) {
multiplayer = p_multiplayer;
}
diff --git a/platform/android/SCsub b/platform/android/SCsub
index e4d04f1df9..1f3bbc2350 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -56,7 +56,10 @@ if lib_arch_dir != "":
if env.dev_build:
lib_type_dir = "dev"
elif env.debug_features:
- lib_type_dir = "debug"
+ if env.editor_build and env["store_release"]:
+ lib_type_dir = "release"
+ else:
+ lib_type_dir = "debug"
else: # Release
lib_type_dir = "release"
diff --git a/platform/android/detect.py b/platform/android/detect.py
index 7515d0020d..20aced3524 100644
--- a/platform/android/detect.py
+++ b/platform/android/detect.py
@@ -22,6 +22,8 @@ def can_build():
def get_opts():
+ from SCons.Variables import BoolVariable
+
return [
("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()),
(
@@ -29,6 +31,7 @@ def get_opts():
'Target platform (android-<api>, e.g. "android-' + str(get_min_target_api()) + '")',
"android-" + str(get_min_target_api()),
),
+ BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
]
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 4bac6c814a..acc6b22c3a 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -135,7 +135,7 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
String statusValue = map["status"]
if (statusValue == null) {
statusCode = 0
- } else if (statusValue.startsWith("alpha")) {
+ } else if (statusValue.startsWith("alpha") || statusValue.startsWith("dev")) {
statusCode = 1
} else if (statusValue.startsWith("beta")) {
statusCode = 2
diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle
index 10c28a00b2..f94454e2a7 100644
--- a/platform/android/java/build.gradle
+++ b/platform/android/java/build.gradle
@@ -9,7 +9,7 @@ buildscript {
dependencies {
classpath libraries.androidGradlePlugin
classpath libraries.kotlinGradlePlugin
- classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
+ classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
}
}
@@ -38,9 +38,7 @@ ext {
supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
supportedFlavors = ["editor", "template"]
supportedFlavorsBuildTypes = [
- // The editor can't be used with target=release as debugging tools are then not
- // included, and it would crash on errors instead of reporting them.
- "editor": ["dev", "debug"],
+ "editor": ["dev", "debug", "release"],
"template": ["dev", "debug", "release"]
]
@@ -54,6 +52,7 @@ ext {
def rootDir = "../../.."
def binDir = "$rootDir/bin/"
+def androidEditorBuildsDir = "$binDir/android_editor_builds/"
def getSconsTaskName(String flavor, String buildType, String abi) {
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
@@ -221,18 +220,46 @@ def isAndroidStudio() {
return sysProps != null && sysProps['idea.platform.prefix'] != null
}
-task copyEditorDebugBinaryToBin(type: Copy) {
+task copyEditorReleaseApkToBin(type: Copy) {
+ dependsOn ':editor:assembleRelease'
+ from('editor/build/outputs/apk/release')
+ into(androidEditorBuildsDir)
+ include('android_editor-release*.apk')
+}
+
+task copyEditorReleaseAabToBin(type: Copy) {
+ dependsOn ':editor:bundleRelease'
+ from('editor/build/outputs/bundle/release')
+ into(androidEditorBuildsDir)
+ include('android_editor-release*.aab')
+}
+
+task copyEditorDebugApkToBin(type: Copy) {
dependsOn ':editor:assembleDebug'
from('editor/build/outputs/apk/debug')
- into(binDir)
- include('android_editor.apk')
+ into(androidEditorBuildsDir)
+ include('android_editor-debug.apk')
}
-task copyEditorDevBinaryToBin(type: Copy) {
+task copyEditorDebugAabToBin(type: Copy) {
+ dependsOn ':editor:bundleDebug'
+ from('editor/build/outputs/bundle/debug')
+ into(androidEditorBuildsDir)
+ include('android_editor-debug.aab')
+}
+
+task copyEditorDevApkToBin(type: Copy) {
dependsOn ':editor:assembleDev'
from('editor/build/outputs/apk/dev')
- into(binDir)
- include('android_editor_dev.apk')
+ into(androidEditorBuildsDir)
+ include('android_editor-dev.apk')
+}
+
+task copyEditorDevAabToBin(type: Copy) {
+ dependsOn ':editor:bundleDev'
+ from('editor/build/outputs/bundle/dev')
+ into(androidEditorBuildsDir)
+ include('android_editor-dev.aab')
}
/**
@@ -253,7 +280,8 @@ task generateGodotEditor {
&& targetLibs.isDirectory()
&& targetLibs.listFiles() != null
&& targetLibs.listFiles().length > 0) {
- tasks += "copyEditor${target.capitalize()}BinaryToBin"
+ tasks += "copyEditor${target.capitalize()}ApkToBin"
+ tasks += "copyEditor${target.capitalize()}AabToBin"
}
}
@@ -301,9 +329,11 @@ task cleanGodotEditor(type: Delete) {
// Delete the generated binary apks
delete("editor/build/outputs/apk")
- // Delete the Godot editor apks in the Godot bin directory
- delete("$binDir/android_editor.apk")
- delete("$binDir/android_editor_dev.apk")
+ // Delete the generated aab binaries
+ delete("editor/build/outputs/bundle")
+
+ // Delete the Godot editor apks & aabs in the Godot bin directory
+ delete(androidEditorBuildsDir)
}
/**
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 9152492e9d..38034aa47c 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -13,22 +13,67 @@ dependencies {
}
ext {
- // Build number added as a suffix to the version code, and incremented for each build/upload to
- // the Google Play store.
- // This should be reset on each stable release of Godot.
- editorBuildNumber = 0
+ // Retrieve the build number from the environment variable; default to 0 if none is specified.
+ // The build number is added as a suffix to the version code for upload to the Google Play store.
+ getEditorBuildNumber = { ->
+ int buildNumber = 0
+ String versionStatus = System.getenv("GODOT_VERSION_STATUS")
+ if (versionStatus != null && !versionStatus.isEmpty()) {
+ try {
+ buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""));
+ } catch (NumberFormatException ignored) {
+ buildNumber = 0
+ }
+ }
+
+ return buildNumber
+ }
// Value by which the Godot version code should be offset by to make room for the build number
editorBuildNumberOffset = 100
+
+ // Return the keystore file used for signing the release build.
+ getGodotKeystoreFile = { ->
+ def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE")
+ if (keyStore == null) {
+ return null
+ }
+ return file(keyStore)
+ }
+
+ // Return the key alias used for signing the release build.
+ getGodotKeyAlias = { ->
+ def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS")
+ return kAlias
+ }
+
+ // Return the password for the key used for signing the release build.
+ getGodotSigningPassword = { ->
+ def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD")
+ return signingPassword
+ }
+
+ // Returns true if the environment variables contains the configuration for signing the release
+ // build.
+ hasReleaseSigningConfigs = { ->
+ def keystoreFile = getGodotKeystoreFile()
+ def keyAlias = getGodotKeyAlias()
+ def signingPassword = getGodotSigningPassword()
+
+ return keystoreFile != null && keystoreFile.isFile()
+ && keyAlias != null && !keyAlias.isEmpty()
+ && signingPassword != null && !signingPassword.isEmpty()
+ }
}
def generateVersionCode() {
int libraryVersionCode = getGodotLibraryVersionCode()
- return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber
+ return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber()
}
def generateVersionName() {
String libraryVersionName = getGodotLibraryVersionName()
- return libraryVersionName + ".$editorBuildNumber"
+ int buildNumber = getEditorBuildNumber()
+ return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber"
}
android {
@@ -45,6 +90,7 @@ android {
targetSdkVersion versions.targetSdk
missingDimensionStrategy 'products', 'editor'
+ setProperty("archivesBaseName", "android_editor")
}
compileOptions {
@@ -56,6 +102,15 @@ android {
jvmTarget = versions.javaVersion
}
+ signingConfigs {
+ release {
+ storeFile getGodotKeystoreFile()
+ storePassword getGodotSigningPassword()
+ keyAlias getGodotKeyAlias()
+ keyPassword getGodotSigningPassword()
+ }
+ }
+
buildTypes {
dev {
initWith debug
@@ -64,15 +119,14 @@ android {
debug {
initWith release
-
- // Need to swap with the release signing config when this is ready for public release.
+ applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
}
release {
- // This buildtype is disabled below.
- // The editor can't be used with target=release only, as debugging tools are then not
- // included, and it would crash on errors instead of reporting them.
+ if (hasReleaseSigningConfigs()) {
+ signingConfig signingConfigs.release
+ }
}
}
@@ -82,20 +136,4 @@ android {
doNotStrip '**/*.so'
}
}
-
- // Disable 'release' buildtype.
- // The editor can't be used with target=release only, as debugging tools are then not
- // included, and it would crash on errors instead of reporting them.
- variantFilter { variant ->
- if (variant.buildType.name == "release") {
- setIgnore(true)
- }
- }
-
- applicationVariants.all { variant ->
- variant.outputs.all { output ->
- def suffix = variant.name == "dev" ? "_dev" : ""
- output.outputFileName = "android_editor${suffix}.apk"
- }
- }
}
diff --git a/platform/android/java/editor/src/debug/res/values/strings.xml b/platform/android/java/editor/src/debug/res/values/strings.xml
new file mode 100644
index 0000000000..09ee2d77e1
--- /dev/null
+++ b/platform/android/java/editor/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="godot_editor_name_string">Godot Editor 4 (debug)</string>
+</resources>
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 42ef1436f3..8b6efd572f 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
@@ -113,6 +113,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/build.gradle b/platform/android/java/lib/build.gradle
index 38133ddd51..4340250ad3 100644
--- a/platform/android/java/lib/build.gradle
+++ b/platform/android/java/lib/build.gradle
@@ -80,19 +80,11 @@ android {
release.jniLibs.srcDirs = ['libs/release']
// Editor jni library
+ editorRelease.jniLibs.srcDirs = ['libs/tools/release']
editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
editorDev.jniLibs.srcDirs = ['libs/tools/dev']
}
- // Disable 'editorRelease'.
- // The editor can't be used with target=release as debugging tools are then not
- // included, and it would crash on errors instead of reporting them.
- variantFilter { variant ->
- if (variant.name == "editorRelease") {
- setIgnore(true)
- }
- }
-
libraryVariants.all { variant ->
def flavorName = variant.getFlavorName()
if (flavorName == null || flavorName == "") {
@@ -105,9 +97,14 @@ android {
}
boolean devBuild = buildType == "dev"
+ boolean runTests = devBuild
+ boolean productionBuild = !devBuild
+ boolean storeRelease = buildType == "release"
def sconsTarget = flavorName
if (sconsTarget == "template") {
+ // Tests are not supported on template builds
+ runTests = false
switch (buildType) {
case "release":
sconsTarget += "_release"
@@ -135,10 +132,10 @@ android {
def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows()
? [".bat", ".cmd", ".ps1", ".exe"]
: [""])
- logger.lifecycle("Looking for $sconsName executable path")
+ logger.debug("Looking for $sconsName executable path")
for (ext in sconsExts) {
String sconsNameExt = sconsName + ext
- logger.lifecycle("Checking $sconsNameExt")
+ logger.debug("Checking $sconsNameExt")
sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt)
if (sconsExecutableFile != null) {
// We're done!
@@ -155,7 +152,7 @@ android {
if (sconsExecutableFile == null) {
throw new GradleException("Unable to find executable path for the '$sconsName' command.")
} else {
- logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
+ logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
}
for (String selectedAbi : selectedAbis) {
@@ -167,7 +164,7 @@ android {
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
tasks.create(name: taskName, type: Exec) {
executable sconsExecutableFile.absolutePath
- args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
+ args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
}
// Schedule the tasks so the generated libs are present before the aar file is packaged.
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 4c47ca9760..748a1c41fd 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.io.file.FileAccessHandler;
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;
@@ -180,7 +181,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
public GodotIO io;
public GodotNetUtils netUtils;
public GodotTTS tts;
- DirectoryAccessHandler directoryAccessHandler;
+ private DirectoryAccessHandler directoryAccessHandler;
+ private FileAccessHandler fileAccessHandler;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@@ -522,7 +524,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
return cmdline;
} catch (Exception e) {
- e.printStackTrace();
+ // The _cl_ file can be missing with no adverse effect
return new String[0];
}
}
@@ -578,7 +580,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
netUtils = new GodotNetUtils(activity);
Context context = getContext();
directoryAccessHandler = new DirectoryAccessHandler(context);
- FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
+ fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
@@ -605,6 +607,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
@Override
public void onCreate(Bundle icicle) {
+ BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate");
super.onCreate(icicle);
final Activity activity = getActivity();
@@ -653,6 +656,18 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
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]);
}
@@ -723,6 +738,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
mCurrentIntent = activity.getIntent();
initializeGodot();
+ BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate");
}
@Override
@@ -928,20 +944,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
// 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;
@@ -1153,10 +1155,35 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
@Keep
+ public DirectoryAccessHandler getDirectoryAccessHandler() {
+ return directoryAccessHandler;
+ }
+
+ @Keep
+ public FileAccessHandler getFileAccessHandler() {
+ return fileAccessHandler;
+ }
+
+ @Keep
private int createNewGodotInstance(String[] args) {
if (godotHost != null) {
return godotHost.onNewGodotInstanceRequested(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/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
index 330e2ede76..bc7234e2ad 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
@@ -188,10 +188,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
try {
Bitmap bitmap = null;
if (!TextUtils.isEmpty(imagePath)) {
- if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) {
+ if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) {
// Try to load the bitmap from the file system
bitmap = BitmapFactory.decodeFile(imagePath);
- } else if (godot.directoryAccessHandler.assetsFileExists(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);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
index 34490d4625..5439f55b25 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
@@ -162,10 +162,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
try {
Bitmap bitmap = null;
if (!TextUtils.isEmpty(imagePath)) {
- if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) {
+ if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) {
// Try to load the bitmap from the file system
bitmap = BitmapFactory.decodeFile(imagePath);
- } else if (godot.directoryAccessHandler.assetsFileExists(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);
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 357008ca66..984bf607d0 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 0000000000..1552c8f082
--- /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<String, Long>()
+private val benchmarkTracker = ConcurrentSkipListMap<String, Double>()
+
+/**
+ * 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_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 2b504ad69b..862d9f0436 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -80,6 +80,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/GodotRenderView;");
+ _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;");
@@ -371,3 +374,30 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> 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 05144380e6..245ab33dcf 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -71,6 +71,9 @@ private:
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);
@@ -106,6 +109,9 @@ public:
void vibrate(int p_duration_ms);
String get_input_fallback_mapping();
int create_new_godot_instance(List<String> 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 73081e35e7..5c6c8454ec 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -675,6 +675,27 @@ String OS_Android::get_config_path() const {
return get_user_data_dir().path_join("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 == "system_fonts") {
return true;
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index f1d08b7cfe..99fe501975 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -164,6 +164,10 @@ public:
virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override;
+ virtual void benchmark_begin_measure(const String &p_what) override;
+ virtual void benchmark_end_measure(const String &p_what) override;
+ virtual void benchmark_dump() override;
+
virtual bool _check_internal_feature_support(const String &p_feature) override;
OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);
~OS_Android();
diff --git a/platform/windows/vulkan_context_win.cpp b/platform/windows/vulkan_context_win.cpp
index cf4383fc33..4c1e6eebe4 100644
--- a/platform/windows/vulkan_context_win.cpp
+++ b/platform/windows/vulkan_context_win.cpp
@@ -55,6 +55,10 @@ Error VulkanContextWindows::window_create(DisplayServer::WindowID p_window_id, D
}
VulkanContextWindows::VulkanContextWindows() {
+ // Workaround for Vulkan not working on setups with AMD integrated graphics + NVIDIA dedicated GPU (GH-57708).
+ // This prevents using AMD integrated graphics with Vulkan entirely, but it allows the engine to start
+ // even on outdated/broken driver setups.
+ OS::get_singleton()->set_environment("DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1", "1");
}
VulkanContextWindows::~VulkanContextWindows() {
diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp
index 5036dd30b1..3e6a484e72 100644
--- a/scene/2d/path_2d.cpp
+++ b/scene/2d/path_2d.cpp
@@ -268,11 +268,11 @@ void PathFollow2D::_notification(int p_what) {
}
}
-void PathFollow2D::set_cubic_interpolation(bool p_enable) {
- cubic = p_enable;
+void PathFollow2D::set_cubic_interpolation_enabled(bool p_enabled) {
+ cubic = p_enabled;
}
-bool PathFollow2D::get_cubic_interpolation() const {
+bool PathFollow2D::is_cubic_interpolation_enabled() const {
return cubic;
}
@@ -312,18 +312,15 @@ void PathFollow2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_progress_ratio", "ratio"), &PathFollow2D::set_progress_ratio);
ClassDB::bind_method(D_METHOD("get_progress_ratio"), &PathFollow2D::get_progress_ratio);
- ClassDB::bind_method(D_METHOD("set_rotates", "enable"), &PathFollow2D::set_rotates);
- ClassDB::bind_method(D_METHOD("is_rotating"), &PathFollow2D::is_rotating);
+ ClassDB::bind_method(D_METHOD("set_rotates", "enabled"), &PathFollow2D::set_rotation_enabled);
+ ClassDB::bind_method(D_METHOD("is_rotating"), &PathFollow2D::is_rotation_enabled);
- ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enable"), &PathFollow2D::set_cubic_interpolation);
- ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow2D::get_cubic_interpolation);
+ ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enabled"), &PathFollow2D::set_cubic_interpolation_enabled);
+ ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow2D::is_cubic_interpolation_enabled);
ClassDB::bind_method(D_METHOD("set_loop", "loop"), &PathFollow2D::set_loop);
ClassDB::bind_method(D_METHOD("has_loop"), &PathFollow2D::has_loop);
- ClassDB::bind_method(D_METHOD("set_lookahead", "lookahead"), &PathFollow2D::set_lookahead);
- ClassDB::bind_method(D_METHOD("get_lookahead"), &PathFollow2D::get_lookahead);
-
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "progress", PROPERTY_HINT_RANGE, "0,10000,0.01,or_less,or_greater,suffix:px"), "set_progress", "get_progress");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "progress_ratio", PROPERTY_HINT_RANGE, "0,1,0.0001,or_less,or_greater", PROPERTY_USAGE_EDITOR), "set_progress_ratio", "get_progress_ratio");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "h_offset"), "set_h_offset", "get_h_offset");
@@ -331,7 +328,6 @@ void PathFollow2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rotates"), "set_rotates", "is_rotating");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "cubic_interp"), "set_cubic_interpolation", "get_cubic_interpolation");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lookahead", PROPERTY_HINT_RANGE, "0.001,1024.0,0.001"), "set_lookahead", "get_lookahead");
}
void PathFollow2D::set_progress(real_t p_progress) {
@@ -395,20 +391,12 @@ real_t PathFollow2D::get_progress_ratio() const {
}
}
-void PathFollow2D::set_lookahead(real_t p_lookahead) {
- lookahead = p_lookahead;
-}
-
-real_t PathFollow2D::get_lookahead() const {
- return lookahead;
-}
-
-void PathFollow2D::set_rotates(bool p_rotates) {
- rotates = p_rotates;
+void PathFollow2D::set_rotation_enabled(bool p_enabled) {
+ rotates = p_enabled;
_update_transform();
}
-bool PathFollow2D::is_rotating() const {
+bool PathFollow2D::is_rotation_enabled() const {
return rotates;
}
diff --git a/scene/2d/path_2d.h b/scene/2d/path_2d.h
index 89c77c49eb..bfd5cde5e9 100644
--- a/scene/2d/path_2d.h
+++ b/scene/2d/path_2d.h
@@ -70,7 +70,6 @@ private:
Timer *update_timer = nullptr;
real_t h_offset = 0.0;
real_t v_offset = 0.0;
- real_t lookahead = 4.0;
bool cubic = true;
bool loop = true;
bool rotates = true;
@@ -98,17 +97,14 @@ public:
void set_progress_ratio(real_t p_ratio);
real_t get_progress_ratio() const;
- void set_lookahead(real_t p_lookahead);
- real_t get_lookahead() const;
-
void set_loop(bool p_loop);
bool has_loop() const;
- void set_rotates(bool p_rotates);
- bool is_rotating() const;
+ void set_rotation_enabled(bool p_enabled);
+ bool is_rotation_enabled() const;
- void set_cubic_interpolation(bool p_enable);
- bool get_cubic_interpolation() const;
+ void set_cubic_interpolation_enabled(bool p_enabled);
+ bool is_cubic_interpolation_enabled() const;
PackedStringArray get_configuration_warnings() const override;
diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp
index 80289bac52..4f2ee5a3b2 100644
--- a/scene/3d/node_3d.cpp
+++ b/scene/3d/node_3d.cpp
@@ -908,22 +908,23 @@ void Node3D::set_identity() {
set_transform(Transform3D());
}
-void Node3D::look_at(const Vector3 &p_target, const Vector3 &p_up) {
+void Node3D::look_at(const Vector3 &p_target, const Vector3 &p_up, bool p_use_model_front) {
ERR_THREAD_GUARD;
ERR_FAIL_COND_MSG(!is_inside_tree(), "Node not inside tree. Use look_at_from_position() instead.");
Vector3 origin = get_global_transform().origin;
- look_at_from_position(origin, p_target, p_up);
+ look_at_from_position(origin, p_target, p_up, p_use_model_front);
}
-void Node3D::look_at_from_position(const Vector3 &p_pos, const Vector3 &p_target, const Vector3 &p_up) {
+void Node3D::look_at_from_position(const Vector3 &p_pos, const Vector3 &p_target, const Vector3 &p_up, bool p_use_model_front) {
ERR_THREAD_GUARD;
ERR_FAIL_COND_MSG(p_pos.is_equal_approx(p_target), "Node origin and target are in the same position, look_at() failed.");
ERR_FAIL_COND_MSG(p_up.is_zero_approx(), "The up vector can't be zero, look_at() failed.");
ERR_FAIL_COND_MSG(p_up.cross(p_target - p_pos).is_zero_approx(), "Up vector and direction between node origin and target are aligned, look_at() failed.");
- Transform3D lookat = Transform3D(Basis::looking_at(p_target - p_pos, p_up), p_pos);
+ Vector3 forward = p_target - p_pos;
+ Basis lookat_basis = Basis::looking_at(forward, p_up, p_use_model_front);
Vector3 original_scale = get_scale();
- set_global_transform(lookat);
+ set_global_transform(Transform3D(lookat_basis, p_pos));
set_scale(original_scale);
}
@@ -1166,8 +1167,8 @@ void Node3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("orthonormalize"), &Node3D::orthonormalize);
ClassDB::bind_method(D_METHOD("set_identity"), &Node3D::set_identity);
- ClassDB::bind_method(D_METHOD("look_at", "target", "up"), &Node3D::look_at, DEFVAL(Vector3(0, 1, 0)));
- ClassDB::bind_method(D_METHOD("look_at_from_position", "position", "target", "up"), &Node3D::look_at_from_position, DEFVAL(Vector3(0, 1, 0)));
+ ClassDB::bind_method(D_METHOD("look_at", "target", "up", "use_model_front"), &Node3D::look_at, DEFVAL(Vector3(0, 1, 0)), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("look_at_from_position", "position", "target", "up", "use_model_front"), &Node3D::look_at_from_position, DEFVAL(Vector3(0, 1, 0)), DEFVAL(false));
ClassDB::bind_method(D_METHOD("to_local", "global_point"), &Node3D::to_local);
ClassDB::bind_method(D_METHOD("to_global", "local_point"), &Node3D::to_global);
diff --git a/scene/3d/node_3d.h b/scene/3d/node_3d.h
index b274a6af88..935f0b149a 100644
--- a/scene/3d/node_3d.h
+++ b/scene/3d/node_3d.h
@@ -250,8 +250,8 @@ public:
void global_scale(const Vector3 &p_scale);
void global_translate(const Vector3 &p_offset);
- void look_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0));
- void look_at_from_position(const Vector3 &p_pos, const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0));
+ void look_at(const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0), bool p_use_model_front = false);
+ void look_at_from_position(const Vector3 &p_pos, const Vector3 &p_target, const Vector3 &p_up = Vector3(0, 1, 0), bool p_use_model_front = false);
Vector3 to_local(Vector3 p_global) const;
Vector3 to_global(Vector3 p_local) const;
diff --git a/scene/3d/path_3d.cpp b/scene/3d/path_3d.cpp
index f02826b1c4..c71f80ea0e 100644
--- a/scene/3d/path_3d.cpp
+++ b/scene/3d/path_3d.cpp
@@ -192,6 +192,9 @@ void PathFollow3D::_update_transform(bool p_update_xyz_rot) {
t.origin = pos;
} else {
t = c->sample_baked_with_rotation(progress, cubic, false);
+ if (use_model_front) {
+ t.basis *= Basis::from_scale(Vector3(-1.0, 1.0, -1.0));
+ }
Vector3 forward = t.basis.get_column(2); // Retain tangent for applying tilt
t = PathFollow3D::correct_posture(t, rotation_mode);
@@ -230,11 +233,11 @@ void PathFollow3D::_notification(int p_what) {
}
}
-void PathFollow3D::set_cubic_interpolation(bool p_enable) {
- cubic = p_enable;
+void PathFollow3D::set_cubic_interpolation_enabled(bool p_enabled) {
+ cubic = p_enabled;
}
-bool PathFollow3D::get_cubic_interpolation() const {
+bool PathFollow3D::is_cubic_interpolation_enabled() const {
return cubic;
}
@@ -314,8 +317,11 @@ void PathFollow3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_rotation_mode", "rotation_mode"), &PathFollow3D::set_rotation_mode);
ClassDB::bind_method(D_METHOD("get_rotation_mode"), &PathFollow3D::get_rotation_mode);
- ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enable"), &PathFollow3D::set_cubic_interpolation);
- ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow3D::get_cubic_interpolation);
+ ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enabled"), &PathFollow3D::set_cubic_interpolation_enabled);
+ ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow3D::is_cubic_interpolation_enabled);
+
+ ClassDB::bind_method(D_METHOD("set_use_model_front", "enabled"), &PathFollow3D::set_use_model_front);
+ ClassDB::bind_method(D_METHOD("is_using_model_front"), &PathFollow3D::is_using_model_front);
ClassDB::bind_method(D_METHOD("set_loop", "loop"), &PathFollow3D::set_loop);
ClassDB::bind_method(D_METHOD("has_loop"), &PathFollow3D::has_loop);
@@ -330,6 +336,7 @@ void PathFollow3D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "h_offset", PROPERTY_HINT_NONE, "suffix:m"), "set_h_offset", "get_h_offset");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "v_offset", PROPERTY_HINT_NONE, "suffix:m"), "set_v_offset", "get_v_offset");
ADD_PROPERTY(PropertyInfo(Variant::INT, "rotation_mode", PROPERTY_HINT_ENUM, "None,Y,XY,XYZ,Oriented"), "set_rotation_mode", "get_rotation_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_model_front"), "set_use_model_front", "is_using_model_front");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "cubic_interp"), "set_cubic_interpolation", "get_cubic_interpolation");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tilt_enabled"), "set_tilt_enabled", "is_tilt_enabled");
@@ -412,6 +419,14 @@ PathFollow3D::RotationMode PathFollow3D::get_rotation_mode() const {
return rotation_mode;
}
+void PathFollow3D::set_use_model_front(bool p_use_model_front) {
+ use_model_front = p_use_model_front;
+}
+
+bool PathFollow3D::is_using_model_front() const {
+ return use_model_front;
+}
+
void PathFollow3D::set_loop(bool p_loop) {
loop = p_loop;
}
@@ -420,8 +435,8 @@ bool PathFollow3D::has_loop() const {
return loop;
}
-void PathFollow3D::set_tilt_enabled(bool p_enable) {
- tilt_enabled = p_enable;
+void PathFollow3D::set_tilt_enabled(bool p_enabled) {
+ tilt_enabled = p_enabled;
}
bool PathFollow3D::is_tilt_enabled() const {
diff --git a/scene/3d/path_3d.h b/scene/3d/path_3d.h
index 9fdcc0f0ef..6116e98054 100644
--- a/scene/3d/path_3d.h
+++ b/scene/3d/path_3d.h
@@ -72,6 +72,8 @@ public:
ROTATION_ORIENTED
};
+ bool use_model_front = false;
+
static Transform3D correct_posture(Transform3D p_transform, PathFollow3D::RotationMode p_rotation_mode);
private:
@@ -108,14 +110,17 @@ public:
void set_loop(bool p_loop);
bool has_loop() const;
- void set_tilt_enabled(bool p_enable);
+ void set_tilt_enabled(bool p_enabled);
bool is_tilt_enabled() const;
void set_rotation_mode(RotationMode p_rotation_mode);
RotationMode get_rotation_mode() const;
- void set_cubic_interpolation(bool p_enable);
- bool get_cubic_interpolation() const;
+ void set_use_model_front(bool p_use_model_front);
+ bool is_using_model_front() const;
+
+ void set_cubic_interpolation_enabled(bool p_enabled);
+ bool is_cubic_interpolation_enabled() const;
PackedStringArray get_configuration_warnings() const override;
diff --git a/scene/gui/control.h b/scene/gui/control.h
index e36e279715..7cb8fc5bf6 100644
--- a/scene/gui/control.h
+++ b/scene/gui/control.h
@@ -349,7 +349,7 @@ protected:
GDVIRTUAL0RC(Vector2, _get_minimum_size)
GDVIRTUAL1RC(String, _get_tooltip, Vector2)
- GDVIRTUAL1RC(Variant, _get_drag_data, Vector2)
+ GDVIRTUAL1R(Variant, _get_drag_data, Vector2)
GDVIRTUAL2RC(bool, _can_drop_data, Vector2, Variant)
GDVIRTUAL2(_drop_data, Vector2, Variant)
GDVIRTUAL1RC(Object *, _make_custom_tooltip, String)
diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp
index e12180f2f9..6f3b7d140e 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -2624,6 +2624,12 @@ void RichTextLabel::_fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack
}
}
+void RichTextLabel::_normalize_subtags(Vector<String> &subtags) {
+ for (String &subtag : subtags) {
+ subtag = subtag.unquote();
+ }
+}
+
bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) {
Item *item = p_item;
@@ -2712,9 +2718,11 @@ bool RichTextLabel::_find_layout_subitem(Item *from, Item *to) {
}
void RichTextLabel::_thread_function(void *p_userdata) {
+ set_current_thread_safe_for_nodes(true);
_process_line_caches();
updating.store(false);
call_deferred(SNAME("thread_end"));
+ set_current_thread_safe_for_nodes(false);
}
void RichTextLabel::_thread_end() {
@@ -3775,7 +3783,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
const String &expr = split_tag_block[i];
int value_pos = expr.find("=");
if (value_pos > -1) {
- bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1);
+ bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1).unquote();
}
}
} else {
@@ -3884,6 +3892,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front(tag);
} else if (tag.begins_with("table=")) {
Vector<String> subtag = tag.substr(6, tag.length()).split(",");
+ _normalize_subtags(subtag);
+
int columns = subtag[0].to_int();
if (columns < 1) {
columns = 1;
@@ -3943,9 +3953,12 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("cell");
} else if (tag.begins_with("cell ")) {
Vector<String> subtag = tag.substr(5, tag.length()).split(" ");
+ _normalize_subtags(subtag);
for (int i = 0; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=");
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
if (subtag_a[0] == "expand") {
int ratio = subtag_a[1].to_int();
@@ -3960,12 +3973,16 @@ void RichTextLabel::append_text(const String &p_bbcode) {
const Color fallback_color = Color(0, 0, 0, 0);
for (int i = 0; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=");
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
if (subtag_a[0] == "border") {
Color color = Color::from_string(subtag_a[1], fallback_color);
set_cell_border_color(color);
} else if (subtag_a[0] == "bg") {
Vector<String> subtag_b = subtag_a[1].split(",");
+ _normalize_subtags(subtag_b);
+
if (subtag_b.size() == 2) {
Color color1 = Color::from_string(subtag_b[0], fallback_color);
Color color2 = Color::from_string(subtag_b[1], fallback_color);
@@ -3977,6 +3994,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
}
} else if (subtag_a[0] == "padding") {
Vector<String> subtag_b = subtag_a[1].split(",");
+ _normalize_subtags(subtag_b);
+
if (subtag_b.size() == 4) {
set_cell_padding(Rect2(subtag_b[0].to_float(), subtag_b[1].to_float(), subtag_b[2].to_float(), subtag_b[3].to_float()));
}
@@ -4113,6 +4132,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("p");
} else if (tag.begins_with("p ")) {
Vector<String> subtag = tag.substr(2, tag.length()).split(" ");
+ _normalize_subtags(subtag);
+
HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT;
Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED;
String lang;
@@ -4121,6 +4142,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
BitField<TextServer::JustificationFlag> jst_flags = default_jst_flags;
for (int i = 0; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=");
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
if (subtag_a[0] == "justification_flags" || subtag_a[0] == "jst") {
Vector<String> subtag_b = subtag_a[1].split(",");
@@ -4193,24 +4216,26 @@ void RichTextLabel::append_text(const String &p_bbcode) {
if (end == -1) {
end = p_bbcode.length();
}
- String url = p_bbcode.substr(brk_end + 1, end - brk_end - 1);
+ String url = p_bbcode.substr(brk_end + 1, end - brk_end - 1).unquote();
push_meta(url);
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("url=")) {
- String url = tag.substr(4, tag.length());
+ String url = tag.substr(4, tag.length()).unquote();
push_meta(url);
pos = brk_end + 1;
tag_stack.push_front("url");
} else if (tag.begins_with("hint=")) {
- String description = tag.substr(5, tag.length());
+ String description = tag.substr(5, tag.length()).unquote();
push_hint(description);
pos = brk_end + 1;
tag_stack.push_front("hint");
} else if (tag.begins_with("dropcap")) {
Vector<String> subtag = tag.substr(5, tag.length()).split(" ");
+ _normalize_subtags(subtag);
+
int fs = theme_cache.normal_font_size * 3;
Ref<Font> f = theme_cache.normal_font;
Color color = theme_cache.default_color;
@@ -4220,6 +4245,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
for (int i = 0; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=");
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
if (subtag_a[0] == "font" || subtag_a[0] == "f") {
String fnt = subtag_a[1];
@@ -4231,6 +4258,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
fs = subtag_a[1].to_int();
} else if (subtag_a[0] == "margins") {
Vector<String> subtag_b = subtag_a[1].split(",");
+ _normalize_subtags(subtag_b);
+
if (subtag_b.size() == 4) {
dropcap_margins.position.x = subtag_b[0].to_float();
dropcap_margins.position.y = subtag_b[1].to_float();
@@ -4261,6 +4290,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
int alignment = INLINE_ALIGNMENT_CENTER;
if (tag.begins_with("img=")) {
Vector<String> subtag = tag.substr(4, tag.length()).split(",");
+ _normalize_subtags(subtag);
+
if (subtag.size() > 1) {
if (subtag[0] == "top" || subtag[0] == "t") {
alignment = INLINE_ALIGNMENT_TOP_TO;
@@ -4344,14 +4375,14 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = end;
tag_stack.push_front(bbcode_name);
} else if (tag.begins_with("color=")) {
- String color_str = tag.substr(6, tag.length());
+ String color_str = tag.substr(6, tag.length()).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_color(color);
pos = brk_end + 1;
tag_stack.push_front("color");
} else if (tag.begins_with("outline_color=")) {
- String color_str = tag.substr(14, tag.length());
+ String color_str = tag.substr(14, tag.length()).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_outline_color(color);
pos = brk_end + 1;
@@ -4367,6 +4398,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
int value_pos = tag.find("=");
String fnt_ftr = tag.substr(value_pos + 1);
Vector<String> subtag = fnt_ftr.split(",");
+ _normalize_subtags(subtag);
+
if (subtag.size() > 0) {
Ref<Font> font = theme_cache.normal_font;
DefaultFont def_font = NORMAL_FONT;
@@ -4381,6 +4414,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
Dictionary features;
for (int i = 0; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=");
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
features[TS->name_to_tag(subtag_a[0])] = subtag_a[1].to_int();
} else if (subtag_a.size() == 1) {
@@ -4404,7 +4439,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front(tag.substr(0, value_pos));
} else if (tag.begins_with("font=")) {
- String fnt = tag.substr(5, tag.length());
+ String fnt = tag.substr(5, tag.length()).unquote();
Ref<Font> fc = ResourceLoader::load(fnt, "Font");
if (fc.is_valid()) {
@@ -4416,6 +4451,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
} else if (tag.begins_with("font ")) {
Vector<String> subtag = tag.substr(2, tag.length()).split(" ");
+ _normalize_subtags(subtag);
Ref<Font> font = theme_cache.normal_font;
DefaultFont def_font = NORMAL_FONT;
@@ -4434,6 +4470,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
int fnt_size = -1;
for (int i = 1; i < subtag.size(); i++) {
Vector<String> subtag_a = subtag[i].split("=", true, 2);
+ _normalize_subtags(subtag_a);
+
if (subtag_a.size() == 2) {
if (subtag_a[0] == "name" || subtag_a[0] == "n") {
String fnt = subtag_a[1];
@@ -4471,6 +4509,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
Vector<String> variation_tags = subtag_a[1].split(",");
for (int j = 0; j < variation_tags.size(); j++) {
Vector<String> subtag_b = variation_tags[j].split("=");
+ _normalize_subtags(subtag_b);
+
if (subtag_b.size() == 2) {
variations[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
}
@@ -4483,6 +4523,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
Vector<String> feature_tags = subtag_a[1].split(",");
for (int j = 0; j < feature_tags.size(); j++) {
Vector<String> subtag_b = feature_tags[j].split("=");
+ _normalize_subtags(subtag_b);
+
if (subtag_b.size() == 2) {
features[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
} else if (subtag_b.size() == 1) {
@@ -4623,7 +4665,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
set_process_internal(true);
} else if (tag.begins_with("bgcolor=")) {
- String color_str = tag.substr(8, tag.length());
+ String color_str = tag.substr(8, tag.length()).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_bgcolor(color);
@@ -4631,7 +4673,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("bgcolor");
} else if (tag.begins_with("fgcolor=")) {
- String color_str = tag.substr(8, tag.length());
+ String color_str = tag.substr(8, tag.length()).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_fgcolor(color);
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index f6cc9b2ac4..b502e71a4f 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -510,6 +510,7 @@ private:
Color _find_fgcolor(Item *p_item);
bool _find_layout_subitem(Item *from, Item *to);
void _fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack);
+ void _normalize_subtags(Vector<String> &subtags);
void _update_fx(ItemFrame *p_frame, double p_delta_time);
void _scroll_changed(double);
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index b8fc8004c9..9ec461cad4 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -3190,15 +3190,6 @@ void Tree::_go_up() {
selected_col = 0;
} else {
prev = selected_item->get_prev_visible();
- if (last_keypress != 0) {
- //incr search next
- int col;
- prev = _search_item_text(prev, incr_search, &col, true, true);
- if (!prev) {
- accept_event();
- return;
- }
- }
}
if (select_mode == SELECT_MULTI) {
@@ -3231,16 +3222,6 @@ void Tree::_go_down() {
}
} else {
next = selected_item->get_next_visible();
-
- if (last_keypress != 0) {
- //incr search next
- int col;
- next = _search_item_text(next, incr_search, &col, true);
- if (!next) {
- accept_event();
- return;
- }
- }
}
if (select_mode == SELECT_MULTI) {
diff --git a/scene/main/node.h b/scene/main/node.h
index 4253928427..35748d63b7 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -543,7 +543,7 @@ public:
if (current_process_thread_group == nullptr) {
// Not thread processing. Only accessible if node is outside the scene tree,
// if accessing from the main thread or being loaded.
- return !data.inside_tree || Thread::is_main_thread() || ResourceLoader::is_within_load();
+ return !data.inside_tree || is_current_thread_safe_for_nodes();
} else {
// Thread processing
return current_process_thread_group == data.process_thread_group_owner;
@@ -552,7 +552,7 @@ public:
_FORCE_INLINE_ bool is_readable_from_caller_thread() const {
if (current_process_thread_group == nullptr) {
- return Thread::is_main_thread() || ResourceLoader::is_within_load();
+ return Thread::is_main_thread() || is_current_thread_safe_for_nodes();
} else {
return true;
}
@@ -731,8 +731,8 @@ Error Node::rpc_id(int p_peer_id, const StringName &p_method, VarArgs... p_args)
#ifdef DEBUG_ENABLED
#define ERR_THREAD_GUARD ERR_FAIL_COND_MSG(!is_accessible_from_caller_thread(), "Caller thread can't call this function in this node. Use call_deferred() or call_thread_group() instead.");
#define ERR_THREAD_GUARD_V(m_ret) ERR_FAIL_COND_V_MSG(!is_accessible_from_caller_thread(), (m_ret), "Caller thread can't call this function in this node. Use call_deferred() or call_thread_group() instead.")
-#define ERR_MAIN_THREAD_GUARD ERR_FAIL_COND_MSG(is_inside_tree() && !Thread::is_main_thread(), "This function in this node can only be accessed from the main thread. Use call_deferred() instead.");
-#define ERR_MAIN_THREAD_GUARD_V(m_ret) ERR_FAIL_COND_V_MSG(is_inside_tree() && !Thread::is_main_thread(), (m_ret), "This function in this node can only be accessed from the main thread. Use call_deferred() instead.")
+#define ERR_MAIN_THREAD_GUARD ERR_FAIL_COND_MSG(is_inside_tree() && !is_current_thread_safe_for_nodes(), "This function in this node can only be accessed from the main thread. Use call_deferred() instead.");
+#define ERR_MAIN_THREAD_GUARD_V(m_ret) ERR_FAIL_COND_V_MSG(is_inside_tree() && !is_current_thread_safe_for_nodes(), (m_ret), "This function in this node can only be accessed from the main thread. Use call_deferred() instead.")
#define ERR_READ_THREAD_GUARD ERR_FAIL_COND_MSG(!is_readable_from_caller_thread(), "This function in this node can only be accessed from either the main thread or a thread group. Use call_deferred() instead.")
#define ERR_READ_THREAD_GUARD_V(m_ret) ERR_FAIL_COND_V_MSG(!is_readable_from_caller_thread(), (m_ret), "This function in this node can only be accessed from either the main thread or a thread group. Use call_deferred() instead.")
#else
diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp
index a0bd22c79b..64e466cf75 100644
--- a/scene/resources/curve.cpp
+++ b/scene/resources/curve.cpp
@@ -1648,9 +1648,9 @@ void Curve3D::_bake() const {
Vector3 forward = forward_ptr[0];
if (abs(forward.dot(Vector3(0, 1, 0))) > 1.0 - UNIT_EPSILON) {
- frame_prev = Basis::looking_at(-forward, Vector3(1, 0, 0));
+ frame_prev = Basis::looking_at(forward, Vector3(1, 0, 0));
} else {
- frame_prev = Basis::looking_at(-forward, Vector3(0, 1, 0));
+ frame_prev = Basis::looking_at(forward, Vector3(0, 1, 0));
}
up_write[0] = frame_prev.get_column(1);
@@ -1809,8 +1809,8 @@ Basis Curve3D::_sample_posture(Interval p_interval, bool p_apply_tilt) const {
}
// Build frames at both ends of the interval, then interpolate.
- const Basis frame_begin = Basis::looking_at(-forward_begin, up_begin);
- const Basis frame_end = Basis::looking_at(-forward_end, up_end);
+ const Basis frame_begin = Basis::looking_at(forward_begin, up_begin);
+ const Basis frame_end = Basis::looking_at(forward_end, up_end);
const Basis frame = frame_begin.slerp(frame_end, frac).orthonormalized();
if (!p_apply_tilt) {
diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp
index 706477cedb..097580f3bd 100644
--- a/servers/rendering/renderer_canvas_cull.cpp
+++ b/servers/rendering/renderer_canvas_cull.cpp
@@ -76,7 +76,7 @@ void RendererCanvasCull::_render_canvas_item_tree(RID p_to_render_target, Canvas
}
}
-void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2D p_transform, RendererCanvasCull::Item *p_material_owner, RendererCanvasCull::Item **r_items, int &r_index, int p_z) {
+void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2D p_transform, RendererCanvasCull::Item *p_material_owner, const Color &p_modulate, RendererCanvasCull::Item **r_items, int &r_index, int p_z) {
int child_item_count = p_canvas_item->child_items.size();
RendererCanvasCull::Item **child_items = p_canvas_item->child_items.ptrw();
for (int i = 0; i < child_item_count; i++) {
@@ -87,6 +87,7 @@ void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2
child_items[i]->ysort_xform = p_transform;
child_items[i]->ysort_pos = p_transform.xform(child_items[i]->xform.columns[2]);
child_items[i]->material_owner = child_items[i]->use_parent_material ? p_material_owner : nullptr;
+ child_items[i]->ysort_modulate = p_modulate;
child_items[i]->ysort_index = r_index;
child_items[i]->ysort_parent_abs_z_index = p_z;
@@ -101,7 +102,7 @@ void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2
r_index++;
if (child_items[i]->sort_y) {
- _collect_ysort_children(child_items[i], p_transform * child_items[i]->xform, child_items[i]->use_parent_material ? p_material_owner : child_items[i], r_items, r_index, abs_z);
+ _collect_ysort_children(child_items[i], p_transform * child_items[i]->xform, child_items[i]->use_parent_material ? p_material_owner : child_items[i], p_modulate * child_items[i]->modulate, r_items, r_index, abs_z);
}
}
}
@@ -301,7 +302,7 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2
if (allow_y_sort) {
if (ci->ysort_children_count == -1) {
ci->ysort_children_count = 0;
- _collect_ysort_children(ci, Transform2D(), p_material_owner, nullptr, ci->ysort_children_count, p_z);
+ _collect_ysort_children(ci, Transform2D(), p_material_owner, Color(1, 1, 1, 1), nullptr, ci->ysort_children_count, p_z);
}
child_item_count = ci->ysort_children_count + 1;
@@ -310,14 +311,14 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2
ci->ysort_parent_abs_z_index = parent_z;
child_items[0] = ci;
int i = 1;
- _collect_ysort_children(ci, Transform2D(), p_material_owner, child_items, i, p_z);
+ _collect_ysort_children(ci, Transform2D(), p_material_owner, Color(1, 1, 1, 1), child_items, i, p_z);
ci->ysort_xform = ci->xform.affine_inverse();
SortArray<Item *, ItemPtrSort> sorter;
sorter.sort(child_items, child_item_count);
for (i = 0; i < child_item_count; i++) {
- _cull_canvas_item(child_items[i], xform * child_items[i]->ysort_xform, p_clip_rect, modulate, child_items[i]->ysort_parent_abs_z_index, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner, false, canvas_cull_mask);
+ _cull_canvas_item(child_items[i], xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, child_items[i]->ysort_parent_abs_z_index, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner, false, canvas_cull_mask);
}
} else {
RendererCanvasRender::Item *canvas_group_from = nullptr;
@@ -880,8 +881,9 @@ void RendererCanvasCull::canvas_item_add_polyline(RID p_item, const Vector<Point
PackedColorArray colors;
PackedVector2Array points;
- colors.resize(polyline_point_count);
- points.resize(polyline_point_count);
+ // Additional 2+2 vertices to antialias begin+end of the middle triangle strip.
+ colors.resize(polyline_point_count + ((p_antialiased && !loop) ? 4 : 0));
+ points.resize(polyline_point_count + ((p_antialiased && !loop) ? 4 : 0));
Vector2 *points_ptr = points.ptrw();
Color *colors_ptr = colors.ptrw();
@@ -897,96 +899,30 @@ void RendererCanvasCull::canvas_item_add_polyline(RID p_item, const Vector<Point
}
Color color2 = Color(1, 1, 1, 0);
- PackedColorArray colors_begin;
- PackedVector2Array points_begin;
-
- colors_begin.resize(4);
- points_begin.resize(4);
-
- PackedColorArray colors_begin_left_corner;
- PackedVector2Array points_begin_left_corner;
-
- colors_begin_left_corner.resize(4);
- points_begin_left_corner.resize(4);
-
- PackedColorArray colors_begin_right_corner;
- PackedVector2Array points_begin_right_corner;
-
- colors_begin_right_corner.resize(4);
- points_begin_right_corner.resize(4);
-
- PackedColorArray colors_end;
- PackedVector2Array points_end;
-
- colors_end.resize(4);
- points_end.resize(4);
-
- PackedColorArray colors_end_left_corner;
- PackedVector2Array points_end_left_corner;
-
- colors_end_left_corner.resize(4);
- points_end_left_corner.resize(4);
-
- PackedColorArray colors_end_right_corner;
- PackedVector2Array points_end_right_corner;
+ Item::CommandPolygon *pline_left = canvas_item->alloc_command<Item::CommandPolygon>();
+ ERR_FAIL_COND(!pline_left);
- colors_end_right_corner.resize(4);
- points_end_right_corner.resize(4);
+ Item::CommandPolygon *pline_right = canvas_item->alloc_command<Item::CommandPolygon>();
+ ERR_FAIL_COND(!pline_right);
PackedColorArray colors_left;
PackedVector2Array points_left;
- colors_left.resize(polyline_point_count);
- points_left.resize(polyline_point_count);
-
PackedColorArray colors_right;
PackedVector2Array points_right;
- colors_right.resize(polyline_point_count);
- points_right.resize(polyline_point_count);
-
- Item::CommandPolygon *pline_begin = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_begin);
-
- Item::CommandPolygon *pline_begin_left_corner = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_begin_left_corner);
+ // 2+2 additional vertices for begin+end corners.
+ // 1 additional vertex to swap the orientation of the triangles within the end corner's quad.
+ colors_left.resize(polyline_point_count + (loop ? 0 : 5));
+ points_left.resize(polyline_point_count + (loop ? 0 : 5));
- Item::CommandPolygon *pline_begin_right_corner = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_begin_right_corner);
+ colors_right.resize(polyline_point_count + (loop ? 0 : 5));
+ points_right.resize(polyline_point_count + (loop ? 0 : 5));
- Item::CommandPolygon *pline_end = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_end);
-
- Item::CommandPolygon *pline_end_left_corner = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_end_left_corner);
-
- Item::CommandPolygon *pline_end_right_corner = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_end_right_corner);
-
- Item::CommandPolygon *pline_left = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_left);
-
- Item::CommandPolygon *pline_right = canvas_item->alloc_command<Item::CommandPolygon>();
- ERR_FAIL_COND(!pline_right);
-
- // Makes nine triangle strips for drawing the antialiased line.
-
- Vector2 *points_begin_ptr = points_begin.ptrw();
- Vector2 *points_begin_left_corner_ptr = points_begin_left_corner.ptrw();
- Vector2 *points_begin_right_corner_ptr = points_begin_right_corner.ptrw();
- Vector2 *points_end_ptr = points_end.ptrw();
- Vector2 *points_end_left_corner_ptr = points_end_left_corner.ptrw();
- Vector2 *points_end_right_corner_ptr = points_end_right_corner.ptrw();
+ Color *colors_left_ptr = colors_left.ptrw();
Vector2 *points_left_ptr = points_left.ptrw();
- Vector2 *points_right_ptr = points_right.ptrw();
- Color *colors_begin_ptr = colors_begin.ptrw();
- Color *colors_begin_left_corner_ptr = colors_begin_left_corner.ptrw();
- Color *colors_begin_right_corner_ptr = colors_begin_right_corner.ptrw();
- Color *colors_end_ptr = colors_end.ptrw();
- Color *colors_end_left_corner_ptr = colors_end_left_corner.ptrw();
- Color *colors_end_right_corner_ptr = colors_end_right_corner.ptrw();
- Color *colors_left_ptr = colors_left.ptrw();
+ Vector2 *points_right_ptr = points_right.ptrw();
Color *colors_right_ptr = colors_right.ptrw();
Vector2 prev_segment_dir;
@@ -1014,117 +950,85 @@ void RendererCanvasCull::canvas_item_add_polyline(RID p_item, const Vector<Point
Vector2 border = base_edge_offset * border_size;
Vector2 pos = p_points[i];
- points_ptr[i * 2 + 0] = pos + edge_offset;
- points_ptr[i * 2 + 1] = pos - edge_offset;
+ int j = i * 2 + (loop ? 0 : 2);
- points_left_ptr[i * 2 + 0] = pos + edge_offset + border;
- points_left_ptr[i * 2 + 1] = pos + edge_offset;
+ points_ptr[j + 0] = pos + edge_offset;
+ points_ptr[j + 1] = pos - edge_offset;
- points_right_ptr[i * 2 + 0] = pos - edge_offset;
- points_right_ptr[i * 2 + 1] = pos - edge_offset - border;
+ points_left_ptr[j + 0] = pos + edge_offset;
+ points_left_ptr[j + 1] = pos + edge_offset + border;
+
+ points_right_ptr[j + 0] = pos - edge_offset;
+ points_right_ptr[j + 1] = pos - edge_offset - border;
if (i < p_colors.size()) {
color = p_colors[i];
color2 = Color(color.r, color.g, color.b, 0);
}
- colors_ptr[i * 2 + 0] = color;
- colors_ptr[i * 2 + 1] = color;
+ colors_ptr[j + 0] = color;
+ colors_ptr[j + 1] = color;
- colors_left_ptr[i * 2 + 0] = color2;
- colors_left_ptr[i * 2 + 1] = color;
+ colors_left_ptr[j + 0] = color;
+ colors_left_ptr[j + 1] = color2;
- colors_right_ptr[i * 2 + 0] = color;
- colors_right_ptr[i * 2 + 1] = color2;
+ colors_right_ptr[j + 0] = color;
+ colors_right_ptr[j + 1] = color2;
- if (is_first_point) {
- Vector2 begin_border = loop ? Vector2() : -segment_dir * border_size;
+ if (is_first_point && !loop) {
+ Vector2 begin_border = -segment_dir * border_size;
- points_begin_ptr[0] = pos + edge_offset + begin_border;
- points_begin_ptr[1] = pos - edge_offset + begin_border;
- points_begin_ptr[2] = pos + edge_offset;
- points_begin_ptr[3] = pos - edge_offset;
+ points_ptr[0] = pos + edge_offset + begin_border;
+ points_ptr[1] = pos - edge_offset + begin_border;
- colors_begin_ptr[0] = color2;
- colors_begin_ptr[1] = color2;
- colors_begin_ptr[2] = color;
- colors_begin_ptr[3] = color;
+ colors_ptr[0] = color2;
+ colors_ptr[1] = color2;
- points_begin_left_corner_ptr[0] = pos - edge_offset - border;
- points_begin_left_corner_ptr[1] = pos - edge_offset + begin_border - border;
- points_begin_left_corner_ptr[2] = pos - edge_offset;
- points_begin_left_corner_ptr[3] = pos - edge_offset + begin_border;
+ points_left_ptr[0] = pos + edge_offset + begin_border;
+ points_left_ptr[1] = pos + edge_offset + begin_border + border;
- colors_begin_left_corner_ptr[0] = color2;
- colors_begin_left_corner_ptr[1] = color2;
- colors_begin_left_corner_ptr[2] = color;
- colors_begin_left_corner_ptr[3] = color2;
+ colors_left_ptr[0] = color2;
+ colors_left_ptr[1] = color2;
- points_begin_right_corner_ptr[0] = pos + edge_offset + begin_border;
- points_begin_right_corner_ptr[1] = pos + edge_offset + begin_border + border;
- points_begin_right_corner_ptr[2] = pos + edge_offset;
- points_begin_right_corner_ptr[3] = pos + edge_offset + border;
+ points_right_ptr[0] = pos - edge_offset + begin_border;
+ points_right_ptr[1] = pos - edge_offset + begin_border - border;
- colors_begin_right_corner_ptr[0] = color2;
- colors_begin_right_corner_ptr[1] = color2;
- colors_begin_right_corner_ptr[2] = color;
- colors_begin_right_corner_ptr[3] = color2;
+ colors_right_ptr[0] = color2;
+ colors_right_ptr[1] = color2;
}
- if (is_last_point) {
- Vector2 end_border = loop ? Vector2() : prev_segment_dir * border_size;
-
- points_end_ptr[0] = pos + edge_offset + end_border;
- points_end_ptr[1] = pos - edge_offset + end_border;
- points_end_ptr[2] = pos + edge_offset;
- points_end_ptr[3] = pos - edge_offset;
-
- colors_end_ptr[0] = color2;
- colors_end_ptr[1] = color2;
- colors_end_ptr[2] = color;
- colors_end_ptr[3] = color;
-
- points_end_left_corner_ptr[0] = pos - edge_offset - border;
- points_end_left_corner_ptr[1] = pos - edge_offset + end_border - border;
- points_end_left_corner_ptr[2] = pos - edge_offset;
- points_end_left_corner_ptr[3] = pos - edge_offset + end_border;
-
- colors_end_left_corner_ptr[0] = color2;
- colors_end_left_corner_ptr[1] = color2;
- colors_end_left_corner_ptr[2] = color;
- colors_end_left_corner_ptr[3] = color2;
-
- points_end_right_corner_ptr[0] = pos + edge_offset + end_border;
- points_end_right_corner_ptr[1] = pos + edge_offset + end_border + border;
- points_end_right_corner_ptr[2] = pos + edge_offset;
- points_end_right_corner_ptr[3] = pos + edge_offset + border;
-
- colors_end_right_corner_ptr[0] = color2;
- colors_end_right_corner_ptr[1] = color2;
- colors_end_right_corner_ptr[2] = color;
- colors_end_right_corner_ptr[3] = color2;
- }
+ if (is_last_point && !loop) {
+ Vector2 end_border = prev_segment_dir * border_size;
+ int end_index = polyline_point_count + 2;
- prev_segment_dir = segment_dir;
- }
+ points_ptr[end_index + 0] = pos + edge_offset + end_border;
+ points_ptr[end_index + 1] = pos - edge_offset + end_border;
- pline_begin->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_begin->polygon.create(indices, points_begin, colors_begin);
+ colors_ptr[end_index + 0] = color2;
+ colors_ptr[end_index + 1] = color2;
- pline_begin_left_corner->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_begin_left_corner->polygon.create(indices, points_begin_left_corner, colors_begin_left_corner);
+ // Swap orientation of the triangles within both end corner quads so the visual seams
+ // between triangles goes from the edge corner. Done by going back to the edge corner
+ // (1 additional vertex / zero-area triangle per left/right corner).
+ points_left_ptr[end_index + 0] = pos + edge_offset;
+ points_left_ptr[end_index + 1] = pos + edge_offset + end_border + border;
+ points_left_ptr[end_index + 2] = pos + edge_offset + end_border;
- pline_begin_right_corner->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_begin_right_corner->polygon.create(indices, points_begin_right_corner, colors_begin_right_corner);
+ colors_left_ptr[end_index + 0] = color;
+ colors_left_ptr[end_index + 1] = color2;
+ colors_left_ptr[end_index + 2] = color2;
- pline_end->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_end->polygon.create(indices, points_end, colors_end);
+ points_right_ptr[end_index + 0] = pos - edge_offset;
+ points_right_ptr[end_index + 1] = pos - edge_offset + end_border - border;
+ points_right_ptr[end_index + 2] = pos - edge_offset + end_border;
- pline_end_left_corner->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_end_left_corner->polygon.create(indices, points_end_left_corner, colors_end_left_corner);
+ colors_right_ptr[end_index + 0] = color;
+ colors_right_ptr[end_index + 1] = color2;
+ colors_right_ptr[end_index + 2] = color2;
+ }
- pline_end_right_corner->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
- pline_end_right_corner->polygon.create(indices, points_end_right_corner, colors_end_right_corner);
+ prev_segment_dir = segment_dir;
+ }
pline_left->primitive = RS::PRIMITIVE_TRIANGLE_STRIP;
pline_left->polygon.create(indices, points_left, colors_left);
diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
index efd961fd89..16bc36f4b8 100644
--- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
+++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
@@ -964,7 +964,12 @@ void RendererSceneRenderRD::render_scene(const Ref<RenderSceneBuffers> &p_render
scene_data.z_far = p_camera_data->main_projection.get_z_far();
// this should be the same for all cameras..
- scene_data.lod_distance_multiplier = p_camera_data->main_projection.get_lod_multiplier();
+ const float lod_distance_multiplier = p_camera_data->main_projection.get_lod_multiplier();
+
+ // Also, take into account resolution scaling for the multiplier, since we have more leeway with quality
+ // degradation visibility. Conversely, allow upwards scaling, too, for increased mesh detail at high res.
+ const float scaling_3d_scale = GLOBAL_GET("rendering/scaling_3d/scale");
+ scene_data.lod_distance_multiplier = lod_distance_multiplier * (1.0 / scaling_3d_scale);
if (get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_DISABLE_LOD) {
scene_data.screen_mesh_lod_threshold = 0.0;
diff --git a/servers/rendering/renderer_rd/shaders/environment/sky.glsl b/servers/rendering/renderer_rd/shaders/environment/sky.glsl
index e8e5de7020..01301e2187 100644
--- a/servers/rendering/renderer_rd/shaders/environment/sky.glsl
+++ b/servers/rendering/renderer_rd/shaders/environment/sky.glsl
@@ -290,6 +290,6 @@ void main() {
}
#ifdef USE_DEBANDING
- frag_color.rgb += interleaved_gradient_noise(gl_FragCoord.xy);
+ frag_color.rgb += interleaved_gradient_noise(gl_FragCoord.xy) * params.luminance_multiplier;
#endif
}
diff --git a/tests/scene/test_curve_3d.h b/tests/scene/test_curve_3d.h
index 0f0d413354..4d7b718d7e 100644
--- a/tests/scene/test_curve_3d.h
+++ b/tests/scene/test_curve_3d.h
@@ -178,9 +178,9 @@ TEST_CASE("[Curve3D] Sampling") {
}
SUBCASE("sample_baked_with_rotation") {
- CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 0, 0))) == Transform3D(Basis(Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0)), Vector3(0, 0, 0)));
- CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 25, 0))) == Transform3D(Basis(Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0)), Vector3(0, 25, 0)));
- CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 50, 0))) == Transform3D(Basis(Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0)), Vector3(0, 50, 0)));
+ CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 0, 0))) == Transform3D(Basis(Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, -1, 0)), Vector3(0, 0, 0)));
+ CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 25, 0))) == Transform3D(Basis(Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, -1, 0)), Vector3(0, 25, 0)));
+ CHECK(curve->sample_baked_with_rotation(curve->get_closest_offset(Vector3(0, 50, 0))) == Transform3D(Basis(Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, -1, 0)), Vector3(0, 50, 0)));
}
SUBCASE("sample_baked_tilt") {