vulkanscenegraph显示倾斜模型(6.5)-vsg::DatabasePager
前言
上章深入分析了帧循环过程中,多线程下的记录与提交机制。本章将分析vsg::DatabasePager在更新场景图过程中的作用,进一步揭露vsg中场景图管理机制,并通过分析代码,详细解释vsg中场景图管理机制中的节点添加、节点删除、节点加载过程。同时本章也作为vulkanscenegraph显示倾斜模型主体内容的最后一章。
目录
- 1 vsg::PagedLOD
- 2 vsg::DatabasePager
- 3 帧循环过程中的场景图更新机制
1 vsg::PagedLOD
vsg::PagedLOD作为VSG中的动态细节级别节点,在场景遍历过程中通过计算当前视口下子节点是否可见,从而选择或加载最优精度的子节点。
上图为vsg::PagedLOD的继承关系,其继承自vsg::Node,可作为vsg场景图中普通的节点来使用,如作为组节点vsg::Group的子节点等。
vsg::PagedLOD通过重写read和write,支持自定义序列化(如从内存写到磁盘)和反序列化(如从磁盘写到内存)。
void PagedLOD::read(Input& input)
{Node::read(input);input.read("bound", bound);input.read("child.minimumScreenHeightRatio", children[0].minimumScreenHeightRatio);input.read("child.filename", filename);children[0].node = nullptr;if (input.filename){auto path = filePath(input.filename);if (path){filename = (path / filename).lexically_normal();}}input.read("child.minimumScreenHeightRatio", children[1].minimumScreenHeightRatio);input.read("child.node", children[1].node);options = Options::create_if(input.options, *input.options);
}
void PagedLOD::write(Output& output) const
{Node::write(output);output.write("bound", bound);output.write("child.minimumScreenHeightRatio", children[0].minimumScreenHeightRatio);output.write("child.filename", filename);output.write("child.minimumScreenHeightRatio", children[1].minimumScreenHeightRatio);output.write("child.node", children[1].node);
}
其中读写涉及到的变量如下:
Path filename;dsphere bound;using Children = std::array<Child, 2>;Children children;
bound(dsphere)表示节点范围,children(Children)表示子节点,数量为2,其中第一个为动态子节点,即可卸载和加载的节点,其对应的路径为filename(PATH),相比常规子节点(第二个子节点),其分辨率会更高(对应的模型更精细)。
struct Child{double minimumScreenHeightRatio = 0.0; // 0.0 is always visibleref_ptr<Node> node;};
Child结构体包含两个变量,其中minimumScreenHeightRatio用于控制子节点是否可见。
和其它节点类似,vsg::PagedLOD的访问通过vsg::Visitor或其子类实现,vsg::PagedLOD动态子节点的加载与否由vsg::RecordTraversal遍历节点过程中判断,具体实现在void RecordTraversal::apply(const PagedLOD& plod)函数中(由于太长此处不全部列出)。
void RecordTraversal::apply(const PagedLOD& plod)函数的实现共分为三个部分:判断节点是否可见、判断高分辨率的节点(第一个子节点)是否可见、判断低分辨率的节点(第二个子节点)是否可见。
const auto& sphere = plod.bound;auto frameCount = _frameStamp->frameCount;// check if lod bounding sphere is in view frustum.auto lodDistance = _state->lodDistance(sphere);if (lodDistance < 0.0){if ((frameCount - plod.frameHighResLastUsed) > 1 && _culledPagedLODs){_culledPagedLODs->highresCulled.emplace_back(&plod);}return;}
其中调用的loadDistance函数返回值小于0表示节点所在范围在视锥体范围外,此时节点不可见。
if (child.node){// high res visible and available so traverse itchild.node->accept(*this);return;}else if (_databasePager){auto priority = sphere.r / cutoff;exchange_if_greater(plod.priority, priority);auto previousRequestCount = plod.requestCount.fetch_add(1);if (previousRequestCount == 0){// we are the first request so tell the databasePager about it_databasePager->request(ref_ptr<PagedLOD>(const_cast<PagedLOD*>(&plod)));}else{//debug("repeat request ",&plod,", ",plod.filename,", ",plod.requestCount.load(),", plod.requestStatus = ",plod.requestStatus.load());}}
上述代码的主体为当高分辨率子节点可见且为空时,动态加载节点。节点的加载通过vsg::DatabasePager发出请求实现。
{const auto& child = plod.children[1];auto cutoff = lodDistance * child.minimumScreenHeightRatio;bool child_visible = sphere.r > cutoff;if (child_visible){if (child.node){child.node->accept(*this);}}}
上述代码的主体为当低分辨率子节点可见时,则遍历节点。
2 vsg::DatabasePager
vsg::DatabasePager为多线程数据库分页器,用于读取、编译加载的PagedLOD子图,用新加载的子图更新场景图,并修剪过期的PagedLOD子图。vsg::DatabasePager中的主要内容可分为关键的容器、关键的函数两个部分,其中关键容器包含线程池、请求池、融合池、删除池,而关键的函数包含节点请求函数、更新场景图、多线程执行函数。
2.1 关键的容器
std::list<std::thread> threads;protected:virtual ~DatabasePager();void requestDiscarded(PagedLOD* plod);ref_ptr<ActivityStatus> _status;ref_ptr<DatabaseQueue> _requestQueue;ref_ptr<DatabaseQueue> _toMergeQueue;ref_ptr<DeleteQueue> _deleteQueue;
上述代码为vsg::DatabasePager中的部分成员变量,其中threads为线程池、_requestQueue为请求池、_toMergeQueue为融合池、_deleteQueue为删除池,如下图所示:
DatabasePager::DatabasePager()
{if (!_status) _status = ActivityStatus::create();culledPagedLODs = CulledPagedLODs::create();_requestQueue = DatabaseQueue::create(_status);_toMergeQueue = DatabaseQueue::create(_status);_deleteQueue = DeleteQueue::create(_status);pagedLODContainer = PagedLODContainer::create(4000);
}
上述代码为请求池、融合池、删除池的初始化,而线程池的初始化在vsg::DatabasePager::start函数中,包括读线程和删除线程。
2.1 关键的函数
关键的函数包含节点请求函数、更新场景图、多线程执行函数。
2.1.1 节点请求函数
void DatabasePager::request(ref_ptr<PagedLOD> plod)
{++numActiveRequests;bool hasPending = false;{std::scoped_lock<std::mutex> lock(pendingPagedLODMutex);hasPending = plod->pending.valid();}if (!hasPending){if (compare_exchange(plod->requestStatus, PagedLOD::NoRequest, PagedLOD::ReadRequest)){// debug("DatabasePager::request(", plod.get(), ") adding to requestQueue ", plod->filename, ", ", plod->priority, " plod=", plod.get());_requestQueue->add(plod);}else{// debug("Attempted DatabasePager::request(", plod.get(), ") but plod.requestState() = ", plod->requestStatus.load(), " is not NoRequest");}}else{// debug("Attempted DatabasePager::request(", plod.get(), ") but plod.pending is not null.");}
}
上节介绍vsg::RecordTraversal::apply(const PagedLOD&)函数中,当高分辨率的节点(第一个子节点)可见且为空时,则调用vsg::DatabasePager::request方法请求节点。上述代码,将传入的PagedLOD放置到请求池中。
2.1.2 更新场景图
更新场景图主要的内容为,将融合池中的节点合并到场景图中,同时将场景图中满足一定帧数未绘制的节点放置到删除池中用于后续在删除线程中删除节点。对应的函数为vsg::DatabasePager::unpdateSceneGraph。
auto nodes = _toMergeQueue->take_all(cr);
上述代码为DatabasePager.cpp中第267行代码,即从融合池中取出所有加载好的节点。
for (auto& plod : nodes){if (compare_exchange(plod->requestStatus, PagedLOD::MergeRequest, PagedLOD::Merging)){debug(" Merged ", plod->filename, " after ", plod->requestCount.load(), " priority ", plod->priority.load(), " ", frameCount - plod->frameHighResLastUsed.load(), " plod = ", plod);{
#if LOCAL_MUTEXstd::scoped_lock<std::mutex> lock(pendingPagedLODMutex);
#endifplod->children[0].node = plod->pending;}plod->requestStatus.exchange(PagedLOD::NoRequest);}}
上述代码为DatabasePager.cpp中357-371行代码,即将加载好的节点(plod->pending)合并到场景图中。
if ((nodes.size() + total) > targetMaxNumPagedLODWithHighResSubgraphs){uint32_t numPagedLODHighRestSubgraphsToRemove = (static_cast<uint32_t>(nodes.size()) + total) - targetMaxNumPagedLODWithHighResSubgraphs;uint32_t targetNumInactive = (numPagedLODHighRestSubgraphsToRemove < pagedLODContainer->inactiveList.count) ? (pagedLODContainer->inactiveList.count - numPagedLODHighRestSubgraphsToRemove) : 0;debug("Need to remove, inactive count = ", pagedLODContainer->inactiveList.count, ", target = ", targetNumInactive);for (uint32_t index = pagedLODContainer->inactiveList.head; (index != 0) && (pagedLODContainer->inactiveList.count > targetNumInactive);){auto& element = elements[index];index = element.next;if (compare_exchange(element.plod->requestStatus, PagedLOD::NoRequest, PagedLOD::DeleteRequest)){ref_ptr<PagedLOD> plod = element.plod;plod->children[0].node = nullptr;plod->requestCount.exchange(0);plod->requestStatus.exchange(PagedLOD::NoRequest);deleteList.push_back(plod->pending);plod->pending = {};deleteList.push_back(plod);pagedLODContainer->remove(plod);debug(" trimming ", plod, " ", plod->filename);}}}
上述代码为DatabasePager.cpp中317-345行代码,即当场景图中的节点树大于阈值(targetMaxNumPagedLODWithHighResSubgraphs)时,将满足一定帧数未绘制的节点放置到删除池中。
2.1.3 多线程执行函数
多线程执行函数包含节点读取和节点删除函数。
auto read_object = vsg::read(plod->filename, plod->options);
上述代码为DatabasePager.cpp中163行代码,即通过给定路径加载节点到内存中。
auto deleteThread = [](ref_ptr<DeleteQueue> deleteQueue, ref_ptr<ActivityStatus> status, const DatabasePager& databasePager, const std::string& threadName) {debug("Started DatabaseThread deletethread");auto local_instrumentation = shareOrDuplicateForThreadSafety(databasePager.instrumentation);if (local_instrumentation) local_instrumentation->setThreadName(threadName);while (status->active()){deleteQueue->wait_then_clear();}debug("Finished DatabaseThread delete thread");};
上述代码为DatabasePager.cpp中201-212行代码,即在线程中清除删除池中的节点。
3 帧循环过程中的场景图更新机制
帧循环过程中场景图的更新一共涉及到两处,更新阶段和记录与提交阶段(参照vulkanscenegraph显示倾斜模型(6)-帧循环-CSDN博客)。
3.1 更新阶段中的场景图更新
void Viewer::update()
{CPU_INSTRUMENTATION_L1_NC(instrumentation, "Viewer update", COLOR_UPDATE);// merge any updates from the DatabasePagerfor (const auto& task : recordAndSubmitTasks){if (task->databasePager){CompileResult cr;task->databasePager->updateSceneGraph(_frameStamp, cr);if (cr.requiresViewerUpdate()) updateViewer(*this, cr);}}// run update operationsupdateOperations->run();// run aniamtionsanimationManager->run(_frameStamp);
}
在更新阶段,通过调用vsg::DatabasePager的updateSceneGraph函数,实现场景图的更新,包含将融合池中的节点合并到场景图中,同时将场景图中满足一定帧数未绘制的节点放置到删除池中用于后续在删除线程中删除节点,总结为场景图的节点添加与节点删除过程。
3.2 记录与提交阶段的场景图更新
{COMMAND_BUFFER_INSTRUMENTATION(instrumentation, *commandBuffer, "CommandGraph record", COLOR_RECORD)traverse(*recordTraversal);}
上述代码为CommandGraph.cpp中135-138行代码,即通过vsg::RecordTraversal遍历场景图实现命令的录制,由本章第一节可知,当vsg::RecordTraversal遍历vsg::PagedLOD时,当高分辨率的节点(第一个子节点)可见且为空时,则调用vsg::DatabasePager::request方法请求节点。
文末:本章深入分析了vsg::DatabasePager在更新场景图过程中的作用,场景图的更新包含节点加载、节点添加和节点删除。在vsg渲染帧循环中,更新阶段、记录与提交阶段涉及场景图的更新。vsg::DatabasePager与vsg::PagedLOD、vsg::RecordTraversal实现了数据的动态加载与卸载,有效支持了大规模场景的高效加载与内存管理。同时本章将作为vulkanscenegraph显示倾斜模型主体内容的最后一章,下章将对整个系列作一个总结。