你的PagedLOD 為什么沒有卸載
轉自:http://bbs.osgchina.org/forum.php?mod=viewthread&tid=7612&highlight=PagedLOD&_dsign=ed8fb143
分頁對於大型場景而言是一個必不可少的調度渲染技術。當你發現osg自身就帶有PagedLOD 功能時,何嘗不興致沖沖的立即使用。可是,在使用時突然發現只有加載沒有卸載,內存隨着運行不斷攀升,過期的PagedLOD 節點竟然沒有被osg踢出內存?苦惱啊,糾結啊?難道是bug?其實不然,osg的PagedLOD 技術已十分成熟,那么究竟是何原因造成的?焦躁的你請耐心閱讀
1、導致PagedLOD 不被卸載的第一個原因
在使用osgViewr::Viewer::setSceneData設置場景給Viewr之前,沒有把你的PagedLOD 節點加入到場景根節點下。考慮如下兩種情況:
osg::Group * root=new osg::Group; osg::PagedLOD * lod1=new osg::PagedLOD; lod1->setFileName(0,"cow.osg"); //添加子節點 lod1->setRange(0,0,10); //設置子節點顯示范圍 root->addChild(lod1); viewer->setSceneData(root /*createTeapot()*/ );
osg::Group * root=new osg::Group; osg::PagedLOD * lod1=new osg::PagedLOD; lod1->setFileName(0,"cow.osg"); lod1->setRange(0,0,10); viewer->setSceneData(root /*createTeapot()*/ ); root->addChild(lod1);
兩種情況下lod1節點都會在過期時刪除嗎?
不是!
只有第一種情況中的PagedLOD 會在過期時從內存中卸載。
說起原因,就不得不告訴你一個setSceneData函數的小秘密。看下面代碼:
void Scene::setSceneData(osg::Node* node) { _sceneData = node; if (_databasePager.valid()) { // register any PagedLOD that need to be tracked in the scene graph if (node) _databasePager->registerPagedLODs(node); } }
在setSceneDAta時向DatabasePagerr注冊了場景中的所有PagedLOD 節點。
如果此時場景中還沒有設置PagedLOD 節點,那么很抱歉,即使你在隨后設置了,DatabasePager中注冊的PagedLOD 節點依然是當時注冊的那些,你隨后設置的只能是計划外人口,黑戶,對不起DatabasePager是不管這些黑PagedLOD 的死活。
既然DatabasePager不管這些PagedLOD 節點的死活,那么為什么這些PagedLOD 節點可以被動態加載呢?
因為PagedLOD 節點的動態加載請求是PagedLOD 節點自身發出,而卸載則是由DatabasePager管理!請查看PagedLOD 的traverse函數,它會告訴你一切。
講到這里,也許你已經心中有數,使用PagedLOD 節點時應該預先設置然后再setSceneData。
如果我想在運行過程中添加PagedLOD 節點又想讓其可以卸載該怎么辦呢?
答案就是改變時請注意調用DatabasePager::registerPagedLODs(node);函數注冊戶口。
2、導致PagedLOD 節點不能卸載的第二個原因:
如果你使用PagedLOD 節點完全避免1中提到的問題,但是你的PagedLOD 節點依然頑固的賴在內存中不走,那么請你看看這里。
我們知道分頁的功能與內存有關,那么內存不夠時PagedLOD 節點就應該自動退出內存才是,為什么他還賴在那里呢?
答案是,他並不知道內存不夠用了,需要你告訴他!DatabasePager::setTargetMaximumNumberOfPageLOD函數或環境變量OSG_MAX_PAGEDLOD就是干這個的。他告訴DatabasePager我的電腦內存有限只能容納指定數量的PagedLOD ,超出這個數的過期PagedLOD 就讓他滾蛋吧。
也許你會問PagedLOD 個數和內存使用情況有個毛關系?
確實有關系,在你進行PagedLOD 分頁規划時你的PagedLOD 節點的每一級都有一定的大小,那么這個大小和個數相乘就是要占用的總內存。
值得一提的是osg中此數默認數量是300,osg覺得你的電腦配置很高!
OSG動態調度DatabasePager,pagedLOD
轉自:https://blog.csdn.net/u012130706/article/details/77175586
使用動態調度的原因
當用戶需要瀏覽的數據量很大,比如地形模擬、虛擬小區和城市等的時候,會對計算機系統產生極大的負擔。
在內存中可能要存儲海量數據,這些海量數據指的是數百GB甚至TB級別的數據(例如中國境內的山形地貌等),這些不可能全部載入內存中,就算未來的計算機能夠將它們一次性讀入,也已經損耗了太多的系統性能。
動態調度的原理
在顯示當前視域中的場景元素(可見元素)的同時,預判斷下一步可能載入的數據(預可見元素),以及那些短時間內不可能被看到的對象(不可見元素),從而作出正確的數據加載和卸載處理,確保內存中始終維持有限的數據額度,並且不會因此造成場景瀏覽時重要信息的丟失或者過於遲緩。
數據的動態調度可以使用多線程的工作方式,使數據的動態調度和場景的實時繪制同時進行。由於動態數據的加載/卸載可能影響到場景樹的結構,因此這一工作需要在場景更新的階段完成,以免影響到裁剪和繪制的過程。
動態調度的過程
(1) 刪除過期的場景數據:過期數據指的是那些長時間沒有處於用戶視域內,並且有理由認為它們不會立即顯現的場景元素。場景的更新遍歷函數負責將檢索到的過期對象收集並送入相應的過期對象列表;而列表中的數據通常可以在數據線程中統一予以刪除。
(2) 獲取新的數據加載請求:請求加載的可能是新的數據信息,也可能是已有的場景數據(曾經從“當前頁面”中去除,更新又回到“當前頁面”中);數據可能是本地的文件,也可能來自網絡;從網絡下載的數據往往還需要緩存在本地磁盤上。這些都需要在數據線程中一一加以判斷。
(3) 編譯加載的數據:有些數據如果提前進行編譯可以有效地提升效率,例如為幾何體數據創建顯示列表,以及將紋理對象提前加載到紋理內存。雖然OSG同樣可以在負責渲染的主進程中根據用戶需要執行這些工作,但是那樣有可能造成幀的延遲。例如,一個大型場景的顯示和調度過程中,如果大量的地塊數據同時被加載入內存,那么下一幀的數據編譯任務將變得十分繁重,繼而造成較為嚴重的幀延遲。此時如果由數據線程負責預編譯的工作,則可以在一定程度上緩解這一壓力。
(4) 將加載的數據合並至場景圖形:直接由數據線程來完成這一工作顯然是不合適的,因為系統主進程不知道當DatabaseThread線程視圖操作場景中的結點時,OSG的渲染器在做些什么。最好的方法是將讀入的數據先保存在一個列表中,並且由仿真循環負責獲取和執行合並新節點的操作。
DatabasePager與PagedLOD
在OSG中,osgDB::DatabasePager類負責執行場景動態調度的工作。與osgDB::DatabasePager搭配使用的是PagedLOD和osg::ProxyNode。這里主要討論PagedLOD。
PagedLOD既具有將大量數據或者模型使用細節層次(LOD)原則划分的特性,又有動態調度以保證渲染效率和內存管理的特性。
代碼示例
#include <osg/ShapeDrawable>
#include <osg/Geode>
#include <osg/PagedLOD>
#include <osgViewer/Viewer>
osg::Geode* createBox(const osg::Vec3& center, float width)
{
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(
new osg::ShapeDrawable(new osg::Box(center, width)));
return geode.release();
}
osg::Group* createPagedLOD(int row, int col)
{
osg::ref_ptr<osg::Group> root = new osg::Group;
char buffer[5] = "";
for (int i = 0; i<row; i++)
{
for (int j = 0; j<col; j++)
{
std::string filename = "cow.osg.";
#ifdef _WIN32
_itoa_s(i * 10, buffer, 5, 10);
filename += buffer; filename += ",";
_itoa_s(j * 10, buffer, 5, 10);
filename += buffer; filename += ",0.trans";
#else
gcvt(i * 10, 5, buffer);
filename += buffer; filename += ",";
gcvt(j * 10, 5, buffer);
filename += buffer; filename += ",0.trans";
#endif
osg::ref_ptr<osg::PagedLOD> lod = new osg::PagedLOD;
lod->setCenter(osg::Vec3(i * 10, j * 10, 0.0));
lod->addChild(createBox(osg::Vec3(i * 10, j * 10, 0.0), 1), 200.0, FLT_MAX);
lod->setFileName(1, filename);
lod->setRange(1, 0.0, 200.0);
root->addChild(lod.get());
}
}
return root.release();
}
int main(int argc, char** argv)
{
osgViewer::Viewer viewer;
viewer.setSceneData(createPagedLOD(30, 30));
return viewer.run();
}
運行結果圖:

深入理解osg::PagedLOD
轉自:https://blog.csdn.net/qq_16123279/article/details/82665053
1.先看看繼承關系:
PagedLOD繼承了LOD繼承了Group繼承了Node;
2.簡單說說OSG的每一幀干的事:
OSG其實很簡單就是封裝了一個循環,在這個循環里面,osg不斷調用各種NodeVisitor,去處理加入場景的各個Node。
void ViewerBase::frame(double simulationTime)
{
if (_done) return;
// OSG_NOTICE<<std::endl<<"CompositeViewer::frame()"<<std::endl<<std::endl;
if (_firstFrame)
{
viewerInit();
if (!isRealized())
{
realize();
}
_firstFrame = false;
}
advance(simulationTime);
eventTraversal();
updateTraversal();
renderingTraversals();
}
其中只有renderingTraversals();是viewerBase自己實現的,eventTraversal()和 updateTraversal();交由CompositeViewer和Viewer實現。
3.訪問器工作原理
這三大遍歷函數里面又是很多具體的實現,其中各種訪問器(NodeVisitor的apply(Node* pNode))和各種Node(Node的traverse(NodeVisitor* pVisitor))在里面扮演了重要的角色。首先apply某一個node,然后apply里面可以寫對應的操作,然后node的traverse可以應用一個遍歷器寫上某個具體node的功能,node還有Node::accept(NodeVisitor& nv)里面如果不寫什么的話基本就是nv.apply(*this);我們來看一下具體的代碼:
Node里面遍歷相關函數
/** Visitor Pattern : calls the apply method of a NodeVisitor with this node's type.*/
virtual void accept(NodeVisitor& nv);
/** Traverse upwards : calls parents' accept method with NodeVisitor.*/
virtual void ascend(NodeVisitor& nv);
/** Traverse downwards : calls children's accept method with NodeVisitor.*/
virtual void traverse(NodeVisitor& /*nv*/) {}
void Node::accept(NodeVisitor& nv)
{
if (nv.validNodeMask(*this))
{
nv.pushOntoNodePath(this);
nv.apply(*this);
nv.popFromNodePath();
}
}
void Node::ascend(NodeVisitor& nv)
{
std::for_each(_parents.begin(),_parents.end(),NodeAcceptOp(nv));
}
可以看到 accpt就是調用一個visitor的apply()函數,traverse函數沒有實現,因為這個函數代表了某個節點的特性,比如Group節點,LOD節點,PagedLOD節點等等,這些節點的traverse實現都是不同的。而ascend會像父節點遍歷,是traverse的反向。向孩子或向父節點遍歷是visitor說了算。
NodeVisitor
inline void NodeVisitor::traverse(Node& node)
{
if (_traversalMode==TRAVERSE_PARENTS) node.ascend(*this);
else if (_traversalMode!=TRAVERSE_NONE) node.traverse(*this);
}
void NodeVisitor::apply(Node& node)
{
traverse(node);
}
void NodeVisitor::apply(Drawable& drawable)
{
apply(static_cast<Node&>(drawable));
}
void NodeVisitor::apply(Geometry& drawable)
{
apply(static_cast<Drawable&>(drawable));
}
void NodeVisitor::apply(Geode& node)
{
apply(static_cast<Group&>(node));
}
void NodeVisitor::apply(Billboard& node)
{
apply(static_cast<Geode&>(node));
}
void NodeVisitor::apply(Group& node)
{
apply(static_cast<Node&>(node));
}
可以看到NodeVisitor里面出了對Node基本遍歷做了處理,其他的節點均未實現,需要的什么功能,我們可以繼承來解決,調用的時候有兩種方式:
Node.accept(NodeVisitor);//最好的辦法
NodeVisitor.apply(Node);//也行(不推薦)
所以結合以上的說了一大堆,我們明白一個節點的特性應該去他的traverse函數里面去看,然后我們來看看PagedLOD的特性:
LOD特性
詳細代碼可以自己去對應的源碼部分看,這里只是說一下其算法步驟:
1.根據用戶設定的距離模式(視點到包圍球中心距離,在屏幕上占有的像素大小),計算一個距離;
2.判斷_rangeList的尺寸與numChildren,如果小於孩子數量就讓numChildren與_randgelist的數量相等
3.遍歷numChildren,在范圍內的就accep(nv),不accept(nv)的節點將不會被渲染遍歷到就不會被渲染出來
相關代碼:
switch(nv.getTraversalMode())
{
case(NodeVisitor::TRAVERSE_ALL_CHILDREN):
std::for_each(_children.begin(),_children.end(),NodeAcceptOp(nv));
break;
case(NodeVisitor::TRAVERSE_ACTIVE_CHILDREN):
{
float required_range = 0;
if (_rangeMode==DISTANCE_FROM_EYE_POINT)
{
required_range = nv.getDistanceToViewPoint(getCenter(),true);
}
else
{
osg::CullStack* cullStack = nv.asCullStack();
if (cullStack && cullStack->getLODScale())
{
required_range = cullStack->clampedPixelSize(getBound()) / cullStack->getLODScale();
}
else
{
// fallback to selecting the highest res tile by
// finding out the max range
for(unsigned int i=0;i<_rangeList.size();++i)
{
required_range = osg::maximum(required_range,_rangeList[i].first);
}
}
}
unsigned int numChildren = _children.size();
if (_rangeList.size()<numChildren) numChildren=_rangeList.size();
for(unsigned int i=0;i<numChildren;++i)
{
if (_rangeList[i].first<=required_range && required_range<_rangeList[i].second)
{
_children[i]->accept(nv);
}
}
break;
}
PagedLOD
pagedLOD繼承了LOD其traveser比LOD復雜得多,先看看算法步驟:
1.如果遍歷器類型是CULL_VISITOR,就把該遍歷器上一幀遍歷完成后的幀數記錄下來(意思就是保存上一幀是第幾幀)。
2.對於每一個孩子計算其距離(required_range),跟LOD的相同。
3.循環_rangeList(這個東西保存了所有孩子的范圍),判斷剛才計算到的required_range是否在某個個_rangeList里面,如果范圍鏈的尺寸沒有超過當前孩子的數量就accept(顯示) 。
bool updateTimeStamp = nv.getVisitorType()==osg::NodeVisitor::CULL_VISITOR;
int lastChildTraversed = -1;
bool needToLoadChild = false;
for(unsigned int i=0;i<_rangeList.size();++i)
{
if (_rangeList[i].first<=required_range && required_range<_rangeList[i].second)
{
if (i<_children.size())
{
if (updateTimeStamp)//如果是裁剪遍歷器
{
_perRangeDataList[i]._timeStamp=timeStamp;//記錄相對時間
_perRangeDataList[i]._frameNumber=frameNumber;//記錄幀數
}
//如果請求的距離在節點的可見范圍內,且該節點存在,
//那么渲染該節點,且記錄該節點所在的索引
_children[i]->accept(nv);
lastChildTraversed = (int)i;
}
else
{
//如果請求距離在節點范圍內,但是該組下沒有這個節點那么就加載
needToLoadChild = true;
}
}
}
4.加載之前消失的節點
if (needToLoadChild)
{
unsigned int numChildren = _children.size();
//lastChildTraversed記錄了最后一個被for遍歷渲染節點的索引
//這個判斷的意思就是:
//1.組里面有節點
//2.組里面被渲染的最后一個節點,
//但不是組里面最后一個節點。(這里可能是上面accept會漏然后做的補充)
//條件1、2成立為真
if (numChildren > 0 && ((int)numChildren - 1) != lastChildTraversed)
{
//如果訪問器是篩選訪問器
if (updateTimeStamp)
{
//記錄渲染開始到此刻的總時間
_perRangeDataList[numChildren - 1]._timeStamp = timeStamp;
//記錄渲染開始到此刻的總幀數
_perRangeDataList[numChildren - 1]._frameNumber = frameNumber;
}
//渲染組里面現有的最后一個節點,因為lastChildTraversed不是組里的最后一個
_children[numChildren - 1]->accept(nv);
}
//從磁盤加載節點
//@@_disableExternalChildrenPaging用戶可以通過設置這個變量來禁用加載
//@@nv.getDatabaseRequestHandler()默認就是osgDB::DatabasePager
//@@numChildren < _perRangeDataList.size()有被卸載掉的節點
if (!_disableExternalChildrenPaging &&
nv.getDatabaseRequestHandler() &&
numChildren < _perRangeDataList.size())
{
// compute priority from where abouts in the required range the distance falls.
float priority = (_rangeList[numChildren].second - required_range) / (_rangeList[numChildren].second - _rangeList[numChildren].first);
// invert priority for PIXEL_SIZE_ON_SCREEN mode
if (_rangeMode == PIXEL_SIZE_ON_SCREEN)
{
priority = -priority;
}
// modify the priority according to the child's priority offset and scale.
priority = _perRangeDataList[numChildren]._priorityOffset + priority * _perRangeDataList[numChildren]._priorityScale;
//_databasePath一個字符串存放文件夾路徑,
//就是說需要用pagedLOD加載的模型節點,可以統一放到一個文件夾下
//然后設置了_databasePath直接用模型的文件名就行了,減少內存占用
if (_databasePath.empty())
{
nv.getDatabaseRequestHandler()->requestNodeFile(_perRangeDataList[numChildren]._filename, nv.getNodePath(), priority, nv.getFrameStamp(), _perRangeDataList[numChildren]._databaseRequest, _databaseOptions.get());
}
else
{
// 將DabasePath預置到子文件名。
//requestNodeFile是個重點,下面分析
nv.getDatabaseRequestHandler()->requestNodeFile(_databasePath + _perRangeDataList[numChildren]._filename, nv.getNodePath(), priority, nv.getFrameStamp(), _perRangeDataList[numChildren]._databaseRequest, _databaseOptions.get());
}
}
}
requestNodeFile
void DatabasePager::requestNodeFile(const std::string& fileName, osg::NodePath& nodePath,
float priority, const osg::FrameStamp* framestamp,
osg::ref_ptr<osg::Referenced>& databaseRequestRef,
const osg::Referenced* options)
{
osgDB::Options* loadOptions = dynamic_cast<osgDB::Options*>(const_cast<osg::Referenced*>(options));
if (!loadOptions)
{
loadOptions = Registry::instance()->getOptions();
}
else
{
// OSG_NOTICE<<"options from requestNodeFile "<<std::endl;
}
if (!_acceptNewRequests) return;
if (nodePath.empty())
{
OSG_NOTICE << "Warning: DatabasePager::requestNodeFile(..) passed empty NodePath, so nowhere to attach new subgraph to." << std::endl;
return;
}
osg::Group* group = nodePath.back()->asGroup();
if (!group)
{
OSG_NOTICE << "Warning: DatabasePager::requestNodeFile(..) passed NodePath without group as last node in path, so nowhere to attach new subgraph to." << std::endl;
return;
}
osg::Node* terrain = 0;
for (osg::NodePath::reverse_iterator itr = nodePath.rbegin();
itr != nodePath.rend();
++itr)
{
if ((*itr)->asTerrain()) terrain = *itr;
}
double timestamp = framestamp ? framestamp->getReferenceTime() : 0.0;
unsigned int frameNumber = framestamp ? framestamp->getFrameNumber() : static_cast<unsigned int>(_frameNumber);
// #define WITH_REQUESTNODEFILE_TIMING
#ifdef WITH_REQUESTNODEFILE_TIMING
osg::Timer_t start_tick = osg::Timer::instance()->tick();
static int previousFrame = -1;
static double totalTime = 0.0;
if (previousFrame != frameNumber)
{
OSG_NOTICE << "requestNodeFiles for " << previousFrame << " time = " << totalTime << std::endl;
previousFrame = frameNumber;
totalTime = 0.0;
}
#endif
// search to see if filename already exist in the file loaded list.
bool foundEntry = false;
if (databaseRequestRef.valid())
{
DatabaseRequest* databaseRequest = dynamic_cast<DatabaseRequest*>(databaseRequestRef.get());
bool requeue = false;
if (databaseRequest)
{
OpenThreads::ScopedLock<OpenThreads::Mutex> drLock(_dr_mutex);
if (!(databaseRequest->valid()))
{
OSG_INFO << "DatabaseRequest has been previously invalidated whilst still attached to scene graph." << std::endl;
databaseRequest = 0;
}
else
{
OSG_INFO << "DatabasePager::requestNodeFile(" << fileName << ") updating already assigned." << std::endl;
databaseRequest->_valid = true;
databaseRequest->_frameNumberLastRequest = frameNumber;
databaseRequest->_timestampLastRequest = timestamp;
databaseRequest->_priorityLastRequest = priority;
++(databaseRequest->_numOfRequests);
foundEntry = true;
if (databaseRequestRef->referenceCount() == 1)
{
OSG_INFO << "DatabasePager::requestNodeFile(" << fileName << ") orphaned, resubmitting." << std::endl;
databaseRequest->_frameNumberLastRequest = frameNumber;
databaseRequest->_timestampLastRequest = timestamp;
databaseRequest->_priorityLastRequest = priority;
databaseRequest->_group = group;
databaseRequest->_terrain = terrain;
databaseRequest->_loadOptions = loadOptions;
databaseRequest->_objectCache = 0;
requeue = true;
}
}
}
if (requeue)
_fileRequestQueue->add(databaseRequest);
}
if (!foundEntry)
{
OSG_INFO << "In DatabasePager::requestNodeFile(" << fileName << ")" << std::endl;
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_fileRequestQueue->_requestMutex);
if (!databaseRequestRef.valid() || databaseRequestRef->referenceCount() == 1)
{
osg::ref_ptr<DatabaseRequest> databaseRequest = new DatabaseRequest;
databaseRequestRef = databaseRequest.get();
databaseRequest->_valid = true;
databaseRequest->_fileName = fileName;
databaseRequest->_frameNumberFirstRequest = frameNumber;
databaseRequest->_timestampFirstRequest = timestamp;
databaseRequest->_priorityFirstRequest = priority;
databaseRequest->_frameNumberLastRequest = frameNumber;
databaseRequest->_timestampLastRequest = timestamp;
databaseRequest->_priorityLastRequest = priority;
databaseRequest->_group = group;
databaseRequest->_terrain = terrain;
databaseRequest->_loadOptions = loadOptions;
databaseRequest->_objectCache = 0;
_fileRequestQueue->addNoLock(databaseRequest.get());
}
}
if (!_startThreadCalled)
{
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_run_mutex);
if (!_startThreadCalled)
{
OSG_INFO << "DatabasePager::startThread()" << std::endl;
if (_databaseThreads.empty())
{
setUpThreads(
osg::DisplaySettings::instance()->getNumOfDatabaseThreadsHint(),
osg::DisplaySettings::instance()->getNumOfHttpDatabaseThreadsHint());
}
_startThreadCalled = true;
_done = false;
for (DatabaseThreadList::const_iterator dt_itr = _databaseThreads.begin();
dt_itr != _databaseThreads.end();
++dt_itr)
{
(*dt_itr)->startThread();
}
}
}
#ifdef WITH_REQUESTNODEFILE_TIMING
totalTime += osg::Timer::instance()->delta_m(start_tick, osg::Timer::instance()->tick());
#endif
}
