2
0
Эх сурвалжийг харах

Improve Scene Tree editor performance

We now cache the Node*<>TreeItem* mapping in the SceneTreeEditor. This
allows us to make targeted updates to the Tree used to display the scene
tree in the editor.

Previously on almost all changes to the scene tree the editor would
rebuild the entire widget, causing a large number of deallocations an
allocations. We now carefully manipulate the Tree widget in-situ saving
a large number of these allocations.

In order to know what Nodes need to be updated we add a
editor_state_changed signal to Node, this is a TOOLS_ENABLED,
editor-only signal fired when changes to Node happen that are relevant
to editor state.

We also now make sure that when nodes are moved/renamed we don't check
expensive properties that cannot contain NodePaths. This saves a lot of
time when SceneTreeDock renames a node in a scene with a lot of
MeshInstances. This makes renaming nodes go from ~27 seconds to ~2
seconds on large scenes.

SceneTreeEditor instances will now also not do all of the potentially
expensive update work if they are invisible. This behavior is turned off
by default so it won't affect existing users. This change allows the
editor to only update SceneTreeEditors that actually in view. In
practice this means that for most changes instead of updating 6
SceneTreeEditors we only update 1 instantly, and the others only when
they become visible.

There is definitely more that could be done, but this is already a
massive improvement. In complex scenes we see an improvement of 10x,
things that used to take ~30 seconds now only take 2.

This fixes #83460

I want to thank KoBeWi, TokisanGames, a-johnston, aniel080400 for
their tireless testing. And AeioMuch for their testing and providing a
fix for the hover issue.
HP van Braam 8 сар өмнө
parent
commit
6f7525c396

+ 1 - 0
core/object/object.h

@@ -89,6 +89,7 @@ enum PropertyHint {
 	PROPERTY_HINT_DICTIONARY_TYPE,
 	PROPERTY_HINT_TOOL_BUTTON,
 	PROPERTY_HINT_ONESHOT, ///< the property will be changed by self after setting, such as AudioStreamPlayer.playing, Particles.emitting.
+	PROPERTY_HINT_NO_NODEPATH, /// < this property will not contain a NodePath, regardless of type (Array, Dictionary, List, etc.). Needed for SceneTreeDock.
 	PROPERTY_HINT_MAX,
 };
 

+ 1 - 1
doc/classes/@GlobalScope.xml

@@ -2943,7 +2943,7 @@
 		<constant name="PROPERTY_HINT_ONESHOT" value="40" enum="PropertyHint">
 			Hints that a property will be changed on its own after setting, such as [member AudioStreamPlayer.playing] or [member GPUParticles3D.emitting].
 		</constant>
-		<constant name="PROPERTY_HINT_MAX" value="41" enum="PropertyHint">
+		<constant name="PROPERTY_HINT_MAX" value="42" enum="PropertyHint">
 			Represents the size of the [enum PropertyHint] enum.
 		</constant>
 		<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags" is_bitfield="true">

+ 5 - 0
doc/classes/Node.xml

@@ -1068,6 +1068,11 @@
 				Emitted when the node's editor description field changed.
 			</description>
 		</signal>
+		<signal name="editor_state_changed">
+			<description>
+				Emitted when an attribute of the node that is relevant to the editor is changed. Only emitted in the editor.
+			</description>
+		</signal>
 		<signal name="ready">
 			<description>
 				Emitted when the node is considered ready, after [method _ready] is called.

+ 6 - 0
doc/classes/TreeItem.xml

@@ -36,6 +36,12 @@
 				Calls the [param method] on the actual TreeItem and its children recursively. Pass parameters as a comma separated list.
 			</description>
 		</method>
+		<method name="clear_buttons">
+			<return type="void" />
+			<description>
+				Removes all buttons from all columns of this item.
+			</description>
+		</method>
 		<method name="clear_custom_bg_color">
 			<return type="void" />
 			<param index="0" name="column" type="int" />

+ 1 - 0
editor/connections_dialog.cpp

@@ -735,6 +735,7 @@ ConnectDialog::ConnectDialog() {
 	from_signal->set_editable(false);
 
 	tree = memnew(SceneTreeEditor(false));
+	tree->set_update_when_invisible(false);
 	tree->set_connecting_signal(true);
 	tree->set_show_enabled_subscene(true);
 	tree->set_v_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND);

+ 575 - 127
editor/gui/scene_tree_editor.cpp

@@ -61,7 +61,7 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
 	}
 
 	if (connect_to_script_mode) {
-		return; //don't do anything in this mode
+		return; // Don't do anything in this mode.
 	}
 
 	TreeItem *item = Object::cast_to<TreeItem>(p_item);
@@ -152,7 +152,8 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
 			const String line = all_warnings.substr(start, end - start);
 			lines.append(line);
 		}
-		all_warnings = String("\n").join(lines).indent("    ").replace(U"    •", U"\n•").substr(2); // We don't want the first two newlines.
+		// We don't want the first two newlines.
+		all_warnings = String("\n").join(lines).indent("    ").replace(U"    •", U"\n•").substr(2);
 
 		warning->set_text(all_warnings);
 		warning->popup_centered();
@@ -217,12 +218,35 @@ void SceneTreeEditor::_toggle_visible(Node *p_node) {
 	}
 }
 
-void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
+void SceneTreeEditor::_update_node_path(Node *p_node, bool p_recursive) {
 	if (!p_node) {
 		return;
 	}
 
-	// only owned nodes are editable, since nodes can create their own (manually owned) child nodes,
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node);
+	if (!I) {
+		return;
+	}
+
+	I->value.item->set_metadata(0, p_node->get_path());
+
+	if (!p_recursive) {
+		return;
+	}
+
+	int cc = p_node->get_child_count(false);
+	for (int i = 0; i < cc; i++) {
+		Node *c = p_node->get_child(i, false);
+		_update_node_path(c, p_recursive);
+	}
+}
+
+void SceneTreeEditor::_update_node_subtree(Node *p_node, TreeItem *p_parent, bool p_force) {
+	if (!p_node) {
+		return;
+	}
+
+	// Only owned nodes are editable, since nodes can create their own (manually owned) child nodes,
 	// which the editor needs not to know about.
 
 	bool part_of_subscene = false;
@@ -230,42 +254,152 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 	if (!display_foreign && p_node->get_owner() != get_scene_node() && p_node != get_scene_node()) {
 		if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) {
 			part_of_subscene = true;
-			//allow
+			// Allow.
 		} else {
+			// Stale node, remove recursively.
+			node_cache.remove(p_node, true);
 			return;
 		}
 	} else {
 		part_of_subscene = p_node != get_scene_node() && get_scene_node()->get_scene_inherited_state().is_valid() && get_scene_node()->get_scene_inherited_state()->find_node_by_path(get_scene_node()->get_path_to(p_node)) >= 0;
 	}
 
-	TreeItem *item = tree->create_item(p_parent);
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node);
+	TreeItem *item = nullptr;
+
+	bool is_new = false;
+
+	if (I) {
+		item = I->value.item;
+		TreeItem *current_parent = item->get_parent();
+
+		// Our parent might be re-created because of a changed type.
+		if (p_parent && p_parent != current_parent) {
+			if (current_parent) {
+				current_parent->remove_child(item);
+			}
+			p_parent->add_child(item);
+			I->value.removed = false;
+			_move_node_item(p_parent, I);
+		}
+
+		if (I->value.has_moved_children) {
+			_move_node_children(I);
+		}
+	} else {
+		int index = -1;
+		// Check to see if there is a root node for us to reuse.
+		if (!p_parent) {
+			item = tree->get_root();
+			if (!item) {
+				item = tree->create_item(nullptr);
+				index = 0;
+			}
+		} else {
+			index = p_node->get_index(false);
+			item = tree->create_item(p_parent, index);
+		}
+
+		I = node_cache.add(p_node, item);
+		I->value.index = index;
+		is_new = true;
+	}
+
+	if (!(p_force || I->value.dirty)) {
+		// Nothing to do.
+		return;
+	}
 
