Merge pull request #105728 from lawnjelly/fti_optimize_scene_tree

[3.x] FTI - Optimize `SceneTree` traversal
This commit is contained in:
lawnjelly
2025-05-26 06:14:29 +01:00
committed by GitHub
8 changed files with 711 additions and 45 deletions

View File

@ -1418,6 +1418,12 @@
Sets which physics engine to use for 3D physics.
"DEFAULT" is currently the [url=https://bulletphysics.org]Bullet[/url] physics engine. The "GodotPhysics" engine is still supported as an alternative.
</member>
<member name="physics/3d/physics_interpolation/scene_traversal" type="String" setter="" getter="" default="&quot;DEFAULT&quot;">
The approach used for 3D scene traversal when physics interpolation is enabled.
- [code]DEFAULT[/code]: The default optimized method.
- [code]Legacy[/code]: The previous reference method used for scene tree traversal, which is slower.
- [code]Debug[/code]: Swaps between [code]DEFAULT[/code] and [code]Legacy[/code] methods on alternating frames, and provides logging information (which in turn makes it slower). Intended for debugging only; you should use the [code]DEFAULT[/code] method in most cases.
</member>
<member name="physics/3d/smooth_trimesh_collision" type="bool" setter="" getter="" default="false">
If [code]true[/code], smooths out collision with trimesh shapes ([ConcavePolygonShape]) by telling the Bullet physics engine to generate internal edge information for every trimesh shape created.
[b]Note:[/b] Only effective if [member physics/3d/physics_engine] is set to [code]DEFAULT[/code] or [code]Bullet[/code], [i]not[/i] [code]GodotPhysics[/code].

View File

@ -285,6 +285,10 @@ void Spatial::_notification(int p_what) {
// unless they need to perform specific tasks (like changing process modes).
fti_pump_xform();
fti_pump_property();
// Detect whether we are using an identity transform.
// This is an optimization for faster tree transform concatenation.
data.fti_is_identity_xform = data.local_transform == Transform();
} break;
case NOTIFICATION_PAUSED: {
@ -1127,6 +1131,8 @@ Spatial::Spatial() :
data.fti_on_tick_property_list = false;
data.fti_global_xform_interp_set = false;
data.fti_frame_xform_force_update = false;
data.fti_is_identity_xform = false;
data.fti_processed = false;
data.merging_mode = MERGING_MODE_INHERIT;

View File

@ -53,6 +53,7 @@ class Spatial : public Node {
OBJ_CATEGORY("3D");
friend class SceneTreeFTI;
friend class SceneTreeFTITests;
public:
enum MergingMode : unsigned int {
@ -129,6 +130,8 @@ private:
bool fti_on_tick_property_list : 1;
bool fti_global_xform_interp_set : 1;
bool fti_frame_xform_force_update : 1;
bool fti_is_identity_xform : 1;
bool fti_processed : 1;
bool merging_allowed : 1;

View File

@ -111,8 +111,8 @@ private:
HashMap<StringName, Node *> owned_unique_nodes;
bool unique_name_in_owner = false;
int32_t depth;
int pos;
int depth;
int blocked; // safeguard that throws an error when attempting to modify the tree in a harmful way while being traversed.
StringName name;
SceneTree *tree;
@ -266,6 +266,7 @@ protected:
bool _is_physics_interpolation_reset_requested() const { return data.physics_interpolation_reset_requested; }
void _set_use_identity_transform(bool p_enable);
bool _is_using_identity_transform() const { return data.use_identity_transform; }
int32_t _get_scene_tree_depth() const { return data.depth; }
public:
enum {

View File

@ -38,10 +38,21 @@
#include "scene/3d/spatial.h"
#include "scene/3d/visual_instance.h"
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
#include "scene_tree_fti_tests.h"
#endif
#ifdef DEV_ENABLED
// Uncomment this to enable some slow extra DEV_ENABLED
// checks to ensure there aren't more than one object added to the lists.
// #define GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
// Uncomment this to regularly print the tree that is being interpolated.
// #define GODOT_SCENE_TREE_FTI_PRINT_TREE
#endif
void SceneTreeFTI::_reset_spatial_flags(Spatial &r_spatial) {
r_spatial.data.fti_on_tick_xform_list = false;
r_spatial.data.fti_on_tick_property_list = false;
@ -49,6 +60,7 @@ void SceneTreeFTI::_reset_spatial_flags(Spatial &r_spatial) {
r_spatial.data.fti_on_frame_property_list = false;
r_spatial.data.fti_global_xform_interp_set = false;
r_spatial.data.fti_frame_xform_force_update = false;
r_spatial.data.fti_processed = false;
}
void SceneTreeFTI::_reset_flags(Node *p_node) {
@ -103,18 +115,23 @@ void SceneTreeFTI::tick_update() {
// Needs a reset so jittering will stop.
s->fti_pump_xform();
// Optimization - detect whether we have rested at identity xform.
s->data.fti_is_identity_xform = s->data.local_transform == Transform();
// This may not get updated so set it to the same as global xform.
// TODO: double check this is the best value.
s->data.global_transform_interpolated = s->get_global_transform();
// Remove from interpolation list.
if (s->data.fti_on_frame_xform_list) {
s->data.fti_on_frame_xform_list = false;
_spatial_remove_from_frame_list(*s, false);
}
// Ensure that the spatial gets at least ONE further
// update in the resting position in the next frame update.
s->data.fti_frame_xform_force_update = true;
if (!s->data.fti_frame_xform_force_update) {
_spatial_add_to_frame_list(*s, true);
}
}
}
@ -230,16 +247,117 @@ void SceneTreeFTI::_spatial_notify_set_property(Spatial &r_spatial) {
}
}
void SceneTreeFTI::_create_depth_lists() {
uint32_t first_list = data.frame_start ? 0 : 1;
for (uint32_t l = first_list; l < 2; l++) {
LocalVector<Spatial *> &source_list = l == 0 ? data.frame_xform_list : data.frame_xform_list_forced;
#ifdef DEBUG_ENABLED
bool log_nodes_moved_on_frame = (data.traversal_mode == TM_DEBUG) && !data.frame_start && data.periodic_debug_log;
if (log_nodes_moved_on_frame) {
if (source_list.size()) {
print_line(String("\n") + itos(source_list.size()) + " nodes moved during frame:");
} else {
print_line("0 nodes moved during frame.");
}
}
#endif
for (uint32_t n = 0; n < source_list.size(); n++) {
Spatial *s = source_list[n];
s->data.fti_processed = false;
int32_t depth = s->_get_scene_tree_depth();
// This shouldn't happen, but wouldn't be terrible if it did.
DEV_ASSERT(depth >= 0);
depth = MIN(depth, (int32_t)data.scene_tree_depth_limit);
LocalVector<Spatial *> &dest_list = data.dirty_spatial_depth_lists[depth];
#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
// Shouldn't really happen, but duplicates don't really matter that much.
if (dest_list.find(s) != -1) {
ERR_FAIL_COND(dest_list.find(s) != -1);
}
#endif
#ifdef DEBUG_ENABLED
if (log_nodes_moved_on_frame) {
print_line("\t" + s->get_name());
}
#endif
if ((l == 0) && s->data.fti_frame_xform_force_update) {
continue;
}
dest_list.push_back(s);
}
}
}
void SceneTreeFTI::_clear_depth_lists() {
for (uint32_t d = 0; d < data.scene_tree_depth_limit; d++) {
data.dirty_spatial_depth_lists[d].clear();
}
}
void SceneTreeFTI::_spatial_add_to_frame_list(Spatial &r_spatial, bool p_forced) {
if (p_forced) {
DEV_ASSERT(!r_spatial.data.fti_frame_xform_force_update);
#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
int64_t found = data.frame_xform_list_forced.find(&r_spatial);
if (found != -1) {
ERR_FAIL_COND(found != -1);
}
#endif
data.frame_xform_list_forced.push_back(&r_spatial);
r_spatial.data.fti_frame_xform_force_update = true;
} else {
DEV_ASSERT(!r_spatial.data.fti_on_frame_xform_list);
#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
int64_t found = data.frame_xform_list.find(&r_spatial);
if (found != -1) {
ERR_FAIL_COND(found != -1);
}
#endif
data.frame_xform_list.push_back(&r_spatial);
r_spatial.data.fti_on_frame_xform_list = true;
}
}
void SceneTreeFTI::_spatial_remove_from_frame_list(Spatial &r_spatial, bool p_forced) {
if (p_forced) {
DEV_ASSERT(r_spatial.data.fti_frame_xform_force_update);
data.frame_xform_list_forced.erase_unordered(&r_spatial);
r_spatial.data.fti_frame_xform_force_update = false;
} else {
DEV_ASSERT(r_spatial.data.fti_on_frame_xform_list);
data.frame_xform_list.erase_unordered(&r_spatial);
r_spatial.data.fti_on_frame_xform_list = false;
}
}
void SceneTreeFTI::_spatial_notify_set_xform(Spatial &r_spatial) {
DEV_CHECK_ONCE(data.enabled);
if (!r_spatial.is_physics_interpolated()) {
// Force an update of non-interpolated to servers
// on the next traversal.
r_spatial.data.fti_frame_xform_force_update = true;
if (!r_spatial.data.fti_frame_xform_force_update) {
_spatial_add_to_frame_list(r_spatial, true);
}
// ToDo: Double check this is a win,
// non-interpolated nodes we always check for identity,
// *just in case*.
r_spatial.data.fti_is_identity_xform = r_spatial.get_transform() == Transform();
return;
}
r_spatial.data.fti_is_identity_xform = false;
if (!r_spatial.data.fti_on_tick_xform_list) {
r_spatial.data.fti_on_tick_xform_list = true;
@ -259,7 +377,15 @@ void SceneTreeFTI::_spatial_notify_set_xform(Spatial &r_spatial) {
}
if (!r_spatial.data.fti_on_frame_xform_list) {
r_spatial.data.fti_on_frame_xform_list = true;
_spatial_add_to_frame_list(r_spatial, false);
}
// If we are in the second half of a frame, always add to the force update list,
// because we ignore the tick update list during the second update.
if (data.in_frame) {
if (!r_spatial.data.fti_frame_xform_force_update) {
_spatial_add_to_frame_list(r_spatial, true);
}
}
}
@ -270,6 +396,14 @@ void SceneTreeFTI::spatial_notify_delete(Spatial *p_spatial) {
ERR_FAIL_NULL(p_spatial);
// Remove from frame lists.
if (p_spatial->data.fti_on_frame_xform_list) {
_spatial_remove_from_frame_list(*p_spatial, false);
}
if (p_spatial->data.fti_frame_xform_force_update) {
_spatial_remove_from_frame_list(*p_spatial, true);
}
// Ensure this is kept in sync with the lists, in case a node
// is removed and readded to the scene tree multiple times
// on the same frame / tick.
@ -301,12 +435,19 @@ void SceneTreeFTI::spatial_notify_delete(Spatial *p_spatial) {
DEV_CHECK_ONCE(data.frame_property_list.find(p_spatial) == -1);
DEV_CHECK_ONCE(data.request_reset_list.find(p_spatial) == -1);
DEV_CHECK_ONCE(data.frame_xform_list.find(p_spatial) == -1);
DEV_CHECK_ONCE(data.frame_xform_list_forced.find(p_spatial) == -1);
#endif
}
void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform, int p_depth) {
void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_half_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform, int p_depth) {
Spatial *s = Object::cast_to<Spatial>(p_node);
#ifdef DEBUG_ENABLED
data.debug_node_count++;
#endif
// Don't recurse into hidden branches.
if (s && !s->is_visible()) {
// NOTE : If we change from recursing entire tree, we should do an is_visible_in_tree()
@ -319,7 +460,7 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// so we should still recurse to children.
if (!s) {
for (int n = 0; n < p_node->get_child_count(); n++) {
_update_dirty_spatials(p_node->get_child(n), p_current_frame, p_interpolation_fraction, p_active, nullptr, p_depth + 1);
_update_dirty_spatials(p_node->get_child(n), p_current_half_frame, p_interpolation_fraction, p_active, nullptr, p_depth + 1);
}
return;
}
@ -346,10 +487,18 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// since the frame start.
if (s->data.dirty & Spatial::DIRTY_GLOBAL_INTERPOLATED) {
p_active = true;
#if 0
if (data.periodic_debug_log) {
print_line("activating on : " + s->get_name());
}
#endif
}
}
}
// ToDo : Check global_xform_interp is up to date for nodes
// that are not traversed by the depth lists.
if (data.frame_start) {
// Mark on the Spatial whether we have set global_transform_interp.
// This can later be used when calling `get_global_transform_interpolated()`
@ -358,15 +507,15 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
}
if (p_active) {
#if 0
bool dirty = s->data.dirty & Spatial::DIRTY_GLOBAL_INTERP;
#ifdef GODOT_SCENE_TREE_FTI_PRINT_TREE
bool dirty = s->data.dirty & Spatial::DIRTY_GLOBAL_INTERPOLATED;
if (data.debug) {
if (data.periodic_debug_log && !data.use_optimized_traversal_method && !data.frame_start) {
String sz;
for (int n = 0; n < p_depth; n++) {
sz += "\t";
}
print_line(sz + p_node->get_name() + (dirty ? " DIRTY" : ""));
print_line(sz + p_node->get_name() + (dirty ? " DIRTY" : "") + (s->get_transform() == Transform() ? "\t[IDENTITY]" : ""));
}
#endif
@ -374,9 +523,15 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// This will either use interpolation, or just use the current local if not interpolated.
Transform local_interp;
if (s->is_physics_interpolated()) {
// Make sure to call `get_transform()` rather than using local_transform directly, because
// local_transform may be dirty and need updating from rotation / scale.
TransformInterpolator::interpolate_transform(s->data.local_transform_prev, s->get_transform(), local_interp, p_interpolation_fraction);
// There may be no need to interpolate if the spatial has not been moved recently
// and is therefore not on the tick list...
if (s->data.fti_on_tick_xform_list) {
// Make sure to call `get_transform()` rather than using local_transform directly, because
// local_transform may be dirty and need updating from rotation / scale.
TransformInterpolator::interpolate_transform(s->data.local_transform_prev, s->get_transform(), local_interp, p_interpolation_fraction);
} else {
local_interp = s->get_transform();
}
} else {
local_interp = s->get_transform();
}
@ -384,13 +539,13 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// Concatenate parent xform.
if (!s->is_set_as_toplevel()) {
if (p_parent_global_xform) {
s->data.global_transform_interpolated = (*p_parent_global_xform) * local_interp;
s->data.global_transform_interpolated = s->data.fti_is_identity_xform ? *p_parent_global_xform : ((*p_parent_global_xform) * local_interp);
} else {
const Spatial *parent = s->get_parent_spatial();
if (parent) {
const Transform &parent_glob = parent->data.fti_global_xform_interp_set ? parent->data.global_transform_interpolated : parent->data.global_transform;
s->data.global_transform_interpolated = parent_glob * local_interp;
const Transform &parent_glob = parent->data.fti_global_xform_interp_set ? parent->data.global_transform_interpolated : parent->get_global_transform();
s->data.global_transform_interpolated = s->data.fti_is_identity_xform ? parent_glob : parent_glob * local_interp;
} else {
s->data.global_transform_interpolated = local_interp;
}
@ -413,6 +568,12 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// that have a deferred frame update.
s->data.fti_frame_xform_force_update = false;
// Ensure branches are only processed once on each traversal.
s->data.fti_processed = true;
#ifdef DEBUG_ENABLED
data.debug_nodes_processed++;
#endif
} // if active.
// Remove the dirty interp flag from EVERYTHING as we go.
@ -420,7 +581,7 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
// Recurse to children.
for (int n = 0; n < p_node->get_child_count(); n++) {
_update_dirty_spatials(p_node->get_child(n), p_current_frame, p_interpolation_fraction, p_active, s->data.fti_global_xform_interp_set ? &s->data.global_transform_interpolated : &s->data.global_transform, p_depth + 1);
_update_dirty_spatials(p_node->get_child(n), p_current_half_frame, p_interpolation_fraction, p_active, s->data.fti_global_xform_interp_set ? &s->data.global_transform_interpolated : &s->data.global_transform, p_depth + 1);
}
}
@ -429,37 +590,127 @@ void SceneTreeFTI::frame_update(Node *p_root, bool p_frame_start) {
return;
}
data.frame_start = p_frame_start;
data.in_frame = true;
_update_request_resets();
data.frame_start = p_frame_start;
float f = Engine::get_singleton()->get_physics_interpolation_fraction();
float interpolation_fraction = Engine::get_singleton()->get_physics_interpolation_fraction();
uint32_t frame = Engine::get_singleton()->get_frames_drawn();
// #define SCENE_TREE_FTI_TAKE_TIMINGS
#ifdef SCENE_TREE_FTI_TAKE_TIMINGS
uint64_t before = OS::get_singleton()->get_ticks_usec();
#endif
uint64_t before = 0;
#ifdef DEBUG_ENABLED
if (data.traversal_mode == TM_DEBUG) {
before = OS::get_singleton()->get_ticks_usec();
if (data.debug) {
if (p_frame_start && ((frame % ((60 * 15) - 3)) == 0)) {
data.periodic_debug_log = true;
}
}
#ifdef GODOT_SCENE_TREE_FTI_PRINT_TREE
if (data.periodic_debug_log) {
print_line(String("\nScene: ") + (data.frame_start ? "start" : "end") + "\n");
}
// Probably not the most optimal approach as we traverse the entire SceneTree
// but simple and foolproof.
// Can be optimized later.
_update_dirty_spatials(p_root, frame, f, false);
if (!p_frame_start && data.debug) {
data.debug = false;
}
#ifdef SCENE_TREE_FTI_TAKE_TIMINGS
uint64_t after = OS::get_singleton()->get_ticks_usec();
if ((Engine::get_singleton()->get_frames_drawn() % 60) == 0) {
print_line("Took " + itos(after - before) + " usec " + (data.frame_start ? "start" : "end"));
}
#endif
#endif
data.debug_node_count = 0;
data.debug_nodes_processed = 0;
uint32_t half_frame = p_frame_start ? (frame * 2) : ((frame * 2) + 1);
bool print_debug_stats = false;
switch (data.traversal_mode) {
case TM_LEGACY: {
data.use_optimized_traversal_method = false;
} break;
case TM_DEBUG: {
// Switch on alternate frames between the two methods.
data.use_optimized_traversal_method = (frame % 2) == 1;
// Odd number ensures we debug stats for both methods.
print_debug_stats = (frame % ((60 * 8) - 1)) == 0;
} break;
default: {
data.use_optimized_traversal_method = true;
} break;
}
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
_tests->frame_update(p_root, half_frame, interpolation_fraction);
#else
uint32_t skipped = 0;
if (!data.use_optimized_traversal_method) {
// Reference approach.
// Traverse the entire scene tree.
// Slow, but robust.
_update_dirty_spatials(p_root, half_frame, interpolation_fraction, false);
} else {
// Optimized approach.
// Traverse from depth lists.
// Be sure to check against the reference
// implementation when making changes.
_create_depth_lists();
for (uint32_t d = 0; d < data.scene_tree_depth_limit; d++) {
const LocalVector<Spatial *> &list = data.dirty_spatial_depth_lists[d];
#if 0
if (list.size() > 0) {
print_line("depth " + itos(d) + ", contains " + itos(list.size()));
}
#endif
for (uint32_t n = 0; n < list.size(); n++) {
// Already processed this frame?
Spatial *s = list[n];
if (s->data.fti_processed) {
#ifdef DEBUG_ENABLED
skipped++;
#endif
continue;
}
// The first node requires a recursive visibility check
// up the tree, because `is_visible()` only returns the node
// local flag.
if (Object::cast_to<VisualInstance>(s)) {
if (!s->_is_vi_visible()) {
#ifdef DEBUG_ENABLED
skipped++;
#endif
continue;
}
} else if (!s->is_visible_in_tree()) {
#ifdef DEBUG_ENABLED
skipped++;
#endif
continue;
}
_update_dirty_spatials(s, half_frame, interpolation_fraction, true);
}
}
_clear_depth_lists();
}
if (print_debug_stats) {
uint64_t after = OS::get_singleton()->get_ticks_usec();
print_line(String(data.use_optimized_traversal_method ? "FTI optimized" : "FTI reference") + " nodes traversed : " + itos(data.debug_node_count) + (skipped == 0 ? "" : ", skipped " + itos(skipped)) + ", processed : " + itos(data.debug_nodes_processed) + ", took " + itos(after - before) + " usec " + (data.frame_start ? "(start)" : "(end)"));
}
#endif // not GODOT_SCENE_TREE_FTI_VERIFY
data.frame_xform_list_forced.clear();
if (!p_frame_start && data.periodic_debug_log) {
data.periodic_debug_log = false;
}
// Update the properties once off at the end of the frame.
// No need for two passes for properties.
@ -469,6 +720,64 @@ void SceneTreeFTI::frame_update(Node *p_root, bool p_frame_start) {
s->fti_update_servers_property();
}
}
// Marks the end of the frame.
// Enables us to recognise when change notifications
// come in _during_ a frame (they get treated differently).
if (!data.frame_start) {
data.in_frame = false;
}
}
SceneTreeFTI::SceneTreeFTI() {
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
_tests = memnew(SceneTreeFTITests(*this));
#endif
Variant traversal_mode_string = GLOBAL_DEF("physics/3d/physics_interpolation/scene_traversal", "DEFAULT");
ProjectSettings::get_singleton()->set_custom_property_info("physics/3d/physics_interpolation/scene_traversal", PropertyInfo(Variant::STRING, "physics/3d/physics_interpolation/scene_traversal", PROPERTY_HINT_ENUM, "DEFAULT,Legacy,Debug"));
data.traversal_mode = TM_DEFAULT;
if (traversal_mode_string == "Legacy") {
data.traversal_mode = TM_LEGACY;
} else if (traversal_mode_string == "Debug") {
// Don't allow debug mode in final exports,
// it will almost certainly be a mistake.
#ifdef DEBUG_ENABLED
data.traversal_mode = TM_DEBUG;
#else
data.traversal_mode = TM_DEFAULT;
#endif
}
switch (data.traversal_mode) {
default: {
print_verbose("SceneTreeFTI: traversal method DEFAULT");
} break;
case TM_LEGACY: {
print_verbose("SceneTreeFTI: traversal method Legacy");
} break;
case TM_DEBUG: {
print_verbose("SceneTreeFTI: traversal method Debug");
} break;
}
#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
print_line("SceneTreeFTI : GODOT_SCENE_TREE_FTI_EXTRA_CHECKS defined");
#endif
#ifdef GODOT_SCENE_TREE_FTI_PRINT_TREE
print_line("SceneTreeFTI : GODOT_SCENE_TREE_FTI_PRINT_TREE defined");
#endif
}
SceneTreeFTI::~SceneTreeFTI() {
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
if (_tests) {
memfree(_tests);
_tests = nullptr;
}
#endif
}
#endif // ndef _3D_DISABLED

View File

@ -37,6 +37,12 @@
class Spatial;
class Node;
class Transform;
class SceneTreeFTITests;
#ifdef DEV_ENABLED
// Uncomment this to verify traversal method results.
// #define GODOT_SCENE_TREE_FTI_VERIFY
#endif
#ifdef _3D_DISABLED
// Stubs
@ -65,19 +71,37 @@ public:
// This class is not thread safe, but can be made thread safe easily with a mutex as in the 4.x version.
class SceneTreeFTI {
friend class SceneTreeFTITests;
enum TraversalMode : unsigned {
TM_DEFAULT,
TM_LEGACY,
TM_DEBUG,
};
struct Data {
static const uint32_t scene_tree_depth_limit = 32;
// Prev / Curr lists of spatials having local xforms pumped.
LocalVector<Spatial *> tick_xform_list[2];
// The frame lists are changed nodes that need to start traversal,
// either longterm (on the tick list) or single frame forced.
LocalVector<Spatial *> frame_xform_list;
LocalVector<Spatial *> frame_xform_list_forced;
// Prev / Curr lists of spatials having actively interpolated properties.
LocalVector<Spatial *> tick_property_list[2];
LocalVector<Spatial *> frame_property_list;
LocalVector<Spatial *> request_reset_list;
LocalVector<Spatial *> dirty_spatial_depth_lists[scene_tree_depth_limit];
// When we are using two alternating lists,
// which one is current.
uint32_t mirror = 0;
// Global on / off switch for SceneTreeFTI.
bool enabled = false;
// Whether we are in physics ticks, or in a frame.
@ -86,10 +110,21 @@ class SceneTreeFTI {
// Updating at the start of the frame, or the end on second pass.
bool frame_start = true;
bool debug = false;
TraversalMode traversal_mode = TM_DEFAULT;
bool use_optimized_traversal_method = true;
// DEBUGGING
bool periodic_debug_log = false;
uint32_t debug_node_count = 0;
uint32_t debug_nodes_processed = 0;
} data;
void _update_dirty_spatials(Node *p_node, uint32_t p_current_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform = nullptr, int p_depth = 0);
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
SceneTreeFTITests *_tests = nullptr;
#endif
void _update_dirty_spatials(Node *p_node, uint32_t p_current_half_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform = nullptr, int p_depth = 0);
void _update_request_resets();
void _reset_flags(Node *p_node);
@ -97,6 +132,12 @@ class SceneTreeFTI {
void _spatial_notify_set_xform(Spatial &r_spatial);
void _spatial_notify_set_property(Spatial &r_spatial);
void _spatial_add_to_frame_list(Spatial &r_spatial, bool p_forced);
void _spatial_remove_from_frame_list(Spatial &r_spatial, bool p_forced);
void _create_depth_lists();
void _clear_depth_lists();
public:
// Hottest function, allow inlining the data.enabled check.
void spatial_notify_changed(Spatial &r_spatial, bool p_transform_changed) {
@ -122,7 +163,10 @@ public:
void set_enabled(Node *p_root, bool p_enabled);
bool is_enabled() const { return data.enabled; }
void set_debug_next_frame() { data.debug = true; }
void set_debug_next_frame() { data.periodic_debug_log = true; }
SceneTreeFTI();
~SceneTreeFTI();
};
#endif // ndef _3D_DISABLED

View File

@ -0,0 +1,246 @@
/**************************************************************************/
/* scene_tree_fti_tests.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 _3D_DISABLED
#ifdef GODOT_SCENE_TREE_FTI_VERIFY
#include "scene_tree_fti_tests.h"
#include "scene/3d/spatial.h"
#include "scene/3d/visual_instance.h"
#include "scene/main/scene_tree_fti.h"
void SceneTreeFTITests::debug_verify_failed(const Spatial *p_spatial, const Transform &p_test) {
print_line("VERIFY FAILED\n");
print_line("test xform : " + String(Variant(p_test)));
bool first = true;
while (p_spatial) {
int32_t depth = MAX(p_spatial->_get_scene_tree_depth(), 0);
String tabs;
for (int32_t n = 0; n < depth; n++) {
tabs += "\t";
}
bool interp_equal = p_spatial->_get_cached_global_transform_interpolated() == p_test;
bool glob_equal = p_spatial->get_global_transform() == p_test;
String sz = tabs + p_spatial->get_name() + " [ " + p_spatial->get_class_name() + " ]\n";
if (first) {
sz += tabs + "... " + String(Variant(p_test)) + "\n";
}
sz += tabs + (p_spatial->data.fti_global_xform_interp_set ? "[I] " : "[i] ") + String(Variant(p_spatial->_get_cached_global_transform_interpolated())) + (interp_equal ? " ***" : "") + "\n";
sz += tabs + "[g] " + String(Variant(p_spatial->get_global_transform())) + (glob_equal ? " ***" : "");
print_line(sz);
p_spatial = p_spatial->get_parent_spatial();
first = false;
}
}
void SceneTreeFTITests::update_dirty_spatials(Node *p_node, uint32_t p_current_half_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform, int p_depth) {
SceneTreeFTI::Data &data = _fti.data;
// There are two runs going on here.
// FIRST the naive entire scene tree (reference), where we are
// setting state (i.e. writing out xforms, and other state)
// SECOND the optimized run, where we are NOT
// writing state, but only verifying that the xforms calculated
// match those from the reference approach.
bool should_verify = (data.traversal_mode == SceneTreeFTI::TM_DEBUG) && data.use_optimized_traversal_method;
bool set_state = !should_verify;
Spatial *s = Object::cast_to<Spatial>(p_node);
if (s && !s->is_visible()) {
return;
}
if (!s) {
for (int n = 0; n < p_node->get_child_count(); n++) {
update_dirty_spatials(p_node->get_child(n), p_current_half_frame, p_interpolation_fraction, p_active, nullptr, p_depth + 1);
}
return;
}
if (s->data.dirty & Spatial::DIRTY_GLOBAL) {
_ALLOW_DISCARD_ s->get_global_transform();
}
if (!p_active) {
if (data.frame_start) {
if (s->data.fti_on_frame_xform_list || s->data.fti_frame_xform_force_update) {
p_active = true;
}
} else {
if (s->data.dirty & Spatial::DIRTY_GLOBAL_INTERPOLATED) {
p_active = true;
}
}
}
if (data.frame_start) {
s->data.fti_global_xform_interp_set = p_active;
}
if (p_active) {
Transform local_interp;
if (s->is_physics_interpolated()) {
if (s->data.fti_on_tick_xform_list) {
TransformInterpolator::interpolate_transform(s->data.local_transform_prev, s->get_transform(), local_interp, p_interpolation_fraction);
} else {
local_interp = s->get_transform();
}
} else {
local_interp = s->get_transform();
}
if (!s->is_set_as_toplevel()) {
if (p_parent_global_xform) {
if (should_verify) {
Transform test = (*p_parent_global_xform) * local_interp;
if (s->data.disable_scale) {
test.basis.orthonormalize();
}
if (s->data.global_transform_interpolated != test) {
debug_verify_failed(s, test);
DEV_ASSERT(s->data.global_transform_interpolated == test);
}
} else {
s->data.global_transform_interpolated = s->data.fti_is_identity_xform ? (*p_parent_global_xform) : (*p_parent_global_xform) * local_interp;
}
} else {
const Spatial *parent = s->get_parent_spatial();
if (parent) {
const Transform &parent_glob = parent->data.fti_global_xform_interp_set ? parent->data.global_transform_interpolated : parent->get_global_transform();
if (should_verify) {
Transform test = parent_glob * local_interp;
if (s->data.disable_scale) {
test.basis.orthonormalize();
}
if (s->data.global_transform_interpolated != test) {
debug_verify_failed(s, test);
DEV_ASSERT(s->data.global_transform_interpolated == test);
}
} else {
s->data.global_transform_interpolated = s->data.fti_is_identity_xform ? parent_glob : parent_glob * local_interp;
}
} else {
if (set_state) {
s->data.global_transform_interpolated = local_interp;
}
}
}
} else {
if (set_state) {
s->data.global_transform_interpolated = local_interp;
}
}
if (set_state) {
if (s->data.disable_scale) {
s->data.global_transform_interpolated.basis.orthonormalize();
}
s->fti_update_servers_xform();
s->data.fti_frame_xform_force_update = false;
}
s->data.fti_processed = true;
} // if active.
if (set_state) {
s->data.dirty &= ~Spatial::DIRTY_GLOBAL_INTERPOLATED;
}
for (int n = 0; n < p_node->get_child_count(); n++) {
update_dirty_spatials(p_node->get_child(n), p_current_half_frame, p_interpolation_fraction, p_active, s->data.fti_global_xform_interp_set ? &s->data.global_transform_interpolated : &s->data.global_transform, p_depth + 1);
}
}
void SceneTreeFTITests::frame_update(Node *p_root, uint32_t p_half_frame, float p_interpolation_fraction) {
SceneTreeFTI::Data &data = _fti.data;
// For testing, use both methods.
// FIRST the entire tree, writing out state.
{
data.use_optimized_traversal_method = false;
update_dirty_spatials(p_root, p_half_frame, p_interpolation_fraction, false);
}
// SECOND the optimized depth lists only,
// no writing of state, and verifying results.
{
data.use_optimized_traversal_method = true;
_fti._create_depth_lists();
for (uint32_t d = 0; d < data.scene_tree_depth_limit; d++) {
const LocalVector<Spatial *> &list = data.dirty_spatial_depth_lists[d];
for (uint32_t n = 0; n < list.size(); n++) {
Spatial *s = list[n];
if (s->data.fti_processed) {
continue;
}
if (Object::cast_to<VisualInstance>(s)) {
if (!s->_is_vi_visible()) {
continue;
}
} else if (!s->is_visible_in_tree()) {
continue;
}
update_dirty_spatials(s, p_half_frame, p_interpolation_fraction, true);
}
}
_fti._clear_depth_lists();
}
}
SceneTreeFTITests::SceneTreeFTITests(SceneTreeFTI &p_fti) :
_fti(p_fti) {
print_line("SceneTreeFTI : GODOT_SCENE_TREE_FTI_VERIFY defined");
}
#endif // def GODOT_SCENE_TREE_FTI_VERIFY
#endif // ndef _3D_DISABLED

View File

@ -0,0 +1,51 @@
/**************************************************************************/
/* scene_tree_fti_tests.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef SCENE_TREE_FTI_TESTS_H
#define SCENE_TREE_FTI_TESTS_H
class Spatial;
class Node;
class Transform;
class SceneTreeFTI;
class SceneTreeFTITests {
SceneTreeFTI &_fti;
void debug_verify_failed(const Spatial *p_spatial, const Transform &p_test);
public:
void update_dirty_spatials(Node *p_node, uint32_t p_current_half_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform = nullptr, int p_depth = 0);
void frame_update(Node *p_root, uint32_t p_half_frame, float p_interpolation_fraction);
SceneTreeFTITests(SceneTreeFTI &p_fti);
};
#endif // SCENE_TREE_FTI_TESTS_H