#include "stats.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cachestats.hpp" namespace Resource { namespace { constexpr float statsWidth = 1280.0f; constexpr float statsHeight = 1024.0f; constexpr float characterSize = 17.0f; constexpr float backgroundMargin = 5; constexpr float backgroundSpacing = 3; constexpr float maxStatsHeight = 420.0f; constexpr std::size_t pageSize = static_cast((maxStatsHeight - 2 * backgroundMargin) / characterSize); constexpr int statsHandlerKey = osgGA::GUIEventAdapter::KEY_F4; const VFS::Path::Normalized fontName("Fonts/DejaVuLGCSansMono.ttf"); bool collectStatRendering = false; bool collectStatCameraObjects = false; bool collectStatViewerObjects = false; bool collectStatResource = false; bool collectStatGPU = false; bool collectStatEvent = false; bool collectStatFrameRate = false; bool collectStatUpdate = false; bool collectStatEngine = false; std::vector generateAllStatNames() { constexpr std::size_t itemsPerPage = 24; constexpr std::string_view firstPage[] = { "FrameNumber", "", "Loading", "Compiling", "WorkQueue", "WorkThread", "UnrefQueue", "", "Texture", "StateSet", "Composite", "", "Mechanics Actors", "Mechanics Objects", "", "Physics Actors", "Physics Objects", "Physics Projectiles", "Physics HeightFields", "", "Lua UsedMemory", "", "", "", }; static_assert(std::size(firstPage) == itemsPerPage); constexpr std::string_view caches[] = { "Node", "Shape", "Shape Instance", "Image", "Nif", "Keyframe", "BSShader Material", "Groundcover Chunk", "Object Chunk", "Terrain Chunk", "Terrain Texture", "Land", "Blending Rules", }; constexpr std::string_view cellPreloader[] = { "CellPreloader Count", "CellPreloader Added", "CellPreloader Evicted", "CellPreloader Loaded", "CellPreloader Expired", }; constexpr std::string_view navMesh[] = { "NavMesh Jobs", "NavMesh Removing", "NavMesh Updating", "NavMesh Delayed", "NavMesh Pushed", "NavMesh Processing", "NavMesh DbJobs Write", "NavMesh DbJobs Read", "NavMesh DbCache Get", "NavMesh DbCache Hit", "NavMesh CacheSize", "NavMesh UsedTiles", "NavMesh CachedTiles", "NavMesh Cache Get", "NavMesh Cache Hit", "NavMesh Recast Tiles", "NavMesh Recast Objects", "NavMesh Recast Heightfields", "NavMesh Recast Water", }; std::vector statNames; for (std::string_view name : firstPage) statNames.emplace_back(name); for (std::size_t i = 0; i < std::size(caches); ++i) { Resource::addCacheStatsAttibutes(caches[i], statNames); if ((i + 1) % 5 != 0) statNames.emplace_back(); } for (std::string_view name : cellPreloader) statNames.emplace_back(name); while (statNames.size() % itemsPerPage != 0) statNames.emplace_back(); for (std::string_view name : navMesh) statNames.emplace_back(name); return statNames; } void setupStatCollection() { const char* envList = getenv("OPENMW_OSG_STATS_LIST"); if (envList == nullptr) return; std::string_view kwList(envList); auto kwBegin = kwList.begin(); while (kwBegin != kwList.end()) { auto kwEnd = std::find(kwBegin, kwList.end(), ';'); const auto kw = kwList.substr(std::distance(kwList.begin(), kwBegin), std::distance(kwBegin, kwEnd)); if (kw == "gpu") collectStatGPU = true; else if (kw == "event") collectStatEvent = true; else if (kw == "frame_rate") collectStatFrameRate = true; else if (kw == "update") collectStatUpdate = true; else if (kw == "engine") collectStatEngine = true; else if (kw == "rendering") collectStatRendering = true; else if (kw == "cameraobjects") collectStatCameraObjects = true; else if (kw == "viewerobjects") collectStatViewerObjects = true; else if (kw == "resource") collectStatResource = true; else if (kw == "times") { collectStatGPU = true; collectStatEvent = true; collectStatFrameRate = true; collectStatUpdate = true; collectStatEngine = true; collectStatRendering = true; } if (kwEnd == kwList.end()) break; kwBegin = std::next(kwEnd); } } osg::ref_ptr createBackgroundRectangle( const osg::Vec3& pos, const float width, const float height, const osg::Vec4& color) { osg::ref_ptr geometry = new osg::Geometry; geometry->setUseDisplayList(false); osg::ref_ptr stateSet = new osg::StateSet; geometry->setStateSet(stateSet); osg::ref_ptr vertices = new osg::Vec3Array; vertices->push_back(osg::Vec3(pos.x(), pos.y(), 0)); vertices->push_back(osg::Vec3(pos.x(), pos.y() - height, 0)); vertices->push_back(osg::Vec3(pos.x() + width, pos.y() - height, 0)); vertices->push_back(osg::Vec3(pos.x() + width, pos.y(), 0)); geometry->setVertexArray(vertices); osg::ref_ptr colors = new osg::Vec4Array; colors->push_back(color); geometry->setColorArray(colors, osg::Array::BIND_OVERALL); osg::ref_ptr base = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_FAN, 0); base->push_back(0); base->push_back(1); base->push_back(2); base->push_back(3); geometry->addPrimitiveSet(base); return geometry; } osg::ref_ptr getMonoFont(const VFS::Manager& vfs) { if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf") && vfs.exists(fontName)) { const Files::IStreamPtr streamPtr = vfs.get(fontName); return osgText::readRefFontStream(*streamPtr); } return nullptr; } class SetFontVisitor : public osg::NodeVisitor { public: SetFontVisitor(osgText::Font* font) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mFont(font) { } void apply(osg::Drawable& node) override { if (osgText::Text* text = dynamic_cast(&node)) { text->setFont(mFont); } } private: osgText::Font* mFont; }; } Profiler::Profiler(bool offlineCollect, const VFS::Manager& vfs) : mOfflineCollect(offlineCollect) , mTextFont(getMonoFont(vfs)) { _characterSize = characterSize; _font.clear(); setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3); setupStatCollection(); } void Profiler::setUpFonts() { if (mTextFont != nullptr) { SetFontVisitor visitor(mTextFont); _switch->accept(visitor); } mInitFonts = true; } bool Profiler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) { osgViewer::ViewerBase* viewer = nullptr; bool handled = StatsHandler::handle(ea, aa); if (_initialized && !mInitFonts) setUpFonts(); auto* view = dynamic_cast(&aa); if (view) viewer = view->getViewerBase(); if (viewer != nullptr) { // Add/remove openmw stats to the osd as necessary viewer->getViewerStats()->collectStats("engine", _statsType >= StatsHandler::StatsType::VIEWER_STATS); if (mOfflineCollect) collectStatistics(*viewer); } return handled; } StatsHandler::StatsHandler(bool offlineCollect, const VFS::Manager& vfs) : mOfflineCollect(offlineCollect) , mSwitch(new osg::Switch) , mCamera(new osg::Camera) , mTextFont(getMonoFont(vfs)) , mStatNames(generateAllStatNames()) { osg::ref_ptr stateset = mSwitch->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); #ifdef OSG_GL1_AVAILABLE stateset->setAttribute(new osg::PolygonMode(), osg::StateAttribute::PROTECTED); #endif mCamera->getOrCreateStateSet()->setGlobalDefaults(); mCamera->setRenderer(new osgViewer::Renderer(mCamera.get())); mCamera->setProjectionResizePolicy(osg::Camera::FIXED); mCamera->addChild(mSwitch); } bool StatsHandler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) { if (ea.getHandled()) return false; switch (ea.getEventType()) { case (osgGA::GUIEventAdapter::KEYDOWN): { if (ea.getKey() == statsHandlerKey) { osgViewer::View* const view = dynamic_cast(&aa); if (view == nullptr) return false; osgViewer::ViewerBase* const viewer = view->getViewerBase(); if (viewer == nullptr) return false; toggle(*viewer); if (mOfflineCollect) collectStatistics(*viewer); aa.requestRedraw(); return true; } break; } case osgGA::GUIEventAdapter::RESIZE: { setWindowSize(ea.getWindowWidth(), ea.getWindowHeight()); break; } default: break; } return false; } void StatsHandler::setWindowSize(int width, int height) { if (width <= 0 || height <= 0) return; mCamera->setViewport(0, 0, width, height); if (std::abs(height * statsWidth) <= std::abs(width * statsHeight)) { mCamera->setProjectionMatrix( osg::Matrix::ortho2D(statsWidth - width * statsHeight / height, statsWidth, 0.0, statsHeight)); } else { mCamera->setProjectionMatrix( osg::Matrix::ortho2D(0.0, statsWidth, statsHeight - height * statsWidth / width, statsHeight)); } } void StatsHandler::toggle(osgViewer::ViewerBase& viewer) { if (!mInitialized) { setUpHUDCamera(viewer); setUpScene(viewer); mInitialized = true; } if (mPage == mSwitch->getNumChildren()) { mPage = 0; mCamera->setNodeMask(0); mSwitch->setAllChildrenOff(); viewer.getViewerStats()->collectStats("resource", false); } else { mCamera->setNodeMask(0xffffffff); mSwitch->setSingleChildOn(mPage); viewer.getViewerStats()->collectStats("resource", true); ++mPage; } } void StatsHandler::setUpHUDCamera(osgViewer::ViewerBase& viewer) { // Try GraphicsWindow first so we're likely to get the main viewer window osg::GraphicsContext* context = dynamic_cast(mCamera->getGraphicsContext()); if (!context) { osgViewer::Viewer::Windows windows; viewer.getWindows(windows); if (!windows.empty()) context = windows.front(); else { // No GraphicsWindows were found, so let's try to find a GraphicsContext context = mCamera->getGraphicsContext(); if (!context) { osgViewer::Viewer::Contexts contexts; viewer.getContexts(contexts); if (contexts.empty()) return; context = contexts.front(); } } } mCamera->setGraphicsContext(context); mCamera->setRenderOrder(osg::Camera::POST_RENDER, 11); mCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); mCamera->setViewMatrix(osg::Matrix::identity()); setWindowSize(context->getTraits()->width, context->getTraits()->height); // only clear the depth buffer mCamera->setClearMask(0); mCamera->setAllowEventFocus(false); mCamera->setRenderer(new osgViewer::Renderer(mCamera.get())); } namespace { class ResourceStatsTextDrawCallback : public osg::Drawable::DrawCallback { public: explicit ResourceStatsTextDrawCallback(osg::Stats* stats, std::span statNames) : mStats(stats) , mStatNames(statNames) { } void drawImplementation(osg::RenderInfo& renderInfo, const osg::Drawable* drawable) const override { if (mStats == nullptr) return; osgText::Text* text = (osgText::Text*)(drawable); std::ostringstream viewStr; viewStr.setf(std::ios::left, std::ios::adjustfield); viewStr.width(14); // Used fixed formatting, as scientific will switch to "...e+.." notation for // large numbers of vertices/drawables/etc. viewStr.setf(std::ios::fixed); viewStr.precision(0); const unsigned int frameNumber = renderInfo.getState()->getFrameStamp()->getFrameNumber() - 1; for (const std::string& statName : mStatNames) { if (statName.empty()) viewStr << std::endl; else { double value = 0.0; if (mStats->getAttribute(frameNumber, statName, value)) viewStr << std::setw(8) << value << std::endl; else viewStr << std::setw(8) << "." << std::endl; } } text->setText(viewStr.str()); text->drawImplementation(renderInfo); } private: osg::ref_ptr mStats; std::span mStatNames; }; } void StatsHandler::setUpScene(osgViewer::ViewerBase& viewer) { const osg::Vec4 backgroundColor(0.0, 0.0, 0.0f, 0.3); const osg::Vec4 staticTextColor(1.0, 1.0, 0.0f, 1.0); const osg::Vec4 dynamicTextColor(1.0, 1.0, 1.0f, 1.0); const auto longest = std::max_element(mStatNames.begin(), mStatNames.end(), [](const std::string& lhs, const std::string& rhs) { return lhs.size() < rhs.size(); }); const std::size_t longestSize = longest->size(); const float statNamesWidth = longestSize * characterSize * 0.6 + 2 * backgroundMargin; const float statTextWidth = 7 * characterSize + 2 * backgroundMargin; const float statHeight = pageSize * characterSize + 2 * backgroundMargin; const float width = statNamesWidth + backgroundSpacing + statTextWidth; for (std::size_t offset = 0; offset < mStatNames.size(); offset += pageSize) { osg::ref_ptr group = new osg::Group; group->setCullingActive(false); const std::size_t count = std::min(mStatNames.size() - offset, pageSize); std::span currentStatNames(mStatNames.data() + offset, count); osg::Vec3 pos(statsWidth - width, statHeight - characterSize, 0.0f); group->addChild( createBackgroundRectangle(pos + osg::Vec3(-backgroundMargin, backgroundMargin + characterSize, 0), statNamesWidth, statHeight, backgroundColor)); osg::ref_ptr staticText = new osgText::Text; group->addChild(staticText.get()); staticText->setColor(staticTextColor); staticText->setCharacterSize(characterSize); staticText->setPosition(pos); std::ostringstream viewStr; viewStr.clear(); viewStr.setf(std::ios::left, std::ios::adjustfield); viewStr.width(longestSize); for (const std::string& statName : currentStatNames) viewStr << statName << std::endl; staticText->setText(viewStr.str()); pos.x() += statNamesWidth + backgroundSpacing; group->addChild( createBackgroundRectangle(pos + osg::Vec3(-backgroundMargin, backgroundMargin + characterSize, 0), statTextWidth, statHeight, backgroundColor)); osg::ref_ptr statsText = new osgText::Text; group->addChild(statsText.get()); statsText->setColor(dynamicTextColor); statsText->setCharacterSize(characterSize); statsText->setPosition(pos); statsText->setText(""); statsText->setDrawCallback(new ResourceStatsTextDrawCallback(viewer.getViewerStats(), currentStatNames)); if (mTextFont != nullptr) { staticText->setFont(mTextFont); statsText->setFont(mTextFont); } mSwitch->addChild(group, false); } } void StatsHandler::getUsage(osg::ApplicationUsage& usage) const { usage.addKeyboardMouseBinding(statsHandlerKey, "On screen resource usage stats."); } void collectStatistics(osgViewer::ViewerBase& viewer) { osgViewer::Viewer::Cameras cameras; viewer.getCameras(cameras); for (auto* camera : cameras) { if (collectStatGPU) camera->getStats()->collectStats("gpu", true); if (collectStatRendering) camera->getStats()->collectStats("rendering", true); if (collectStatCameraObjects) camera->getStats()->collectStats("scene", true); } if (collectStatEvent) viewer.getViewerStats()->collectStats("event", true); if (collectStatFrameRate) viewer.getViewerStats()->collectStats("frame_rate", true); if (collectStatUpdate) viewer.getViewerStats()->collectStats("update", true); if (collectStatResource) viewer.getViewerStats()->collectStats("resource", true); if (collectStatViewerObjects) viewer.getViewerStats()->collectStats("scene", true); if (collectStatEngine) viewer.getViewerStats()->collectStats("engine", true); } }