diff --git a/examples/acScripts/animatedAvatarAgent.js b/examples/acScripts/animatedAvatarAgent.js index 4e550e9789..3e5c90ed1a 100644 --- a/examples/acScripts/animatedAvatarAgent.js +++ b/examples/acScripts/animatedAvatarAgent.js @@ -14,16 +14,17 @@ // An assignment client script that animates one avatar at random location within 'spread' meters of 'origin'. // In Domain Server Settings, go to scripts and give the url of this script. Press '+', and then 'Save and restart'. -var origin = {x: 500, y: 502, z: 500}; -var spread = 10; // meters +var origin = {x: 500, y: 500, z: 500}; +var spread = 20; // meters var animationData = {url: "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", lastFrame: 35}; Avatar.skeletonModelURL = "https://hifi-public.s3.amazonaws.com/marketplace/contents/dd03b8e3-52fb-4ab3-9ac9-3b17e00cd85d/98baa90b3b66803c5d7bd4537fca6993.fst"; //lovejoy Avatar.displayName = "'Bot"; var millisecondsToWaitBeforeStarting = 10 * 1000; // To give the various servers a chance to start. Agent.isAvatar = true; +function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2. Script.setTimeout(function () { - Avatar.position = Vec3.sum(origin, {x: Math.random() * spread, y: 0, z: Math.random() * spread}); + Avatar.position = Vec3.sum(origin, {x: coord(), y: 0, z: coord()}); print("Starting at", JSON.stringify(Avatar.position)); Avatar.startAnimation(animationData.url, animationData.fps || 30, 1, true, false, animationData.firstFrame || 0, animationData.lastFrame); }, millisecondsToWaitBeforeStarting); diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index c98d4741b0..84381cc754 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -256,6 +256,12 @@ Item { visible: root.expanded text: "LOD: " + root.lodStatus; } + Text { + color: root.fontColor; + font.pixelSize: root.fontSize + visible: root.expanded + text: "Renderable avatars: " + root.avatarRenderableCount + " w/in " + root.avatarRenderDistance + "m"; + } } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4dc218ecdc..22822bdf74 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1077,8 +1077,10 @@ void Application::paintGL() { uint64_t now = usecTimestampNow(); static uint64_t lastPaintBegin{ now }; uint64_t diff = now - lastPaintBegin; + float instantaneousFps = 0.0f; if (diff != 0) { - _framesPerSecond.updateAverage((float)USECS_PER_SECOND / (float)diff); + instantaneousFps = (float)USECS_PER_SECOND / (float)diff; + _framesPerSecond.updateAverage(_lastInstantaneousFps); } lastPaintBegin = now; @@ -1109,6 +1111,29 @@ void Application::paintGL() { _inPaint = true; Finally clearFlagLambda([this] { _inPaint = false; }); + // Some LOD-like controls need to know a smoothly varying "potential" frame rate that doesn't + // include time waiting for vsync, and which can report a number above target if we've got the headroom. + // For example, if we're shooting for 75fps and paintWait is 3.3333ms (= 75% * 13.33ms), our deducedNonVSyncFps + // would be 100fps. In principle, a paintWait of zero would have deducedNonVSyncFps=75. + // Here we make a guess for deducedNonVSyncFps = 1 / deducedNonVSyncPeriod. + // + // Time between previous paintGL call and this one, which can vary not only with vSync misses, but also with QT timing. + // We're using this as a proxy for the time between vsync and displayEnd, below. (Not exact, but tends to be the same over time.) + // This is not the same as update(deltaTime), because the latter attempts to throttle to 60hz and also clamps to 1/4 second. + const float actualPeriod = diff / (float)USECS_PER_SECOND; // same as 1/instantaneousFps but easier for compiler to optimize + // Note that _lastPaintWait (stored at end of last call) is for the same paint cycle. + float deducedNonVSyncPeriod = actualPeriod - _lastPaintWait + _marginForDeducedFramePeriod; // plus a some non-zero time for machinery we can't measure + // We don't know how much time to allow for that, but if we went over the target period, we know it's at least the portion + // of paintWait up to the next vSync. This gives us enough of a penalty so that when actualPeriod crosses two cycles, + // the key part (and not an exagerated part) of _lastPaintWait is accounted for. + const float targetPeriod = getTargetFramePeriod(); + if (_lastPaintWait > EPSILON && actualPeriod > targetPeriod) { + // Don't use C++ remainder(). It's authors are mathematically insane. + deducedNonVSyncPeriod += fmod(actualPeriod, _lastPaintWait); + } + _lastDeducedNonVSyncFps = 1.0f / deducedNonVSyncPeriod; + _lastInstantaneousFps = instantaneousFps; + auto displayPlugin = getActiveDisplayPlugin(); displayPlugin->preRender(); _offscreenContext->makeCurrent(); @@ -1355,6 +1380,7 @@ void Application::paintGL() { // Ensure all operations from the previous context are complete before we try to read the fbo glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); glDeleteSync(sync); + uint64_t displayStart = usecTimestampNow(); { PROFILE_RANGE(__FUNCTION__ "/pluginDisplay"); @@ -1367,6 +1393,10 @@ void Application::paintGL() { PerformanceTimer perfTimer("bufferSwap"); displayPlugin->finishFrame(); } + uint64_t displayEnd = usecTimestampNow(); + const float displayPeriodUsec = (float)(displayEnd - displayStart); // usecs + _lastPaintWait = displayPeriodUsec / (float)USECS_PER_SECOND; + } { diff --git a/interface/src/Application.h b/interface/src/Application.h index 39f93f4b72..730158c689 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -159,6 +159,14 @@ public: bool isForeground() const { return _isForeground; } float getFps() const { return _fps; } + float const HMD_TARGET_FRAME_RATE = 75.0f; + float const DESKTOP_TARGET_FRAME_RATE = 60.0f; + float getTargetFrameRate() { return isHMDMode() ? HMD_TARGET_FRAME_RATE : DESKTOP_TARGET_FRAME_RATE; } + float getTargetFramePeriod() { return isHMDMode() ? 1.0f / HMD_TARGET_FRAME_RATE : 1.0f / DESKTOP_TARGET_FRAME_RATE; } // same as 1/getTargetFrameRate, but w/compile-time division + float getLastInstanteousFps() const { return _lastInstantaneousFps; } + float getLastPaintWait() const { return _lastPaintWait; }; + float getLastDeducedNonVSyncFps() const { return _lastDeducedNonVSyncFps; } + void setMarginForDeducedFramePeriod(float newValue) { _marginForDeducedFramePeriod = newValue; } float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); @@ -429,6 +437,10 @@ private: float _fps; QElapsedTimer _timerStart; QElapsedTimer _lastTimeUpdated; + float _lastInstantaneousFps { 0.0f }; + float _lastPaintWait { 0.0f }; + float _lastDeducedNonVSyncFps { 0.0f }; + float _marginForDeducedFramePeriod{ 0.002f }; // 2ms, adjustable ShapeManager _shapeManager; PhysicalEntitySimulation _entitySimulation; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f0f77cfd20..f6d73e3beb 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -96,6 +96,7 @@ Avatar::Avatar(RigPointer rig) : _moving(false), _initialized(false), _shouldRenderBillboard(true), + _shouldSkipRender(true), _voiceSphereID(GeometryCache::UNKNOWN_ID) { // we may have been created in the network thread, but we live in the main thread @@ -183,9 +184,29 @@ void Avatar::simulate(float deltaTime) { if (_shouldRenderBillboard) { if (getLODDistance() < BILLBOARD_LOD_DISTANCE * (1.0f - BILLBOARD_HYSTERESIS_PROPORTION)) { _shouldRenderBillboard = false; + qCDebug(interfaceapp) << "Unbillboarding" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for LOD" << getLODDistance(); } } else if (getLODDistance() > BILLBOARD_LOD_DISTANCE * (1.0f + BILLBOARD_HYSTERESIS_PROPORTION)) { _shouldRenderBillboard = true; + qCDebug(interfaceapp) << "Billboarding" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for LOD" << getLODDistance(); + } + + const bool isControllerLogging = DependencyManager::get()->getRenderDistanceControllerIsLogging(); + float renderDistance = DependencyManager::get()->getRenderDistance(); + const float SKIP_HYSTERESIS_PROPORTION = isControllerLogging ? 0.0f : BILLBOARD_HYSTERESIS_PROPORTION; + float distance = glm::distance(qApp->getCamera()->getPosition(), _position); + if (_shouldSkipRender) { + if (distance < renderDistance * (1.0f - SKIP_HYSTERESIS_PROPORTION)) { + _shouldSkipRender = false; + if (!isControllerLogging) { // Test for isMyAvatar is prophylactic. Never occurs in current code. + qCDebug(interfaceapp) << "Rerendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance; + } + } + } else if (distance > renderDistance * (1.0f + SKIP_HYSTERESIS_PROPORTION)) { + _shouldSkipRender = true; + if (!isControllerLogging) { + qCDebug(interfaceapp) << "Unrendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance; + } } // simple frustum check @@ -198,7 +219,7 @@ void Avatar::simulate(float deltaTime) { getHand()->simulate(deltaTime, false); } - if (!_shouldRenderBillboard && inViewFrustum) { + if (!_shouldRenderBillboard && !_shouldSkipRender && inViewFrustum) { { PerformanceTimer perfTimer("skeleton"); for (int i = 0; i < _jointData.size(); i++) { @@ -588,6 +609,10 @@ void Avatar::fixupModelsInScene() { // check to see if when we added our models to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = qApp->getMain3DScene(); + _skeletonModel.setVisibleInScene(!_shouldSkipRender, scene); + if (_shouldSkipRender) { + return; + } render::PendingChanges pendingChanges; if (_skeletonModel.isRenderable() && _skeletonModel.needsFixupInScene()) { _skeletonModel.removeFromScene(scene, pendingChanges); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index dd317fcacd..47cd20fe74 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -140,6 +140,8 @@ public: Q_INVOKABLE glm::vec3 getAngularVelocity() const { return _angularVelocity; } Q_INVOKABLE glm::vec3 getAngularAcceleration() const { return _angularAcceleration; } + Q_INVOKABLE bool getShouldRender() const { return !_shouldSkipRender; } + /// Scales a world space position vector relative to the avatar position and scale /// \param vector position to be scaled. Will store the result void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const; @@ -226,6 +228,7 @@ private: bool _initialized; NetworkTexturePointer _billboardTexture; bool _shouldRenderBillboard; + bool _shouldSkipRender { false }; bool _isLookAtTarget; void renderBillboard(RenderArgs* renderArgs); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 8e5166d7b7..c14d5b1df0 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -90,6 +90,21 @@ void AvatarManager::init() { _myAvatar->addToScene(_myAvatar, scene, pendingChanges); } scene->enqueuePendingChanges(pendingChanges); + + const float target_fps = qApp->getTargetFrameRate(); + _renderDistanceController.setMeasuredValueSetpoint(target_fps); + const float SMALLEST_REASONABLE_HORIZON = 5.0f; // meters + _renderDistanceController.setControlledValueHighLimit(1.0f / SMALLEST_REASONABLE_HORIZON); + _renderDistanceController.setControlledValueLowLimit(1.0f / (float) TREE_SCALE); + // Advice for tuning parameters: + // See PIDController.h. There's a section on tuning in the reference. + // Turn on logging with the following (or from js with AvatarList.setRenderDistanceControllerHistory("avatar render", 300)) + //_renderDistanceController.setHistorySize("avatar render", target_fps * 4); + // Note that extra logging/hysteresis is turned off in Avatar.cpp when the above logging is on. + _renderDistanceController.setKP(0.0008f); // Usually about 0.6 of largest that doesn't oscillate when other parameters 0. + _renderDistanceController.setKI(0.0006f); // Big enough to bring us to target with the above KP. + _renderDistanceController.setKD(0.000001f); // A touch of kd increases the speed by which we get there. + } void AvatarManager::updateMyAvatar(float deltaTime) { @@ -123,6 +138,17 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { PerformanceWarning warn(showWarnings, "Application::updateAvatars()"); PerformanceTimer perfTimer("otherAvatars"); + + _renderDistanceController.setMeasuredValueSetpoint(qApp->getTargetFrameRate()); // No problem updating in flight. + // The PID controller raises the controlled value when the measured value goes up. + // The measured value is frame rate. When the controlled value (1 / render cutoff distance) + // goes up, the render cutoff distance gets closer, the number of rendered avatars is less, and frame rate + // goes up. + const float deduced = qApp->getLastDeducedNonVSyncFps(); + const float distance = 1.0f / _renderDistanceController.update(deduced, deltaTime); + _renderDistanceAverage.updateAverage(distance); + _renderDistance = _renderDistanceAverage.getAverage(); + int renderableCount = 0; // simulate avatars auto hashCopy = getHashCopy(); @@ -141,10 +167,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } else { avatar->startUpdate(); avatar->simulate(deltaTime); + if (avatar->getShouldRender()) { + renderableCount++; + } avatar->endUpdate(); ++avatarIterator; } } + _renderedAvatarCount = renderableCount; // simulate avatar fades simulateAvatarFades(deltaTime); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f911dacc4d..96383b7e60 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -18,6 +18,8 @@ #include #include +#include +#include #include "Avatar.h" #include "AvatarMotionState.h" @@ -43,6 +45,7 @@ public: void clearOtherAvatars(); bool shouldShowReceiveStats() const { return _shouldShowReceiveStats; } + PIDController& getRenderDistanceController() { return _renderDistanceController; } class LocalLight { public: @@ -64,6 +67,17 @@ public: void handleCollisionEvents(const CollisionEvents& collisionEvents); void updateAvatarPhysicsShape(Avatar* avatar); + + // Expose results and parameter-tuning operations to other systems, such as stats and javascript. + Q_INVOKABLE float getRenderDistance() { return _renderDistance; } + Q_INVOKABLE int getNumberInRenderRange() { return _renderedAvatarCount; } + Q_INVOKABLE bool getRenderDistanceControllerIsLogging() { return _renderDistanceController.getIsLogging(); } + Q_INVOKABLE void setRenderDistanceControllerHistory(QString label, int size) { return _renderDistanceController.setHistorySize(label, size); } + Q_INVOKABLE void setRenderDistanceKP(float newValue) { _renderDistanceController.setKP(newValue); } + Q_INVOKABLE void setRenderDistanceKI(float newValue) { _renderDistanceController.setKI(newValue); } + Q_INVOKABLE void setRenderDistanceKD(float newValue) { _renderDistanceController.setKD(newValue); } + Q_INVOKABLE void setRenderDistanceLowLimit(float newValue) { _renderDistanceController.setControlledValueLowLimit(newValue); } + Q_INVOKABLE void setRenderDistanceHighLimit(float newValue) { _renderDistanceController.setControlledValueHighLimit(newValue); } public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } @@ -90,6 +104,10 @@ private: QVector _localLights; bool _shouldShowReceiveStats = false; + float _renderDistance { (float) TREE_SCALE }; + int _renderedAvatarCount { 0 }; + PIDController _renderDistanceController { }; + SimpleMovingAverage _renderDistanceAverage { 10 }; SetOfAvatarMotionStates _avatarMotionStates; SetOfMotionStates _motionStatesToAdd; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index c379f31aab..12692698e7 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -115,6 +115,8 @@ void Stats::updateStats(bool force) { auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves STAT_UPDATE(avatarCount, avatarManager->size() - 1); + STAT_UPDATE(avatarRenderableCount, avatarManager->getNumberInRenderRange()); + STAT_UPDATE(avatarRenderDistance, (int) round(avatarManager->getRenderDistance())); // deliberately truncating STAT_UPDATE(serverCount, nodeList->size()); STAT_UPDATE(framerate, (int)qApp->getFps()); STAT_UPDATE(simrate, (int)qApp->getAverageSimsPerSecond()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 39e3c6b24a..d1c0dd19d7 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -36,6 +36,8 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, simrate, 0) STATS_PROPERTY(int, avatarSimrate, 0) STATS_PROPERTY(int, avatarCount, 0) + STATS_PROPERTY(int, avatarRenderableCount, 0) + STATS_PROPERTY(int, avatarRenderDistance, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) STATS_PROPERTY(float, mbpsIn, 0) @@ -117,6 +119,8 @@ signals: void simrateChanged(); void avatarSimrateChanged(); void avatarCountChanged(); + void avatarRenderableCountChanged(); + void avatarRenderDistanceChanged(); void packetInCountChanged(); void packetOutCountChanged(); void mbpsInChanged(); diff --git a/libraries/shared/src/PIDController.cpp b/libraries/shared/src/PIDController.cpp new file mode 100644 index 0000000000..a853553b89 --- /dev/null +++ b/libraries/shared/src/PIDController.cpp @@ -0,0 +1,78 @@ +// +// PIDController.cpp +// libraries/shared/src +// +// Created by Howard Stearns 11/13/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 +#include +#include "SharedLogging.h" +#include "PIDController.h" + +float PIDController::update(float measuredValue, float dt, bool resetAccumulator) { + const float error = getMeasuredValueSetpoint() - measuredValue; // Sign is the direction we want measuredValue to go. Positive means go higher. + + const float p = getKP() * error; // term is Proportional to error + + const float accumulatedError = glm::clamp(error * dt + (resetAccumulator ? 0 : _lastAccumulation), // integrate error + getAccumulatedValueLowLimit(), // but clamp by anti-windup limits + getAccumulatedValueHighLimit()); + const float i = getKI() * accumulatedError; // term is Integral of error + + const float changeInError = (error - _lastError) / dt; // positive value denotes increasing deficit + const float d = getKD() * changeInError; // term is Derivative of Error + + const float computedValue = glm::clamp(p + i + d, + getControlledValueLowLimit(), + getControlledValueHighLimit()); + + if (getIsLogging()) { // if logging/reporting + updateHistory(measuredValue, dt, error, accumulatedError, changeInError, p, i, d, computedValue); + } + Q_ASSERT(!isnan(computedValue)); + + // update state for next time + _lastError = error; + _lastAccumulation = accumulatedError; + return computedValue; +} + +// Just for logging/reporting. Used when picking/verifying the operational parameters. +void PIDController::updateHistory(float measuredValue, float dt, float error, float accumulatedError, float changeInError, float p, float i, float d, float computedValue) { + // Don't report each update(), as the I/O messes with the results a lot. + // Instead, add to history, and then dump out at once when full. + // Typically, the first few values reported in each batch should be ignored. + const int n = _history.size(); + _history.resize(n + 1); + Row& next = _history[n]; + next.measured = measuredValue; + next.dt = dt; + next.error = error; + next.accumulated = accumulatedError; + next.changed = changeInError; + next.p = p; + next.i = i; + next.d = d; + next.computed = computedValue; + if (_history.size() == _history.capacity()) { // report when buffer is full + reportHistory(); + _history.resize(0); + } +} +void PIDController::reportHistory() { + qCDebug(shared) << _label << "measured dt FIXME || error accumulated changed || p i d controlled"; + for (int i = 0; i < _history.size(); i++) { + Row& row = _history[i]; + qCDebug(shared) << row.measured << row.dt << + "||" << row.error << row.accumulated << row.changed << + "||" << row.p << row.i << row.d << row.computed << 1.0f/row.computed; + } + qCDebug(shared) << "Limits: setpoint" << getMeasuredValueSetpoint() << "accumulate" << getAccumulatedValueLowLimit() << getAccumulatedValueHighLimit() << + "controlled" << getControlledValueLowLimit() << getControlledValueHighLimit() << + "kp/ki/kd" << getKP() << getKI() << getKD(); +} \ No newline at end of file diff --git a/libraries/shared/src/PIDController.h b/libraries/shared/src/PIDController.h new file mode 100644 index 0000000000..0b2411530a --- /dev/null +++ b/libraries/shared/src/PIDController.h @@ -0,0 +1,89 @@ +// +// PIDController.h +// libraries/shared/src +// +// Given a measure of system performance (such as frame rate, where bigger denotes more system work), +// compute a value that the system can take as input to control the amount of work done (such as an 1/LOD-distance, +// where bigger tends to give a higher measured system performance value). The controller's job is to compute a +// controlled value such that the measured value stays near the specified setpoint, even as system load changes. +// See http://www.wetmachine.com/inventing-the-future/mostly-reliable-performance-of-software-processes-by-dynamic-control-of-quality-parameters/ +// +// Created by Howard Stearns 11/13/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 +// + +#ifndef hifi_PIDController_h +#define hifi_PIDController_h + +#include +#include + +// Although our coding standard shuns abbreviations, the control systems literature uniformly uses p, i, d, and dt rather than +// proportionalTerm, integralTerm, derivativeTerm, and deltaTime. Here we will be consistent with the literature. +class PIDController { + +public: + // These are the main interfaces: + void setMeasuredValueSetpoint(float newValue) { _measuredValueSetpoint = newValue; } + float update(float measuredValue, float dt, bool resetAccumulator = false); // returns the new computedValue + void setHistorySize(QString label = QString(""), int size = 0) { _history.reserve(size); _history.resize(0); _label = label; } // non-empty does logging + + bool getIsLogging() { return _history.capacity(); } + float getMeasuredValueSetpoint() const { return _measuredValueSetpoint; } + // In normal operation (where we can easily reach setpoint), controlledValue is typcially pinned at max. + // Defaults to [0, max float], but for 1/LODdistance, it might be, say, [0, 0.2 or 0.1] + float getControlledValueLowLimit() const { return _controlledValueLowLimit; } + float getControlledValueHighLimit() const { return _controlledValueHighLimit; } + float getAntiWindupFactor() const { return _antiWindupFactor; } // default 10 + float getKP() const { return _kp; } // proportional to error. See comment above class. + float getKI() const { return _ki; } // to time integral of error + float getKD() const { return _kd; } // to time derivative of error + float getAccumulatedValueHighLimit() const { return getAntiWindupFactor() * getMeasuredValueSetpoint(); } + float getAccumulatedValueLowLimit() const { return -getAntiWindupFactor() * getMeasuredValueSetpoint(); } + + // There are several values that rarely change and might be thought of as "constants", but which do change during tuning, debugging, or other + // special-but-expected circumstances. Thus the instance vars are not const. + void setControlledValueLowLimit(float newValue) { _controlledValueLowLimit = newValue; } + void setControlledValueHighLimit(float newValue) { _controlledValueHighLimit = newValue; } + void setAntiWindupFactor(float newValue) { _antiWindupFactor = newValue; } + void setKP(float newValue) { _kp = newValue; } + void setKI(float newValue) { _ki = newValue; } + void setKD(float newValue) { _kd = newValue; } + + class Row { // one row of accumulated history, used only for logging (if at all) + public: + float measured; + float dt; + float error; + float accumulated; + float changed; + float p; + float i; + float d; + float computed; + }; +protected: + void reportHistory(); + void updateHistory(float measured, float dt, float error, float accumulatedError, float changeInErro, float p, float i, float d, float computedValue); + float _measuredValueSetpoint { 0.0f }; + float _controlledValueLowLimit { 0.0f }; + float _controlledValueHighLimit { std::numeric_limits::max() }; + float _antiWindupFactor { 10.0f }; + float _kp { 0.0f }; + float _ki { 0.0f }; + float _kd { 0.0f }; + + // Controller operating state + float _lastError{ 0.0f }; + float _lastAccumulation{ 0.0f }; + + // reporting + QVector _history{}; + QString _label{ "" }; + +}; + +#endif // hifi_PIDController_h