Hi dark_sylinc,
I’m running into a deterministic crash in ParticleFX2 that appears to be an engine-side bookkeeping bug, not script misuse. After extensive isolation and testing, this can be reproduced with valid and relatively simple particle definitions.
Observed crash
The crash consistently happens in:
Code: Select all
Ogre::ParticleSystemDef::deallocParticle()Code: Select all
Call stack (debug):
FastArray<unsigned __int64>::operator[]()
bitset64::findLastBitSetPlusOne()
ParticleSystemDef::deallocParticle()
ParticleSystemManager2::updateSerialPos()
ParticleSystemManager2::update()
At the time of crash:
Code: Select all
bitset64 internalArraySize = 3
mValues.size() = 2This indicates a capacity mismatch between bitset64 and its internal FastArray, leading to an out-of-bounds access.
Key observations:
- Happens after some runtime, not immediately
- Happens in debug and release
- Reproduced with:
- single emitter
- multiple emitters
- ColourInterpolator (no ColourFader)
- generous quota (180+)
- Still occurs even with constant emission rate and TTL
- Script complexity does not matter once runtime is long enough
This rules out script misuse, quota exhaustion, or affector misuse.
Root cause (analysis)
The issue appears to be triggered by non-FIFO particle deallocation, which can happen naturally when:
- multiple particles expire in the same frame
- frame delta fluctuates slightly
- TTL-based death occurs in batches
In ParticleSystemDef::deallocParticle(), this code path is reached:
Code: Select all
mLastParticleIdx =
static_cast<uint32>(
mActiveParticles.findLastBitSetPlusOne( mLastParticleIdx )
);
However, at this point:
- mLastParticleIdx can be greater than the bitset capacity
- findLastBitSetPlusOne(startFrom) then assumes more storage blocks than actually allocated
- This results in bitset64 believing it has more blocks than mValues actually contains
Hence:
Code: Select all
internalArraySize > mValues.size()-> out-of-bounds read -> crash
This is pure engine-side state drift, not something a particle script can prevent.
Minimal safe fix (suggested)
Clamping mLastParticleIdx to the bitset capacity before calling findLastBitSetPlusOne() fully resolves the crash in my tests.
Example fix:
Code: Select all
if( mActiveParticles.empty() )
{
mFirstParticleIdx = 0;
mLastParticleIdx = 0;
return;
}
const uint32 cap = static_cast<uint32>( mActiveParticles.capacity() );
if( mLastParticleIdx > cap )
mLastParticleIdx = cap;
const uint32 safeStart = std::min( mLastParticleIdx, cap );
mLastParticleIdx =
static_cast<uint32>(
mActiveParticles.findLastBitSetPlusOne( safeStart )
);
This:
- preserves behavior
- prevents invalid memory access
- has no measurable performance impact
- aligns with how wraparound should be handled safely
Conclusion
ParticleFX2 currently assumes FIFO-ish deallocation, but its own code explicitly supports non-FIFO paths — those paths are where the bookkeeping breaks.
This makes long-running particle systems inherently unstable without engine fixes.
Thanks for your time, and for Ogre-Next in general — this is a great system, just one sharp edge that’s easy to miss.
Best regards
Lax
