# CWE-407: 12 O(N²) algorithmic complexity defects — HashSet/HashMap shadow indexes ## Summary This PR fixes 12 algorithmic complexity defects (CWE-407) across the scene system, physics engines, A* pathfinding, skeletal animation, GLTF pipeline, font rendering, graph layout, and spring bone simulation. Every defect follows the same pattern: a `Vector::has()` or `Vector::find()` — O(n) linear scan — used as a membership or lookup test inside a loop that runs on every frame, every physics tick, or every import operation. The fix in every case is a HashSet or HashMap shadow index alongside the existing Vector, providing O(1) membership at the hot path while preserving the Vector for ordered iteration. These are sedimentary defects — written before O(1) data structures were idiomatic, invisible at small scene sizes, catastrophic at production scale. ## Defects Fixed ### redot-0001 — `scene/main/scene_tree.cpp` — **CRITICAL — 1,000× speedup** `SceneTree::add_to_group()` calls `nodes.has(p_node)` where `nodes` is `Vector`. This is a linear scan that fires on every `add_child()` / `enter_tree()` event. In a large dynamic scene with 2,000 nodes in a shared group: - Before: 1,999 + 1,998 + ... ≈ 2,000,000 comparisons to build the group - After: 2,000 comparisons **1,000× reduction. Fires every frame in games with dynamic spawning.** Fix: `HashSet node_set` shadow index in `struct Group`. `node_set.has()` O(1), `node_set.insert()` O(1), `node_set.erase()` O(1). Vector preserved for ordered iteration. ### redot-0002 — `modules/godot_physics_2d/godot_body_2d.h` — **HIGH — 50×** `GodotBody2D::add_area()` and `remove_area()` call `areas.find(AreaCMP(p_area))` — O(n) scan — from `GodotAreaPair2D::pre_solve()` every physics tick (60Hz) for every body-area overlap pair. At 500 bodies × 200 areas: ~10M comparisons/tick → ~200K comparisons/tick after fix. Fix: `HashMap area_index` maintained alongside `Vector areas`. ### redot-0003 — `modules/godot_physics_3d/godot_body_3d.h` — **HIGH — 50×** Identical to redot-0002, 3D physics variant. `GodotBody3D::add_area()` / `remove_area()`, fired from `GodotAreaPair3D::pre_solve()` every tick. ### redot-0004 — `modules/godot_physics_3d/godot_soft_body_3d.cpp` `generate_bending_constraints()` uses `node_links[ia].has(ib)` — `LocalVector` linear scan — inside the link-building loop. O(L × D) total for L links and avg degree D. Fix: `LocalVector> node_link_set` shadow per node. ### redot-0005 — `core/math/a_star.cpp` — A* decrease-key O(N) → O(1) `open_list.find(e)` — O(N) scan — called on every A* neighbor relaxation in the decrease-key path. Degrades A* from O(N log N) to O(N²) on large graphs. Fix: `open_index` field on `Point` struct, maintained on every push/pop. Also fixes `AStar2D` and `AStarGrid2D`. ### redot-0006 — `scene/3d/skeleton_3d.cpp` `_update_process_order()` calls `child_bones.has(i)` where `child_bones` is `Vector`. O(B²) over all B bones. Fires on every skeleton rebuild. Fix: change `child_bones` to `HashSet`. ### redot-0007 — `editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp` Two `Vector` instances used as sets: `bones_to_process` and `keep_bone_rest`. Both call `.has()` inside track loops. O(T × K) total at import time. Fix: both to `HashSet`. `push_back()` → `insert()`. ### redot-0008 — `modules/gltf/gltf_state.h` + `modules/gltf/gltf_document.cpp` `extensions_used` in `GLTFState` is `Vector`. Every node and animation track checks `extensions_used.has("...")` — O(E) — before pushing. Pattern repeats across `_serialize_nodes()`, `_serialize_animations()`, `_parse_extensions()`. Fix: `HashSet extensions_used`. `insert()` is idempotent and O(1). ### redot-0009 — `scene/resources/font.cpp` — `_is_cyclic()` `Font::_is_cyclic()` has no visited set. In a diamond fallback graph (shared fonts), each shared node is re-traversed once per path — O(F^D) total. Fix: `HashSet visited` guard threaded through `_is_cyclic_internal()`. ### redot-0010 — `scene/resources/font.cpp` — `_update_rids_fb()` `Font::_update_rids_fb()` traverses the fallback DAG without a visited set. Shared fonts in a diamond topology are re-traversed, bloating `rids[]` with duplicates. O(N²). Affects `Font`, `FontVariation`, `SystemFont`. Fix: `HashSet* r_visited` parameter. O(N). ### redot-0011 — `scene/gui/graph_edit_arranger.cpp` `ORDER` and `PRED` macros use `Vector::find()` — O(N) — inside loops over C connections. O(C × N) per layout invocation. Fix: pre-build `HashMap node_order` and `HashMap predecessor_map` — O(N) once. Look-up O(1). Total: O(N + C). ### redot-0012 — `scene/3d/spring_bone_simulator_3d.cpp` `_process_collisions()` calls `collisions.has(id)` and `collisions.find(id)` — both `LocalVector` O(N) — inside S settings × C collision paths loops. O(S × C × N) per physics tick. Fix: `HashSet collision_set` + `HashMap collision_index_map` built once at tick start. O(N + S×C). ## Data Structures Used All fixes use Redot's own `HashSet` and `HashMap` from `core/templates/`. No new dependencies introduced. No external libraries. Inline with existing codebase style. ## Testing Each fix preserves existing behavior: Vectors used for ordered iteration are unchanged. HashSet/HashMap shadow indexes are kept in sync on insert and remove. Suggested test coverage: - `test_scene_tree_group_large`: add 5,000 nodes to a group, verify timing <5s - `test_physics_area_overlap_large`: 500 bodies × 200 areas, verify tick time ratio <17.5× - `test_astar_large_grid`: 1,000-node grid, verify timing scales as N log N not N² - `test_skeleton_rebuild_large`: 500-bone skeleton rebuild timing ## Origin These defects are identical in Godot Engine upstream. They were inherited by Redot verbatim. This PR patches Redot first. A parallel PR to Godot is staged — they can follow our lead. Discovered and patched by [undefect.com](https://undefect.com) — CWE-407 campaign. Contact: security@undefect.com