// Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of NVIDIA CORPORATION nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved. // Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved. // Copyright (c) 2001-2004 NovodeX AG. All rights reserved. #include "GuQuerySystem.h" #include "GuBounds.h" #include "GuBVH.h" #include "foundation/PxAlloca.h" #include "common/PxProfileZone.h" using namespace physx; using namespace Gu; /////////////////////////////////////////////////////////////////////////////// bool contains(PxArray& pruners, PxU32 index) { const PxU32 nb = pruners.size(); for(PxU32 i=0;ipreallocate(preallocated); } QuerySystem::PrunerExt::~PrunerExt() { PX_DELETE(mPruner); } void QuerySystem::PrunerExt::flushMemory() { if(!mDirtyList.size()) mDirtyList.reset(); // PT: TODO: flush bitmap here // PT: TODO: flush pruner here? } // PT: ok things became more complicated than before here. We'd like to delay the update of *both* the transform and the bounds, // since immediately updating only one of them doesn't make much sense (it invalidates the pruner's data structure anyway). When both // are delayed it gives users the ability to query the pruners *without* commiting the changes, i.e. they can query the old snapshot // for as long as they please (i.e. a raycast wouldn't automatically trigger a structure update). // // Now the problem is that we need to store (at least) the transform until the update actually happens, and the initial code didn't // support this. We also want to do this in an efficient way, which of course makes things more difficult. // // A naive version would simply use a per-pruner hashmap between the PrunerHandle and its data. Might be slower than before. // // Another version could build on the initial bitmap-based solution and use arrays of transforms/bounds as companions to the array // of PrunerHandle (or we could mix all that data in a single structure). The issue with this is that two consecutive updates on the // same object wouldn't work anymore: the second call would check the bitmap, see that the bit is set already, and skip the work. // We'd need to update the cached data instead, i.e. we'd need a mapping between the PrunerHandle and its position in mDirtyList. // And we don't have that. // // A potential way to fix this could be to allow the same PrunerHandle to appear multiple times in mDirtyList, with the assumption // that users will not update the same object multiple times very often (...). The way it would work: // - during "add", dirtyMap is set, handle/transform/bounds are pushed to mDirtyList. // - during "remove", dirtyMap is reset *and that's it*. We don't bother purging mDirtyList (i.e. we kill the current O(n) search there) // - during "process" we use dirtyMap to validate the update. If bit is cleared, ignore mDirtyList entry. Duplicate entries work as long // as mDirtyList is processed in linear order. One issue is that the current mDirtyList is also passed to the pruner as-is for the // update, so we'd need to rebuild a separate array for that and/or make sure all pruners accept duplicate entries in that array. // Deep down that specific rabbit hole we'll actually find the recently discovered issue regarding the mToRefit array... // // Bit tricky. This is only for user-updates anyway (as opposed to sim updates) so this probably doesn't need ultimate perf? Note however // that we "remove from dirty list" when an object is removed, which happens all the time with or without user updates (e.g. streaming etc). static const bool gUseOldCode = false; void QuerySystem::PrunerExt::addToDirtyList(PrunerHandle handle, PxU32 dynamic, const PxTransform& transform, const PxBounds3* userBounds) { PxBitMap& dirtyMap = mDirtyMap; { if(dirtyMap.size() <= handle) { PxU32 size = PxMax(dirtyMap.size()*2, 1024); const PxU32 minSize = handle+1; if(minSize>size) size = minSize*2; dirtyMap.resize(size); PX_ASSERT(handlegetPayloadData(handle, &payloadData); computeBounds(*payloadData.mBounds, adapter.getGeometry(pp), *payloadData.mTransform, 0.0f, inflation); } // PT: batch update happens after the loop instead of once per loop iteration mPruner->updateObjects(prunerHandles, numDirtyList); mDirtyList.clear(); } else { // PT: TODO: this stuff is not 100% satisfying, since we do allow the same object to be updated multiple times. // Would be nice to revisit & improve at some point. PrunerHandle* prunerHandles = mDirtyList.begin(); PxU32 nbValid = 0; for(PxU32 i=0; igetPayloadData(handle, &payloadData); *payloadData.mTransform = mDirtyData[i].mPose; if(mDirtyData[i].mBounds.isEmpty()) computeBounds(*payloadData.mBounds, adapter.getGeometry(pp), mDirtyData[i].mPose, 0.0f, inflation); else *payloadData.mBounds = mDirtyData[i].mBounds; prunerHandles[nbValid++] = handle; } else { // PT: if not set, object has been added to the list then removed } } // PT: batch update happens after the loop instead of once per loop iteration mPruner->updateObjects(prunerHandles, nbValid); // PT: have to reset the bits *after* the above loop now. Unclear if clearing the // whole map would be faster ("it depends" I guess). while(nbValid--) { const PrunerHandle handle = *prunerHandles++; mDirtyMap.reset(handle); } mDirtyList.clear(); mDirtyData.clear(); } const bool ret = mDirtyStatic; mDirtyStatic = false; return ret; } /////////////////////////////////////////////////////////////////////////////// static PX_FORCE_INLINE QuerySystem::PrunerExt* checkPrunerIndex(PxU32 prunerIndex, const PxArray& prunerExt) { if(prunerIndex>=prunerExt.size() || !prunerExt[prunerIndex]) { PxGetFoundation().error(PxErrorCode::eINVALID_PARAMETER, PX_FL, "Invalid pruner index"); return NULL; } return prunerExt[prunerIndex]; } QuerySystem::QuerySystem(PxU64 contextID, float inflation, const Adapter& adapter, bool usesTreeOfPruners) : mAdapter (adapter), mTreeOfPruners (NULL), mContextID (contextID), mStaticTimestamp (0), mInflation (inflation), mPrunerNeedsUpdating (false), mTimestampNeedsUpdating (false), mUsesTreeOfPruners (usesTreeOfPruners) //mBatchUserUpdates (batchUserUpdates) { } QuerySystem::~QuerySystem() { PX_DELETE(mTreeOfPruners); const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;imNbStatic) invalidateStaticTimestamp(); PX_DELETE(pe); mPrunerExt[prunerIndex] = NULL; mFreePruners.pushBack(prunerIndex); // We don't bother searching mDirtyPruners since it's going to be cleared next frame } void QuerySystem::flushMemory() { const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;iflushMemory(); } } ActorShapeData QuerySystem::addPrunerShape(const PrunerPayload& payload, PxU32 prunerIndex, bool dynamic, const PxTransform& transform, const PxBounds3* userBounds) { PrunerExt* pe = checkPrunerIndex(prunerIndex, mPrunerExt); if(!pe) return INVALID_ACTOR_SHAPE_DATA; mPrunerNeedsUpdating = true; if(dynamic) { pe->mNbDynamic++; } else { pe->mNbStatic++; invalidateStaticTimestamp(); } PX_ASSERT(pe->mPruner); const PxBounds3* boundsPtr; PxBounds3 bounds; if(userBounds) { boundsPtr = userBounds; } else { computeBounds(bounds, mAdapter.getGeometry(payload), transform, 0.0f, 1.0f + mInflation); boundsPtr = &bounds; } PrunerHandle handle; pe->mPruner->addObjects(&handle, boundsPtr, &payload, &transform, 1, false); return createActorShapeData(createPrunerInfo(prunerIndex, dynamic), handle); } void QuerySystem::removePrunerShape(ActorShapeData data, PrunerPayloadRemovalCallback* removalCallback) { const PrunerInfo info = getPrunerInfo(data); const PxU32 prunerIndex = getPrunerIndex(info); PrunerExt* pe = checkPrunerIndex(prunerIndex, mPrunerExt); if(!pe) return; mPrunerNeedsUpdating = true; const PxU32 dynamic = getDynamic(info); const PrunerHandle handle = getPrunerHandle(data); PX_ASSERT(pe->mPruner); if(dynamic) { PX_ASSERT(pe->mNbDynamic); pe->mNbDynamic--; } else { PX_ASSERT(pe->mNbStatic); pe->mNbStatic--; invalidateStaticTimestamp(); } //if(mBatchUserUpdates) pe->removeFromDirtyList(handle); pe->mPruner->removeObjects(&handle, 1, removalCallback); } void QuerySystem::updatePrunerShape(ActorShapeData data, bool immediately, const PxTransform& transform, const PxBounds3* userBounds) { const PrunerInfo info = getPrunerInfo(data); const PxU32 prunerIndex = getPrunerIndex(info); PrunerExt* pe = checkPrunerIndex(prunerIndex, mPrunerExt); if(!pe) return; mPrunerNeedsUpdating = true; const PxU32 dynamic = getDynamic(info); const PrunerHandle handle = getPrunerHandle(data); PX_ASSERT(pe->mPruner); Pruner* pruner = pe->mPruner; if(immediately) { if(!dynamic) invalidateStaticTimestamp(); PrunerPayloadData payloadData; const PrunerPayload& pp = pruner->getPayloadData(handle, &payloadData); *payloadData.mTransform = transform; if(userBounds) *payloadData.mBounds = *userBounds; else computeBounds(*payloadData.mBounds, mAdapter.getGeometry(pp), transform, 0.0f, 1.0f + mInflation); // PT: TODO: would it be better to pass the bounds & transform directly to this function? pruner->updateObjects(&handle, 1); } else { // PT: we don't update the static timestamp immediately, so that users can query the // old state of the structure without invalidating their caches. This will be resolved // in processDirtyLists. if(gUseOldCode) pruner->setTransform(handle, transform); // PT: we don't shrink mDirtyList anymore in removePrunerShape so the size of that array can be reused as // a flag telling us whether we already encountered this pruner or not. If not, we add its index to mDirtyPruners. // Goal is to avoid processing all pruners in processDirtyLists. if(!pe->mDirtyList.size()) { PX_ASSERT(!contains(mDirtyPruners, prunerIndex)); mDirtyPruners.pushBack(prunerIndex); } else { PX_ASSERT(contains(mDirtyPruners, prunerIndex)); } pe->addToDirtyList(handle, dynamic, transform, userBounds); } } const PrunerPayload& QuerySystem::getPayloadData(ActorShapeData data, PrunerPayloadData* ppd) const { const PrunerInfo info = getPrunerInfo(data); const PxU32 prunerIndex = getPrunerIndex(info); PX_ASSERT(checkPrunerIndex(prunerIndex, mPrunerExt)); const PrunerHandle handle = getPrunerHandle(data); PX_ASSERT(mPrunerExt[prunerIndex]->mPruner); return mPrunerExt[prunerIndex]->mPruner->getPayloadData(handle, ppd); } void QuerySystem::processDirtyLists() { PX_PROFILE_ZONE("QuerySystem.processDirtyLists", mContextID); const PxU32 nbDirtyPruners = mDirtyPruners.size(); if(!nbDirtyPruners) return; // must already have acquired writer lock here const float inflation = 1.0f + mInflation; bool mustInvalidateStaticTimestamp = false; for(PxU32 ii=0;iiprocessDirtyList(mAdapter, inflation)) mustInvalidateStaticTimestamp = true; } if(mustInvalidateStaticTimestamp) invalidateStaticTimestamp(); mDirtyPruners.clear(); } void QuerySystem::update(bool buildStep, bool commit) { PX_PROFILE_ZONE("QuerySystem::update", mContextID); if(!buildStep && !commit) { //mPrunerNeedsUpdating = true; // PT: removed, why was it here? return; } // flush user modified objects // if(mBatchUserUpdates) processDirtyLists(); const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;imPruner; if(pruner) { if(buildStep && pruner->isDynamic()) static_cast(pruner)->buildStep(true); if(commit) pruner->commit(); } } if(commit) { if(mUsesTreeOfPruners) createTreeOfPruners(); } mPrunerNeedsUpdating = !commit; } void QuerySystem::commitUpdates() { PX_PROFILE_ZONE("QuerySystem.commitUpdates", mContextID); if(mPrunerNeedsUpdating) { mSQLock.lock(); if(mPrunerNeedsUpdating) { //if(mBatchUserUpdates) processDirtyLists(); const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;imPruner; if(pruner) pruner->commit(); } if(mUsesTreeOfPruners) createTreeOfPruners(); PxMemoryBarrier(); mPrunerNeedsUpdating = false; } mSQLock.unlock(); } } PxU32 QuerySystem::startCustomBuildstep() { PX_PROFILE_ZONE("QuerySystem.startCustomBuildstep", mContextID); mTimestampNeedsUpdating = false; return mPrunerExt.size(); } void QuerySystem::customBuildstep(PxU32 index) { PX_PROFILE_ZONE("QuerySystem.customBuildstep", mContextID); PX_ASSERT(indexmPruner; //void QuerySystem::processDirtyLists() { PX_PROFILE_ZONE("QuerySystem.processDirtyLists", mContextID); // must already have acquired writer lock here const float inflation = 1.0f + mInflation; // PT: note that we don't use the mDirtyPruners array here if(pe->processDirtyList(mAdapter, inflation)) mTimestampNeedsUpdating = true; } if(pruner) { if(pruner->isDynamic()) static_cast(pruner)->buildStep(true); // PT: "true" because that parameter was made for PxSceneQuerySystem::sceneQueryBuildStep(), not us pruner->commit(); } } void QuerySystem::finishCustomBuildstep() { PX_PROFILE_ZONE("QuerySystem.finishCustomBuildstep", mContextID); if(mUsesTreeOfPruners) createTreeOfPruners(); mPrunerNeedsUpdating = false; if(mTimestampNeedsUpdating) invalidateStaticTimestamp(); mDirtyPruners.clear(); } void QuerySystem::sync(PxU32 prunerIndex, const PrunerHandle* handles, const PxU32* boundsIndices, const PxBounds3* bounds, const PxTransform32* transforms, PxU32 count) { if(!count) return; PrunerExt* pe = checkPrunerIndex(prunerIndex, mPrunerExt); if(!pe) return; Pruner* pruner = pe->mPruner; if(pruner) pruner->updateObjects(handles, count, mInflation, boundsIndices, bounds, transforms); } /////////////////////////////////////////////////////////////////////////////// namespace { struct LocalRaycastCB : PxBVH::RaycastCallback { LocalRaycastCB(const PxArray& pruners, const PrunerFilter* prunerFilter, const PxVec3& origin, const PxVec3& unitDir, PrunerRaycastCallback& cb) : mPrunerExt(pruners), mPrunerFilter(prunerFilter), mOrigin(origin), mUnitDir(unitDir), mCB(cb) {} virtual bool reportHit(PxU32 boundsIndex, PxReal& distance) { QuerySystem::PrunerExt* pe = mPrunerExt[boundsIndex]; // Can be NULL if the pruner has been removed if(pe && (!mPrunerFilter || mPrunerFilter->processPruner(boundsIndex))) { Pruner* pruner = pe->mPruner; if(!pruner->raycast(mOrigin, mUnitDir, distance, mCB)) return false; } return true; } const PxArray& mPrunerExt; const PrunerFilter* mPrunerFilter; const PxVec3& mOrigin; const PxVec3& mUnitDir; PrunerRaycastCallback& mCB; PX_NOCOPY(LocalRaycastCB) }; struct LocalOverlapCB : PxBVH::OverlapCallback { LocalOverlapCB(const PxArray& pruners, const PrunerFilter* prunerFilter, const ShapeData& queryVolume, PrunerOverlapCallback& cb) : mPrunerExt(pruners), mPrunerFilter(prunerFilter), mQueryVolume(queryVolume), mCB(cb) {} virtual bool reportHit(PxU32 boundsIndex) { QuerySystem::PrunerExt* pe = mPrunerExt[boundsIndex]; // Can be NULL if the pruner has been removed if(pe && (!mPrunerFilter || mPrunerFilter->processPruner(boundsIndex))) { Pruner* pruner = pe->mPruner; if(!pruner->overlap(mQueryVolume, mCB)) return false; } return true; } const PxArray& mPrunerExt; const PrunerFilter* mPrunerFilter; const ShapeData& mQueryVolume; PrunerOverlapCallback& mCB; PX_NOCOPY(LocalOverlapCB) }; struct LocalSweepCB : PxBVH::RaycastCallback { LocalSweepCB(const PxArray& pruners, const PrunerFilter* prunerFilter, const ShapeData& queryVolume, const PxVec3& unitDir, PrunerRaycastCallback& cb) : mPrunerExt(pruners), mPrunerFilter(prunerFilter), mQueryVolume(queryVolume), mUnitDir(unitDir), mCB(cb) {} virtual bool reportHit(PxU32 boundsIndex, PxReal& distance) { QuerySystem::PrunerExt* pe = mPrunerExt[boundsIndex]; // Can be NULL if the pruner has been removed if(pe && (!mPrunerFilter || mPrunerFilter->processPruner(boundsIndex))) { Pruner* pruner = pe->mPruner; if(!pruner->sweep(mQueryVolume, mUnitDir, distance, mCB)) return false; } return true; } const PxArray& mPrunerExt; const PrunerFilter* mPrunerFilter; const ShapeData& mQueryVolume; const PxVec3& mUnitDir; PrunerRaycastCallback& mCB; PX_NOCOPY(LocalSweepCB) }; } void QuerySystem::raycast(const PxVec3& origin, const PxVec3& unitDir, float& inOutDistance, PrunerRaycastCallback& cb, const PrunerFilter* prunerFilter) const { if(mTreeOfPruners) { LocalRaycastCB localCB(mPrunerExt, prunerFilter, origin, unitDir, cb); mTreeOfPruners->raycast(origin, unitDir, inOutDistance, localCB, PxGeometryQueryFlag::Enum(0)); } else { const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;iprocessPruner(i)) { Pruner* pruner = pe->mPruner; if(!pruner->raycast(origin, unitDir, inOutDistance, cb)) return; } } } } void QuerySystem::overlap(const ShapeData& queryVolume, PrunerOverlapCallback& cb, const PrunerFilter* prunerFilter) const { if(mTreeOfPruners) { LocalOverlapCB localCB(mPrunerExt, prunerFilter, queryVolume, cb); mTreeOfPruners->overlap(queryVolume, localCB, PxGeometryQueryFlag::Enum(0)); } else { const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;iprocessPruner(i)) { Pruner* pruner = pe->mPruner; if(!pruner->overlap(queryVolume, cb)) return; } } } } void QuerySystem::sweep(const ShapeData& queryVolume, const PxVec3& unitDir, float& inOutDistance, PrunerRaycastCallback& cb, const PrunerFilter* prunerFilter) const { if(mTreeOfPruners) { LocalSweepCB localCB(mPrunerExt, prunerFilter, queryVolume, unitDir, cb); mTreeOfPruners->sweep(queryVolume, unitDir, inOutDistance, localCB, PxGeometryQueryFlag::Enum(0)); } else { const PxU32 nb = mPrunerExt.size(); for(PxU32 i=0;iprocessPruner(i)) { Pruner* pruner = pe->mPruner; if(!pruner->sweep(queryVolume, unitDir, inOutDistance, cb)) return; } } } } void QuerySystem::createTreeOfPruners() { PX_PROFILE_ZONE("QuerySystem.createTreeOfPruners", mContextID); PX_DELETE(mTreeOfPruners); mTreeOfPruners = PX_NEW(BVH)(NULL); const PxU32 nb = mPrunerExt.size(); PxBounds3* prunerBounds = reinterpret_cast(PxAlloca(sizeof(PxBounds3)*(nb+1))); PxU32 nbBounds = 0; for(PxU32 i=0;imPruner; if(pruner) pruner->getGlobalBounds(prunerBounds[nbBounds++]); } mTreeOfPruners->init(nbBounds, NULL, prunerBounds, sizeof(PxBounds3), BVH_SPLATTER_POINTS, 1, 0.01f); }