Skip to content

Latest commit

 

History

History
236 lines (163 loc) · 7.75 KB

File metadata and controls

236 lines (163 loc) · 7.75 KB

btSequentialImpulseConstraintSolverMt Bug Report

Date: January 2025 Status: FIXED - Batching re-enabled (threshold=4) Severity: Resolved


Executive Summary

Parallel constraint solving via btSequentialImpulseConstraintSolverMt with lowered batching threshold was causing crashes. Multiple bugs were identified and fixed in the Bullet Physics batching code path.

Original Crash Symptoms (Now Fixed)

  • Exception Code: 0xC0000005 (Access Violation)
  • Pattern: All crashes at same RIP address
  • Bad addresses being read: 0x1, 0xA, 0x238, 0xFFFFFFFFFFFFFFFF, etc.
  • Root Cause: Stale batch data from previous frames being accessed

Changes Made

  1. btDiscreteDynamicsWorldMt.cpp:94 - Changed solver pool to create btSequentialImpulseConstraintSolverMt
  2. btSequentialImpulseConstraintSolverMt.cpp:26 - Lowered threshold from 250 to 4
  3. btSequentialImpulseConstraintSolverMt.cpp:838-849 - FIX: Clear batch structures at frame start
  4. btDiscreteDynamicsWorldMt.cpp:117-123 - FIX: RAII mutex guard for exception safety
  5. btBatchedConstraints.cpp:520-521 - FIX: NULL scheduler check in writeGrainSizes

Current Status

WORKING - Batching enabled with threshold=4. All identified bugs fixed. Tested successfully.


Bugs Already Fixed

FIX #1: Double Increment in Friction Loop

Location: btSequentialImpulseConstraintSolverMt.cpp:1043

Original (buggy):

