// // LODManager.cpp // interface/src/LODManager.h // // Created by Clement on 1/16/15. // Copyright 2015 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 "LODManager.h" #include #include #include #include "Application.h" #include "ui/DialogsManager.h" #include "InterfaceLogging.h" const float LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_DESKTOP_FPS; const float LODManager::DEFAULT_HMD_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS; Setting::Handle desktopLODDecreaseFPS("desktopLODDecreaseFPS", LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS); Setting::Handle hmdLODDecreaseFPS("hmdLODDecreaseFPS", LODManager::DEFAULT_HMD_LOD_DOWN_FPS); LODManager::LODManager() { } // We use a "time-weighted running average" of the maxRenderTime and compare it against min/max thresholds // to determine if we should adjust the level of detail (LOD). // // A time-weighted running average has a timescale which determines how fast the average tracks the measured // value in real-time. Given a step-function in the mesured value, and assuming measurements happen // faster than the runningAverage is computed, the error between the value and its runningAverage will be // reduced by 1/e every timescale of real-time that passes. const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.08f; // sec // batchTIme is always contained in presentTime. // We favor using batchTime instead of presentTime as a representative value for rendering duration (on present thread) // if batchTime + cushionTime < presentTime. // since we are shooting for fps around 60, 90Hz, the ideal frames are around 10ms // so we are picking a cushion time of 3ms const float LOD_BATCH_TO_PRESENT_CUSHION_TIME = 3.0f; // msec void LODManager::setRenderTimes(float presentTime, float engineRunTime, float batchTime, float gpuTime) { // Make sure the sampled time are positive values _presentTime = std::max(0.0f, presentTime); _engineRunTime = std::max(0.0f, engineRunTime); _batchTime = std::max(0.0f, batchTime); _gpuTime = std::max(0.0f, gpuTime); } void LODManager::autoAdjustLOD(float realTimeDelta) { // The "render time" is the worse of: // - engineRunTime: Time spent in the render thread in the engine producing the gpu::Frame N // - batchTime: Time spent in the present thread processing the batches of gpu::Frame N+1 // - presentTime: Time spent in the present thread between the last 2 swap buffers considered the total time to submit gpu::Frame N+1 // - gpuTime: Time spent in the GPU executing the gpu::Frame N + 2 // But Present time is in reality synched with the monitor/display refresh rate, it s always longer than batchTime. // So if batchTime is fast enough relative to presentTime we are using it, otherwise we are using presentTime. got it ? auto presentTime = (_presentTime > _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME ? _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME : _presentTime); float maxRenderTime = glm::max(glm::max(presentTime, _engineRunTime), _gpuTime); // maxRenderTime must be a realistic valid duration in order for the regulation to work correctly. // We make sure it s a non zero positive value (1.0ms) under 1 sec maxRenderTime = std::max(1.0f, std::min(maxRenderTime, (float)MSECS_PER_SECOND)); // realTimeDelta must be a realistic valid duration in order for the regulation to work correctly. // We make sure it a positive value under 1 sec // note that if real time delta is very small we will early exit to avoid division by zero realTimeDelta = std::max(0.0f, std::min(realTimeDelta, 1.0f)); // compute time-weighted running average render time (now and smooth) // We MUST clamp the blend between 0.0 and 1.0 for stability float nowBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f; float smoothBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) ? realTimeDelta / (LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) : 1.0f; //Evaluate the running averages for the render time // We must sanity check for the output average evaluated to be in a valid range to avoid issues _nowRenderTime = (1.0f - nowBlend) * _nowRenderTime + nowBlend * maxRenderTime; // msec _nowRenderTime = std::max(0.0f, std::min(_nowRenderTime, (float)MSECS_PER_SECOND)); _smoothRenderTime = (1.0f - smoothBlend) * _smoothRenderTime + smoothBlend * maxRenderTime; // msec _smoothRenderTime = std::max(0.0f, std::min(_smoothRenderTime, (float)MSECS_PER_SECOND)); // Early exit if not regulating or if the simulation or render times don't matter if (!_automaticLODAdjust || realTimeDelta <= 0.0f || _nowRenderTime <= 0.0f || _smoothRenderTime <= 0.0f) { return; } // Previous values for output float oldOctreeSizeScale = getOctreeSizeScale(); float oldLODAngle = getLODAngleDeg(); // Target fps is slightly overshooted by 5hz float targetFPS = getLODTargetFPS() + LOD_OFFSET_FPS; // Current fps based on latest measurments float currentNowFPS = (float)MSECS_PER_SECOND / _nowRenderTime; float currentSmoothFPS = (float)MSECS_PER_SECOND / _smoothRenderTime; // Compute the Variance of the FPS signal (FPS - smouthFPS)^2 // Also scale it by a percentage for fine tuning (default is 100%) float currentVarianceFPS = (currentSmoothFPS - currentNowFPS); currentVarianceFPS *= currentVarianceFPS; currentVarianceFPS *= _pidCoefs.w; // evaluate current error between the current smoothFPS and target FPS // and the sqaure of the error to compare against the Variance auto currentErrorFPS = (targetFPS - currentSmoothFPS); auto currentErrorFPSSquare = currentErrorFPS * currentErrorFPS; // Define a noiseCoef that is trying to adjust the error to the FPS target value based on its strength // relative to the current Variance of the FPS signal. // If the error is within the variance, just set to 0. // if its within 2x the variance scale the control // and full control if error is bigger than 2x variance auto noiseCoef = 1.0f; if (currentErrorFPSSquare < currentVarianceFPS) { noiseCoef = 0.0f; } else if (currentErrorFPSSquare < 2.0f * currentVarianceFPS) { noiseCoef = (currentErrorFPSSquare - currentVarianceFPS) / currentVarianceFPS; } // The final normalized error is the the error to the FPS target, weighted by the noiseCoef, then normailzed by the target FPS. // it s also clamped in the [-1, 1] range auto error = noiseCoef * currentErrorFPS / targetFPS; error = glm::clamp(error, -1.0f, 1.0f); // Now we are getting into the P.I.D. controler code // retreive the history of pid error and integral auto previous_error = _pidHistory.x; auto previous_integral = _pidHistory.y; // The dt used for temporal values of the controller is the current realTimedelta // clamped to a reasonable granularity to make sure we are not over reacting auto dt = std::min(realTimeDelta, LOD_ADJUST_RUNNING_AVG_TIMESCALE); // Compute the current integral and clamp to avoid accumulation auto integral = previous_integral + error * dt; glm::clamp(integral, -1.0f, 1.0f); // Compute derivative // dt is never zero because realTimeDelta would have early exit above, but if it ever was let's zero the derivative term auto derivative = (dt <= 0.0f ? 0.0f : (error - previous_error) / dt); // remember history _pidHistory.x = error; _pidHistory.y = integral; _pidHistory.z = derivative; // Compute the output of the PID and record intermediate results for tuning _pidOutputs.x = _pidCoefs.x * error; // Kp * error _pidOutputs.y = _pidCoefs.y * integral; // Ki * integral _pidOutputs.z = _pidCoefs.z * derivative; // Kd * derivative auto output = _pidOutputs.x + _pidOutputs.y + _pidOutputs.z; _pidOutputs.w = output; // And now add the output of the controller to the LODAngle where we will guarantee it is in the proper range setLODAngleDeg(oldLODAngle + output); if (oldOctreeSizeScale != _octreeSizeScale) { auto lodToolsDialog = DependencyManager::get()->getLodToolsDialog(); if (lodToolsDialog) { lodToolsDialog->reloadSliders(); } } } float LODManager::getLODAngleHalfTan() const { return getPerspectiveAccuracyAngleTan(_octreeSizeScale, _boundaryLevelAdjust); } float LODManager::getLODAngle() const { return 2.0f * atanf(getLODAngleHalfTan()); } float LODManager::getLODAngleDeg() const { return glm::degrees(getLODAngle()); } void LODManager::setLODAngleDeg(float lodAngle) { auto newSolidAngle = std::max(0.5f, std::min(lodAngle, 90.f)); auto halTan = glm::tan(glm::radians(newSolidAngle * 0.5f)); auto octreeSizeScale = TREE_SCALE * OCTREE_TO_MESH_RATIO / halTan; setOctreeSizeScale(octreeSizeScale); } void LODManager::setSmoothScale(float t) { _smoothScale = glm::max(1.0f, t); } float LODManager::getPidKp() const { return _pidCoefs.x; } float LODManager::getPidKi() const { return _pidCoefs.y; } float LODManager::getPidKd() const { return _pidCoefs.z; } float LODManager::getPidKv() const { return _pidCoefs.w; } void LODManager::setPidKp(float k) { _pidCoefs.x = k; } void LODManager::setPidKi(float k) { _pidCoefs.y = k; } void LODManager::setPidKd(float k) { _pidCoefs.z = k; } void LODManager::setPidKv(float t) { _pidCoefs.w = t; } float LODManager::getPidOp() const { return _pidOutputs.x; } float LODManager::getPidOi() const { return _pidOutputs.y; } float LODManager::getPidOd() const { return _pidOutputs.z; } float LODManager::getPidO() const { return _pidOutputs.w; } void LODManager::resetLODAdjust() { } void LODManager::setAutomaticLODAdjust(bool value) { _automaticLODAdjust = value; emit autoLODChanged(); } bool LODManager::shouldRender(const RenderArgs* args, const AABox& bounds) { // To decide if the bound should be rendered or not at the specified Args->lodAngle, // we need to compute the apparent angle of the bound from the frustum origin, // and compare it against the lodAngle, if it is greater or equal we should render the content of that bound. // we abstract the bound as a sphere centered on the bound center and of radius half diagonal of the bound. // Instead of comparing angles, we are comparing the tangent of the half angle which are more efficient to compute: // we are comparing the square of the half tangent apparent angle for the bound against the LODAngle Half tangent square // if smaller, the bound is too small and we should NOT render it, return true otherwise. // Tangent Adjacent side is eye to bound center vector length auto pos = args->getViewFrustum().getPosition() - bounds.calcCenter(); auto halfTanAdjacentSq = glm::dot(pos, pos); // Tangent Opposite side is the half length of the dimensions vector of the bound auto dim = bounds.getDimensions(); auto halfTanOppositeSq = 0.25f * glm::dot(dim, dim); // The test is: // isVisible = halfTanSq >= lodHalfTanSq = (halfTanOppositeSq / halfTanAdjacentSq) >= lodHalfTanSq // which we express as below to avoid division // (halfTanOppositeSq) >= lodHalfTanSq * halfTanAdjacentSq return (halfTanOppositeSq >= args->_lodAngleHalfTanSq * halfTanAdjacentSq); }; void LODManager::setOctreeSizeScale(float sizeScale) { _octreeSizeScale = sizeScale; } void LODManager::setBoundaryLevelAdjust(int boundaryLevelAdjust) { _boundaryLevelAdjust = boundaryLevelAdjust; } QString LODManager::getLODFeedbackText() { // determine granularity feedback int boundaryLevelAdjust = getBoundaryLevelAdjust(); QString granularityFeedback; switch (boundaryLevelAdjust) { case 0: { granularityFeedback = QString("."); } break; case 1: { granularityFeedback = QString(" at half of standard granularity."); } break; case 2: { granularityFeedback = QString(" at a third of standard granularity."); } break; default: { granularityFeedback = QString(" at 1/%1th of standard granularity.").arg(boundaryLevelAdjust + 1); } break; } // distance feedback float octreeSizeScale = getOctreeSizeScale(); float relativeToDefault = octreeSizeScale / DEFAULT_OCTREE_SIZE_SCALE; int relativeToTwentyTwenty = 20 / relativeToDefault; QString result; if (relativeToDefault > 1.01f) { result = QString("20:%1 or %2 times further than average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 2).arg(granularityFeedback); } else if (relativeToDefault > 0.99f) { result = QString("20:20 or the default distance for average vision%1").arg(granularityFeedback); } else if (relativeToDefault > 0.01f) { result = QString("20:%1 or %2 of default distance for average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback); } else { result = QString("%2 of default distance for average vision%3").arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback); } return result; } void LODManager::loadSettings() { setDesktopLODTargetFPS(desktopLODDecreaseFPS.get()); setHMDLODTargetFPS(hmdLODDecreaseFPS.get()); } void LODManager::saveSettings() { desktopLODDecreaseFPS.set(getDesktopLODTargetFPS()); hmdLODDecreaseFPS.set(getHMDLODTargetFPS()); } const float MIN_DECREASE_FPS = 0.5f; void LODManager::setDesktopLODTargetFPS(float fps) { if (fps < MIN_DECREASE_FPS) { // avoid divide by zero fps = MIN_DECREASE_FPS; } _desktopTargetFPS = fps; } float LODManager::getDesktopLODTargetFPS() const { return _desktopTargetFPS; } void LODManager::setHMDLODTargetFPS(float fps) { if (fps < MIN_DECREASE_FPS) { // avoid divide by zero fps = MIN_DECREASE_FPS; } _hmdTargetFPS = fps; } float LODManager::getHMDLODTargetFPS() const { return _hmdTargetFPS; } float LODManager::getLODTargetFPS() const { if (qApp->isHMDMode()) { return getHMDLODTargetFPS(); } return getDesktopLODTargetFPS(); } void LODManager::setWorldDetailQuality(float quality) { static const float MIN_FPS = 10; static const float LOW = 0.25f; bool isLowestValue = quality == LOW; bool isHMDMode = qApp->isHMDMode(); float maxFPS = isHMDMode ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS; float desiredFPS = maxFPS; if (!isLowestValue) { float calculatedFPS = (maxFPS - (maxFPS * quality)); desiredFPS = calculatedFPS < MIN_FPS ? MIN_FPS : calculatedFPS; } if (isHMDMode) { setHMDLODTargetFPS(desiredFPS); } else { setDesktopLODTargetFPS(desiredFPS); } emit worldDetailQualityChanged(); } float LODManager::getWorldDetailQuality() const { static const float LOW = 0.25f; static const float MEDIUM = 0.5f; static const float HIGH = 0.75f; bool inHMD = qApp->isHMDMode(); float targetFPS = 0.0f; if (inHMD) { targetFPS = getHMDLODTargetFPS(); } else { targetFPS = getDesktopLODTargetFPS(); } float maxFPS = inHMD ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS; float percentage = 1.0f - targetFPS / maxFPS; if (percentage <= LOW) { return LOW; } else if (percentage <= MEDIUM) { return MEDIUM; } return HIGH; } void LODManager::setLODQualityLevel(float quality) { _lodQualityLevel = quality; } float LODManager::getLODQualityLevel() const { return _lodQualityLevel; }