package unit; import java.util.*; /** * BulletAlgorithm — unit tests for three Bullet Physics CWE-407 defects. * * bullet-0001: btGhostObject::addOverlappingObjectInternal / removeOverlappingObjectInternal * m_overlappingObjects.findLinearSearch(otherObject) called per broadphase pair per step. * Slow: ArrayList.contains / indexOf — O(N) per call → O(N²) over N overlapping objects. * Fast: HashMap index — O(1) per call. * * bullet-0002: btCollisionObject::checkCollideWithOverride * m_objectsWithoutCollisionCheck.findLinearSearch(co) inside processAllOverlappingPairs. * Slow: ArrayList.contains — O(E) per pair × M pairs → O(M·E). * Fast: HashSet.contains — O(1) per pair. * * bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair / findPair * m_overlappingPairArray.findLinearSearch(pair) per removal. * Slow: ArrayList.indexOf — O(P) per removal × P removals → O(P²). * Fast: HashMap — O(1) per removal. * * Each test: correctness assertion + performance ratio >= 5x at N=500-1000. * Prints: N/N PASS */ public class BulletAlgorithm { static int passed = 0; static int total = 0; static void pass(String name) { passed++; total++; System.out.println(" PASS " + name); } static void fail(String name, String reason) { total++; System.out.println(" FAIL " + name + " — " + reason); } // ----------------------------------------------------------------------- // bullet-0001: btGhostObject overlapping-object add/remove // ----------------------------------------------------------------------- /** Slow path: mirrors btGhostObject::addOverlappingObjectInternal (btAlignedObjectArray::findLinearSearch). */ static int[] simulateGhostSlow(int[] bodies) { List overlapping = new ArrayList<>(); long ops = 0; // add phase for (int b : bodies) { int idx = overlapping.indexOf(b); // O(N) scan ops += overlapping.size(); if (idx < 0) overlapping.add(b); } // remove phase for (int b : bodies) { int idx = overlapping.indexOf(b); // O(N) scan ops += overlapping.size(); if (idx >= 0) { overlapping.set(idx, overlapping.get(overlapping.size() - 1)); overlapping.remove(overlapping.size() - 1); } } return new int[]{overlapping.size(), (int)ops}; } /** Fast path: HashMap index — mirrors btHashMap fix. */ static int[] simulateGhostFast(int[] bodies) { List overlapping = new ArrayList<>(); Map index = new HashMap<>(); long ops = 0; // add phase for (int b : bodies) { ops++; if (!index.containsKey(b)) { index.put(b, overlapping.size()); overlapping.add(b); } } // remove phase for (int b : bodies) { ops++; Integer pos = index.remove(b); if (pos != null) { int last = overlapping.size() - 1; if (pos != last) { Integer moved = overlapping.get(last); overlapping.set(pos, moved); index.put(moved, pos); } overlapping.remove(last); } } return new int[]{overlapping.size(), (int)ops}; } static void testGhostOverlapping() { // Correctness: both paths produce same final state int N = 20; int[] bodies = new int[N]; for (int i = 0; i < N; i++) bodies[i] = i; // add duplicates to stress dedup int[] withDups = new int[N + 5]; System.arraycopy(bodies, 0, withDups, 0, N); withDups[N] = 3; withDups[N+1] = 7; withDups[N+2] = 11; withDups[N+3] = 1; withDups[N+4] = 0; int[] slowResult = simulateGhostSlow(withDups); int[] fastResult = simulateGhostFast(withDups); if (slowResult[0] != fastResult[0]) { fail("bullet-0001 correctness N=20+dups", "final size mismatch: slow=" + slowResult[0] + " fast=" + fastResult[0]); return; } pass("bullet-0001 correctness N=20+dups"); // Performance: measure op count ratio at N=500 int P = 500; int[] bigBodies = new int[P]; for (int i = 0; i < P; i++) bigBodies[i] = i; long slowOps = 0, fastOps = 0; for (int rep = 0; rep < 50; rep++) { slowOps += simulateGhostSlow(bigBodies)[1]; fastOps += simulateGhostFast(bigBodies)[1]; } double ratio = (double) slowOps / fastOps; System.out.printf(" bullet-0001 P=500 slow_ops=%,d fast_ops=%,d ratio=%.0fx%n", slowOps, fastOps, ratio); if (ratio < 5.0) { fail("bullet-0001 performance P=500", "ratio=" + ratio + " < 5x"); return; } pass("bullet-0001 performance ratio >= 5x at P=500"); } // ----------------------------------------------------------------------- // bullet-0002: btCollisionObject::checkCollideWithOverride // ----------------------------------------------------------------------- static long benchCheckCollideSlow(int M, int E) { // E exclusions, M pair checks List exclusions = new ArrayList<>(); for (int i = 0; i < E; i++) exclusions.add(i); long ops = 0; for (int p = 0; p < M; p++) { int candidate = p % (E + 5); // most misses ops += exclusions.size(); // count comparisons boolean found = exclusions.contains(candidate); if (found) ops -= (exclusions.size() - exclusions.indexOf(candidate) - 1); } return ops; } static long benchCheckCollideFast(int M, int E) { Set exclusions = new HashSet<>(); for (int i = 0; i < E; i++) exclusions.add(i); long ops = 0; for (int p = 0; p < M; p++) { int candidate = p % (E + 5); ops++; // O(1) hash lookup exclusions.contains(candidate); } return ops; } static void testCheckCollideWith() { // Correctness: same decisions for same inputs int M = 30, E = 8; List exclusionList = new ArrayList<>(); Set exclusionSet = new HashSet<>(); for (int i = 0; i < E; i++) { exclusionList.add(i); exclusionSet.add(i); } boolean mismatch = false; for (int p = 0; p < M; p++) { int candidate = p % (E + 5); boolean slow = !exclusionList.contains(candidate); boolean fast = !exclusionSet.contains(candidate); if (slow != fast) { mismatch = true; break; } } if (mismatch) { fail("bullet-0002 correctness M=30 E=8", "decision mismatch"); return; } pass("bullet-0002 correctness M=30 E=8"); // Performance: op-count ratio at M=1000 E=20 int bM = 1000, bE = 20; long slowOps = benchCheckCollideSlow(bM, bE); long fastOps = benchCheckCollideFast(bM, bE); double ratio = (double) slowOps / fastOps; System.out.printf(" bullet-0002 M=%d E=%d slow_ops=%,d fast_ops=%,d ratio=%.0fx%n", bM, bE, slowOps, fastOps, ratio); if (ratio < 5.0) { fail("bullet-0002 performance M=1000 E=20", "ratio=" + ratio + " < 5x"); return; } pass("bullet-0002 performance ratio >= 5x at M=1000 E=20"); } // ----------------------------------------------------------------------- // bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair // ----------------------------------------------------------------------- static long benchSortedCacheSlow(int P) { // P pairs, remove all — mirrors btSortedOverlappingPairCache::removeOverlappingPair List pairArray = new ArrayList<>(); for (int i = 0; i < P; i++) pairArray.add(i); long ops = 0; for (int i = 0; i < P; i++) { int key = i; // findLinearSearch: scan all remaining for (int j = 0; j < pairArray.size(); j++) { ops++; if (pairArray.get(j).equals(key)) { pairArray.set(j, pairArray.get(pairArray.size() - 1)); pairArray.remove(pairArray.size() - 1); break; } } } return ops; } static long benchSortedCacheFast(int P) { // P pairs, remove all — HashMap List pairArray = new ArrayList<>(); Map pairIndex = new HashMap<>(P * 2); for (int i = 0; i < P; i++) { pairArray.add(i); pairIndex.put(i, i); } long ops = 0; for (int i = 0; i < P; i++) { ops++; // O(1) map lookup Integer pos = pairIndex.remove(i); if (pos != null) { int last = pairArray.size() - 1; if (pos != last) { Integer moved = pairArray.get(last); pairArray.set(pos, moved); pairIndex.put(moved, pos); } pairArray.remove(last); } } return ops; } static void testSortedCacheRemove() { // Correctness: both produce empty array after removing all pairs int P = 20; List slowArray = new ArrayList<>(); for (int i = 0; i < P; i++) slowArray.add(i); for (int i = 0; i < P; i++) { int idx = slowArray.indexOf(i); if (idx >= 0) { slowArray.set(idx, slowArray.get(slowArray.size() - 1)); slowArray.remove(slowArray.size() - 1); } } List fastArray = new ArrayList<>(); Map fastIndex = new HashMap<>(); for (int i = 0; i < P; i++) { fastArray.add(i); fastIndex.put(i, i); } for (int i = 0; i < P; i++) { Integer pos = fastIndex.remove(i); if (pos != null) { int last = fastArray.size() - 1; if (pos != last) { Integer moved = fastArray.get(last); fastArray.set(pos, moved); fastIndex.put(moved, pos); } fastArray.remove(last); } } if (slowArray.size() != 0 || fastArray.size() != 0) { fail("bullet-0003 correctness P=20", "not empty: slow=" + slowArray.size() + " fast=" + fastArray.size()); return; } pass("bullet-0003 correctness P=20 (both empty after all removals)"); // Performance: op-count ratio at P=500 long slowOps = benchSortedCacheSlow(500); long fastOps = benchSortedCacheFast(500); double ratio = (double) slowOps / fastOps; System.out.printf(" bullet-0003 P=500 slow_ops=%,d fast_ops=%,d ratio=%.0fx%n", slowOps, fastOps, ratio); if (ratio < 5.0) { fail("bullet-0003 performance P=500", "ratio=" + ratio + " < 5x"); return; } pass("bullet-0003 performance ratio >= 5x at P=500"); } public static void main(String[] args) { System.out.println("Bullet Physics CWE-407 unit tests"); System.out.println("=".repeat(60)); System.out.println("\n[bullet-0001] btGhostObject::addOverlappingObjectInternal"); testGhostOverlapping(); System.out.println("\n[bullet-0002] btCollisionObject::checkCollideWithOverride"); testCheckCollideWith(); System.out.println("\n[bullet-0003] btSortedOverlappingPairCache::removeOverlappingPair"); testSortedCacheRemove(); System.out.println(); System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }