diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 493c47f9c..df8bceb96 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -115,6 +115,7 @@ IF(DYNAMIC_OPENSCENEGRAPH) ADD_SUBDIRECTORY(osgtexturerectangle) ADD_SUBDIRECTORY(osgtexturecompression) ADD_SUBDIRECTORY(osgthirdpersonview) + ADD_SUBDIRECTORY(osguserstats) ADD_SUBDIRECTORY(osgvertexprogram) ADD_SUBDIRECTORY(osgvertexattributes) ADD_SUBDIRECTORY(osgvolume) diff --git a/examples/osguserstats/CMakeLists.txt b/examples/osguserstats/CMakeLists.txt new file mode 100644 index 000000000..37c323155 --- /dev/null +++ b/examples/osguserstats/CMakeLists.txt @@ -0,0 +1,6 @@ +#this file is automatically generated + + +SET(TARGET_SRC osguserstats.cpp ) +#### end var setup ### +SETUP_EXAMPLE(osguserstats) diff --git a/examples/osguserstats/osguserstats.cpp b/examples/osguserstats/osguserstats.cpp new file mode 100644 index 000000000..f672f8283 --- /dev/null +++ b/examples/osguserstats/osguserstats.cpp @@ -0,0 +1,373 @@ +/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2010 Robert Osfield + * + * This application is open source and may be redistributed and/or modified + * freely and without restriction, both in commericial and non commericial applications, + * as long as this copyright notice is maintained. + * + * This application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include + +#include +#include + +#include +#include + +#include + + +// The idea of user stats is that you record times or values in the viewer's +// stats, and you also tell the stats handler to watch those values each +// frame. The stats handler can display the stats in three ways: +// - A numeric time beside the stat name +// Requires that an elapsed time be recorded in the viewer's stats for the +// "timeTakenName". +// - A bar in the top bar graph +// Requires that two times (relative to the viewer's start tick) be +// recorded in the viewer's stats for the "beginTimeName" and "endTimeName". +// - A line in the bottom graph +// Requires that an elapsed time be recorded in the viewer's stats for the +// "timeTakenName". + + +// Anything you want to time has to use a consistent name in both the stats +// handler and the viewer stats, so it's a good idea to use constants to make +// sure the names are the same everywhere. +const std::string frameNumberName = "Custom Frame Number"; +const std::string frameTimeName = "Custom Frame Time"; +const std::string customTimeName = "Custom"; +const std::string operation1TimeName = "Operation1"; +const std::string operation2TimeName = "Operation2"; +const std::string otherThreadTimeName = "Thread"; + + +void initUserStats(osgViewer::StatsHandler* statsHandler) +{ + // This line displays the frame number. It's not averaged, just displayed as is. + statsHandler->addUserStatsLine("Frame", osg::Vec4(0.7,0.7,0.7,1), osg::Vec4(0.7,0.7,0.7,0.5), + frameNumberName, 1.0, false, false, "", "", 0.0); + + // This line displays the frame time (from beginning of event to end of draw). No bars. + statsHandler->addUserStatsLine("MS/frame", osg::Vec4(1,0,1,1), osg::Vec4(1,0,1,0.5), + frameTimeName, 1000.0, true, false, "", "", 0.02); + + // This line displays the sum of update and main camera cull times. + statsHandler->addUserStatsLine("Custom", osg::Vec4(1,1,1,1), osg::Vec4(1,1,1,0.5), + customTimeName + " time taken", 1000.0, true, false, customTimeName + " begin", customTimeName + " end", 0.016); + + // This line displays the time taken by a function below ( doSomethingAndTimeIt() ) + statsHandler->addUserStatsLine("Sleep1", osg::Vec4(1,0,0,1), osg::Vec4(1,0,0,0.5), + operation1TimeName + " time taken", 1000.0, true, false, operation1TimeName + " begin", operation1TimeName + " end", 0.016); + + // This line displays the time taken by a function below ( doSomethingAndTimeIt() ) + statsHandler->addUserStatsLine("Sleep2", osg::Vec4(1,0.5,0.5,1), osg::Vec4(1,0.5,0.5,0.5), + operation2TimeName + " time taken", 1000.0, true, false, operation2TimeName + " begin", operation2TimeName + " end", 0.016); + + // This line displays the time taken by a function below ( doSomethingAndTimeIt() ) + statsHandler->addUserStatsLine("Thread", osg::Vec4(0,0.5,0,1), osg::Vec4(0,0.5,0,0.5), + otherThreadTimeName + " time taken", 1000.0, true, false, otherThreadTimeName + " begin", otherThreadTimeName + " end", 0.016); +} + + +void updateUserStats(osgViewer::Viewer& viewer) +{ + // Test the custom stats line by just adding up the update and cull + // times for the viewer main camera for the previous frame. + if (viewer.getViewerStats()->collectStats("update") && viewer.getCamera()->getStats()->collectStats("rendering")) + { + // First get the frame number. The code below assumes that + // updateUserStats() is called after advance(), so the frame number + // that will be returned is for the frame that has just started and is + // not rendered yet. The previous frame is framenumber-1, but we can't + // use that frame's timings because it's probably not finished + // rendering yet (in multithreaded viewer modes). So we'll use the + // timings for framenumber-2 for this demo. + int framenumber = viewer.getFrameStamp()->getFrameNumber(); + + // Get the update time and the viewer main camera's cull time. We use + // getAveragedAttribute() in order to get the actual time elapsed as + // calculated by the stats. + double update = 0.0, cull = 0.0; + viewer.getViewerStats()->getAveragedAttribute("Update traversal time taken", update); + viewer.getCamera()->getStats()->getAveragedAttribute("Cull traversal time taken", cull); + + // Get various begin and end times, note these are not elapsed times + // in a frame but rather the simulation time at those moments. + double eventBegin = 0.0, updateBegin = 0.0, cullEnd = 0.0, drawEnd = 0.0; + viewer.getViewerStats()->getAttribute(framenumber-2, "Event traversal begin time", eventBegin); + viewer.getViewerStats()->getAttribute(framenumber-2, "Update traversal begin time", updateBegin); + viewer.getCamera()->getStats()->getAttribute(framenumber-2, "Cull traversal end time", cullEnd); + viewer.getCamera()->getStats()->getAttribute(framenumber-2, "Draw traversal end time", drawEnd); + + // This line displays the frame number. It's not averaged, just displayed as is. + viewer.getViewerStats()->setAttribute(framenumber, frameNumberName, framenumber); + + // This line displays the frame time (from beginning of event to end of draw). No bars. + viewer.getViewerStats()->setAttribute(framenumber-1, frameTimeName, drawEnd - eventBegin); + + // This line displays the sum of update and main camera cull times. + viewer.getViewerStats()->setAttribute(framenumber-1, customTimeName + " time taken", update+cull); + // Since we give begin and end times that correspond to the begin of + // the update phase and the end of the cull phase, the bar in the + // graph will not correspond to the summed times above if something + // happened between update and cull (as in this demo). Also, we need + // to translate the updateBegin and cullEnd times by one frame since + // we're taking the times for framenumber-2 but using them to display + // in the framenumber-1 graph. + viewer.getViewerStats()->setAttribute(framenumber-1, customTimeName + " begin", updateBegin + (1.0/60.0)); + viewer.getViewerStats()->setAttribute(framenumber-1, customTimeName + " end", cullEnd + (1.0/60.0)); + } +} + + +/// Utility function you call before something you want to time, so that the +/// recorded times will all be consistent using the viewer's time. +void startTiming(osgViewer::Viewer& viewer, const std::string& name) +{ + osg::Timer_t tick = osg::Timer::instance()->tick(); + double currentTime = osg::Timer::instance()->delta_s(viewer.getStartTick(), tick); + int framenumber = viewer.getFrameStamp()->getFrameNumber(); + + viewer.getViewerStats()->setAttribute(framenumber, name + " begin", currentTime); +} + +/// Utility function you call after something you want to time, so that the +/// recorded times will all be consistent using the viewer's time. +void endTiming(osgViewer::Viewer& viewer, const std::string& name) +{ + osg::Timer_t tick = osg::Timer::instance()->tick(); + double currentTime = osg::Timer::instance()->delta_s(viewer.getStartTick(), tick); + int framenumber = viewer.getFrameStamp()->getFrameNumber(); + + viewer.getViewerStats()->setAttribute(framenumber, name + " end", currentTime); + + double begin = 0.0; + double elapsed = 0.0; + if (viewer.getViewerStats()->getAttribute(framenumber, name + " begin", begin)) + { + elapsed = currentTime - begin; + } + + viewer.getViewerStats()->setAttribute(framenumber, name + " time taken", elapsed); +} + + +/// Will just sleep for the given number of milliseconds in the same thread +/// as the caller, recording the time taken in the viewer's stats. +void doSomethingAndTimeIt(osgViewer::Viewer& viewer, const std::string& name, double milliseconds) +{ + startTiming(viewer, name); + + //------------------------------------------------------------ + // Your processing goes here. + + // Do nothing for the specified number of milliseconds, just so we can + // see it in the stats. + osg::Timer_t startTick = osg::Timer::instance()->tick(); + while (osg::Timer::instance()->delta_m(startTick, osg::Timer::instance()->tick()) < milliseconds) + { + OpenThreads::Thread::YieldCurrentThread(); + } + //------------------------------------------------------------ + + endTiming(viewer, name); +} + + +/// Thread that will sleep for the given number of milliseconds, recording +/// the time taken in the viewer's stats, whenever its process() method is +/// called. +class UselessThread : public OpenThreads::Thread +{ +public: + UselessThread(osgViewer::Viewer& viewer, double timeToRun) + : _viewer(viewer) + , _timeToRun(timeToRun) + , _done(false) + , _process(false) + { + } + + void run() + { + while (!_done) + { + if (_process) + { + startTiming(_viewer, otherThreadTimeName); + + //------------------------------------------------------------ + // Your processing goes here. + + // Do nothing for the specified number of milliseconds, just so we can + // see it in the stats. + osg::Timer_t startTick = osg::Timer::instance()->tick(); + while (osg::Timer::instance()->delta_m(startTick, osg::Timer::instance()->tick()) < _timeToRun) + { + OpenThreads::Thread::YieldCurrentThread(); + } + //------------------------------------------------------------ + + endTiming(_viewer, otherThreadTimeName); + + _process = false; + } + else + { + OpenThreads::Thread::microSleep(50); + } + } + } + + int cancel() + { + _done = true; + return OpenThreads::Thread::cancel(); + } + + void process() + { + _process = true; + } + +protected: + osgViewer::Viewer& _viewer; + double _timeToRun; + bool _done; + bool _process; +}; + + +int main(int argc, char** argv) +{ + // use an ArgumentParser object to manage the program arguments. + osg::ArgumentParser arguments(&argc,argv); + + arguments.getApplicationUsage()->setApplicationName(arguments.getApplicationName()); + arguments.getApplicationUsage()->setDescription(arguments.getApplicationName()+" is the standard OpenSceneGraph example which loads and visualises 3d models."); + arguments.getApplicationUsage()->setCommandLineUsage(arguments.getApplicationName()+" [options] filename ..."); + arguments.getApplicationUsage()->addCommandLineOption("--image ","Load an image and render it on a quad"); + arguments.getApplicationUsage()->addCommandLineOption("--dem ","Load an image/DEM and render it on a HeightField"); + arguments.getApplicationUsage()->addCommandLineOption("--login ","Provide authentication information for http file access."); + + osgViewer::Viewer viewer(arguments); + + unsigned int helpType = 0; + if ((helpType = arguments.readHelpType())) + { + arguments.getApplicationUsage()->write(std::cout, helpType); + return 1; + } + + // report any errors if they have occurred when parsing the program arguments. + if (arguments.errors()) + { + arguments.writeErrorMessages(std::cout); + return 1; + } + + if (arguments.argc()<=1) + { + arguments.getApplicationUsage()->write(std::cout,osg::ApplicationUsage::COMMAND_LINE_OPTION); + return 1; + } + + std::string url, username, password; + while(arguments.read("--login",url, username, password)) + { + if (!osgDB::Registry::instance()->getAuthenticationMap()) + { + osgDB::Registry::instance()->setAuthenticationMap(new osgDB::AuthenticationMap); + osgDB::Registry::instance()->getAuthenticationMap()->addAuthenticationDetails( + url, + new osgDB::AuthenticationDetails(username, password) + ); + } + } + + viewer.setCameraManipulator(new osgGA::TrackballManipulator); + + // add the state manipulator + viewer.addEventHandler( new osgGA::StateSetManipulator(viewer.getCamera()->getOrCreateStateSet()) ); + + // add the thread model handler + viewer.addEventHandler(new osgViewer::ThreadingHandler); + + // add the window size toggle handler + viewer.addEventHandler(new osgViewer::WindowSizeHandler); + + // add the stats handler + osgViewer::StatsHandler* statsHandler = new osgViewer::StatsHandler; + viewer.addEventHandler(statsHandler); + + initUserStats(statsHandler); + + // add the help handler + viewer.addEventHandler(new osgViewer::HelpHandler(arguments.getApplicationUsage())); + + // load the data + osg::ref_ptr loadedModel = osgDB::readNodeFiles(arguments); + if (!loadedModel) + { + std::cout << arguments.getApplicationName() <<": No data loaded" << std::endl; + return 1; + } + + // any option left unread are converted into errors to write out later. + arguments.reportRemainingOptionsAsUnrecognized(); + + // report any errors if they have occurred when parsing the program arguments. + if (arguments.errors()) + { + arguments.writeErrorMessages(std::cout); + return 1; + } + + + // optimize the scene graph, remove redundant nodes and state etc. + osgUtil::Optimizer optimizer; + optimizer.optimize(loadedModel.get()); + + viewer.setSceneData( loadedModel.get() ); + + viewer.realize(); + + // Start up a thread that will just run for a fixed time each frame, in + // parallel to the frame loop. + UselessThread thread(viewer, 6.0); + thread.start(); + + while (!viewer.done()) + { + viewer.advance(); + + updateUserStats(viewer); + + // Eat up some time on the viewer thread before the event phase. + doSomethingAndTimeIt(viewer, operation1TimeName, 2.0); + + // Start taking some time on the other thread. + thread.process(); + + viewer.eventTraversal(); + viewer.updateTraversal(); + + // Eat up some time on the viewer thread between the update and cull + // phases. + doSomethingAndTimeIt(viewer, operation2TimeName, 3.0); + + viewer.renderingTraversals(); + } + + thread.cancel(); + while (thread.isRunning()) + { + OpenThreads::Thread::YieldCurrentThread(); + } + +} diff --git a/include/osgViewer/ViewerEventHandlers b/include/osgViewer/ViewerEventHandlers index ba3c7fec9..bbe87bd04 100644 --- a/include/osgViewer/ViewerEventHandlers +++ b/include/osgViewer/ViewerEventHandlers @@ -110,10 +110,40 @@ class OSGVIEWER_EXPORT StatsHandler : public osgGA::GUIEventHandler /** This allows the user to register additional stats lines that will be added to the graph. The stats for these will be gotten from the - viewer (viewer->getViewerStats()). */ + viewer (viewer->getViewerStats()). The stats can be displayed in + either or all of the following ways: + + - A numeric time beside the stat name + Requires that an elapsed time be recorded in the viewer's stats + for the "timeTakenName". + - A bar in the top bar graph + Requires that two times (relative to the viewer's start tick) be + recorded in the viewer's stats for the "beginTimeName" and + "endTimeName". + - A line in the bottom graph + Requires that an elapsed time be recorded in the viewer's stats + for the "timeTakenName" and that the value be used as an average. + + If you don't want a numeric value and a line in the bottom line + graph for your value, pass the empty string for timeTakenName. If + you don't want a bar in the graph, pass the empty string for + beginTimeName and endTimeName. + + @param label The label to be displayed to identify the line in the stats. + @param textColor Will be used for the text label, the numeric time and the bottom line graph. + @param barColor Will be used for the bar in the top bar graph. + @param timeTakenName The name to be queried in the viewer stats for the numeric value (also used for the bottom line graph). + @param multiplier The multiplier to apply to the numeric value before displaying it in the stats. + @param average Whether to use the average value of the numeric value. + @param averageInInverseSpace Whether to average in inverse space (used for frame rate). + @param beginTimeName The name to be queried in the viewer stats for the begin time for the top bar graph. + @param endTimeName The name to be queried in the viewer stats for the end time for the top bar graph. + @param maxValue The value to use as maximum in the bottom line graph. Stats will be clamped between 0 and that value, and it will be the highest visible value in the graph. + */ void addUserStatsLine(const std::string& label, const osg::Vec4& textColor, const osg::Vec4& barColor, const std::string& timeTakenName, float multiplier, bool average, bool averageInInverseSpace, const std::string& beginTimeName, const std::string& endTimeName, float maxValue); + void removeUserStatsLine(const std::string& label); protected: @@ -169,6 +199,7 @@ protected: float _startBlocks; float _leftPos; float _characterSize; + float _lineHeight; struct UserStatsLine { diff --git a/src/osgViewer/StatsHandler.cpp b/src/osgViewer/StatsHandler.cpp index 5e5119202..e663d1afd 100644 --- a/src/osgViewer/StatsHandler.cpp +++ b/src/osgViewer/StatsHandler.cpp @@ -46,7 +46,8 @@ StatsHandler::StatsHandler(): _font("fonts/arial.ttf"), _startBlocks(150.0f), _leftPos(10.0f), - _characterSize(20.0f) + _characterSize(20.0f), + _lineHeight(1.5f) { _camera = new osg::Camera; _camera->setRenderer(new Renderer(_camera.get())); @@ -1146,7 +1147,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) frameRateValue->setDrawCallback(new AveragedValueTextDrawCallback(viewer->getViewerStats(),"Frame rate",-1, true, 1.0)); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } @@ -1180,21 +1181,23 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) updateThreadingModelText(); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } float topOfViewerStats = pos.y() + _characterSize; - double cameraSize = 4.5 * cameras.size(); + double cameraSize = _lineHeight * 3.0 * cameras.size(); if(!acquireGPUStats) //reduce size if GPU stats not needed { - cameraSize -= 1.5 * cameras.size(); + cameraSize -= _lineHeight * cameras.size(); } + double userStatsLinesSize = _lineHeight * _userStatsLines.size(); + _statsGeode->addDrawable(createBackgroundRectangle( pos + osg::Vec3(-backgroundMargin, _characterSize + backgroundMargin, 0), _statsWidth - 2 * backgroundMargin, - (3 + cameraSize) * _characterSize + 2 * backgroundMargin, + (3 + cameraSize + userStatsLinesSize) * _characterSize + 2 * backgroundMargin, backgroundColor) ); // Add user stats lines before the normal viewer and per-camera stats. @@ -1206,7 +1209,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) createTimeStatsLine(line.label, pos, line.textColor, line.barColor, viewer->getViewerStats(), viewer->getViewerStats(), line.timeTakenName, line.multiplier, line.average, line.averageInInverseSpace, line.beginTimeName, line.endTimeName); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } { @@ -1215,7 +1218,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) createTimeStatsLine("Event", pos, colorUpdate, colorUpdateAlpha, viewer->getViewerStats(), viewer->getViewerStats(), "Event traversal time taken", 1000.0, true, false, "Event traversal begin time", "Event traversal end time"); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } { @@ -1224,7 +1227,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) createTimeStatsLine("Update", pos, colorUpdate, colorUpdateAlpha, viewer->getViewerStats(), viewer->getViewerStats(), "Update traversal time taken", 1000.0, true, false, "Update traversal begin time", "Update traversal end time"); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } pos.x() = _leftPos; @@ -1275,7 +1278,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase* viewer) for (unsigned int i = 0; i < _userStatsLines.size(); ++i) { UserStatsLine& line = _userStatsLines[i]; - if (line.average) + if (!line.timeTakenName.empty() && line.average) { statsGraph->addStatGraph(viewer->getViewerStats(), viewer->getViewerStats(), line.textColor, line.maxValue, line.timeTakenName); } @@ -1658,7 +1661,7 @@ void StatsHandler::createCameraTimeStats(osg::Vec3& pos, bool acquireGPUStats, o createTimeStatsLine("Cull", pos, colorCull, colorCullAlpha, viewerStats, stats, "Cull traversal time taken", 1000.0, true, false, "Cull traversal begin time", "Cull traversal end time"); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } { @@ -1667,7 +1670,7 @@ void StatsHandler::createCameraTimeStats(osg::Vec3& pos, bool acquireGPUStats, o createTimeStatsLine("Draw", pos, colorDraw, colorDrawAlpha, viewerStats, stats, "Draw traversal time taken", 1000.0, true, false, "Draw traversal begin time", "Draw traversal end time"); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } if (acquireGPUStats) @@ -1677,7 +1680,7 @@ void StatsHandler::createCameraTimeStats(osg::Vec3& pos, bool acquireGPUStats, o createTimeStatsLine("GPU", pos, colorGPU, colorGPUAlpha, viewerStats, stats, "GPU draw time taken", 1000.0, true, false, "GPU draw begin time", "GPU draw end time"); - pos.y() -= _characterSize*1.5f; + pos.y() -= _characterSize*_lineHeight; } }