-	item->set_text(0, p_node->get_name());
-	item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING);
-	if (can_rename && !part_of_subscene) {
-		item->set_editable(0, true);
+	_update_node(p_node, item, part_of_subscene);
+	I->value.dirty = false;
+	I->value.can_process = p_node->can_process();
+
+	// Force update all our children if we are new or if we were forced to update.
+	bool force_update_children = p_force || is_new;
+	// Update all our children.
+	for (int i = 0; i < p_node->get_child_count(false); i++) {
+		_update_node_subtree(p_node->get_child(i, false), item, force_update_children);
+	}
+
+	if (valid_types.size()) {
+		bool valid = false;
+		for (const StringName &E : valid_types) {
+			if (p_node->is_class(E) ||
+					EditorNode::get_singleton()->is_object_of_custom_type(p_node, E)) {
+				valid = true;
+				break;
+			} else {
+				Ref<Script> node_script = p_node->get_script();
+				while (node_script.is_valid()) {
+					if (node_script->get_path() == E) {
+						valid = true;
+						break;
+					}
+					node_script = node_script->get_base_script();
+				}
+				if (valid) {
+					break;
+				}
+			}
+		}
+
+		if (!valid) {
+			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+			item->set_selectable(0, false);
+			item->deselect(0);
+			if (selected == p_node) {
+				set_selected(nullptr, false);
+			}
+		}
+	}
+}
+
+void SceneTreeEditor::_update_node(Node *p_node, TreeItem *p_item, bool p_part_of_subscene) {
+	// Reset item properties that are not explicitly set in the default case.
+	p_item->clear_buttons();
+	p_item->remove_meta(SNAME("custom_color"));
+	p_item->clear_custom_color(0);
+	p_item->set_selectable(0, true);
+
+	p_item->set_text(0, p_node->get_name());
+	p_item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING);
+	if (can_rename && !p_part_of_subscene) {
+		p_item->set_editable(0, true);
 	}
 
-	item->set_selectable(0, true);
 	if (can_rename) {
 		bool collapsed = p_node->is_displayed_folded();
 		if (collapsed) {
-			item->set_collapsed(true);
+			p_item->set_collapsed(true);
 		}
 	}
 
 	Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(p_node, "Node");
-	item->set_icon(0, icon);
-	item->set_metadata(0, p_node->get_path());
+	p_item->set_icon(0, icon);
+	p_item->set_metadata(0, p_node->get_path());
+
+	if (!p_node->is_connected("child_order_changed", callable_mp(this, &SceneTreeEditor::_node_child_order_changed))) {
+		p_node->connect("child_order_changed", callable_mp(this, &SceneTreeEditor::_node_child_order_changed).bind(p_node));
+	}
+
+	if (!p_node->is_connected("editor_state_changed", callable_mp(this, &SceneTreeEditor::_node_editor_state_changed))) {
+		p_node->connect("editor_state_changed", callable_mp(this, &SceneTreeEditor::_node_editor_state_changed).bind(p_node));
+	}
+
+	if (connecting_signal || (can_open_instance && is_scene_tree_dock)) {
+		if (!p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
+			p_node->connect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed).bind(p_node));
+		}
+	}
 
 	if (connecting_signal) {
 		// Add script icons for all scripted nodes.
 		Ref<Script> scr = p_node->get_script();
 		if (scr.is_valid()) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT);
 			if (EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr) {
 				// Disable button on custom scripts (pure visual cue).
-				item->set_button_disabled(0, item->get_button_count(0) - 1, true);
+				p_item->set_button_disabled(0, p_item->get_button_count(0) - 1, true);
 			}
 		}
 	}
@@ -276,8 +410,8 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 		Ref<Script> scr = p_node->get_script();
 		bool has_custom_script = scr.is_valid() && EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr;
 		if (scr.is_null() || has_custom_script) {
-			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
-			item->set_selectable(0, false);
+			_set_item_custom_color(p_item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+			p_item->set_selectable(0, false);
 
 			accent.a *= 0.7;
 		}
@@ -287,36 +421,36 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 			if (connecting_signal) {
 				node_name += " " + TTR("(Connecting From)");
 			}
-			item->set_text(0, node_name);
-			_set_item_custom_color(item, accent);
+			p_item->set_text(0, node_name);
+			_set_item_custom_color(p_item, accent);
 		}
-	} else if (part_of_subscene) {
+	} else if (p_part_of_subscene) {
 		if (valid_types.size() == 0) {
-			_set_item_custom_color(item, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+			_set_item_custom_color(p_item, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
 		}
 	} else if (marked.has(p_node)) {
 		String node_name = p_node->get_name();
 		if (connecting_signal) {
 			node_name += " " + TTR("(Connecting From)");
 		}
-		item->set_text(0, node_name);
-		item->set_selectable(0, marked_selectable);
-		_set_item_custom_color(item, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
+		p_item->set_text(0, node_name);
+		p_item->set_selectable(0, marked_selectable);
+		_set_item_custom_color(p_item, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
 	} else if (!p_node->can_process()) {
-		_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+		_set_item_custom_color(p_item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
 	} else if (!marked_selectable && !marked_children_selectable) {
 		Node *node = p_node;
 		while (node) {
 			if (marked.has(node)) {
-				item->set_selectable(0, false);
-				_set_item_custom_color(item, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+				p_item->set_selectable(0, false);
+				_set_item_custom_color(p_item, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
 				break;
 			}
 			node = node->get_parent();
 		}
 	}
 
-	if (can_rename) { //should be can edit..
+	if (can_rename) { // TODO Should be can edit..
 
 		const PackedStringArray warnings = p_node->get_configuration_warnings();
 		const int num_warnings = warnings.size();
@@ -340,7 +474,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 				all_warnings.remove_at(0); // With only one warning, two newlines do not look great.
 			}
 
-			item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
+			p_item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
 		}
 
 		if (p_node->is_unique_name_in_owner()) {
@@ -349,7 +483,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 			if (!disabled) {
 				button_text += "\n" + TTR("Click to disable this.");
 			}
-			item->add_button(0, get_editor_theme_icon(SNAME("SceneUniqueName")), BUTTON_UNIQUE, disabled, button_text);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("SceneUniqueName")), BUTTON_UNIQUE, disabled, button_text);
 		}
 
 		int num_connections = p_node->get_persistent_signal_connection_count();
@@ -394,24 +528,21 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 
 		if (num_connections >= 1 || num_groups >= 1) {
 			msg_temp += msg_temp_end;
-			item->add_button(0, icon_temp, signal_temp, false, msg_temp);
+			p_item->add_button(0, icon_temp, signal_temp, false, msg_temp);
 		}
 	}
 
 	{
-		_update_node_tooltip(p_node, item);
+		_update_node_tooltip(p_node, p_item);
 		Callable delay_update_tooltip = callable_mp(this, &SceneTreeEditor::_queue_update_node_tooltip);
 		if (p_node->is_connected("editor_description_changed", delay_update_tooltip)) {
 			p_node->disconnect("editor_description_changed", delay_update_tooltip);
 		}
-		p_node->connect("editor_description_changed", delay_update_tooltip.bind(item));
+		p_node->connect("editor_description_changed", delay_update_tooltip.bind(p_item));
 	}
 
-	if (can_open_instance && is_scene_tree_dock) { // Show buttons only when necessary (SceneTreeDock) to avoid crashes.
-		if (!p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
-			p_node->connect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed).bind(p_node));
-		}
-
+	// Show buttons only when necessary (SceneTreeDock) to avoid crashes.
+	if (can_open_instance && is_scene_tree_dock) {
 		Ref<Script> scr = p_node->get_script();
 		if (!scr.is_null()) {
 			String additional_notes;
@@ -425,84 +556,61 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 				additional_notes += "\n" + TTR("This script is a custom type.");
 				button_color.a = 0.5;
 			}
-			item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT, false, TTR("Open Script:") + " " + scr->get_path() + additional_notes);
-			item->set_button_color(0, item->get_button_count(0) - 1, button_color);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT, false, TTR("Open Script:") + " " + scr->get_path() + additional_notes);
+			p_item->set_button_color(0, p_item->get_button_count(0) - 1, button_color);
 		}
 
 		if (p_node->has_meta("_edit_lock_")) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
 		}
 		if (p_node->has_meta("_edit_group_")) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
 		}
 
 		if (p_node->has_method("is_visible") && p_node->has_method("set_visible") && p_node->has_signal(SceneStringName(visibility_changed))) {
 			bool is_visible = p_node->call("is_visible");
 			if (is_visible) {
-				item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
 			} else {
-				item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
 			}
 			const Callable vis_changed = callable_mp(this, &SceneTreeEditor::_node_visibility_changed);
 			if (!p_node->is_connected(SceneStringName(visibility_changed), vis_changed)) {
 				p_node->connect(SceneStringName(visibility_changed), vis_changed.bind(p_node));
 			}
-			_update_visibility_color(p_node, item);
+			_update_visibility_color(p_node, p_item);
 		}
 
 		if (p_node->is_class("AnimationMixer")) {
 			bool is_pinned = AnimationPlayerEditor::get_singleton()->get_editing_node() == p_node && AnimationPlayerEditor::get_singleton()->is_pinned();
 
 			if (is_pinned) {
-				item->add_button(0, get_editor_theme_icon(SNAME("Pin")), BUTTON_PIN, false, TTR("AnimationPlayer is pinned.\nClick to unpin."));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("Pin")), BUTTON_PIN, false, TTR("AnimationPlayer is pinned.\nClick to unpin."));
 			}
 		}
 	}
 
 	if (editor_selection) {
 		if (editor_selection->is_selected(p_node)) {
-			item->select(0);
+			p_item->select(0);
 		}
 	}
 
 	if (selected == p_node) {
 		if (!editor_selection) {
-			item->select(0);
+			p_item->select(0);
 		}
-		item->set_as_cursor(0);
+		p_item->set_as_cursor(0);
 	}
