java-topology/defects/bullet3/patch/bullet3-0001-ghost-object-overlapping-hashset.md

5.1 KiB
Raw Blame History

UNDF: UNDF-2026-000000603

bullet3-0001: btGhostObject::addOverlappingObjectInternal — O(N²) linear dedup

CWE-407 — Algorithmic Complexity

Field Value
ID bullet3-0001
Severity HIGH
Ecosystem bullet3
Package BulletCollision/CollisionDispatch
File src/BulletCollision/CollisionDispatch/btGhostObject.cpp
Lines 3742, 4954, 7580, 9095
Complexity O(N²) total per broadphase step
Hot path per broadphase overlap pair update (every physics step)

Background

btGhostObject is used for trigger volumes and character controllers (btKinematicCharacterController uses btPairCachingGhostObject). Every physics step the broadphase calls addOverlappingObjectInternal and removeOverlappingObjectInternal for each new/removed pair that involves the ghost object.

Defect

m_overlappingObjects is a btAlignedObjectArray<btCollisionObject*> (a plain dynamic array). Both add and remove call findLinearSearch which scans the entire array to find duplicates or locate the element to remove.

// btGhostObject.cpp line 37 — the code itself contains the admission:
///if this linearSearch becomes too slow (too many overlapping objects) we should add
///a more appropriate data structure
int index = m_overlappingObjects.findLinearSearch(otherObject);
if (index == m_overlappingObjects.size())
{
    //not found
    m_overlappingObjects.push_back(otherObject);
}

The same pattern occurs in btPairCachingGhostObject::addOverlappingObjectInternal (line 75) and both removeOverlappingObjectInternal variants (lines 49, 90).

With N objects overlapping the ghost, each broadphase update requires O(N) work per new pair. In a dense scene where G ghost objects each overlap N bodies, the total cost per step is O(G × N²) in the worst case (N re-insertions checked against an N-element list).

Fix

Replace m_overlappingObjects with a btHashSet-backed structure. Since btCollisionObject* pointers are unique, pointer value works as the hash key.

Option A — drop-in O(1) membership check using btHashMap as a set:

// btGhostObject.h — add alongside m_overlappingObjects
#include "LinearMath/btHashMap.h"

btAlignedObjectArray<btCollisionObject*> m_overlappingObjects;
btHashMap<btHashPtr, int> m_overlappingIndex;   // ptr → index in array

// addOverlappingObjectInternal
void btGhostObject::addOverlappingObjectInternal(btBroadphaseProxy* otherProxy,
                                                  btBroadphaseProxy* thisProxy)
{
    btCollisionObject* otherObject = (btCollisionObject*)otherProxy->m_clientObject;
    btAssert(otherObject);
    btHashPtr key(otherObject);
    if (m_overlappingIndex.find(key) == nullptr)
    {
        int idx = m_overlappingObjects.size();
        m_overlappingObjects.push_back(otherObject);
        m_overlappingIndex.insert(key, idx);
    }
}

// removeOverlappingObjectInternal
void btGhostObject::removeOverlappingObjectInternal(btBroadphaseProxy* otherProxy,
                                                     btDispatcher* dispatcher,
                                                     btBroadphaseProxy* thisProxy)
{
    btCollisionObject* otherObject = (btCollisionObject*)otherProxy->m_clientObject;
    btAssert(otherObject);
    btHashPtr key(otherObject);
    const int* pIdx = m_overlappingIndex.find(key);
    if (pIdx)
    {
        int index = *pIdx;
        // swap-erase: move last element into freed slot
        int lastIdx = m_overlappingObjects.size() - 1;
        if (index != lastIdx)
        {
            btCollisionObject* last = m_overlappingObjects[lastIdx];
            m_overlappingObjects[index] = last;
            m_overlappingIndex.insert(btHashPtr(last), index);
        }
        m_overlappingObjects.pop_back();
        m_overlappingIndex.remove(key);
    }
}

Option B (minimal change) — keep existing array, guard entry using btHashSet:

// Use btHashMap<btHashPtr, bool> as a membership guard alongside the existing
// array to preserve the indexed getOverlappingObject(int) API.

The library already provides btHashedOverlappingPairCache as a hash-based alternative to btSortedOverlappingPairCache, so the pattern is established.

Speedup

N (overlapping objects per ghost) Before (ops) After (ops) Speedup
10 100 10 10×
50 2,500 50 50×
100 10,000 100 100×
500 250,000 500 500×

Character controllers with large trigger volumes (area-of-effect spells, AI perception spheres) in complex scenes are the primary beneficiaries.

Note

b3SortedOverlappingPairCache::findPair in src/Bullet3Collision/BroadPhaseCollision/b3OverlappingPairCache.cpp carries the same pattern with an even more explicit comment:

///this findPair becomes really slow. Either sort the list to speedup the query,
///or use a different solution.
b3BroadphasePair* b3SortedOverlappingPairCache::findPair(int proxy0, int proxy1)
{
    int findIndex = m_overlappingPairArray.findLinearSearch(tmpPair);

This is the Bullet3 (GPU) broadphase equivalent of the same defect.