for (int iFriction = iBegin; iFriction < iEnd; ++iFriction)
{
    btSolverConstraint& solveManifold = m_tmpSolverContactFrictionConstraintPool[iFriction++];

Fixed:

for (int iFriction = iBegin; iFriction < iEnd; ++iFriction)
{
    btSolverConstraint& solveManifold = m_tmpSolverContactFrictionConstraintPool[iFriction];

Impact: Every other friction constraint was being skipped, causing incorrect physics.


FIX #2: Variable Shadowing in Split Impulse

Location: btSequentialImpulseConstraintSolverMt.cpp:908

Original (buggy):

btScalar leastSquaresResidual = 0.f;  // Line 903
if (m_useBatching)
{
    // ...
    btScalar leastSquaresResidual = 0.f;  // Line 908 - SHADOWS OUTER!
    for (int iiPhase = 0; iiPhase < batchedCons.m_phases.size(); ++iiPhase)
    {
        leastSquaresResidual += btParallelSum(...);  // Updates inner only
    }
}
// ...
if (leastSquaresResidual <= threshold...)  // Uses outer (always 0)!

Fixed: Removed inner declaration at line 908.

Impact: Early termination check always saw 0, breaking convergence.


FIX #3: Stale Batch Data Between Frames (PRIMARY CRASH CAUSE)

Location: btSequentialImpulseConstraintSolverMt.cpp:838-849 Status: FIXED

Root Cause:

m_batchedContactConstraints and m_batchedJointConstraints contain arrays (m_constraintIndices, m_batches, m_phases) that persisted between frames without being cleared.

When batching state changed between frames (enabled one frame, disabled the next, or vice versa), stale indices from previous frames were accessed, causing wild pointer dereferences.

The garbage values (0x1, 0xA, 0x238) were stale constraint indices from previous frames.

Fix Applied:

// At start of solveGroupCacheFriendlySetup():
m_batchedContactConstraints.m_constraintIndices.resizeNoInitialize(0);
m_batchedContactConstraints.m_batches.resizeNoInitialize(0);
m_batchedContactConstraints.m_phases.resizeNoInitialize(0);
m_batchedContactConstraints.m_phaseGrainSize.resizeNoInitialize(0);
m_batchedContactConstraints.m_phaseOrder.resizeNoInitialize(0);
// Same for m_batchedJointConstraints

FIX #4: Mutex Not Released on Exception

Location: btDiscreteDynamicsWorldMt.cpp:117-140 Status: FIXED

Original (buggy):

ThreadSolver* ts = getAndLockThreadSolver();
ts->solver->solveGroup(...);  // If this throws...
ts->mutex.unlock();           // ...never called

Fix Applied: Added RAII mutex guard:

struct SpinMutexGuard {
    btSpinMutex& m_mutex;
    SpinMutexGuard(btSpinMutex& mutex) : m_mutex(mutex) {}
    ~SpinMutexGuard() { m_mutex.unlock(); }
};
// Usage:
SpinMutexGuard guard(ts->mutex);

FIX #5: NULL Task Scheduler in writeGrainSizes

Location: btBatchedConstraints.cpp:520-521 Status: FIXED

Original (buggy):

int numThreads = btGetTaskScheduler()->getNumThreads();  // Crashes if scheduler is NULL

Fix Applied:

btITaskScheduler* scheduler = btGetTaskScheduler();
int numThreads = scheduler ? scheduler->getNumThreads() : 1;  // Safe fallback

Impact: Crash in joint constraint batching when task scheduler not yet initialized or temporarily NULL.


Analysis Notes (Not Bugs)

Order Array Initialization - NOT A BUG

Initial analysis suggested m_orderTmpConstraintPool was initialized incorrectly. After deeper analysis:

  • Base class calls convertContacts() which is virtual
  • Mt version populates pools before returning
  • Order arrays are set up AFTER the virtual call completes
  • The order is correct; the real issue was stale batch data (FIX #3)

Rolling Friction Table - NOT A BUG

resizeNoInitialize() is followed by a parallel loop that sets EVERY index. The loop iterates over all manifolds and sets each contact's rolling friction index (either to a valid index or -1). No indices are left uninitialized.


Remaining Low-Priority Issues

Potential Integer Overflow in Grid Calculation

Severity: LOW Location: btBatchedConstraints.cpp:930-935

Code:

numGridChunks = gridChunkDim[0] * gridChunkDim[1] * gridChunkDim[2];
float nChunks = float(gridChunkDim[0]) * float(gridChunkDim[1]) * float(gridChunkDim[2]);
if (nChunks >= 8.0f * 8.0f * 8.0f)  // Overflow check using float

Problem:

Integer multiplication on line 933 could overflow BEFORE the float comparison check on subsequent lines.


Root Cause Analysis

The crash pattern (small garbage values 0x1, 0xA, 0x238 used as pointers) was caused by stale batch data (FIX #3):

  1. Lowering threshold to 4 enabled batching for small contact counts
  2. Batch structures (m_batchedContactConstraints) persisted between frames
  3. When batching state changed between frames, stale indices were accessed
  4. The garbage values were old constraint indices from previous frames
  5. These stale indices were used to index into constraint pools
  6. Dereference of pool[stale_index] caused access violation

The simultaneous crashes across multiple threads occurred because all threads were accessing the same stale batch data.


Testing Notes

To Verify Fix

  1. Threshold is now set to 4 (batching enabled)
  2. Test with 12+ physics actors in dense scene
  3. Profile with Tracy to verify batching is active
  4. Confirm no crashes over extended play session
  5. Compare performance with batching disabled (threshold=9999)

Files Modified

File Changes
btSequentialImpulseConstraintSolverMt.cpp Fixed double increment, variable shadowing, clear batch data at frame start, enabled batching (threshold=4)
btDiscreteDynamicsWorldMt.cpp Added Mt solver include, changed pool to use Mt solver, added RAII mutex guard
hdtSkinnedMeshWorld.cpp Added try-catch around solveGroup
main.cpp Added VEH crash handler for logging

Conclusion

The btSequentialImpulseConstraintSolverMt batching code had a critical bug where batch structures weren't cleared between frames. The Bullet Physics library's default threshold of 250 manifolds masked this because most use cases don't trigger the edge case of batching state changes between frames.

All critical bugs have been fixed. Batching is now enabled with threshold=4, allowing parallel constraint solving for hdtSMP64's cloth/hair physics.

Current Status: Batching ENABLED (threshold = 4), Mt solver active with parallel batched solving.