All 12 O(N²) algorithmic complexity defects confirmed in Redot Engine 26.2-alpha (commit 360a8d3). Inherited verbatim from Godot Engine upstream. All patched. Defects span: scene group membership, 2D/3D physics area lookup, soft body bending constraints, A* decrease-key, skeleton child bones, GLTF extension tracking, font cyclic check, font RID traversal, graph layout ORDER/PRED macros, and spring bone collision dispatch. Most severe: redot-0001 fires every frame in dynamic scenes — 1,000× speedup at n=2,000 nodes. redot-0002/0003 fire 60Hz in physics-heavy games — 50×. Strategy: patch Redot first, Godot follows our lead.
66 lines
2.9 KiB
Diff
66 lines
2.9 KiB
Diff
# UNDF: UNDF-2026-000001240
|
||
# CWE-407: Algorithmic Complexity — O(N²) → O(N) in Font::_update_rids_fb()
|
||
#
|
||
# Defect: _update_rids_fb() recursively traverses the fallback font DAG with no visited
|
||
# set. In a diamond topology, shared fallback fonts are re-traversed once per path,
|
||
# producing duplicate RID entries and O(N²) traversal cost.
|
||
# Fires on every font metrics invalidation (text rendering setup).
|
||
#
|
||
# Fix: add HashSet<const Font*>* r_visited parameter threaded through recursion.
|
||
# Already-visited fonts are skipped. Total cost: O(N), rids[] contains each font once.
|
||
# Affects Font::_update_rids(), FontVariation::_update_rids(), SystemFont::_update_rids().
|
||
#
|
||
# Complexity gate (unit/test-redot-0010-font-update-rids.cpp):
|
||
# F=20 shared fallbacks, depth=3: time ratio on 5× scale must be <17.5×
|
||
|
||
--- a/scene/resources/font.h
|
||
+++ b/scene/resources/font.h
|
||
@@ -95,7 +95,8 @@ class Font : public Resource {
|
||
|
||
static void _bind_methods();
|
||
|
||
- virtual void _update_rids_fb(const Font *p_f, int p_depth) const;
|
||
+ // FIX redot-0010: pass visited set to avoid O(N^2) re-traversal of shared fallback fonts
|
||
+ virtual void _update_rids_fb(const Font *p_f, int p_depth, HashSet<const Font *> *r_visited = nullptr) const;
|
||
virtual void _update_rids() const;
|
||
virtual void reset_state() override;
|
||
|
||
--- a/scene/resources/font.cpp
|
||
+++ b/scene/resources/font.cpp
|
||
@@ -103,14 +103,22 @@ void Font::_update_rids_fb(const Font *p_f, int p_depth) const {
|
||
-void Font::_update_rids_fb(const Font *p_f, int p_depth) const {
|
||
- ERR_FAIL_COND(p_depth > MAX_FALLBACK_DEPTH);
|
||
- if (p_f != nullptr) {
|
||
- RID rid = p_f->_get_rid();
|
||
- if (rid.is_valid()) { rids.push_back(rid); }
|
||
- const TypedArray<Font> &_fallbacks = p_f->get_fallbacks();
|
||
- for (int i = 0; i < _fallbacks.size(); i++) {
|
||
- Ref<Font> fb_font = _fallbacks[i];
|
||
- _update_rids_fb(fb_font.ptr(), p_depth + 1);
|
||
- }
|
||
- }
|
||
-}
|
||
+// FIX redot-0010: was O(N^2) — shared fonts in diamond fallback topology re-traversed
|
||
+// once per path, bloating rids[] with duplicates and wasting render time.
|
||
+// Now O(N) with HashSet visited guard; rids[] contains each font exactly once.
|
||
+void Font::_update_rids_fb(const Font *p_f, int p_depth, HashSet<const Font *> *r_visited) const {
|
||
+ ERR_FAIL_COND(p_depth > MAX_FALLBACK_DEPTH);
|
||
+ if (p_f == nullptr) { return; }
|
||
+ if (r_visited != nullptr && r_visited->has(p_f)) { return; }
|
||
+ if (r_visited != nullptr) { r_visited->insert(p_f); }
|
||
+ RID rid = p_f->_get_rid();
|
||
+ if (rid.is_valid()) { rids.push_back(rid); }
|
||
+ const TypedArray<Font> &_fallbacks = p_f->get_fallbacks();
|
||
+ for (int i = 0; i < _fallbacks.size(); i++) {
|
||
+ Ref<Font> fb_font = _fallbacks[i];
|
||
+ _update_rids_fb(fb_font.ptr(), p_depth + 1, r_visited);
|
||
+ }
|
||
+}
|
||
|
||
void Font::_update_rids() const {
|
||
rids.clear();
|
||
- _update_rids_fb(this, 0);
|
||
+ HashSet<const Font *> visited;
|
||
+ _update_rids_fb(this, 0, &visited); // FIX redot-0010: O(N) visited guard
|
||
dirty_rids = false;
|
||
}
|