diff --git a/simgear/scene/model/particles.cxx b/simgear/scene/model/particles.cxx index e8f2baca..bcd70d26 100644 --- a/simgear/scene/model/particles.cxx +++ b/simgear/scene/model/particles.cxx @@ -35,10 +35,10 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -47,67 +47,241 @@ #include +using ParticleSystemRef = osg::ref_ptr; + + namespace simgear { class ParticlesGlobalManager::ParticlesGlobalManagerPrivate : public osg::NodeCallback { public: - ParticlesGlobalManagerPrivate() : _updater(new osgParticle::ParticleSystemUpdater), - _commonGeode(new osg::Geode) + ParticlesGlobalManagerPrivate(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + + // only call this with the lock held! + osg::Group* internalGetCommonRoot(); + + void updateParticleSystemsFromCullCallback(int currentFrameNumber, osg::NodeVisitor* nv); + + void addParticleSystem(osgParticle::ParticleSystem* ps); + + void registerNewLocalParticleSystem(osg::Node* node, ParticleSystemRef ps); + void registerNewWorldParticleSystem(osg::Node* node, ParticleSystemRef ps, osg::Group* frame); + + std::mutex _lock; + bool _frozen = false; + double _simulationDt = 0.0; + osg::ref_ptr _commonRoot; + osg::ref_ptr _commonGeode; + osg::Vec3 _wind; + bool _enabled = true; + osg::Vec3 _gravity; + // osg::Vec3 _localWind; + SGGeod _currentPosition; + + SGConstPropertyNode_ptr _enabledNode; + + using ParticleSystemWeakRef = osg::observer_ptr; + using ParticleSystemsWeakRefVec = std::vector; + + using ParticleSystemsStrongRefVec = std::vector; + + ParticleSystemsWeakRefVec _systems; + osg::ref_ptr _cullCallback; +}; + +/** + @brief this class replaces the need to use osgParticle::ParticleSystemUpdater, which has some + thread-safety and ownership complications in our us case + */ +class ParticlesGlobalManager::UpdaterCallback : public osg::NodeCallback +{ +public: + UpdaterCallback(ParticlesGlobalManagerPrivate* p) : _manager(p) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { - std::lock_guard g(_lock); - _enabled = !_enabledNode || _enabledNode->getBoolValue(); - - if (!_enabled) - return; - - const auto q = SGQuatd::fromLonLatDeg(_longitudeNode->getFloatValue(), _latitudeNode->getFloatValue()); - osg::Matrix om(toOsg(q)); - osg::Vec3 v(0, 0, 9.81); - - _gravity = om.preMult(v); - - // NOTE: THIS WIND COMPUTATION DOESN'T SEEM TO AFFECT PARTICLES - // const osg::Vec3& zUpWind = _wind; - // osg::Vec3 w(zUpWind.y(), zUpWind.x(), -zUpWind.z()); - // _localWind = om.preMult(w); - } - - // only call this with the lock held! - osg::Group* internalGetCommonRoot() - { - if (!_commonRoot.valid()) { - SG_LOG(SG_PARTICLES, SG_DEBUG, "Particle common root called."); - _commonRoot = new osg::Group; - _commonRoot->setName("common particle system root"); - _commonGeode->setName("common particle system geode"); - _commonRoot->addChild(_commonGeode); - _commonRoot->addChild(_updater); - _commonRoot->setNodeMask(~simgear::MODELLIGHT_BIT); + osgUtil::CullVisitor* cv = dynamic_cast(nv); + if (cv && nv->getFrameStamp()) { + if (_frameNumber < nv->getFrameStamp()->getFrameNumber()) { + _frameNumber = nv->getFrameStamp()->getFrameNumber(); + _manager->updateParticleSystemsFromCullCallback(_frameNumber, nv); + } } - return _commonRoot.get(); } - std::mutex _lock; - bool _frozen = false; - osg::ref_ptr _updater; - osg::ref_ptr _commonRoot; - osg::ref_ptr _commonGeode; - osg::Vec3 _wind; - bool _globalCallbackRegistered = false; - bool _enabled = true; - osg::Vec3 _gravity; - // osg::Vec3 _localWind; - - SGConstPropertyNode_ptr _enabledNode; - SGConstPropertyNode_ptr _longitudeNode, _latitudeNode; + unsigned int _frameNumber = 0; + ParticlesGlobalManagerPrivate* _manager; }; +/** + single-shot node callback, used to register a particle system with the global manager. Once run, removes + itself. We used this to avoid updating a particle system until the load is complete and merged into the + main scene. + */ +class ParticlesGlobalManager::RegistrationCallback : public osg::NodeCallback +{ +public: + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + auto d = ParticlesGlobalManager::instance()->d; + d->addParticleSystem(_system); + + // for world-attached particle systems, attach the frame to the + // common root. This is the first safe place we can do this + if (_frame) { + d->internalGetCommonRoot()->addChild(_frame); + } + + node->removeUpdateCallback(this); // suicide + } + + ParticleSystemRef _system; + osg::ref_ptr _frame; +}; + +ParticlesGlobalManager::ParticlesGlobalManagerPrivate::ParticlesGlobalManagerPrivate() : _commonGeode(new osg::Geode), + _cullCallback(new UpdaterCallback(this)) +{ + // callbacks are registered in initFromMainThread : depending on timing, + // this constructor might be called from an osgDB thread +} + +void ParticlesGlobalManager::ParticlesGlobalManagerPrivate::addParticleSystem(osgParticle::ParticleSystem* ps) +{ + std::lock_guard g(_lock); + _systems.push_back(ps); +} + +void ParticlesGlobalManager::ParticlesGlobalManagerPrivate::registerNewLocalParticleSystem(osg::Node* node, ParticleSystemRef ps) +{ + auto cb = new RegistrationCallback; + cb->_system = ps; + node->addUpdateCallback(cb); +} + +void ParticlesGlobalManager::ParticlesGlobalManagerPrivate::registerNewWorldParticleSystem(osg::Node* node, ParticleSystemRef ps, osg::Group* frame) +{ + auto cb = new RegistrationCallback; + cb->_system = ps; + cb->_frame = frame; + node->addUpdateCallback(cb); +} + +// this is called from the main thread during scenery init +// after this, we beign updarting particle systems +void ParticlesGlobalManager::initFromMainThread() +{ + std::lock_guard g(d->_lock); + d->internalGetCommonRoot(); + d->_commonRoot->addUpdateCallback(d.get()); + d->_commonRoot->addCullCallback(d->_cullCallback); +} + +void ParticlesGlobalManager::setCurrentPosition(const SGGeod& pos) +{ + std::lock_guard g(d->_lock); + d->_currentPosition = pos; +} + +void ParticlesGlobalManager::setSimTime(double dt) +{ + std::lock_guard g(d->_lock); + d->_simulationDt = dt; +} + +// this is called from the main thread, since it's an update callback +// lock any state used by updateParticleSystemsFromCullCallback, which +// runs during culling, potentialy on a different thread +void ParticlesGlobalManager::ParticlesGlobalManagerPrivate::operator()(osg::Node* node, osg::NodeVisitor* nv) +{ + std::lock_guard g(_lock); + _enabled = !_enabledNode || _enabledNode->getBoolValue(); + + if (!_enabled) + return; + + const auto q = SGQuatd::fromLonLat(_currentPosition); + osg::Matrix om(toOsg(q)); + osg::Vec3 v(0, 0, 9.81); + _gravity = om.preMult(v); + + // NOTE: THIS WIND COMPUTATION DOESN'T SEEM TO AFFECT PARTICLES + // const osg::Vec3& zUpWind = _wind; + // osg::Vec3 w(zUpWind.y(), zUpWind.x(), -zUpWind.z()); + // _localWind = om.preMult(w); + + // while we have the lock, remove all expired systems + auto firstInvalid = std::remove_if(_systems.begin(), _systems.end(), [](const ParticleSystemWeakRef& weak) { + return !weak.valid(); + }); + _systems.erase(firstInvalid, _systems.end()); +} + +// only call this with the lock held! +osg::Group* ParticlesGlobalManager::ParticlesGlobalManagerPrivate::internalGetCommonRoot() +{ + if (!_commonRoot.valid()) { + _commonRoot = new osg::Group; + _commonRoot->setName("common particle system root"); + _commonGeode->setName("common particle system geode"); + _commonRoot->addChild(_commonGeode); + _commonRoot->setNodeMask(~simgear::MODELLIGHT_BIT); + } + return _commonRoot.get(); +} + +void ParticlesGlobalManager::ParticlesGlobalManagerPrivate::updateParticleSystemsFromCullCallback(int frameNumber, osg::NodeVisitor* nv) +{ + ParticleSystemsStrongRefVec activeSystems; + double dt = 0.0; + + // begin locked section + { + std::lock_guard g(_lock); + activeSystems.reserve(_systems.size()); + for (const auto& psref : _systems) { + ParticleSystemRef owningRef; + if (!psref.lock(owningRef)) { + // pointed to system is gone, skip it + // we will clean these up in the update callback, don't + // worry about that here + continue; + } + + // add to the list we will update now + activeSystems.push_back(owningRef); + } + + dt = _simulationDt; + } // of locked section + + // from here on, don't access class data; copy it all to local variables + // before this line. this is important so we're not holding _lock during + // culling, which might block osgDB threads for example. + + if (dt <= 0.0) { + return; + } + + for (const auto& ps : activeSystems) { + // code inside here is copied from osgParticle::ParticleSystemUpdate + osgParticle::ParticleSystem::ScopedWriteLock lock(*(ps->getReadWriteMutex())); + + // We need to allow at least 2 frames difference, because the particle system's lastFrameNumber + // is updated in the draw thread which may not have completed yet. + if (!ps->isFrozen() && + (!ps->getFreezeOnCull() || ((frameNumber - ps->getLastFrameNumber()) <= 2))) { + ps->update(dt, *nv); + } + // end of code copied from osgParticle::ParticleSystemUpdate + } +} + static std::mutex static_managerLock; static std::unique_ptr static_instance; @@ -131,14 +305,6 @@ ParticlesGlobalManager::ParticlesGlobalManager() : d(new ParticlesGlobalManagerP { } -ParticlesGlobalManager::~ParticlesGlobalManager() -{ - if (d->_globalCallbackRegistered) { - // is this actually necessary? possibly not - d->_updater->setUpdateCallback(nullptr); - } -} - bool ParticlesGlobalManager::isEnabled() const { std::lock_guard g(d->_lock); @@ -727,23 +893,10 @@ osg::ref_ptr ParticlesGlobalManager::appendParticles(const SGPropert emitter->setUpdateCallback(callback.get()); } - // touch shared data now (and not before) - - { - std::lock_guard g(d->_lock); - d->_updater->addParticleSystem(particleSys); - - if (attach != "local") { - d->internalGetCommonRoot()->addChild(callback()->particleFrame); - } - - if (!d->_globalCallbackRegistered) { - SG_LOG(SG_PARTICLES, SG_INFO, "Registering global particles callback"); - d->_globalCallbackRegistered = true; - d->_longitudeNode = modelRoot->getNode("/position/longitude-deg", true); - d->_latitudeNode = modelRoot->getNode("/position/latitude-deg", true); - d->_updater->setUpdateCallback(d.get()); - } + if (attach == "local") { + d->registerNewLocalParticleSystem(align, particleSys); + } else { + d->registerNewWorldParticleSystem(align, particleSys, callback()->particleFrame); } return align; diff --git a/simgear/scene/model/particles.hxx b/simgear/scene/model/particles.hxx index ee928ff7..60cc70b8 100644 --- a/simgear/scene/model/particles.hxx +++ b/simgear/scene/model/particles.hxx @@ -41,7 +41,6 @@ class NodeVisitor; namespace osgParticle { class ParticleSystem; -class ParticleSystemUpdater; } #include @@ -151,7 +150,7 @@ protected: class ParticlesGlobalManager { public: - ~ParticlesGlobalManager(); + ~ParticlesGlobalManager() = default; static ParticlesGlobalManager* instance(); static void clear(); @@ -161,14 +160,16 @@ public: osg::ref_ptr appendParticles(const SGPropertyNode* configNode, SGPropertyNode* modelRoot, const osgDB::Options* options); osg::Group* getCommonRoot(); - osg::Geode* getCommonGeode(); - osgParticle::ParticleSystemUpdater* getPSU(); + void initFromMainThread(); void setFrozen(bool e); bool isFrozen() const; + void setCurrentPosition(const SGGeod& pos); + void setSimTime(double dt); + void setSwitchNode(const SGPropertyNode* n); /** @@ -185,6 +186,8 @@ private: ParticlesGlobalManager(); class ParticlesGlobalManagerPrivate; + class UpdaterCallback; + class RegistrationCallback; // because Private inherits NodeCallback, we need to own it // via an osg::ref_ptr