java-topology/docs/tickets/bullet-0001-ghostobject-overlapping-linear-scan.md

2.9 KiB

bullet-0001: btGhostObject — O(N²) findLinearSearch in broadphase per-frame callback

Severity: HIGH File: src/BulletCollision/CollisionDispatch/btGhostObject.cpp Lines: 37, 49, 75, 90 Status: PATCHED

Description

btGhostObject maintains a list of overlapping collision objects in m_overlappingObjects (btAlignedObjectArray<btCollisionObject*>). The add and remove callbacks — addOverlappingObjectInternal and removeOverlappingObjectInternal — use findLinearSearch (O(N) sequential scan) to check membership before insert/remove.

These callbacks are invoked by btGhostPairCallback::addOverlappingPair / removeOverlappingPair, which are called every broadphase frame for every AABB pair involving a ghost object. In a scene with a ghost region and P dynamic bodies overlapping it, every simulation step calls findLinearSearch once per pair: O(P²) total per step.

The comment in the source acknowledges the defect:

// btGhostObject.cpp:36
///if this linearSearch becomes too slow (too many overlapping objects)
///we should add a more appropriate data structure
int index = m_overlappingObjects.findLinearSearch(otherObject);

Root Cause

btAlignedObjectArray::findLinearSearch is a plain for loop over the array (btAlignedObjectArray.h:438-452). No hash structure is used for the m_overlappingObjects membership check.

int findLinearSearch(const T& key) const {
    int index = size();
    for (int i = 0; i < size(); i++) {
        if (m_data[i] == key) { index = i; break; }
    }
    return index;
}

Fix

Replace m_overlappingObjects (array) with a pair of structures:

  • btAlignedObjectArray<btCollisionObject*> for ordered iteration (used in convexSweepTest, rayTest)
  • btHashMap<btHashPtr, int> (Bullet's own hash map) or a std::unordered_set<btCollisionObject*> for O(1) membership

Quick fix: use a parallel btHashMap<btHashPtr, bool> m_overlappingSet for the contain-check:

// add
if (!m_overlappingSet.find(btHashPtr(otherObject))) {
    m_overlappingObjects.push_back(otherObject);
    m_overlappingSet.insert(btHashPtr(otherObject), true);
}
// remove
if (m_overlappingSet.find(btHashPtr(otherObject))) {
    int index = ... // O(1) via reverse index or search once
    m_overlappingObjects[index] = m_overlappingObjects.back();
    m_overlappingObjects.pop_back();
    m_overlappingSet.remove(btHashPtr(otherObject));
}

Speedup

Overlapping objects (P) Before (per step) After (per step)
10 ~100 ops ~10 ops
100 ~10 000 ops ~100 ops
500 ~250 000 ops ~500 ops

Estimated ~100x speedup at P=100 overlapping objects with a ghost sensor region (character controller, trigger volume).