java-topology/defects/bullet/unit/BulletAlgorithm.java

313 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Object,Integer> 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<key,index> — 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<Integer> 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<Object,Integer> index — mirrors btHashMap fix. */
static int[] simulateGhostFast(int[] bodies) {
List<Integer> overlapping = new ArrayList<>();
Map<Integer, Integer> 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<Integer> 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<Integer> 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<Integer> exclusionList = new ArrayList<>();
Set<Integer> 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<Integer> 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<key, index>
List<Integer> pairArray = new ArrayList<>();
Map<Integer, Integer> 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<Integer> 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<Integer> fastArray = new ArrayList<>();
Map<Integer, Integer> 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);
}
}