// // Stats.cpp // interface/src/ui // // Created by Lucas Crisman on 22/03/14. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include #include #include "Stats.h" #include "BandwidthRecorder.h" #include "InterfaceConfig.h" #include "Menu.h" #include "Util.h" #include "SequenceNumberStats.h" using namespace std; const int STATS_PELS_PER_LINE = 20; const int STATS_GENERAL_MIN_WIDTH = 165; const int STATS_PING_MIN_WIDTH = 190; const int STATS_GEO_MIN_WIDTH = 240; const int STATS_OCTREE_MIN_WIDTH = 410; Stats* Stats::getInstance() { static Stats stats; return &stats; } Stats::Stats(): _expanded(false), _recentMaxPackets(0), _resetRecentMaxPacketsSoon(true), _generalStatsWidth(STATS_GENERAL_MIN_WIDTH), _pingStatsWidth(STATS_PING_MIN_WIDTH), _geoStatsWidth(STATS_GEO_MIN_WIDTH), _octreeStatsWidth(STATS_OCTREE_MIN_WIDTH), _lastHorizontalOffset(0), _metavoxelInternal(0), _metavoxelLeaves(0), _metavoxelSendProgress(0), _metavoxelSendTotal(0), _metavoxelReceiveProgress(0), _metavoxelReceiveTotal(0) { auto glCanvas = DependencyManager::get(); resetWidth(glCanvas->width(), 0); } void Stats::toggleExpanded() { _expanded = !_expanded; } // called on mouse click release // check for clicks over stats in order to expand or contract them void Stats::checkClick(int mouseX, int mouseY, int mouseDragStartedX, int mouseDragStartedY, int horizontalOffset) { auto glCanvas = DependencyManager::get(); if (0 != glm::compMax(glm::abs(glm::ivec2(mouseX - mouseDragStartedX, mouseY - mouseDragStartedY)))) { // not worried about dragging on stats return; } int statsHeight = 0, statsWidth = 0, statsX = 0, statsY = 0, lines = 0; statsX = horizontalOffset; // top-left stats click lines = _expanded ? 5 : 3; statsHeight = lines * STATS_PELS_PER_LINE + 10; if (mouseX > statsX && mouseX < statsX + _generalStatsWidth && mouseY > statsY && mouseY < statsY + statsHeight) { toggleExpanded(); return; } statsX += _generalStatsWidth; // ping stats click if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { lines = _expanded ? 4 : 3; statsHeight = lines * STATS_PELS_PER_LINE + 10; if (mouseX > statsX && mouseX < statsX + _pingStatsWidth && mouseY > statsY && mouseY < statsY + statsHeight) { toggleExpanded(); return; } statsX += _pingStatsWidth; } // geo stats panel click lines = _expanded ? 4 : 3; statsHeight = lines * STATS_PELS_PER_LINE + 10; if (mouseX > statsX && mouseX < statsX + _geoStatsWidth && mouseY > statsY && mouseY < statsY + statsHeight) { toggleExpanded(); return; } statsX += _geoStatsWidth; // top-right stats click lines = _expanded ? 11 : 3; statsHeight = lines * STATS_PELS_PER_LINE + 10; statsWidth = glCanvas->width() - statsX; if (mouseX > statsX && mouseX < statsX + statsWidth && mouseY > statsY && mouseY < statsY + statsHeight) { toggleExpanded(); return; } } void Stats::resetWidth(int width, int horizontalOffset) { auto glCanvas = DependencyManager::get(); int extraSpace = glCanvas->width() - horizontalOffset -2 - STATS_GENERAL_MIN_WIDTH - (Menu::getInstance()->isOptionChecked(MenuOption::TestPing) ? STATS_PING_MIN_WIDTH -1 : 0) - STATS_GEO_MIN_WIDTH - STATS_OCTREE_MIN_WIDTH; int panels = 4; _generalStatsWidth = STATS_GENERAL_MIN_WIDTH; if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { _pingStatsWidth = STATS_PING_MIN_WIDTH; } else { _pingStatsWidth = 0; panels = 3; } _geoStatsWidth = STATS_GEO_MIN_WIDTH; _octreeStatsWidth = STATS_OCTREE_MIN_WIDTH; if (extraSpace > panels) { _generalStatsWidth += (int) extraSpace / panels; if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { _pingStatsWidth += (int) extraSpace / panels; } _geoStatsWidth += (int) extraSpace / panels; _octreeStatsWidth += glCanvas->width() - (_generalStatsWidth + _pingStatsWidth + _geoStatsWidth + 3); } } // translucent background box that makes stats more readable void Stats::drawBackground(unsigned int rgba, int x, int y, int width, int height) { glm::vec4 color(((rgba >> 24) & 0xff) / 255.0f, ((rgba >> 16) & 0xff) / 255.0f, ((rgba >> 8) & 0xff) / 255.0f, (rgba & 0xff) / 255.0f); DependencyManager::get()->renderQuad(x, y, width, height, color); } bool Stats::includeTimingRecord(const QString& name) { if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayTimingDetails)) { if (name.startsWith("/idle/update/")) { if (name.startsWith("/idle/update/myAvatar/")) { if (name.startsWith("/idle/update/myAvatar/simulate/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandMyAvatarSimulateTiming); } return Menu::getInstance()->isOptionChecked(MenuOption::ExpandMyAvatarTiming); } else if (name.startsWith("/idle/update/otherAvatars/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandOtherAvatarTiming); } return Menu::getInstance()->isOptionChecked(MenuOption::ExpandUpdateTiming); } else if (name.startsWith("/idle/updateGL/paintGL/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPaintGLTiming); } else if (name.startsWith("/paintGL/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPaintGLTiming); } return true; } return false; } // display expanded or contracted stats void Stats::display( const float* color, int horizontalOffset, float fps, int inPacketsPerSecond, int outPacketsPerSecond, int inKbitsPerSecond, int outKbitsPerSecond, int voxelPacketsToProcess) { auto glCanvas = DependencyManager::get(); unsigned int backgroundColor = 0x33333399; int verticalOffset = 0, lines = 0; float scale = 0.10f; float rotation = 0.0f; int font = 2; QLocale locale(QLocale::English); std::stringstream octreeStats; QSharedPointer bandwidthRecorder = DependencyManager::get(); if (_lastHorizontalOffset != horizontalOffset) { resetWidth(glCanvas->width(), horizontalOffset); _lastHorizontalOffset = horizontalOffset; } glPointSize(1.0f); // we need to take one avatar out so we don't include ourselves int totalAvatars = DependencyManager::get()->size() - 1; int totalServers = DependencyManager::get()->size(); lines = _expanded ? 5 : 3; int columnOneWidth = _generalStatsWidth; PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up if (_expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayTimingDetails)) { columnOneWidth = _generalStatsWidth + _pingStatsWidth + _geoStatsWidth; // 3 columns wide... // we will also include room for 1 line per timing record and a header of 4 lines lines += 4; const QMap& allRecords = PerformanceTimer::getAllTimerRecords(); QMapIterator i(allRecords); while (i.hasNext()) { i.next(); if (includeTimingRecord(i.key())) { lines++; } } } drawBackground(backgroundColor, horizontalOffset, 0, columnOneWidth, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; int columnOneHorizontalOffset = horizontalOffset; QString serverNodes = QString("Servers: %1").arg(totalServers); QString avatarNodes = QString("Avatars: %1").arg(totalAvatars); QString framesPerSecond = QString("Framerate: %1 FPS").arg(fps, 3, 'f', 0); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, serverNodes.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarNodes.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, framesPerSecond.toUtf8().constData(), color); // TODO: the display of these timing details should all be moved to JavaScript if (_expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayTimingDetails)) { // Timing details... verticalOffset += STATS_PELS_PER_LINE * 6; // skip 6 lines to be under the other columns drawText(columnOneHorizontalOffset, verticalOffset, scale, rotation, font, "-------------------------------------------------------- Function " "------------------------------------------------------- --msecs- -calls--", color); // First iterate all the records, and for the ones that should be included, insert them into // a new Map sorted by average time... QMap sortedRecords; const QMap& allRecords = PerformanceTimer::getAllTimerRecords(); QMapIterator i(allRecords); while (i.hasNext()) { i.next(); if (includeTimingRecord(i.key())) { float averageTime = (float)i.value().getMovingAverage() / (float)USECS_PER_MSEC; sortedRecords.insertMulti(averageTime, i.key()); } } QMapIterator j(sortedRecords); j.toBack(); while (j.hasPrevious()) { j.previous(); QChar noBreakingSpace = QChar::Nbsp; QString functionName = j.value(); const PerformanceTimerRecord& record = allRecords.value(functionName); QString perfLine = QString("%1: %2 [%3]"). arg(QString(qPrintable(functionName)), 120, noBreakingSpace). arg((float)record.getMovingAverage() / (float)USECS_PER_MSEC, 8, 'f', 3, noBreakingSpace). arg((int)record.getCount(), 6, 10, noBreakingSpace); verticalOffset += STATS_PELS_PER_LINE; drawText(columnOneHorizontalOffset, verticalOffset, scale, rotation, font, perfLine.toUtf8().constData(), color); } } QString packetsPerSecondString = QString("Packets In/Out: %1/%2").arg(inPacketsPerSecond).arg(outPacketsPerSecond); QString averageMegabitsPerSecond = QString("Mbps In/Out: %1/%2"). arg((float)inKbitsPerSecond * 1.0f / 1000.0f). arg((float)outKbitsPerSecond * 1.0f / 1000.0f); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, packetsPerSecondString.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, averageMegabitsPerSecond.toUtf8().constData(), color); verticalOffset = 0; horizontalOffset = _lastHorizontalOffset + _generalStatsWidth + 1; if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { int pingAudio = -1, pingAvatar = -1, pingVoxel = -1, pingOctreeMax = -1; auto nodeList = DependencyManager::get(); SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); pingAudio = audioMixerNode ? audioMixerNode->getPingMs() : -1; pingAvatar = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; // Now handle voxel servers, since there could be more than one, we average their ping times unsigned long totalPingOctree = 0; int octreeServerCount = 0; nodeList->eachNode([&totalPingOctree, &pingOctreeMax, &octreeServerCount](const SharedNodePointer& node){ // TODO: this should also support entities if (node->getType() == NodeType::EntityServer) { totalPingOctree += node->getPingMs(); octreeServerCount++; if (pingOctreeMax < node->getPingMs()) { pingOctreeMax = node->getPingMs(); } } }); if (octreeServerCount) { pingVoxel = totalPingOctree / octreeServerCount; } lines = _expanded ? 4 : 3; // only draw our background if column one didn't draw a wide background if (columnOneWidth == _generalStatsWidth) { drawBackground(backgroundColor, horizontalOffset, 0, _pingStatsWidth, lines * STATS_PELS_PER_LINE + 10); } horizontalOffset += 5; QString audioPing; if (pingAudio >= 0) { audioPing = QString("Audio ping: %1").arg(pingAudio); } else { audioPing = QString("Audio ping: --"); } QString avatarPing; if (pingAvatar >= 0) { avatarPing = QString("Avatar ping: %1").arg(pingAvatar); } else { avatarPing = QString("Avatar ping: --"); } QString voxelAvgPing; if (pingVoxel >= 0) { voxelAvgPing = QString("Entities avg ping: %1").arg(pingVoxel); } else { voxelAvgPing = QString("Entities avg ping: --"); } verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, audioPing.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarPing.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, voxelAvgPing.toUtf8().constData(), color); if (_expanded) { QString voxelMaxPing; if (pingVoxel >= 0) { // Average is only meaningful if pingVoxel is valid. voxelMaxPing = QString("Voxel max ping: %1").arg(pingOctreeMax); } else { voxelMaxPing = QString("Voxel max ping: --"); } verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, voxelMaxPing.toUtf8().constData(), color); } verticalOffset = 0; horizontalOffset = _lastHorizontalOffset + _generalStatsWidth + _pingStatsWidth + 2; } MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); glm::vec3 avatarPos = myAvatar->getPosition(); lines = _expanded ? 8 : 3; if (columnOneWidth == _generalStatsWidth) { drawBackground(backgroundColor, horizontalOffset, 0, _geoStatsWidth, lines * STATS_PELS_PER_LINE + 10); } horizontalOffset += 5; QString avatarPosition = QString("Position: %1, %2, %3"). arg(avatarPos.x, -1, 'f', 1). arg(avatarPos.y, -1, 'f', 1). arg(avatarPos.z, -1, 'f', 1); QString avatarVelocity = QString("Velocity: %1").arg(glm::length(myAvatar->getVelocity()), -1, 'f', 1); QString avatarBodyYaw = QString("Yaw: %1").arg(myAvatar->getBodyYaw(), -1, 'f', 1); QString avatarMixerStats; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarPosition.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarVelocity.toUtf8().constData(), color); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarBodyYaw.toUtf8().constData(), color); if (_expanded) { SharedNodePointer avatarMixer = DependencyManager::get()->soloNodeOfType(NodeType::AvatarMixer); if (avatarMixer) { avatarMixerStats = QString("Avatar Mixer: %1 kbps, %2 pps"). arg(roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer) + bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))). arg(roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer) + bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); } else { avatarMixerStats = QString("No Avatar Mixer"); } verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, avatarMixerStats.toUtf8().constData(), color); stringstream downloads; downloads << "Downloads: "; foreach (Resource* resource, ResourceCache::getLoadingRequests()) { downloads << (int)(resource->getProgress() * 100.0f) << "% "; } downloads << "(" << ResourceCache::getPendingRequestCount() << " pending)"; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, downloads.str().c_str(), color); QMetaObject::invokeMethod(Application::getInstance()->getMetavoxels()->getUpdater(), "getStats", Q_ARG(QObject*, this), Q_ARG(const QByteArray&, "setMetavoxelStats")); stringstream nodes; nodes << "Metavoxels: " << (_metavoxelInternal + _metavoxelLeaves); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, nodes.str().c_str(), color); stringstream nodeTypes; nodeTypes << "Internal: " << _metavoxelInternal << " Leaves: " << _metavoxelLeaves; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, nodeTypes.str().c_str(), color); if (_metavoxelSendTotal > 0 || _metavoxelReceiveTotal > 0) { stringstream reliableStats; if (_metavoxelSendTotal > 0) { reliableStats << "Upload: " << (_metavoxelSendProgress * 100LL / _metavoxelSendTotal) << "% "; } if (_metavoxelReceiveTotal > 0) { reliableStats << "Download: " << (_metavoxelReceiveProgress * 100LL / _metavoxelReceiveTotal) << "%"; } verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, reliableStats.str().c_str(), color); } } verticalOffset = 0; horizontalOffset = _lastHorizontalOffset + _generalStatsWidth + _pingStatsWidth + _geoStatsWidth + 3; lines = _expanded ? 14 : 3; drawBackground(backgroundColor, horizontalOffset, 0, glCanvas->width() - horizontalOffset, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; // Model/Entity render details EntityTreeRenderer* entities = Application::getInstance()->getEntities(); octreeStats.str(""); octreeStats << "Entity Items rendered: " << entities->getItemsRendered() << " / Out of view:" << entities->getItemsOutOfView() << " / Too small:" << entities->getItemsTooSmall(); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); if (_expanded) { octreeStats.str(""); octreeStats << " Meshes rendered: " << entities->getMeshesRendered() << " / Out of view:" << entities->getMeshesOutOfView() << " / Too small:" << entities->getMeshesTooSmall(); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); octreeStats.str(""); octreeStats << " Triangles: " << entities->getTrianglesRendered() << " / Quads:" << entities->getQuadsRendered() << " / Material Switches:" << entities->getMaterialSwitches(); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); octreeStats.str(""); octreeStats << " Mesh Parts Rendered Opaque: " << entities->getOpaqueMeshPartsRendered() << " / Translucent:" << entities->getTranslucentMeshPartsRendered(); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); } // iterate all the current voxel stats, and list their sending modes, and total voxel counts std::stringstream sendingMode(""); sendingMode << "Octree Sending Mode: ["; int serverCount = 0; int movingServerCount = 0; unsigned long totalNodes = 0; unsigned long totalInternal = 0; unsigned long totalLeaves = 0; NodeToOctreeSceneStats* octreeServerSceneStats = Application::getInstance()->getOcteeSceneStats(); for(NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { //const QUuid& uuid = i->first; OctreeSceneStats& stats = i->second; serverCount++; if (_expanded) { if (serverCount > 1) { sendingMode << ","; } if (stats.isMoving()) { sendingMode << "M"; movingServerCount++; } else { sendingMode << "S"; } } // calculate server node totals totalNodes += stats.getTotalElements(); if (_expanded) { totalInternal += stats.getTotalInternal(); totalLeaves += stats.getTotalLeaves(); } } if (_expanded) { if (serverCount == 0) { sendingMode << "---"; } sendingMode << "] " << serverCount << " servers"; if (movingServerCount > 0) { sendingMode << " "; } else { sendingMode << " "; } verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)sendingMode.str().c_str(), color); } // Incoming packets if (_expanded) { octreeStats.str(""); QString packetsString = locale.toString((int)voxelPacketsToProcess); QString maxString = locale.toString((int)_recentMaxPackets); octreeStats << "Octree Packets to Process: " << qPrintable(packetsString) << " [Recent Max: " << qPrintable(maxString) << "]"; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); } if (_resetRecentMaxPacketsSoon && voxelPacketsToProcess > 0) { _recentMaxPackets = 0; _resetRecentMaxPacketsSoon = false; } if (voxelPacketsToProcess == 0) { _resetRecentMaxPacketsSoon = true; } else { if (voxelPacketsToProcess > _recentMaxPackets) { _recentMaxPackets = voxelPacketsToProcess; } } QString serversTotalString = locale.toString((uint)totalNodes); // consider adding: .rightJustified(10, ' '); unsigned long localTotal = OctreeElement::getNodeCount(); QString localTotalString = locale.toString((uint)localTotal); // consider adding: .rightJustified(10, ' '); // Server Octree Elements if (!_expanded) { octreeStats.str(""); octreeStats << "Octree Elements Server: " << qPrintable(serversTotalString) << " Local:" << qPrintable(localTotalString); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); } if (_expanded) { octreeStats.str(""); octreeStats << "Octree Elements -"; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); QString serversInternalString = locale.toString((uint)totalInternal); QString serversLeavesString = locale.toString((uint)totalLeaves); octreeStats.str(""); octreeStats << " Server: " << qPrintable(serversTotalString) << " Internal: " << qPrintable(serversInternalString) << " Leaves: " << qPrintable(serversLeavesString); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); // Local Voxels unsigned long localInternal = OctreeElement::getInternalNodeCount(); unsigned long localLeaves = OctreeElement::getLeafNodeCount(); QString localInternalString = locale.toString((uint)localInternal); QString localLeavesString = locale.toString((uint)localLeaves); octreeStats.str(""); octreeStats << " Local: " << qPrintable(serversTotalString) << " Internal: " << qPrintable(localInternalString) << " Leaves: " << qPrintable(localLeavesString) << ""; verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); } // LOD Details if (_expanded) { octreeStats.str(""); QString displayLODDetails = DependencyManager::get()->getLODFeedbackText(); octreeStats << "LOD: You can see " << qPrintable(displayLODDetails.trimmed()); verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color); } } void Stats::setMetavoxelStats(int internal, int leaves, int sendProgress, int sendTotal, int receiveProgress, int receiveTotal) { _metavoxelInternal = internal; _metavoxelLeaves = leaves; _metavoxelSendProgress = sendProgress; _metavoxelSendTotal = sendTotal; _metavoxelReceiveProgress = receiveProgress; _metavoxelReceiveTotal = receiveTotal; }