diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 07d8716656..35f4b2aa80 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -12,24 +12,25 @@ #include "EntityTreeRenderer.h" #include +#include #include #include #include #include -#include #include #include +#include +#include #include #include #include +#include +#include #include #include -#include -#include #include -#include #include #include "EntitiesRendererLogging.h" @@ -183,6 +184,7 @@ void EntityTreeRenderer::clear() { qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } _entitiesInScene.clear(); + _renderablesToUpdate.clear(); // reset the zone to the default (while we load the next scene) _layeredZones.clear(); @@ -272,7 +274,7 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r } } -void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) { +void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, const ViewFrustum& view, render::Transaction& transaction) { PROFILE_RANGE_EX(simulation_physics, "Change", 0xffff00ff, (uint64_t)_changedEntities.size()); PerformanceTimer pt("change"); std::unordered_set changedEntities; @@ -286,21 +288,91 @@ void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene #endif }); - for (const auto& entityId : changedEntities) { - auto renderable = renderableForEntityId(entityId); - if (!renderable) { - continue; + { + PROFILE_RANGE_EX(simulation_physics, "CopyRenderables", 0xffff00ff, (uint64_t)changedEntities.size()); + for (const auto& entityId : changedEntities) { + auto renderable = renderableForEntityId(entityId); + if (renderable) { + // only add valid renderables _renderablesToUpdate + _renderablesToUpdate.insert({ entityId, renderable }); + } } - _renderablesToUpdate.insert({ entityId, renderable }); } - if (!_renderablesToUpdate.empty()) { + float expectedUpdateCost = _avgRenderableUpdateCost * _renderablesToUpdate.size(); + if (expectedUpdateCost < MAX_UPDATE_RENDERABLES_TIME_BUDGET) { + // we expect to update all renderables within available time budget PROFILE_RANGE_EX(simulation_physics, "UpdateRenderables", 0xffff00ff, (uint64_t)_renderablesToUpdate.size()); + uint64_t updateStart = usecTimestampNow(); for (const auto& entry : _renderablesToUpdate) { const auto& renderable = entry.second; + assert(renderable); // only valid renderables are added to _renderablesToUpdate renderable->updateInScene(scene, transaction); } + size_t numRenderables = _renderablesToUpdate.size() + 1; // add one to avoid divide by zero _renderablesToUpdate.clear(); + + // compute average per-renderable update cost + float cost = (float)(usecTimestampNow() - updateStart) / (float)(numRenderables); + const float blend = 0.1f; + _avgRenderableUpdateCost = (1.0f - blend) * _avgRenderableUpdateCost + blend * cost; + } else { + // we expect the cost to updating all renderables to exceed available time budget + // so we first sort by priority and update in order until out of time + + class SortableRenderer: public PrioritySortUtil::Sortable { + public: + SortableRenderer(const EntityRendererPointer& renderer) : _renderer(renderer) { } + + glm::vec3 getPosition() const override { return _renderer->getEntity()->getPosition(); } + float getRadius() const override { return 0.5f * _renderer->getEntity()->getQueryAACube().getScale(); } + uint64_t getTimestamp() const override { return _renderer->getUpdateTime(); } + + const EntityRendererPointer& getRenderer() const { return _renderer; } + private: + EntityRendererPointer _renderer; + }; + + // prioritize and sort the renderables + uint64_t sortStart = usecTimestampNow(); + PrioritySortUtil::PriorityQueue sortedRenderables(view); + { + PROFILE_RANGE_EX(simulation_physics, "SortRenderables", 0xffff00ff, (uint64_t)_renderablesToUpdate.size()); + std::unordered_map::iterator itr = _renderablesToUpdate.begin(); + while (itr != _renderablesToUpdate.end()) { + assert(itr->second); // only valid renderables are added to _renderablesToUpdate + sortedRenderables.push(SortableRenderer(itr->second)); + ++itr; + } + } + { + PROFILE_RANGE_EX(simulation_physics, "UpdateRenderables", 0xffff00ff, sortedRenderables.size()); + + // compute remaining time budget + uint64_t updateStart = usecTimestampNow(); + uint64_t timeBudget = MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET; + uint64_t sortCost = updateStart - sortStart; + if (sortCost < MAX_UPDATE_RENDERABLES_TIME_BUDGET - MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET) { + timeBudget = MAX_UPDATE_RENDERABLES_TIME_BUDGET - sortCost; + } + uint64_t expiry = updateStart + timeBudget; + + // process the sorted renderables + std::unordered_map::iterator itr; + size_t numSorted = sortedRenderables.size(); + while (!sortedRenderables.empty() && usecTimestampNow() < expiry) { + const EntityRendererPointer& renderable = sortedRenderables.top().getRenderer(); + renderable->updateInScene(scene, transaction); + _renderablesToUpdate.erase(renderable->getEntity()->getID()); + sortedRenderables.pop(); + } + + // compute average per-renderable update cost + size_t numUpdated = numSorted - sortedRenderables.size() + 1; // add one to avoid divide by zero + float cost = (float)(usecTimestampNow() - updateStart) / (float)(numUpdated); + const float blend = 0.1f; + _avgRenderableUpdateCost = (1.0f - blend) * _avgRenderableUpdateCost + blend * cost; + } } } @@ -319,7 +391,9 @@ void EntityTreeRenderer::update(bool simulate) { if (scene) { render::Transaction transaction; addPendingEntities(scene, transaction); - updateChangedEntities(scene, transaction); + ViewFrustum view; + _viewState->copyCurrentViewFrustum(view); + updateChangedEntities(scene, view, transaction); scene->enqueueTransaction(transaction); } } @@ -749,7 +823,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { } void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { - // If it's in the pending queue, remove it + // If it's in a pending queue, remove it + _renderablesToUpdate.erase(entityID); _entitiesToAdd.erase(entityID); auto itr = _entitiesInScene.find(entityID); @@ -757,7 +832,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { // Not in the scene, and no longer potentially in the pending queue, we're done return; } - + if (_tree && !_shuttingDown && _entitiesScriptEngine) { _entitiesScriptEngine->unloadEntityScript(entityID, true); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 1eb44f996a..1f5e6c8d92 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -144,7 +144,7 @@ protected: private: void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction); - void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction); + void updateChangedEntities(const render::ScenePointer& scene, const ViewFrustum& view, render::Transaction& transaction); EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); } render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); } @@ -235,11 +235,12 @@ private: NetworkTexturePointer _skyboxTexture; QString _ambientTextureURL; QString _skyboxTextureURL; + float _avgRenderableUpdateCost { 0.0f }; bool _pendingAmbientTexture { false }; bool _pendingSkyboxTexture { false }; - quint64 _lastZoneCheck { 0 }; - const quint64 ZONE_CHECK_INTERVAL = USECS_PER_MSEC * 100; // ~10hz + uint64_t _lastZoneCheck { 0 }; + const uint64_t ZONE_CHECK_INTERVAL = USECS_PER_MSEC * 100; // ~10hz const float ZONE_CHECK_DISTANCE = 0.001f; ReadWriteLockable _changedEntitiesGuard; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 9e4d832037..24de651247 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -59,7 +59,7 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St const QUuid& myNodeID = nodeList->getSessionUUID(); statusGetters.push_back([entity]() -> render::Item::Status::Value { - quint64 delta = usecTimestampNow() - entity->getLastEditedFromRemote(); + uint64_t delta = usecTimestampNow() - entity->getLastEditedFromRemote(); const float WAIT_THRESHOLD_INV = 1.0f / (0.2f * USECS_PER_SECOND); float normalizedDelta = delta * WAIT_THRESHOLD_INV; // Status icon will scale from 1.0f down to 0.0f after WAIT_THRESHOLD @@ -71,7 +71,7 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St }); statusGetters.push_back([entity] () -> render::Item::Status::Value { - quint64 delta = usecTimestampNow() - entity->getLastBroadcast(); + uint64_t delta = usecTimestampNow() - entity->getLastBroadcast(); const float WAIT_THRESHOLD_INV = 1.0f / (0.4f * USECS_PER_SECOND); float normalizedDelta = delta * WAIT_THRESHOLD_INV; // Status icon will scale from 1.0f down to 0.0f after WAIT_THRESHOLD @@ -278,6 +278,7 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans if (!isValidRenderItem()) { return; } + _updateTime = usecTimestampNow(); // FIXME is this excessive? if (!needsRenderUpdate()) { diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index ed636ebf73..5deb69711d 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -52,6 +52,8 @@ public: void clearSubRenderItemIDs(); void setSubRenderItemIDs(const render::ItemIDs& ids); + const uint64_t& getUpdateTime() const { return _updateTime; } + protected: virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); } virtual void onAddToScene(const EntityItemPointer& entity); @@ -100,7 +102,6 @@ protected: return result; } - signals: void requestRenderUpdate(); @@ -113,14 +114,15 @@ protected: static std::function _entitiesShouldFadeFunction; const Transform& getModelTransform() const; + Item::Bound _bound; SharedSoundPointer _collisionSound; QUuid _changeHandlerId; ItemID _renderItemID{ Item::INVALID_ITEM_ID }; ItemIDs _subRenderItemIDs; - quint64 _fadeStartTime{ usecTimestampNow() }; + uint64_t _fadeStartTime{ usecTimestampNow() }; + uint64_t _updateTime{ usecTimestampNow() }; // used when sorting/throttling render updates bool _isFading{ _entitiesShouldFadeFunction() }; bool _prevIsTransparent { false }; - Item::Bound _bound; bool _visible { false }; bool _moving { false }; // Only touched on the rendering thread diff --git a/libraries/render/src/render/IndexedContainer.h b/libraries/render/src/render/IndexedContainer.h index 4b51f8eb3c..2d21b5cc29 100644 --- a/libraries/render/src/render/IndexedContainer.h +++ b/libraries/render/src/render/IndexedContainer.h @@ -87,7 +87,7 @@ namespace indexed_container { if (index < (Index) _elements.size()) { _elements[index] = e; } else { - assert(index == _elements.size()); + assert(index == (Index)_elements.size()); _elements.emplace_back(e); } } @@ -159,4 +159,4 @@ namespace indexed_container { }; }; } -#endif \ No newline at end of file +#endif diff --git a/libraries/shared/src/PrioritySortUtil.h b/libraries/shared/src/PrioritySortUtil.h new file mode 100644 index 0000000000..1d11a04265 --- /dev/null +++ b/libraries/shared/src/PrioritySortUtil.h @@ -0,0 +1,141 @@ +// +// PrioritySortUtil.h +// +// Created by Andrew Meadows on 2017-11-08 +// Copyright 2017 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 +// +#pragma once +#ifndef hifi_PrioritySortUtil_h +#define hifi_PrioritySortUtil_h + +#include +#include "ViewFrustum.h" + +/* PrioritySortUtil is a helper for sorting 3D things relative to a ViewFrustum. To use: + +(1) Derive a class from pure-virtual PrioritySortUtil::Sortable that wraps a copy of + the Thing you want to prioritize and sort: + + class SortableWrapper: public PrioritySortUtil::Sortable { + public: + SortableWrapper(const Thing& thing) : _thing(thing) { } + glm::vec3 getPosition() const override { return _thing->getPosition(); } + float getRadius() const override { return 0.5f * _thing->getBoundingRadius(); } + uint64_t getTimestamp() const override { return _thing->getLastTime(); } + const Thing& getThing() const { return _thing; } + private: + Thing _thing; + }; + +(2) Make a PrioritySortUtil::PriorityQueue and add them to the queue: + + PrioritySortUtil::Prioritizer prioritizer(viewFrustum); + std::priority_queue< PrioritySortUtil::Sortable > sortedThings; + for (thing in things) { + float priority = prioritizer.computePriority(PrioritySortUtil::PrioritizableThing(thing)); + sortedThings.push(PrioritySortUtil::Sortable entry(thing, priority)); + } + +(3) Loop over your priority queue and do timeboxed work: + + uint64_t cutoffTime = usecTimestampNow() + TIME_BUDGET; + while (!sortedThings.empty()) { + const Thing& thing = sortedThings.top(); + // ...do work on thing... + sortedThings.pop(); + if (usecTimestampNow() > cutoffTime) { + break; + } + } +*/ + +namespace PrioritySortUtil { + + constexpr float DEFAULT_ANGULAR_COEF { 1.0f }; + constexpr float DEFAULT_CENTER_COEF { 0.5f }; + constexpr float DEFAULT_AGE_COEF { 0.25f / (float)(USECS_PER_SECOND) }; + + class Sortable { + public: + virtual glm::vec3 getPosition() const = 0; + virtual float getRadius() const = 0; + virtual uint64_t getTimestamp() const = 0; + + void setPriority(float priority) { _priority = priority; } + bool operator<(const Sortable& other) const { return _priority < other._priority; } + private: + float _priority { 0.0f }; + }; + + template + class PriorityQueue { + public: + PriorityQueue() = delete; + + PriorityQueue(const ViewFrustum& view) : _view(view) { } + + PriorityQueue(const ViewFrustum& view, float angularWeight, float centerWeight, float ageWeight) + : _view(view), _angularWeight(angularWeight), _centerWeight(centerWeight), _ageWeight(ageWeight) + { } + + void setView(const ViewFrustum& view) { _view = view; } + + void setWeights(float angularWeight, float centerWeight, float ageWeight) { + _angularWeight = angularWeight; + _centerWeight = centerWeight; + _ageWeight = ageWeight; + } + + size_t size() const { return _queue.size(); } + void push(T thing) { + thing.setPriority(computePriority(thing)); + _queue.push(thing); + } + const T& top() const { return _queue.top(); } + void pop() { return _queue.pop(); } + bool empty() const { return _queue.empty(); } + + private: + float computePriority(const T& thing) const { + // priority = weighted linear combination of multiple values: + // (a) angular size + // (b) proximity to center of view + // (c) time since last update + // where the relative "weights" are tuned to scale the contributing values into units of "priority". + + glm::vec3 position = thing.getPosition(); + glm::vec3 offset = position - _view.getPosition(); + float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero + float radius = thing.getRadius(); + + float priority = _angularWeight * (radius / distance) + + _centerWeight * (glm::dot(offset, _view.getDirection()) / distance) + + _ageWeight * (float)(usecTimestampNow() - thing.getTimestamp()); + + // decrement priority of things outside keyhole + if (distance - radius > _view.getCenterRadius()) { + if (!_view.sphereIntersectsFrustum(position, radius)) { + constexpr float OUT_OF_VIEW_PENALTY = -10.0f; + priority += OUT_OF_VIEW_PENALTY; + } + } + return priority; + } + + ViewFrustum _view; + std::priority_queue _queue; + float _angularWeight { DEFAULT_ANGULAR_COEF }; + float _centerWeight { DEFAULT_CENTER_COEF }; + float _ageWeight { DEFAULT_AGE_COEF }; + }; +} // namespace PrioritySortUtil + +// for now we're keeping hard-coded sorted time budgets in one spot +const uint64_t MAX_UPDATE_RENDERABLES_TIME_BUDGET = 2000; // usec +const uint64_t MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET = 1000; // usec + +#endif // hifi_PrioritySortUtil_h +