+}
 
-	for (int i = 0; i < p_node->get_child_count(); i++) {
-		_add_nodes(p_node->get_child(i), item);
+void SceneTreeEditor::_update_if_clean() {
+	if (tree_dirty) {
+		return;
 	}
 
-	if (valid_types.size()) {
-		bool valid = false;
-		for (const StringName &E : valid_types) {
-			if (p_node->is_class(E) ||
-					EditorNode::get_singleton()->is_object_of_custom_type(p_node, E)) {
-				valid = true;
-				break;
-			} else {
-				Ref<Script> node_script = p_node->get_script();
-				while (node_script.is_valid()) {
-					if (node_script->get_path() == E) {
-						valid = true;
-						break;
-					}
-					node_script = node_script->get_base_script();
-				}
-				if (valid) {
-					break;
-				}
-			}
-		}
-
-		if (!valid) {
-			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
-			item->set_selectable(0, false);
-		}
-	}
+	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
+	tree_dirty = true;
 }
 
 void SceneTreeEditor::_queue_update_node_tooltip(Node *p_node, TreeItem *p_item) {
@@ -550,6 +658,13 @@ void SceneTreeEditor::_update_node_tooltip(Node *p_node, TreeItem *p_item) {
 }
 
 void SceneTreeEditor::_node_visibility_changed(Node *p_node) {
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (!I) {
+		// We leave these signals connected when switching tabs.
+		// If the node is not in cache it was for a different tab.
+		return;
+	}
+
 	if (!p_node || (p_node != get_scene_node() && !p_node->get_owner())) {
 		return;
 	}
@@ -599,45 +714,155 @@ void SceneTreeEditor::_set_item_custom_color(TreeItem *p_item, Color p_color) {
 }
 
 void SceneTreeEditor::_node_script_changed(Node *p_node) {
-	if (tree_dirty) {
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (!I) {
+		// We leave these signals connected when switching tabs.
+		// If the node is not in cache it was for a different tab.
 		return;
 	}
 
-	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-	tree_dirty = true;
+	node_cache.mark_dirty(p_node);
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_move_node_children(HashMap<Node *, CachedNode>::Iterator &p_I) {
+	TreeItem *item = p_I->value.item;
+	Node *node = p_I->key;
+	int cc = node->get_child_count(false);
+
+	for (int i = 0; i < cc; i++) {
+		HashMap<Node *, CachedNode>::Iterator CI = node_cache.get(node->get_child(i, false));
+		if (CI) {
+			_move_node_item(item, CI);
+		}
+	}
+
+	p_I->value.has_moved_children = false;
+}
+
+void SceneTreeEditor::_move_node_item(TreeItem *p_parent, HashMap<Node *, CachedNode>::Iterator &p_I) {
+	if (!p_parent) {
+		return;
+	}
+
+	Node *node = p_I->key;
+
+	int current_node_index = node->get_index(false);
+	int current_item_index = -1;
+	TreeItem *item = p_I->value.item;
+
+	if (item->get_parent() != p_parent) {
+		TreeItem *p = item->get_parent();
+		if (p) {
+			item->get_parent()->remove_child(item);
+		}
+		p_parent->add_child(item);
+		p_I->value.removed = false;
+		current_item_index = p_parent->get_child_count() - 1;
+		p_I->value.index = current_item_index;
+	}
+
+	if (p_I->value.index != current_node_index) {
+		// If we just re-parented we know our index.
+		if (current_item_index == -1) {
+			current_item_index = item->get_index();
+		}
+
+		// Are we already in the right place?
+		if (current_node_index == current_item_index) {
+			p_I->value.index = current_node_index;
+			return;
+		}
+
+		// Are we the first node?
+		if (current_node_index == 0) {
+			// There has to be at least 1 other node, otherwise we would not have gotten here.
+			TreeItem *neighbor_item = p_parent->get_child(0);
+			item->move_before(neighbor_item);
+		} else {
+			TreeItem *neighbor_item = p_parent->get_child(CLAMP(current_node_index - 1, 0, p_parent->get_child_count() - 1));
+			item->move_after(neighbor_item);
+		}
+
+		p_I->value.index = current_node_index;
+	}
+}
+
+void SceneTreeEditor::_node_child_order_changed(Node *p_node) {
+	// Do not try to change children on nodes currently marked for removal.
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (I) {
+		node_cache.mark_dirty(I->key);
+		I->value.has_moved_children = true;
+	}
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_node_editor_state_changed(Node *p_node) {
+	node_cache.mark_dirty(p_node);
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (I) {
+		if (p_node->is_inside_tree() && p_node->can_process() != I->value.can_process) {
+			// All our children also change process mode.
+			node_cache.mark_children_dirty(p_node, true);
+		}
+	}
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_node_added(Node *p_node) {
+	if (!get_scene_node()) {
+		return;
+	}
+
+	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
+		return;
+	}
+
+	node_cache.mark_dirty(p_node);
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_node_removed(Node *p_node) {
 	if (EditorNode::get_singleton()->is_exiting()) {
-		return; //speed up exit
+		return; // Speed up exit.
 	}
 
-	if (p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
-		p_node->disconnect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed));
+	if (EditorNode::get_singleton()->is_changing_scene()) {
+		return; // Switching tabs we will be destroying node cache anyway.
 	}
 
-	if (p_node->has_signal(SceneStringName(visibility_changed))) {
-		if (p_node->is_connected(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
-			p_node->disconnect(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed));
-		}
+	if (!get_scene_node()) {
+		return;
 	}
 
-	if (p_node == selected) {
-		selected = nullptr;
+	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
+		return;
 	}
+
+	node_cache.remove(p_node);
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_node_renamed(Node *p_node) {
+	if (!get_scene_node()) {
+		return;
+	}
+
 	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
 		return;
 	}
 
+	node_cache.mark_dirty(p_node);
+	// Recursively update child node paths.
+	_update_node_path(p_node, true);
+
 	emit_signal(SNAME("node_renamed"));
 
-	if (!tree_dirty) {
-		callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-		tree_dirty = true;
-	}
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) {
@@ -646,19 +871,49 @@ void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) {
 		return;
 	}
 
+	if (!update_when_invisible && !is_visible_in_tree()) {
+		return;
+	}
+
 	if (tree->is_editing()) {
 		return;
 	}
 
 	updating_tree = true;
-	tree->clear();
+
 	last_hash = hash_djb2_one_64(0);
-	if (get_scene_node()) {
-		_add_nodes(get_scene_node(), nullptr);
+	Node *scene_node = get_scene_node();
+
+	if (node_cache.current_scene_node != scene_node) {
+		_reset();
+		node_cache.current_scene_node = scene_node;
+		node_cache.force_update = true;
+	}
+
+	if (node_cache.current_scene_node) {
+		// Handle pinning/unpinning the animation player only do this once per iteration.
+		Node *pinned_node = AnimationPlayerEditor::get_singleton()->get_editing_node();
+		// If pinned state changed, update the currently pinned node.
+		if (AnimationPlayerEditor::get_singleton()->is_pinned() != node_cache.current_has_pin) {
+			node_cache.current_has_pin = AnimationPlayerEditor::get_singleton()->is_pinned();
+			node_cache.mark_dirty(pinned_node);
+		}
+		// If the current pinned node changed update both the old and new node.
+		if (node_cache.current_pinned_node != pinned_node) {
+			node_cache.mark_dirty(pinned_node);
+			node_cache.mark_dirty(node_cache.current_pinned_node);
+			node_cache.current_pinned_node = pinned_node;
+		}
+
+		_update_node_subtree(get_scene_node(), nullptr, node_cache.force_update);
 		_compute_hash(get_scene_node(), last_hash);
+
+		node_cache.delete_pending();
 	}
+
 	updating_tree = false;
 	tree_dirty = false;
+	node_cache.force_update = false;
 
 	if (!filter.strip_edges().is_empty() || !show_all_nodes) {
 		_update_filter(nullptr, p_scroll_to_selected);
@@ -822,16 +1077,19 @@ bool SceneTreeEditor::_item_matches_all_terms(TreeItem *p_item, const PackedStri
 }
 
 void SceneTreeEditor::_compute_hash(Node *p_node, uint64_t &hash) {
-	hash = hash_djb2_one_64(p_node->get_instance_id(), hash);
-	if (p_node->get_parent()) {
-		hash = hash_djb2_one_64(p_node->get_parent()->get_instance_id(), hash); //so a reparent still produces a different hash
-	}
-
+	// Nodes are added and removed by Node* pointers.
+	hash = hash_djb2_one_64((ptrdiff_t)p_node, hash);
+	// This hash is non-commutative: if the node order changes so will the hash.
 	for (int i = 0; i < p_node->get_child_count(); i++) {
 		_compute_hash(p_node->get_child(i), hash);
 	}
 }
 
+void SceneTreeEditor::_reset() {
+	tree->clear();
+	node_cache.clear();
+}
+
 void SceneTreeEditor::_test_update_tree() {
 	pending_test_update = false;
 
@@ -840,20 +1098,20 @@ void SceneTreeEditor::_test_update_tree() {
 	}
 
 	if (tree_dirty) {
-		return; // don't even bother
+		return; // Don't even bother.
 	}
 
 	uint64_t hash = hash_djb2_one_64(0);
 	if (get_scene_node()) {
 		_compute_hash(get_scene_node(), hash);
 	}
-	//test hash
+
+	// Test hash.
 	if (hash == last_hash) {
-		return; // did not change
+		return; // Did not change.
 	}
 
-	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-	tree_dirty = true;
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_tree_process_mode_changed() {
@@ -863,11 +1121,13 @@ void SceneTreeEditor::_tree_process_mode_changed() {
 
 void SceneTreeEditor::_tree_changed() {
 	if (EditorNode::get_singleton()->is_exiting()) {
-		return; //speed up exit
+		return; // Speed up exit.
 	}
+
 	if (pending_test_update) {
 		return;
 	}
+
 	if (tree_dirty) {
 		return;
 	}
@@ -887,7 +1147,7 @@ void SceneTreeEditor::_selected_changed() {
 		return;
 	}
 
-	selected = get_node(np);
+	selected = n;
 
 	blocked++;
 	emit_signal(SNAME("node_selected"));
@@ -929,7 +1189,7 @@ void SceneTreeEditor::_cell_multi_selected(Object *p_object, int p_cell, bool p_
 		editor_selection->remove_node(n);
 	}
 
-	// Emitted "selected" in _selected_changed() when select single node, so select multiple node emit "changed"
+	// Emitted "selected" in _selected_changed() when select single node, so select multiple node emit "changed".
 	if (editor_selection->get_selected_nodes().size() > 1) {
 		emit_signal(SNAME("node_changed"));
 	}
@@ -948,6 +1208,7 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_ENTER_TREE: {
 			get_tree()->connect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
 			get_tree()->connect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+			get_tree()->connect("node_added", callable_mp(this, &SceneTreeEditor::_node_added));
 			get_tree()->connect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
 			get_tree()->connect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
 			get_tree()->connect(SceneStringName(node_configuration_warning_changed), callable_mp(this, &SceneTreeEditor::_warning_changed));
@@ -960,6 +1221,7 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_EXIT_TREE: {
 			get_tree()->disconnect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
 			get_tree()->disconnect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+			get_tree()->disconnect("node_added", callable_mp(this, &SceneTreeEditor::_node_added));
 			get_tree()->disconnect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
 			get_tree()->disconnect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
 			tree->disconnect("item_collapsed", callable_mp(this, &SceneTreeEditor::_cell_collapsed));
@@ -969,6 +1231,9 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_THEME_CHANGED: {
 			tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)));
 
+			// When we change theme we need to re-do everything.
+			_reset();
+
 			_update_tree();
 		} break;
 
@@ -986,10 +1251,15 @@ void SceneTreeEditor::_notification(int p_what) {
 					}
 				}
 
-				if (item) {
-					// Must wait until tree is properly sized before scrolling.
-					ObjectID item_id = item->get_instance_id();
-					callable_mp(this, &SceneTreeEditor::_tree_scroll_to_item).call_deferred(item_id);
+				bool has_item = item;
+
+				if (update_when_invisible) {
+					if (has_item) {
+						ObjectID item_id = item->get_instance_id();
+						callable_mp(this, &SceneTreeEditor::_tree_scroll_to_item).call_deferred(item_id);
+					}
+				} else {
+					callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(has_item);
 				}
 			}
 		} break;
@@ -1024,6 +1294,7 @@ void SceneTreeEditor::set_selected(Node *p_node, bool p_emit_selected) {
 	if (pending_test_update) {
 		_test_update_tree();
 	}
+
 	if (tree_dirty) {
 		_update_tree();
 	}
@@ -1101,7 +1372,8 @@ void SceneTreeEditor::rename_node(Node *p_node, const String &p_name, TreeItem *
 		}
 	}
 
-	// Trim leading/trailing whitespace to prevent node names from containing accidental whitespace, which would make it more difficult to get the node via `get_node()`.
+	// Trim leading/trailing whitespace to prevent node names from containing accidental whitespace,
+	// which would make it more difficult to get the node via `get_node()`.
 	new_name = new_name.strip_edges();
 	if (new_name.is_empty() && p_node->get_owner() != nullptr && !p_node->get_scene_file_path().is_empty()) {
 		// If name is empty and node is root of an instance, revert to the original name.
@@ -1129,7 +1401,8 @@ void SceneTreeEditor::rename_node(Node *p_node, const String &p_name, TreeItem *
 		return;
 	}
 
-	// We previously made sure name is not the same as current name so that it won't complain about already used unique name when not changing name.
+	// We previously made sure name is not the same as current name
+	// so that it won't complain about already used unique name when not changing name.
 	if (p_node->is_unique_name_in_owner() && get_tree()->get_edited_scene_root()->get_node_or_null("%" + new_name)) {
 		String text = vformat(TTR("A node with the unique name %s already exists in this scene."), new_name);
 		if (error->is_visible()) {
@@ -1207,11 +1480,24 @@ Node *SceneTreeEditor::get_selected() {
 	return selected;
 }
 
-void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
-	if (tree_dirty) {
-		_update_tree();
+void SceneTreeEditor::_update_marking_list(const HashSet<Node *> &p_marked) {
+	for (Node *N : p_marked) {
+		HashMap<Node *, CachedNode>::Iterator I = node_cache.get(N);
+		if (I) {
+			node_cache.mark_dirty(N);
+			node_cache.mark_children_dirty(N, true);
+		}
 	}
+}
+
+void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
+	_update_if_clean();
+
+	_update_marking_list(marked);
+	_update_marking_list(p_marked);
+
 	marked = p_marked;
+
 	marked_selectable = p_selectable;
 	marked_children_selectable = p_children_selectable;
 	_update_tree();
@@ -1254,6 +1540,9 @@ void SceneTreeEditor::set_display_foreign_nodes(bool p_display) {
 
 void SceneTreeEditor::set_valid_types(const Vector<StringName> &p_valid) {
 	valid_types = p_valid;
+	node_cache.force_update = true;
+	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
+	tree_dirty = true;
 }
 
 void SceneTreeEditor::set_editor_selection(EditorSelection *p_selection) {
@@ -1286,7 +1575,9 @@ void SceneTreeEditor::_update_selection(TreeItem *item) {
 		if (item->is_selected(0)) {
 			TreeItem *previous_cursor_item = tree->get_selected();
 			item->deselect(0);
-			previous_cursor_item->set_as_cursor(0);
+			if (previous_cursor_item) {
+				previous_cursor_item->set_as_cursor(0);
+			}
 		}
 	}
 
@@ -1336,11 +1627,11 @@ void SceneTreeEditor::_cell_collapsed(Object *p_obj) {
 
 Variant SceneTreeEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
 	if (!can_rename) {
-		return Variant(); //not editable tree
+		return Variant(); // Not editable tree.
 	}
 
 	if (tree->get_button_id_at_position(p_point) != -1) {
-		return Variant(); //dragging from button
+		return Variant(); // Dragging from button.
 	}
 
 	Vector<Node *> selected_nodes;
@@ -1404,7 +1695,7 @@ bool SceneTreeEditor::_is_script_type(const StringName &p_type) const {
 
 bool SceneTreeEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
 	if (!can_rename) {
-		return false; //not editable tree
+		return false; // Not editable tree.
 	}
 
 	Dictionary d = p_data;
@@ -1426,7 +1717,7 @@ bool SceneTreeEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_d
 		Vector<String> files = d["files"];
 
 		if (files.size() == 0) {
-			return false; //weird
+			return false; // TODO Weird?
 		}
 
 		if (_is_script_type(EditorFileSystem::get_singleton()->get_file_type(files[0]))) {
@@ -1564,7 +1855,9 @@ void SceneTreeEditor::update_warning() {
 }
 
 void SceneTreeEditor::_warning_changed(Node *p_for_node) {
-	//should use a timer
+	node_cache.mark_dirty(p_for_node);
+
+	// Should use a timer.
 	update_timer->start();
 }
 
@@ -1585,6 +1878,11 @@ void SceneTreeEditor::set_connecting_signal(bool p_enable) {
 	update_tree();
 }
 
+void SceneTreeEditor::set_update_when_invisible(bool p_enable) {
+	update_when_invisible = p_enable;
+	update_tree();
+}
+
 void SceneTreeEditor::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_update_tree"), &SceneTreeEditor::_update_tree, DEFVAL(false)); // Still used by UndoRedo.
 
@@ -1604,7 +1902,8 @@ void SceneTreeEditor::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("open_script"));
 }
 
-SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_open_instance) {
+SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_open_instance) :
+		node_cache(this) {
 	selected = nullptr;
 
 	can_rename = p_can_rename;
@@ -1861,6 +2160,7 @@ SceneTreeDialog::SceneTreeDialog() {
 	filter_hbc->add_child(show_all_nodes);
 
 	tree = memnew(SceneTreeEditor(false, false, true));
+	tree->set_update_when_invisible(false);
 	tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	tree->get_scene_tree()->connect("item_activated", callable_mp(this, &SceneTreeDialog::_select));
 	// Initialize button state, must be done after the tree has been created to update its 'show_all_nodes' flag.
@@ -1875,3 +2175,151 @@ SceneTreeDialog::SceneTreeDialog() {
 
 SceneTreeDialog::~SceneTreeDialog() {
 }
+
+/******** CACHE *********/
+
+HashMap<Node *, SceneTreeEditor::CachedNode>::Iterator SceneTreeEditor::NodeCache::add(Node *p_node, TreeItem *p_item) {
+	if (!p_node) {
+		return HashMap<Node *, CachedNode>::Iterator();
+	}
+
+	return cache.insert(p_node, CachedNode(p_node, p_item));
+}
+
+HashMap<Node *, SceneTreeEditor::CachedNode>::Iterator SceneTreeEditor::NodeCache::get(Node *p_node, bool p_deleted_ok) {
+	if (!p_node) {
+		return HashMap<Node *, CachedNode>::Iterator();
+	}
+
+	HashMap<Node *, CachedNode>::Iterator I = cache.find(p_node);
+	if (I) {
+		if (I->value.delete_serial != UINT16_MAX) {
+			// Don't give us a node marked for deletion.
+			if (!p_deleted_ok) {
+				return HashMap<Node *, CachedNode>::Iterator();
+			}
+
+			to_delete.erase(&I->value);
+			I->value.delete_serial = UINT16_MAX;
+
+			// If we were resurrected from near-death we might have been renamed.
+			// Make sure that we are updated properly.
+			mark_dirty(p_node);
+			mark_children_dirty(p_node, true);
+		}
+	}
+
+	return I;
+}
+
+void SceneTreeEditor::NodeCache::remove(Node *p_node, bool p_recursive) {
+	if (!p_node) {
+		return;
+	}
+
+	if (p_node == editor->selected) {
+		editor->selected = nullptr;
+	}
+
+	editor->marked.erase(p_node);
+
+	HashMap<Node *, CachedNode>::Iterator I = cache.find(p_node);
+	if (I) {
+		if (p_recursive) {
+			int cc = p_node->get_child_count(false);
+
+			for (int i = 0; i < cc; i++) {
+				remove(p_node->get_child(i, false), p_recursive);
+			}
+		}
+
+		if (current_scene_node != p_node) {
+			// Do not remove from the Tree control here. See delete_pending below.
+			I->value.item->deselect(0);
+			I->value.delete_serial = delete_serial;
+			I->value.index = -1;
+			I->value.cache_iterator = I;
+			to_delete.insert(&I->value);
+		} else {
+			// If it is the root node, we leave the TreeItem and reuse it later.
+			cache.remove(I);
+		}
+	}
+}
+
+void SceneTreeEditor::NodeCache::mark_dirty(Node *p_node, bool p_parents) {
+	Node *node = p_node;
+	while (node) {
+		HashMap<Node *, CachedNode>::Iterator I = cache.find(node);
+		if (I) {
+			I->value.dirty = true;
+		}
+
+		if (!p_parents) {
+			break;
+		}
+		node = node->get_parent();
+	}
+}
+
+void SceneTreeEditor::NodeCache::mark_children_dirty(Node *p_node, bool p_recursive) {
+	if (!p_node) {
+		return;
+	}
+
+	int cc = p_node->get_child_count(false);
+	for (int i = 0; i < cc; i++) {
+		Node *c = p_node->get_child(i, false);
+		HashMap<Node *, CachedNode>::Iterator IC = cache.find(c);
+
+		if (IC) {
+			IC->value.dirty = true;
+
+			if (p_recursive) {
+				mark_children_dirty(c, p_recursive);
+			}
+		}
+	}
+}
+
+void SceneTreeEditor::NodeCache::delete_pending() {
+	HashSet<CachedNode *>::Iterator I = to_delete.begin();
+	while (I) {
+		// We want to keep TreeItems around just long enough for a Node removal,
+		// and immediate reinsertion. This is what happens with moves and
+		// type changes.
+		if (Math::abs((*I)->delete_serial - delete_serial) >= 2) {
+			memdelete((*I)->item);
+			cache.remove((*I)->cache_iterator);
+			to_delete.remove(I);
+		} else if (!(*I)->removed) {
+			// We don't remove from the tree until now because if the node got
+			// deleted from a @tool script the SceneTreeEditor might have had it
+			// marked or selected before the node was removed. If we immediately
+			// remove from the Tree control then we end up trying to scroll to an
+			// Item without a parent.
+			//
+			// We might already be removed (and thus not have a parent) by rapid
+			// undo/redo.
+			if (!(*I)->removed) {
+				TreeItem *parent = (*I)->item->get_parent();
+				parent->remove_child((*I)->item);
+			}
+			(*I)->removed = true;
+		}
+		++I;
+	}
+
+	++delete_serial;
+}
+
+void SceneTreeEditor::NodeCache::clear() {
+	for (CachedNode *E : to_delete) {
+		// Only removed entries won't be automatically cleaned up by Tree::clear().
+		if (E->removed) {
+			memdelete(E->item);
+		}
+	}
+	cache.clear();
+	to_delete.clear();
+}

+ 67 - 1
editor/gui/scene_tree_editor.h

@@ -58,6 +58,57 @@ class SceneTreeEditor : public Control {
 		BUTTON_UNIQUE = 9,
 	};
 
+	struct CachedNode {
+		Node *node = nullptr;
+		TreeItem *item = nullptr;
+		int index = -1;
+		bool dirty = true;
+		bool has_moved_children = false;
+		bool removed = false;
+
+		// Store the iterator for faster removal. This is safe as
+		// HashMap never moves elements.
+		HashMap<Node *, CachedNode>::Iterator cache_iterator;
+		// This is safe because it gets compared to a uint8_t.
+		uint16_t delete_serial = UINT16_MAX;
+
+		// To know whether to update children or not.
+		bool can_process = false;
+
+		CachedNode() = delete; // Always an error.
+		CachedNode(Node *p_node, TreeItem *p_item) :
+				node(p_node), item(p_item) {}
+	};
+
+	struct NodeCache {
+		~NodeCache() {
+			clear();
+		}
+
+		NodeCache(SceneTreeEditor *p_editor) :
+				editor(p_editor) {}
+
+		HashMap<Node *, CachedNode>::Iterator add(Node *p_node, TreeItem *p_item);
+		HashMap<Node *, CachedNode>::Iterator get(Node *p_node, bool p_deleted_ok = true);
+		void remove(Node *p_node, bool p_recursive = false);
+		void mark_dirty(Node *p_node, bool p_parents = true);
+		void mark_children_dirty(Node *p_node, bool p_recursive = false);
+
+		void delete_pending();
+		void clear();
+
+		SceneTreeEditor *editor;
+		HashMap<Node *, CachedNode> cache;
+		HashSet<CachedNode *> to_delete;
+		Node *current_scene_node = nullptr;
+		Node *current_pinned_node = nullptr;
+		bool current_has_pin = false;
+		bool force_update = false;
+		uint8_t delete_serial = 0;
+	};
+
+	NodeCache node_cache;
+
 	Tree *tree = nullptr;
 	Node *selected = nullptr;
 	ObjectID instance_node;
@@ -77,17 +128,30 @@ class SceneTreeEditor : public Control {
 	bool auto_expand_selected = true;
 	bool connect_to_script_mode = false;
 	bool connecting_signal = false;
+	bool update_when_invisible = true;
 
 	int blocked;
 
 	void _compute_hash(Node *p_node, uint64_t &hash);
+	void _reset();
+
+	void _update_node_path(Node *p_node, bool p_recursive = true);
+	void _update_node_subtree(Node *p_node, TreeItem *p_parent, bool p_force = false);
+	void _update_node(Node *p_node, TreeItem *p_item, bool p_part_of_subscene);
+	void _update_if_clean();
 
-	void _add_nodes(Node *p_node, TreeItem *p_parent);
 	void _test_update_tree();
 	bool _update_filter(TreeItem *p_parent = nullptr, bool p_scroll_to_selected = false);
 	bool _item_matches_all_terms(TreeItem *p_item, const PackedStringArray &p_terms);
 	void _tree_changed();
 	void _tree_process_mode_changed();
+
+	void _move_node_children(HashMap<Node *, CachedNode>::Iterator &p_I);
+	void _move_node_item(TreeItem *p_parent, HashMap<Node *, CachedNode>::Iterator &p_I);
+
+	void _node_child_order_changed(Node *p_node);
+	void _node_editor_state_changed(Node *p_node);
+	void _node_added(Node *p_node);
 	void _node_removed(Node *p_node);
 	void _node_renamed(Node *p_node);
 
@@ -142,6 +206,7 @@ class SceneTreeEditor : public Control {
 	void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);
 
 	void _warning_changed(Node *p_for_node);
+	void _update_marking_list(const HashSet<Node *> &p_marked);
 
 	Timer *update_timer = nullptr;
 
@@ -182,6 +247,7 @@ public:
 	void set_auto_expand_selected(bool p_auto, bool p_update_settings);
 	void set_connect_to_script_mode(bool p_enable);
 	void set_connecting_signal(bool p_enable);
+	void set_update_when_invisible(bool p_enable);
 
 	Tree *get_scene_tree() { return tree; }
 

+ 1 - 0
editor/reparent_dialog.cpp

@@ -76,6 +76,7 @@ ReparentDialog::ReparentDialog() {
 	add_child(vbc);
 
 	tree = memnew(SceneTreeEditor(false));
+	tree->set_update_when_invisible(false);
 	tree->set_show_enabled_subscene(true);
 	tree->get_scene_tree()->connect("item_activated", callable_mp(this, &ReparentDialog::_reparent));
 	vbc->add_margin_child(TTR("Select new parent:"), tree, true);

+ 71 - 38
editor/scene_tree_dock.cpp

@@ -78,17 +78,31 @@ void SceneTreeDock::_quick_open(const String &p_file_path) {
 	instantiate_scenes({ p_file_path }, scene_tree->get_selected());
 }
 
+static void _restore_treeitem_custom_color(TreeItem *p_item) {
+	if (!p_item) {
+		return;
+	}
+	Color custom_color = p_item->get_meta(SNAME("custom_color"), Color(0, 0, 0, 0));
+	if (custom_color != Color(0, 0, 0, 0)) {
+		p_item->set_custom_color(0, custom_color);
+	} else {
+		p_item->clear_custom_color(0);
+	}
+}
+
 void SceneTreeDock::_inspect_hovered_node() {
 	select_node_hovered_at_end_of_drag = true;
 	Tree *tree = scene_tree->get_scene_tree();
 	TreeItem *item = tree->get_item_with_metadata(node_hovered_now->get_path());
+
+	_restore_treeitem_custom_color(tree_item_inspected);
+	tree_item_inspected = item;
+
 	if (item) {
-		if (tree_item_inspected) {
-			tree_item_inspected->clear_custom_color(0);
-		}
-		tree_item_inspected = item;
-		tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
+		Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
+		tree_item_inspected->set_custom_color(0, accent_color);
 	}
+
 	EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
 	editor_history->add_object(node_hovered_now->get_instance_id());
 	InspectorDock::get_inspector_singleton()->edit(node_hovered_now);
@@ -1716,7 +1730,7 @@ void SceneTreeDock::_notification(int p_what) {
 		case NOTIFICATION_DRAG_END: {
 			_reset_hovering_timer();
 			if (tree_item_inspected) {
-				tree_item_inspected->clear_custom_color(0);
+				_restore_treeitem_custom_color(tree_item_inspected);
 				tree_item_inspected = nullptr;
 			} else {
 				return;
@@ -1963,6 +1977,49 @@ bool SceneTreeDock::_update_node_path(Node *p_root_node, NodePath &r_node_path,
 	return false;
 }
 
+_ALWAYS_INLINE_ static bool _recurse_into_property(const PropertyInfo &p_property) {
+	// Only check these types for NodePaths.
+	static const Variant::Type property_type_check[] = { Variant::OBJECT, Variant::NODE_PATH, Variant::ARRAY, Variant::DICTIONARY };
+
+	if (!(p_property.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
+		return false;
+	}
+
+	// Avoid otherwise acceptable types if we marked them as irrelevant.
+	if (p_property.hint == PROPERTY_HINT_NO_NODEPATH) {
+		return false;
+	}
+
+	for (Variant::Type type : property_type_check) {
+		if (p_property.type == type) {
+			return true;
+		}
+	}
+	return false;
+}
+
+void SceneTreeDock::_check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+
+	List<PropertyInfo> properties;
+	p_obj->get_property_list(&properties);
+
+	for (const PropertyInfo &E : properties) {
+		if (!_recurse_into_property(E)) {
+			continue;
+		}
+
+		StringName propertyname = E.name;
+
+		Variant old_variant = p_obj->get(propertyname);
+		Variant updated_variant = old_variant;
+		if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, p_inside_resource)) {
+			undo_redo->add_do_property(p_obj, propertyname, updated_variant);
+			undo_redo->add_undo_property(p_obj, propertyname, old_variant);
+		}
+	}
+}
+
 bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
 	switch (r_variant.get_type()) {
 		case Variant::NODE_PATH: {
@@ -2027,27 +2084,18 @@ bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_var
 				break;
 			}
 
+			if (Object::cast_to<Material>(resource)) {
+				// For performance reasons, assume that Materials don't have NodePaths in them.
+				// TODO This check could be removed when String performance has improved.
+				break;
+			}
+
 			if (!resource->is_built_in()) {
 				// For performance reasons, assume that scene paths are no concern for external resources.
 				break;
 			}
 
-			List<PropertyInfo> properties;
-			resource->get_property_list(&properties);
-
-			for (const PropertyInfo &E : properties) {
-				if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
-					continue;
-				}
-				String propertyname = E.name;
-				Variant old_variant = resource->get(propertyname);
-				Variant updated_variant = old_variant;
-				if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, true)) {
-					EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-					undo_redo->add_do_property(resource, propertyname, updated_variant);
-					undo_redo->add_undo_property(resource, propertyname, old_variant);
-				}
-			}
+			_check_object_properties_recursive(p_root_node, resource, p_renames, true);
 		} break;
 
 		default: {
@@ -2173,22 +2221,7 @@ void SceneTreeDock::perform_node_renames(Node *p_base, HashMap<Node *, NodePath>
 	}
 
 	// Renaming node paths used in node properties.
-	List<PropertyInfo> properties;
-	p_base->get_property_list(&properties);
-
-	for (const PropertyInfo &E : properties) {
-		if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
-			continue;
-		}
-		String propertyname = E.name;
-		Variant old_variant = p_base->get(propertyname);
-		Variant updated_variant = old_variant;
-		if (_check_node_path_recursive(p_base, updated_variant, p_renames)) {
-			EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-			undo_redo->add_do_property(p_base, propertyname, updated_variant);
-			undo_redo->add_undo_property(p_base, propertyname, old_variant);
-		}
-	}
+	_check_object_properties_recursive(p_base, p_base, p_renames);
 
 	for (int i = 0; i < p_base->get_child_count(); i++) {
 		perform_node_renames(p_base->get_child(i), p_renames, r_rem_anims);

+ 1 - 0
editor/scene_tree_dock.h

@@ -302,6 +302,7 @@ class SceneTreeDock : public VBoxContainer {
 	static void _update_configuration_warning();
 
 	bool _update_node_path(Node *p_root_node, NodePath &r_node_path, HashMap<Node *, NodePath> *p_renames) const;
+	void _check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
 	bool _check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
 	bool _check_node_recursive(Variant &r_variant, Node *p_node, Node *p_by_node, const String type_hint, String &r_warn_message);
 	void _replace_node(Node *p_node, Node *p_by_node, bool p_keep_properties = true, bool p_remove_old = true);

+ 14 - 1
scene/gui/tree.cpp

@@ -1258,6 +1258,18 @@ void TreeItem::deselect(int p_column) {
 	_cell_deselected(p_column);
 }
 
+void TreeItem::clear_buttons() {
+	int i = 0;
+	for (Cell &cell : cells) {
+		if (!cell.buttons.is_empty()) {
+			cell.buttons.clear();
+			cell.cached_minimum_size_dirty = true;
+			_changed_notify(i);
+		}
+		++i;
+	}
+}
+
 void TreeItem::add_button(int p_column, const Ref<Texture2D> &p_button, int p_id, bool p_disabled, const String &p_tooltip) {
 	ERR_FAIL_INDEX(p_column, cells.size());
 	ERR_FAIL_COND(!p_button.is_valid());
@@ -1768,6 +1780,7 @@ void TreeItem::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_custom_as_button", "column", "enable"), &TreeItem::set_custom_as_button);
 	ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button);
 
+	ClassDB::bind_method(D_METHOD("clear_buttons"), &TreeItem::clear_buttons);
 	ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_button_count", "column"), &TreeItem::get_button_count);
 	ClassDB::bind_method(D_METHOD("get_button_tooltip_text", "column", "button_index"), &TreeItem::get_button_tooltip_text);
@@ -3726,7 +3739,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 		_determine_hovered_item();
 
 		bool rtl = is_layout_rtl();
-		if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
+		if (pressing_for_editor && popup_pressing_edited_item && !popup_pressing_edited_item->cells.is_empty() && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
 			/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
 			popup_edited_item = popup_pressing_edited_item;
 			popup_edited_item_col = popup_pressing_edited_item_column;

+ 1 - 0
scene/gui/tree.h

@@ -277,6 +277,7 @@ public:
 	void set_icon_max_width(int p_column, int p_max);
 	int get_icon_max_width(int p_column) const;
 
+	void clear_buttons();
 	void add_button(int p_column, const Ref<Texture2D> &p_button, int p_id = -1, bool p_disabled = false, const String &p_tooltip = "");
 	int get_button_count(int p_column) const;
 	String get_button_tooltip_text(int p_column, int p_index) const;

+ 55 - 1
scene/main/node.cpp

@@ -678,6 +678,8 @@ void Node::set_process_mode(ProcessMode p_mode) {
 	if (Engine::get_singleton()->is_editor_hint()) {
 		get_tree()->emit_signal(SNAME("tree_process_mode_changed"));
 	}
+
+	_emit_editor_state_changed();
 #endif
 }
 
@@ -2163,6 +2165,7 @@ void Node::set_unique_name_in_owner(bool p_enabled) {
 	}
 
 	update_configuration_warnings();
+	_emit_editor_state_changed();
 }
 
 bool Node::is_unique_name_in_owner() const {
@@ -2200,6 +2203,8 @@ void Node::set_owner(Node *p_owner) {
 	if (data.unique_name_in_owner) {
 		_acquire_unique_name_in_owner();
 	}
+
+	_emit_editor_state_changed();
 }
 
 Node *Node::get_owner() const {
@@ -2383,6 +2388,9 @@ void Node::add_to_group(const StringName &p_identifier, bool p_persistent) {
 	gd.persistent = p_persistent;
 
 	data.grouped[p_identifier] = gd;
+	if (p_persistent) {
+		_emit_editor_state_changed();
+	}
 }
 
 void Node::remove_from_group(const StringName &p_identifier) {
@@ -2393,11 +2401,21 @@ void Node::remove_from_group(const StringName &p_identifier) {
 		return;
 	}
 
+#ifdef TOOLS_ENABLED
+	bool persistent = E->value.persistent;
+#endif
+
 	if (data.tree) {
 		data.tree->remove_from_group(E->key, this);
 	}
 
 	data.grouped.remove(E);
+
+#ifdef TOOLS_ENABLED
+	if (persistent) {
+		_emit_editor_state_changed();
+	}
+#endif
 }
 
 TypedArray<StringName> Node::_get_groups() const {
@@ -2560,6 +2578,7 @@ Ref<Tween> Node::create_tween() {
 void Node::set_scene_file_path(const String &p_scene_file_path) {
 	ERR_THREAD_GUARD
 	data.scene_file_path = p_scene_file_path;
+	_emit_editor_state_changed();
 }
 
 String Node::get_scene_file_path() const {
@@ -2592,6 +2611,8 @@ void Node::set_editable_instance(Node *p_node, bool p_editable) {
 	} else {
 		p_node->data.editable_instance = true;
 	}
+
+	p_node->_emit_editor_state_changed();
 }
 
 bool Node::is_editable_instance(const Node *p_node) const {
@@ -2702,6 +2723,7 @@ Ref<SceneState> Node::get_scene_instance_state() const {
 void Node::set_scene_inherited_state(const Ref<SceneState> &p_state) {
 	ERR_THREAD_GUARD
 	data.inherited_state = p_state;
+	_emit_editor_state_changed();
 }
 
 Ref<SceneState> Node::get_scene_inherited_state() const {
@@ -2950,6 +2972,14 @@ void Node::remap_nested_resources(Ref<Resource> p_resource, const HashMap<Ref<Re
 		}
 	}
 }
+
+void Node::_emit_editor_state_changed() {
+	// This is required for the SceneTreeEditor to properly keep track of when an update is needed.
+	// This signal might be expensive and not needed for anything outside of the editor.
+	if (Engine::get_singleton()->is_editor_hint()) {
+		emit_signal(SNAME("editor_state_changed"));
+	}
+}
 #endif
 
 // Duplicate node's properties.
@@ -3843,6 +3873,7 @@ void Node::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("child_order_changed"));
 	ADD_SIGNAL(MethodInfo("replacing_by", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
 	ADD_SIGNAL(MethodInfo("editor_description_changed", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
+	ADD_SIGNAL(MethodInfo("editor_state_changed"));
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_name", "get_name");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "unique_name_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_unique_name_in_owner", "is_unique_name_in_owner");
@@ -3966,11 +3997,13 @@ bool Node::has_meta(const StringName &p_name) const {
 void Node::set_meta(const StringName &p_name, const Variant &p_value) {
 	ERR_THREAD_GUARD;
 	Object::set_meta(p_name, p_value);
+	_emit_editor_state_changed();
 }
 
 void Node::remove_meta(const StringName &p_name) {
 	ERR_THREAD_GUARD;
 	Object::remove_meta(p_name);
+	_emit_editor_state_changed();
 }
 
 Variant Node::get_meta(const StringName &p_name, const Variant &p_default) const {
@@ -4020,12 +4053,33 @@ void Node::get_signals_connected_to_this(List<Connection> *p_connections) const
 
 Error Node::connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags) {
 	ERR_THREAD_GUARD_V(ERR_INVALID_PARAMETER);
-	return Object::connect(p_signal, p_callable, p_flags);
+
+	Error retval = Object::connect(p_signal, p_callable, p_flags);
+#ifdef TOOLS_ENABLED
+	if (p_flags & CONNECT_PERSIST) {
+		_emit_editor_state_changed();
+	}
+#endif
+
+	return retval;
 }
 
 void Node::disconnect(const StringName &p_signal, const Callable &p_callable) {
 	ERR_THREAD_GUARD;
+
+#ifdef TOOLS_ENABLED
+	// Already under thread guard, don't check again.
+	int old_connection_count = Object::get_persistent_signal_connection_count();
+#endif
+
 	Object::disconnect(p_signal, p_callable);
+
+#ifdef TOOLS_ENABLED
+	int new_connection_count = Object::get_persistent_signal_connection_count();
+	if (old_connection_count != new_connection_count) {
+		_emit_editor_state_changed();
+	}
+#endif
 }
 
 bool Node::is_connected(const StringName &p_signal, const Callable &p_callable) const {

+ 7 - 0
scene/main/node.h

@@ -331,6 +331,13 @@ private:
 	Variant _call_deferred_thread_group_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
 	Variant _call_thread_safe_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
 
+	// Editor only signal to keep the SceneTreeEditor in sync.
+#ifdef TOOLS_ENABLED
+	void _emit_editor_state_changed();
+#else
+	void _emit_editor_state_changed() {}
+#endif
+
 protected:
 	void _block() { data.blocked++; }
 	void _unblock() { data.blocked--; }

+ 4 - 4
scene/resources/mesh.cpp

@@ -1740,7 +1740,7 @@ void ArrayMesh::_get_property_list(List<PropertyInfo> *p_list) const {
 	}
 
 	for (int i = 0; i < surfaces.size(); i++) {
-		p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR));
+		p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_EDITOR));
 		if (surfaces[i].is_2d) {
 			p_list->push_back(PropertyInfo(Variant::OBJECT, "surface_" + itos(i) + "/material", PROPERTY_HINT_RESOURCE_TYPE, "CanvasItemMaterial,ShaderMaterial", PROPERTY_USAGE_EDITOR));
 		} else {
@@ -2308,10 +2308,10 @@ void ArrayMesh::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_set_surfaces", "surfaces"), &ArrayMesh::_set_surfaces);
 	ClassDB::bind_method(D_METHOD("_get_surfaces"), &ArrayMesh::_get_surfaces);
 
-	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
-	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
+	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_shape_mode", PROPERTY_HINT_ENUM, "Normalized,Relative"), "set_blend_shape_mode", "get_blend_shape_mode");
-	ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NONE, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
+	ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NO_NODEPATH, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shadow_mesh", PROPERTY_HINT_RESOURCE_TYPE, "ArrayMesh"), "set_shadow_mesh", "get_shadow_mesh");
 }