diff --git a/LICENSE b/LICENSE index 53c5ccf39a..60e86a1cc7 100644 --- a/LICENSE +++ b/LICENSE @@ -6,7 +6,7 @@ Licensed under the Apache License version 2.0 (the "License"); You may not use this software except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 -This software includes third-party software. +This software includes third-party and other platform software. Please see each individual software license for additional details. This software is distributed "as-is" without any warranties, conditions, or representations whether express or implied, including without limitation the implied warranties and conditions of merchantability, merchantable quality, fitness for a particular purpose, performance, durability, title, non-infringement, and those arising from statute or from custom or usage of trade or course of dealing. diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index f8f0f7904a..65e193dec6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -290,7 +290,6 @@ void Agent::executeScript() { packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket"); // register ourselves to the script engine _scriptEngine->registerGlobalObject("Agent", this); @@ -341,15 +340,12 @@ void Agent::setIsAvatar(bool isAvatar) { if (_isAvatar && !_avatarIdentityTimer) { // set up the avatar timers _avatarIdentityTimer = new QTimer(this); - _avatarBillboardTimer = new QTimer(this); // connect our slot connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket); - connect(_avatarBillboardTimer, &QTimer::timeout, this, &Agent::sendAvatarBillboardPacket); // start the timers _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - _avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); } if (!_isAvatar) { @@ -359,12 +355,6 @@ void Agent::setIsAvatar(bool isAvatar) { delete _avatarIdentityTimer; _avatarIdentityTimer = nullptr; } - - if (_avatarBillboardTimer) { - _avatarBillboardTimer->stop(); - delete _avatarBillboardTimer; - _avatarBillboardTimer = nullptr; - } } } @@ -375,14 +365,6 @@ void Agent::sendAvatarIdentityPacket() { } } -void Agent::sendAvatarBillboardPacket() { - if (_isAvatar) { - auto scriptedAvatar = DependencyManager::get(); - scriptedAvatar->sendBillboardPacket(); - } -} - - void Agent::processAgentAvatarAndAudio(float deltaTime) { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); @@ -491,7 +473,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { } void Agent::aboutToFinish() { - setIsAvatar(false);// will stop timers for sending billboards and identity packets + setIsAvatar(false);// will stop timers for sending identity packets if (_scriptEngine) { _scriptEngine->stop(); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 2b0d22385d..63d4cfa4d6 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -82,7 +82,6 @@ private: void setAvatarSound(SharedSoundPointer avatarSound) { _avatarSound = avatarSound; } void sendAvatarIdentityPacket(); - void sendAvatarBillboardPacket(); QString _scriptContents; QTimer* _scriptRequestTimeout { nullptr }; @@ -92,7 +91,6 @@ private: int _numAvatarSoundSentBytes = 0; bool _isAvatar = false; QTimer* _avatarIdentityTimer = nullptr; - QTimer* _avatarBillboardTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d7dce75242..10694eb5d4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -864,10 +864,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { auto reticlePos = getApplicationCompositor().getReticlePosition(); - QPoint globalPos(reticlePos.x, reticlePos.y); - - // FIXME - it would be nice if this was self contained in the _compositor or Reticle class - auto localPos = isHMDMode() ? globalPos : _glWidget->mapFromGlobal(globalPos); + QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. if (state) { QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); sendEvent(_glWidget, &mousePress); @@ -888,15 +885,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QPoint(reticlePosition.x, reticlePosition.y))); + offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QPoint(reticlePosition.x, reticlePosition.y))); + offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QPoint(reticlePosition.x, reticlePosition.y))); + offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); } } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); @@ -1159,10 +1156,16 @@ void Application::cleanupBeforeQuit() { getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) + QThreadPool::globalInstance()->clear(); + DependencyManager::get()->saveScripts(); DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts DependencyManager::destroy(); + // Cleanup all overlays after the scripts, as scripts might add more + _overlays.cleanupAllOverlays(); + // first stop all timers directly or by invokeMethod // depending on what thread they run in locationUpdateTimer.stop(); @@ -2268,7 +2271,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { auto offscreenUi = DependencyManager::get(); auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QPoint(reticlePosition.x, reticlePosition.y))); + offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); } _keysPressed.remove(event->key()); @@ -2737,15 +2740,7 @@ void Application::setLowVelocityFilter(bool lowVelocityFilter) { } ivec2 Application::getMouse() const { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - - // in the HMD, the reticlePosition is the mouse position - if (isHMDMode()) { - return reticlePosition; - } - - // in desktop mode, we need to map from global to widget space - return toGlm(_glWidget->mapFromGlobal(QPoint(reticlePosition.x, reticlePosition.y))); + return getApplicationCompositor().getReticlePosition(); } FaceTracker* Application::getActiveFaceTracker() { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 41bcc0332a..ddadcb3909 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -73,7 +73,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::AvatarBillboard, this, "processAvatarBillboardPacket"); } AvatarManager::~AvatarManager() { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a3cf1f9f4f..ddc0407f14 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -675,7 +675,11 @@ void MyAvatar::saveData() { settings.setValue("leanScale", _leanScale); settings.setValue("scale", _targetScale); - settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences); + settings.setValue("fullAvatarURL", + _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() ? + "" : + _fullAvatarURLFromPreferences.toString()); + settings.setValue("fullAvatarModelName", _fullAvatarModelName); settings.setValue("animGraphURL", _animGraphUrl); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 1179fbaa50..9ff7f6268f 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -36,15 +36,8 @@ #include -Overlays::Overlays() : _nextOverlayID(1) { - connect(qApp, &Application::beforeAboutToQuit, [=] { - cleanupAllOverlays(); - }); -} - -Overlays::~Overlays() { -} - +Overlays::Overlays() : + _nextOverlayID(1) {} void Overlays::cleanupAllOverlays() { { diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 25ba00fdf0..f47f8de153 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -62,7 +62,6 @@ class Overlays : public QObject { public: Overlays(); - ~Overlays(); void init(); void update(float deltatime); @@ -73,6 +72,8 @@ public: Overlay::Pointer getOverlay(unsigned int id) const; OverlayPanel::Pointer getPanel(unsigned int id) const { return _panels[id]; } + void cleanupAllOverlays(); + public slots: /// adds an overlay with the specific properties unsigned int addOverlay(const QString& type, const QVariant& properties); @@ -145,7 +146,6 @@ signals: private: void cleanupOverlaysToDelete(); - void cleanupAllOverlays(); QMap _overlaysHUD; QMap _overlaysWorld; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index fd13f8c370..9c556dc42b 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -58,7 +58,6 @@ AvatarData::AvatarData() : _headData(NULL), _displayNameTargetAlpha(1.0f), _displayNameAlpha(1.0f), - _billboard(), _errorLogExpiry(0), _owningAvatarMixer(), _targetVelocity(0.0f), @@ -990,7 +989,7 @@ QByteArray AvatarData::identityByteArray() { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); QUrl emptyURL(""); - const QUrl& urlToSend = (_skeletonModelURL == AvatarData::defaultFullAvatarModelUrl()) ? emptyURL : _skeletonModelURL; + const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; QUrl unusedModelURL; // legacy faceModel support @@ -999,13 +998,6 @@ QByteArray AvatarData::identityByteArray() { return identityData; } -bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& data) { - if (data == _billboard) { - return false; - } - _billboard = data; - return true; -} void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { const QUrl& expanded = skeletonModelURL.isEmpty() ? AvatarData::defaultFullAvatarModelUrl() : skeletonModelURL; @@ -1103,33 +1095,6 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { setAttachmentData(attachmentData); } -void AvatarData::setBillboard(const QByteArray& billboard) { - _billboard = billboard; - - qCDebug(avatars) << "Changing billboard for avatar."; -} - -void AvatarData::setBillboardFromURL(const QString &billboardURL) { - _billboardURL = billboardURL; - - - qCDebug(avatars) << "Changing billboard for avatar to PNG at" << qPrintable(billboardURL); - - QNetworkRequest billboardRequest; - billboardRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - billboardRequest.setUrl(QUrl(billboardURL)); - - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkReply* networkReply = networkAccessManager.get(billboardRequest); - connect(networkReply, SIGNAL(finished()), this, SLOT(setBillboardFromNetworkReply())); -} - -void AvatarData::setBillboardFromNetworkReply() { - QNetworkReply* networkReply = static_cast(sender()); - setBillboard(networkReply->readAll()); - networkReply->deleteLater(); -} - void AvatarData::setJointMappingsFromNetworkReply() { QNetworkReply* networkReply = static_cast(sender()); @@ -1204,21 +1169,6 @@ void AvatarData::sendIdentityPacket() { }); } -void AvatarData::sendBillboardPacket() { - if (!_billboard.isEmpty()) { - auto nodeList = DependencyManager::get(); - - // This makes sure the billboard won't be too large to send. - // Once more protocol changes are done and we can send blocks of data we can support sending > MTU sized billboards. - if (_billboard.size() <= NLPacket::maxPayloadSize(PacketType::AvatarBillboard)) { - auto billboardPacket = NLPacket::create(PacketType::AvatarBillboard, _billboard.size()); - billboardPacket->write(_billboard); - - nodeList->broadcastToNodes(std::move(billboardPacket), NodeSet() << NodeType::AvatarMixer); - } - } -} - void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 900da38ffa..a7b97ef4c0 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -107,7 +107,6 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; -const int AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS = 5000; // See also static AvatarData::defaultFullAvatarModelUrl(). const QString DEFAULT_FULL_AVATAR_MODEL_NAME = QString("Default"); @@ -160,7 +159,6 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName) Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript) Q_PROPERTY(QVector attachmentData READ getAttachmentData WRITE setAttachmentData) - Q_PROPERTY(QString billboardURL READ getBillboardURL WRITE setBillboardFromURL) Q_PROPERTY(QStringList jointNames READ getJointNames) @@ -285,8 +283,6 @@ public: bool hasIdentityChangedAfterParsing(const QByteArray& data); QByteArray identityByteArray(); - bool hasBillboardChangedAfterParsing(const QByteArray& data); - const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } const QString& getDisplayName() const { return _displayName; } virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); @@ -304,12 +300,6 @@ public: Q_INVOKABLE void detachOne(const QString& modelURL, const QString& jointName = QString()); Q_INVOKABLE void detachAll(const QString& modelURL, const QString& jointName = QString()); - virtual void setBillboard(const QByteArray& billboard); - const QByteArray& getBillboard() const { return _billboard; } - - void setBillboardFromURL(const QString& billboardURL); - const QString& getBillboardURL() { return _billboardURL; } - QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); } void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); } @@ -336,9 +326,7 @@ public: public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); - void sendBillboardPacket(); - void setBillboardFromNetworkReply(); void setJointMappingsFromNetworkReply(); void setSessionUUID(const QUuid& sessionUUID) { setID(sessionUUID); } @@ -377,9 +365,6 @@ protected: float _displayNameTargetAlpha; float _displayNameAlpha; - QByteArray _billboard; - QString _billboardURL; - QHash _jointIndices; ///< 1-based, since zero is returned for missing keys QStringList _jointNames; ///< in order of depth-first traversal diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 75fb5e6028..f14e2b0ad3 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -136,17 +136,6 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer } } -void AvatarHashMap::processAvatarBillboardPacket(QSharedPointer message, SharedNodePointer sendingNode) { - QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - - QByteArray billboard = message->read(message->getBytesLeftToRead()); - if (avatar->getBillboard() != billboard) { - avatar->setBillboard(billboard); - } -} - void AvatarHashMap::processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode) { // read the node id QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index ee1197367c..5f58074427 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -53,7 +53,6 @@ private slots: void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); - void processAvatarBillboardPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); protected: diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 814faa8874..bec2fa9b8d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -77,9 +78,30 @@ EntityTreeRenderer::~EntityTreeRenderer() { int EntityTreeRenderer::_entitiesScriptEngineCount = 0; -void EntityTreeRenderer::setupEntitiesScriptEngine() { - QSharedPointer oldEngine = _entitiesScriptEngine; // save the old engine through this function, so the EntityScriptingInterface doesn't have problems with it. - _entitiesScriptEngine = QSharedPointer(new ScriptEngine(NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount)), &QObject::deleteLater); +void entitiesScriptEngineDeleter(ScriptEngine* engine) { + class WaitRunnable : public QRunnable { + public: + WaitRunnable(ScriptEngine* engine) : _engine(engine) {} + virtual void run() override { + _engine->waitTillDoneRunning(); + _engine->deleteLater(); + } + + private: + ScriptEngine* _engine; + }; + + // Wait for the scripting thread from the thread pool to avoid hanging the main thread + QThreadPool::globalInstance()->start(new WaitRunnable(engine)); +} + +void EntityTreeRenderer::resetEntitiesScriptEngine() { + // Keep a ref to oldEngine until newEngine is ready so EntityScriptingInterface has something to use + auto oldEngine = _entitiesScriptEngine; + + auto newEngine = new ScriptEngine(NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount)); + _entitiesScriptEngine = QSharedPointer(newEngine, entitiesScriptEngineDeleter); + _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data()); _entitiesScriptEngine->runInThread(); DependencyManager::get()->setEntitiesScriptEngine(_entitiesScriptEngine.data()); @@ -87,16 +109,16 @@ void EntityTreeRenderer::setupEntitiesScriptEngine() { void EntityTreeRenderer::clear() { leaveAllEntities(); + if (_entitiesScriptEngine) { + // Unload and stop the engine here (instead of in its deleter) to + // avoid marshalling unload signals back to this thread _entitiesScriptEngine->unloadAllEntityScripts(); _entitiesScriptEngine->stop(); } if (_wantScripts && !_shuttingDown) { - // NOTE: you can't actually need to delete it here because when we call setupEntitiesScriptEngine it will - // assign a new instance to our shared pointer, which will deref the old instance and ultimately call - // the custom deleter which calls deleteLater - setupEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } auto scene = _viewState->getMain3DScene(); @@ -125,7 +147,7 @@ void EntityTreeRenderer::init() { entityTree->setFBXService(this); if (_wantScripts) { - setupEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } forceRecheckEntities(); // setup our state to force checking our inside/outsideness of entities diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a6fc58e5f1..5c06c5f5cc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -126,7 +126,7 @@ protected: } private: - void setupEntitiesScriptEngine(); + void resetEntitiesScriptEngine(); void addEntityToScene(EntityItemPointer entity); bool findBestZoneAndMaybeContainingEntities(const glm::vec3& avatarPosition, QVector* entitiesContainingAvatar); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c4ac9b09e5..a537ecd0f3 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -162,6 +162,19 @@ void RenderableModelEntityItem::remapTextures() { } } +void RenderableModelEntityItem::doInitialModelSimulation() { + _model->setScaleToFit(true, getDimensions()); + _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); + _model->setRotation(getRotation()); + _model->setTranslation(getPosition()); + { + PerformanceTimer perfTimer("_model->simulate"); + _model->simulate(0.0f); + } + _needsInitialSimulation = false; +} + + // TODO: we need a solution for changes to the postion/rotation/etc of a model... // this current code path only addresses that in this setup case... not the changing/moving case bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { @@ -172,22 +185,12 @@ bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { getModel(renderer); } if (renderArgs && _model && _needsInitialSimulation && _model->isActive() && _model->isLoaded()) { - _model->setScaleToFit(true, getDimensions()); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - _needsInitialSimulation = false; - + doInitialModelSimulation(); _model->renderSetup(renderArgs); } bool ready = !_needsInitialSimulation && _model && _model->readyToAddToScene(renderArgs); - return ready; + return ready; } class RenderableModelEntityItemMeta { @@ -348,18 +351,7 @@ void RenderableModelEntityItem::updateModelBounds() { _model->getRotation() != getRotation() || _model->getRegistrationPoint() != getRegistrationPoint()) && _model->isActive() && _dimensionsInitialized) { - _model->setScaleToFit(true, dimensions); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - - _needsInitialSimulation = false; + doInitialModelSimulation(); _needsJointSimulation = false; } } @@ -592,8 +584,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { if (_needsInitialSimulation) { // the _model's offset will be wrong until _needsInitialSimulation is false PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - _needsInitialSimulation = false; + doInitialModelSimulation(); } return true; @@ -807,6 +798,29 @@ void RenderableModelEntityItem::locationChanged(bool tellPhysics) { if (_model && _model->isActive()) { _model->setRotation(getRotation()); _model->setTranslation(getPosition()); + + void* key = (void*)this; + std::weak_ptr weakSelf = + std::static_pointer_cast(getThisPointer()); + + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf]() { + auto self = weakSelf.lock(); + if (!self) { + return; + } + + render::ItemID myMetaItem = self->getMetaRenderItem(); + + if (myMetaItem == render::Item::INVALID_ITEM_ID) { + return; + } + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + render::PendingChanges pendingChanges; + + pendingChanges.updateItem(myMetaItem); + scene->enqueuePendingChanges(pendingChanges); + }); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 59208d209d..d2de45f538 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -38,6 +38,8 @@ public: EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; + void doInitialModelSimulation(); + virtual bool readyToAddToScene(RenderArgs* renderArgs = nullptr); virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; @@ -87,6 +89,8 @@ public: bool hasRenderAnimation() const { return !_renderAnimationProperties.getURL().isEmpty(); } const QString& getRenderAnimationURL() const { return _renderAnimationProperties.getURL(); } + render::ItemID getMetaRenderItem() { return _myMetaItem; } + private: QVariantMap parseTexturesToMap(QString textures); void remapTextures(); diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index a199c6b10e..86c3f5ff35 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -267,7 +267,7 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { if (numBytes == 0) { return; } - memcpy(particleBuffer->editData(), particlePrimitives->data(), numBytes); + particleBuffer->setData(numBytes, (const gpu::Byte*)particlePrimitives->data()); // Update transform and bounds payload.setModelTransform(transform); diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index 94599496b0..84f801b059 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -23,7 +23,6 @@ UpdateEntityOperator::UpdateEntityOperator(EntityTreePointer tree, _foundOld(false), _foundNew(false), _removeOld(false), - _dontMove(false), // assume we'll be moving _changeTime(usecTimestampNow()), _oldEntityCube(), _newEntityCube(), @@ -43,32 +42,23 @@ UpdateEntityOperator::UpdateEntityOperator(EntityTreePointer tree, _oldEntityCube = _existingEntity->getQueryAACube(); _oldEntityBox = _oldEntityCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); // clamp to domain bounds - // If our new properties don't have bounds details (no change to position, etc) or if this containing element would - // be the best fit for our new properties, then just do the new portion of the store pass, since the change path will - // be the same for both parts of the update - bool oldElementBestFit = _containingElement->bestFitBounds(newQueryAACube); + _newEntityCube = newQueryAACube; + _newEntityBox = _newEntityCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); // clamp to domain bounds + + // set oldElementBestFit true if the entity was in the correct element before this operator was run. + bool oldElementBestFit = _containingElement->bestFitBounds(_oldEntityBox); // For some reason we've seen a case where the original containing element isn't a best fit for the old properties // in this case we want to move it, even if the properties haven't changed. if (!oldElementBestFit) { - _newEntityCube = _oldEntityCube; + _oldEntityBox = _existingEntity->getElement()->getAACube(); _removeOld = true; // our properties are going to move us, so remember this for later processing if (_wantDebug) { - qCDebug(entities) << " **** UNUSUAL CASE **** no changes, but not best fit... consider it a move.... **"; - } - } else { - _foundOld = true; - _newEntityCube = _oldEntityCube; - _dontMove = true; - - if (_wantDebug) { - qCDebug(entities) << " **** TYPICAL NO MOVE CASE **** oldElementBestFit:" << oldElementBestFit; + qCDebug(entities) << " **** UNUSUAL CASE **** not best fit.... **"; } } - _newEntityBox = _newEntityCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); // clamp to domain bounds - if (_wantDebug) { qCDebug(entities) << " _entityItemID:" << _entityItemID; qCDebug(entities) << " _containingElementCube:" << _containingElementCube; diff --git a/libraries/entities/src/UpdateEntityOperator.h b/libraries/entities/src/UpdateEntityOperator.h index ea6faabe3e..8b5bbdf135 100644 --- a/libraries/entities/src/UpdateEntityOperator.h +++ b/libraries/entities/src/UpdateEntityOperator.h @@ -37,7 +37,6 @@ private: bool _foundOld; bool _foundNew; bool _removeOld; - bool _dontMove; quint64 _changeTime; AACube _oldEntityCube; diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index aa8edc3f68..38121555ed 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -624,7 +624,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, return geometryPtr; } - void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << "---------------- fbxGeometry ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints; diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index d9ead354ea..672a29538b 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -65,15 +65,22 @@ public: class GLBuffer : public GPUObject { public: - Stamp _stamp; - GLuint _buffer; - GLuint _size; + const GLuint _buffer; + const GLuint _size; + const Stamp _stamp; - GLBuffer(); + GLBuffer(const Buffer& buffer, GLBuffer* original = nullptr); ~GLBuffer(); - void setSize(GLuint size); + void transfer(); + + private: + bool getNextTransferBlock(GLintptr& outOffset, GLsizeiptr& outSize, size_t& currentPage) const; + + // The owning texture + const Buffer& _gpuBuffer; }; + static GLBuffer* syncGPUObject(const Buffer& buffer); static GLuint getBufferID(const Buffer& buffer); diff --git a/libraries/gpu/src/gpu/GLBackendBuffer.cpp b/libraries/gpu/src/gpu/GLBackendBuffer.cpp index 080d743104..7098fd1feb 100755 --- a/libraries/gpu/src/gpu/GLBackendBuffer.cpp +++ b/libraries/gpu/src/gpu/GLBackendBuffer.cpp @@ -12,12 +12,40 @@ using namespace gpu; -GLBackend::GLBuffer::GLBuffer() : - _stamp(0), - _buffer(0), - _size(0) -{ +GLuint allocateSingleBuffer() { + GLuint result; + glGenBuffers(1, &result); + (void)CHECK_GL_ERROR(); + return result; +} + +GLBackend::GLBuffer::GLBuffer(const Buffer& buffer, GLBuffer* original) : + _buffer(allocateSingleBuffer()), + _size((GLuint)buffer._sysmem.getSize()), + _stamp(buffer._sysmem.getStamp()), + _gpuBuffer(buffer) { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + if (GLEW_VERSION_4_4 || GLEW_ARB_buffer_storage) { + glBufferStorage(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); + (void)CHECK_GL_ERROR(); + } else { + glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); + (void)CHECK_GL_ERROR(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + + if (original) { + glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); + glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); + glBindBuffer(GL_COPY_WRITE_BUFFER, 0); + glBindBuffer(GL_COPY_READ_BUFFER, 0); + (void)CHECK_GL_ERROR(); + } + + Backend::setGPUObject(buffer, this); Backend::incrementBufferGPUCount(); + Backend::updateBufferGPUMemoryUsage(0, _size); } GLBackend::GLBuffer::~GLBuffer() { @@ -28,37 +56,56 @@ GLBackend::GLBuffer::~GLBuffer() { Backend::decrementBufferGPUCount(); } -void GLBackend::GLBuffer::setSize(GLuint size) { - Backend::updateBufferGPUMemoryUsage(_size, size); - _size = size; +void GLBackend::GLBuffer::transfer() { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + (void)CHECK_GL_ERROR(); + GLintptr offset; + GLsizeiptr size; + size_t currentPage { 0 }; + auto data = _gpuBuffer.getSysmem().readData(); + while (getNextTransferBlock(offset, size, currentPage)) { + glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); + (void)CHECK_GL_ERROR(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + (void)CHECK_GL_ERROR(); + _gpuBuffer._flags &= ~Buffer::DIRTY; +} + +bool GLBackend::GLBuffer::getNextTransferBlock(GLintptr& outOffset, GLsizeiptr& outSize, size_t& currentPage) const { + size_t pageCount = _gpuBuffer._pages.size(); + // Advance to the first dirty page + while (currentPage < pageCount && (0 == (Buffer::DIRTY & _gpuBuffer._pages[currentPage]))) { + ++currentPage; + } + + // If we got to the end, we're done + if (currentPage >= pageCount) { + return false; + } + + // Advance to the next clean page + outOffset = static_cast(currentPage * _gpuBuffer._pageSize); + while (currentPage < pageCount && (0 != (Buffer::DIRTY & _gpuBuffer._pages[currentPage]))) { + _gpuBuffer._pages[currentPage] &= ~Buffer::DIRTY; + ++currentPage; + } + outSize = static_cast((currentPage * _gpuBuffer._pageSize) - outOffset); + return true; } GLBackend::GLBuffer* GLBackend::syncGPUObject(const Buffer& buffer) { GLBuffer* object = Backend::getGPUObject(buffer); - if (object && (object->_stamp == buffer.getSysmem().getStamp())) { - return object; + // Has the storage size changed? + if (!object || object->_stamp != buffer.getSysmem().getStamp()) { + object = new GLBuffer(buffer, object); } - // need to have a gpu object? - if (!object) { - object = new GLBuffer(); - glGenBuffers(1, &object->_buffer); - (void) CHECK_GL_ERROR(); - Backend::setGPUObject(buffer, object); + if (0 != (buffer._flags & Buffer::DIRTY)) { + object->transfer(); } - // Now let's update the content of the bo with the sysmem version - // TODO: in the future, be smarter about when to actually upload the glBO version based on the data that did change - //if () { - glBindBuffer(GL_ARRAY_BUFFER, object->_buffer); - glBufferData(GL_ARRAY_BUFFER, buffer.getSysmem().getSize(), buffer.getSysmem().readData(), GL_DYNAMIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - object->_stamp = buffer.getSysmem().getStamp(); - object->setSize((GLuint)buffer.getSysmem().getSize()); - //} - (void) CHECK_GL_ERROR(); - return object; } diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index 545d0a8cdb..d40c5f9b97 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -209,6 +209,8 @@ void GLBackend::resetOutputStage() { _output._drawFBO = 0; glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); } + + glEnable(GL_FRAMEBUFFER_SRGB); } void GLBackend::do_setFramebuffer(Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp index 609451bd13..24b9544168 100755 --- a/libraries/gpu/src/gpu/GLBackendTexture.cpp +++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp @@ -535,13 +535,12 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL GLint minFilter; GLint magFilter; }; - static const GLFilterMode filterModes[] = { + static const GLFilterMode filterModes[Sampler::NUM_FILTERS] = { { GL_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_POINT, { GL_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR, { GL_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT, { GL_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR, - { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_MAG_POINT_MIP_LINEAR, { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, @@ -557,7 +556,7 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL glTexParameteri(object->_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(object->_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); - static const GLenum comparisonFuncs[] = { + static const GLenum comparisonFuncs[NUM_COMPARISON_FUNCS] = { GL_NEVER, GL_LESS, GL_EQUAL, @@ -574,7 +573,7 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL glTexParameteri(object->_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); } - static const GLenum wrapModes[] = { + static const GLenum wrapModes[Sampler::NUM_WRAP_MODES] = { GL_REPEAT, // WRAP_REPEAT, GL_MIRRORED_REPEAT, // WRAP_MIRROR, GL_CLAMP_TO_EDGE, // WRAP_CLAMP, diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index deb17300c3..7dbe662cbc 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -109,40 +109,18 @@ void Resource::Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { } } -Resource::Sysmem::Sysmem() : - _stamp(0), - _size(0), - _data(NULL) -{ -} +Resource::Sysmem::Sysmem() {} -Resource::Sysmem::Sysmem(Size size, const Byte* bytes) : - _stamp(0), - _size(0), - _data(NULL) -{ - if (size > 0) { - _size = allocateMemory(&_data, size); - if (_size >= size) { - if (bytes) { - memcpy(_data, bytes, size); - } - } +Resource::Sysmem::Sysmem(Size size, const Byte* bytes) { + if (size > 0 && bytes) { + setData(_size, bytes); } } -Resource::Sysmem::Sysmem(const Sysmem& sysmem) : - _stamp(0), - _size(0), - _data(NULL) -{ +Resource::Sysmem::Sysmem(const Sysmem& sysmem) { if (sysmem.getSize() > 0) { - _size = allocateMemory(&_data, sysmem.getSize()); - if (_size >= sysmem.getSize()) { - if (sysmem.readData()) { - memcpy(_data, sysmem.readData(), sysmem.getSize()); - } - } + allocate(sysmem._size); + setData(_size, sysmem._data); } } @@ -208,7 +186,6 @@ Resource::Size Resource::Sysmem::setData( Size size, const Byte* bytes ) { if (allocate(size) == size) { if (size && bytes) { memcpy( _data, bytes, _size ); - _stamp++; } } return _size; @@ -217,7 +194,6 @@ Resource::Size Resource::Sysmem::setData( Size size, const Byte* bytes ) { Resource::Size Resource::Sysmem::setSubData( Size offset, Size size, const Byte* bytes) { if (size && ((offset + size) <= getSize()) && bytes) { memcpy( _data + offset, bytes, size ); - _stamp++; return size; } return 0; @@ -264,65 +240,105 @@ Buffer::Size Buffer::getBufferGPUMemoryUsage() { return Context::getBufferGPUMemoryUsage(); } -Buffer::Buffer() : - Resource(), - _sysmem(new Sysmem()) { +Buffer::Buffer(Size pageSize) : + _pageSize(pageSize) { _bufferCPUCount++; - } -Buffer::Buffer(Size size, const Byte* bytes) : - Resource(), - _sysmem(new Sysmem(size, bytes)) { - _bufferCPUCount++; - Buffer::updateBufferCPUMemoryUsage(0, _sysmem->getSize()); +Buffer::Buffer(Size size, const Byte* bytes, Size pageSize) : Buffer(pageSize) { + setData(size, bytes); } -Buffer::Buffer(const Buffer& buf) : - Resource(), - _sysmem(new Sysmem(buf.getSysmem())) { - _bufferCPUCount++; - Buffer::updateBufferCPUMemoryUsage(0, _sysmem->getSize()); +Buffer::Buffer(const Buffer& buf) : Buffer(buf._pageSize) { + setData(buf.getSize(), buf.getData()); } Buffer& Buffer::operator=(const Buffer& buf) { - (*_sysmem) = buf.getSysmem(); + const_cast(_pageSize) = buf._pageSize; + setData(buf.getSize(), buf.getData()); return (*this); } Buffer::~Buffer() { _bufferCPUCount--; - - if (_sysmem) { - Buffer::updateBufferCPUMemoryUsage(_sysmem->getSize(), 0); - delete _sysmem; - _sysmem = NULL; - } + Buffer::updateBufferCPUMemoryUsage(_sysmem.getSize(), 0); } Buffer::Size Buffer::resize(Size size) { + _end = size; auto prevSize = editSysmem().getSize(); - auto newSize = editSysmem().resize(size); - Buffer::updateBufferCPUMemoryUsage(prevSize, newSize); - return newSize; + if (prevSize < size) { + auto newPages = getRequiredPageCount(); + auto newSize = newPages * _pageSize; + editSysmem().resize(newSize); + // All new pages start off as clean, because they haven't been populated by data + _pages.resize(newPages, 0); + Buffer::updateBufferCPUMemoryUsage(prevSize, newSize); + } + return _end; } +void Buffer::markDirty(Size offset, Size bytes) { + if (!bytes) { + return; + } + _flags |= DIRTY; + // Find the starting page + Size startPage = (offset / _pageSize); + // Non-zero byte count, so at least one page is dirty + Size pageCount = 1; + // How much of the page is after the offset? + Size remainder = _pageSize - (offset % _pageSize); + // If there are more bytes than page space remaining, we need to increase the page count + if (bytes > remainder) { + // Get rid of the amount that will fit in the current page + bytes -= remainder; + + pageCount += (bytes / _pageSize); + if (bytes % _pageSize) { + ++pageCount; + } + } + + // Mark the pages dirty + for (Size i = 0; i < pageCount; ++i) { + _pages[i + startPage] |= DIRTY; + } +} + + Buffer::Size Buffer::setData(Size size, const Byte* data) { - auto prevSize = editSysmem().getSize(); - auto newSize = editSysmem().setData(size, data); - Buffer::updateBufferCPUMemoryUsage(prevSize, newSize); - return newSize; + resize(size); + setSubData(0, size, data); + return _end; } Buffer::Size Buffer::setSubData(Size offset, Size size, const Byte* data) { - return editSysmem().setSubData( offset, size, data); + auto changedBytes = editSysmem().setSubData(offset, size, data); + if (changedBytes) { + markDirty(offset, changedBytes); + } + return changedBytes; } Buffer::Size Buffer::append(Size size, const Byte* data) { - auto prevSize = editSysmem().getSize(); - auto newSize = editSysmem().append( size, data); - Buffer::updateBufferCPUMemoryUsage(prevSize, newSize); - return newSize; + auto offset = _end; + resize(_end + size); + setSubData(offset, size, data); + return _end; +} + +Buffer::Size Buffer::getSize() const { + Q_ASSERT(getSysmem().getSize() >= _end); + return _end; +} + +Buffer::Size Buffer::getRequiredPageCount() const { + Size result = _end / _pageSize; + if (_end % _pageSize) { + ++result; + } + return result; } const Element BufferView::DEFAULT_ELEMENT = Element( gpu::SCALAR, gpu::UINT8, gpu::RAW ); diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 570aff00fc..70292f215b 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -88,10 +88,10 @@ protected: // Access the byte array. // The edit version allow to map data. const Byte* readData() const { return _data; } - Byte* editData() { _stamp++; return _data; } + Byte* editData() { return _data; } template< typename T > const T* read() const { return reinterpret_cast< T* > ( _data ); } - template< typename T > T* edit() { _stamp++; return reinterpret_cast< T* > ( _data ); } + template< typename T > T* edit() { return reinterpret_cast< T* > ( _data ); } // Access the current version of the sysmem, used to compare if copies are in sync Stamp getStamp() const { return _stamp; } @@ -102,9 +102,9 @@ protected: bool isAvailable() const { return (_data != 0); } private: - Stamp _stamp; - Size _size; - Byte* _data; + Stamp _stamp { 0 }; + Size _size { 0 }; + Byte* _data { nullptr }; }; }; @@ -115,22 +115,28 @@ class Buffer : public Resource { static void updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: + enum Flag { + DIRTY = 0x01, + }; + + // Currently only one flag... 'dirty' + using PageFlags = std::vector; + static const Size DEFAULT_PAGE_SIZE = 4096; static uint32_t getBufferCPUCount(); static Size getBufferCPUMemoryUsage(); static uint32_t getBufferGPUCount(); static Size getBufferGPUMemoryUsage(); - Buffer(); - Buffer(Size size, const Byte* bytes); + Buffer(Size pageSize = DEFAULT_PAGE_SIZE); + Buffer(Size size, const Byte* bytes, Size pageSize = DEFAULT_PAGE_SIZE); Buffer(const Buffer& buf); // deep copy of the sysmem buffer Buffer& operator=(const Buffer& buf); // deep copy of the sysmem buffer ~Buffer(); // The size in bytes of data stored in the buffer - Size getSize() const { return getSysmem().getSize(); } + Size getSize() const; const Byte* getData() const { return getSysmem().readData(); } - Byte* editData() { return editSysmem().editData(); } - + // Resize the buffer // Keep previous data [0 to min(pSize, mSize)] Size resize(Size pSize); @@ -143,6 +149,23 @@ public: // \return the number of bytes copied Size setSubData(Size offset, Size size, const Byte* data); + template + Size setSubData(Size index, const T& t) { + Size offset = index * sizeof(T); + Size size = sizeof(T); + return setSubData(offset, size, reinterpret_cast(&t)); + } + + template + Size setSubData(Size index, const std::vector& t) { + if (t.empty()) { + return 0; + } + Size offset = index * sizeof(T); + Size size = t.size() * sizeof(T); + return setSubData(offset, size, reinterpret_cast(&t[0])); + } + // Append new data at the end of the current buffer // do a resize( size + getSize) and copy the new data // \return the number of bytes copied @@ -155,18 +178,38 @@ public: template Size append(const std::vector& t) { + if (t.empty()) { + return _end; + } return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); } - // Access the sysmem object. - const Sysmem& getSysmem() const { assert(_sysmem); return (*_sysmem); } - Sysmem& editSysmem() { assert(_sysmem); return (*_sysmem); } - const GPUObjectPointer gpuObject {}; protected: + void markDirty(Size offset, Size bytes); - Sysmem* _sysmem = NULL; + template + void markDirty(Size index, Size count = 1) { + markDirty(sizeof(T) * index, sizeof(T) * count); + } + + // Access the sysmem object, limited to ourselves and GPUObject derived classes + const Sysmem& getSysmem() const { return _sysmem; } + Sysmem& editSysmem() { return _sysmem; } + Byte* editData() { return editSysmem().editData(); } + + Size getRequiredPageCount() const; + + Size _end { 0 }; + mutable uint8_t _flags; + mutable PageFlags _pages; + const Size _pageSize; + Sysmem _sysmem; + + // FIXME find a more generic way to do this. + friend class GLBackend; + friend class BufferView; }; typedef std::shared_ptr BufferPointer; @@ -290,8 +333,14 @@ public: int _stride; }; +#if 0 + // Direct memory access to the buffer contents is incompatible with the paging memory scheme template Iterator begin() { return Iterator(&edit(0), _stride); } template Iterator end() { return Iterator(&edit(getNum()), _stride); } +#else + template Iterator begin() const { return Iterator(&get(), _stride); } + template Iterator end() const { return Iterator(&get(getNum()), _stride); } +#endif template Iterator cbegin() const { return Iterator(&get(), _stride); } template Iterator cend() const { return Iterator(&get(getNum()), _stride); } @@ -328,6 +377,7 @@ public: qDebug() << "Accessing buffer outside the BufferView range, element size = " << sizeof(T) << " when bufferView size = " << _size; } #endif + _buffer->markDirty(_offset, sizeof(T)); T* t = (reinterpret_cast (_buffer->editData() + _offset)); return *(t); } @@ -361,6 +411,7 @@ public: qDebug() << "Accessing buffer outside the BufferView range, index = " << index << " number elements = " << getNum(); } #endif + _buffer->markDirty(elementOffset, sizeof(T)); return *(reinterpret_cast (_buffer->editData() + elementOffset)); } }; diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index ba61732237..53478be536 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -98,6 +98,7 @@ void Material::setFresnel(const Color& fresnel, bool isSRGB) { } void Material::setMetallic(float metallic) { + metallic = glm::clamp(metallic, 0.0f, 1.0f); _key.setMetallic(metallic > 0.0f); _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); _schemaBuffer.edit()._metallic = metallic; diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index 220b673591..11c43eaee4 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -118,29 +118,18 @@ AnimDebugDraw::AnimDebugDraw() : // HACK: add red, green and blue axis at (1,1,1) _animDebugDrawData->_vertexBuffer->resize(sizeof(Vertex) * 6); - Vertex* data = (Vertex*)_animDebugDrawData->_vertexBuffer->editData(); - - data[0].pos = glm::vec3(1.0, 1.0f, 1.0f); - data[0].rgba = toRGBA(255, 0, 0, 255); - data[1].pos = glm::vec3(2.0, 1.0f, 1.0f); - data[1].rgba = toRGBA(255, 0, 0, 255); - - data[2].pos = glm::vec3(1.0, 1.0f, 1.0f); - data[2].rgba = toRGBA(0, 255, 0, 255); - data[3].pos = glm::vec3(1.0, 2.0f, 1.0f); - data[3].rgba = toRGBA(0, 255, 0, 255); - - data[4].pos = glm::vec3(1.0, 1.0f, 1.0f); - data[4].rgba = toRGBA(0, 0, 255, 255); - data[5].pos = glm::vec3(1.0, 1.0f, 2.0f); - data[5].rgba = toRGBA(0, 0, 255, 255); - - _animDebugDrawData->_indexBuffer->resize(sizeof(uint16_t) * 6); - uint16_t* indices = (uint16_t*)_animDebugDrawData->_indexBuffer->editData(); - for (int i = 0; i < 6; i++) { - indices[i] = i; - } - + + static std::vector vertices({ + Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, + Vertex { glm::vec3(2.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, + Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 255, 0, 255) }, + Vertex { glm::vec3(1.0, 2.0f, 1.0f), toRGBA(0, 255, 0, 255) }, + Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 0, 255, 255) }, + Vertex { glm::vec3(1.0, 1.0f, 2.0f), toRGBA(0, 0, 255, 255) }, + }); + static std::vector indices({ 0, 1, 2, 3, 4, 5 }); + _animDebugDrawData->_vertexBuffer->setSubData(0, vertices); + _animDebugDrawData->_indexBuffer->setSubData(0, indices); } AnimDebugDraw::~AnimDebugDraw() { @@ -356,9 +345,13 @@ void AnimDebugDraw::update() { numVerts += (int)DebugDraw::getInstance().getRays().size() * VERTICES_PER_RAY; // allocate verts! - data._vertexBuffer->resize(sizeof(Vertex) * numVerts); - Vertex* verts = (Vertex*)data._vertexBuffer->editData(); - Vertex* v = verts; + std::vector vertices; + vertices.resize(numVerts); + //Vertex* verts = (Vertex*)data._vertexBuffer->editData(); + Vertex* v = nullptr; + if (numVerts) { + v = &vertices[0]; + } // draw absolute poses for (auto& iter : _absolutePoses) { @@ -381,6 +374,8 @@ void AnimDebugDraw::update() { } } } + data._vertexBuffer->resize(sizeof(Vertex) * numVerts); + data._vertexBuffer->setSubData(0, vertices); // draw markers from shared DebugDraw singleton for (auto& iter : markerMap) { @@ -408,20 +403,19 @@ void AnimDebugDraw::update() { } DebugDraw::getInstance().clearRays(); - assert(numVerts == (v - verts)); + assert((!numVerts && !v) || (numVerts == (v - &vertices[0]))); render::Item::Bound theBound; for (int i = 0; i < numVerts; i++) { - theBound += verts[i].pos; + theBound += vertices[i].pos; } data._bound = theBound; data._isVisible = (numVerts > 0); data._indexBuffer->resize(sizeof(uint16_t) * numVerts); - uint16_t* indices = (uint16_t*)data._indexBuffer->editData(); for (int i = 0; i < numVerts; i++) { - indices[i] = i; + data._indexBuffer->setSubData(i, (uint16_t)i);; } }); scene->enqueuePendingChanges(pendingChanges); diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 22bfbfd869..3223ee5535 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -59,7 +59,8 @@ void FramebufferCache::createPrimaryFramebuffer() { _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); - auto colorFormat = gpu::Element::COLOR_RGBA_32; + // auto colorFormat = gpu::Element::COLOR_RGBA_32; + auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto width = _frameBufferSize.width(); auto height = _frameBufferSize.height(); @@ -95,10 +96,7 @@ void FramebufferCache::createPrimaryFramebuffer() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - // FIXME: Decide on the proper one, let s stick to R11G11B10 for now - //_lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, defaultSampler)); _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); - //_lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC4, gpu::HALF, gpu::RGBA), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -212,7 +210,7 @@ gpu::TexturePointer FramebufferCache::getLightingTexture() { gpu::FramebufferPointer FramebufferCache::getFramebuffer() { if (_cachedFramebuffers.isEmpty()) { - _cachedFramebuffers.push_back(gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, _frameBufferSize.width(), _frameBufferSize.height()))); + _cachedFramebuffers.push_back(gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _frameBufferSize.width(), _frameBufferSize.height()))); } gpu::FramebufferPointer result = _cachedFramebuffers.front(); _cachedFramebuffers.pop_front(); diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 5ad9dc174f..0f848ee231 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -18,7 +18,7 @@ // the interpolated normal in vec3 _normal; in vec3 _modelNormal; -in vec3 _color; +in vec4 _color; in vec2 _texCoord0; in vec4 _position; diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index c2724fce42..bfbd123382 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -116,35 +116,25 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, // FIrst thing, we collect the bound and the status for all the items we want to render int nbItems = 0; { - if (!_itemBounds) { - _itemBounds = std::make_shared(); - } - if (!_itemStatus) { - _itemStatus = std::make_shared();; - } - if (!_itemCells) { - _itemCells = std::make_shared();; - } + _itemBounds.resize(inItems.size()); + _itemStatus.resize(inItems.size()); + _itemCells.resize(inItems.size()); - _itemBounds->resize((inItems.size() * sizeof(AABox))); - _itemStatus->resize((inItems.size() * NUM_STATUS_VEC4_PER_ITEM * sizeof(glm::vec4))); - _itemCells->resize((inItems.size() * sizeof(Octree::Location))); - - AABox* itemAABox = reinterpret_cast (_itemBounds->editData()); - glm::ivec4* itemStatus = reinterpret_cast (_itemStatus->editData()); - Octree::Location* itemCell = reinterpret_cast (_itemCells->editData()); - for (auto& item : inItems) { +// AABox* itemAABox = reinterpret_cast (_itemBounds->editData()); +// glm::ivec4* itemStatus = reinterpret_cast (_itemStatus->editData()); +// Octree::Location* itemCell = reinterpret_cast (_itemCells->editData()); + for (size_t i = 0; i < inItems.size(); ++i) { + const auto& item = inItems[i]; if (!item.bound.isInvalid()) { if (!item.bound.isNull()) { - (*itemAABox) = item.bound; + _itemBounds[i] = item.bound; } else { - (*itemAABox).setBox(item.bound.getCorner(), 0.1f); + _itemBounds[i].setBox(item.bound.getCorner(), 0.1f); } auto& itemScene = scene->getItem(item.id); - - (*itemCell) = scene->getSpatialTree().getCellLocation(itemScene.getCell()); + _itemCells[i] = scene->getSpatialTree().getCellLocation(itemScene.getCell()); auto itemStatusPointer = itemScene.getStatus(); if (itemStatusPointer) { @@ -152,25 +142,19 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, auto&& currentStatusValues = itemStatusPointer->getCurrentValues(); int valueNum = 0; for (int vec4Num = 0; vec4Num < NUM_STATUS_VEC4_PER_ITEM; vec4Num++) { - (*itemStatus) = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); + auto& value = (vec4Num ? _itemStatus[i].first : _itemStatus[i].second); + value = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); for (int component = 0; component < VEC4_LENGTH; component++) { valueNum = vec4Num * VEC4_LENGTH + component; if (valueNum < (int)currentStatusValues.size()) { - (*itemStatus)[component] = currentStatusValues[valueNum].getPackedData(); + value[component] = currentStatusValues[valueNum].getPackedData(); } } - itemStatus++; } } else { - (*itemStatus) = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); - itemStatus++; - (*itemStatus) = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); - itemStatus++; + _itemStatus[i].first = _itemStatus[i].second = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); } - nbItems++; - itemAABox++; - itemCell++; } } } @@ -194,25 +178,20 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, // bind the one gpu::Pipeline we need batch.setPipeline(getDrawItemBoundsPipeline()); - AABox* itemAABox = reinterpret_cast (_itemBounds->editData()); - glm::ivec4* itemStatus = reinterpret_cast (_itemStatus->editData()); - Octree::Location* itemCell = reinterpret_cast (_itemCells->editData()); + //AABox* itemAABox = reinterpret_cast (_itemBounds->editData()); + //glm::ivec4* itemStatus = reinterpret_cast (_itemStatus->editData()); + //Octree::Location* itemCell = reinterpret_cast (_itemCells->editData()); const unsigned int VEC3_ADRESS_OFFSET = 3; if (_showDisplay) { for (int i = 0; i < nbItems; i++) { - batch._glUniform3fv(_drawItemBoundPosLoc, 1, (const float*) (itemAABox + i)); - batch._glUniform3fv(_drawItemBoundDimLoc, 1, ((const float*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); - - - glm::ivec4 cellLocation(itemCell->pos.x, itemCell->pos.y, itemCell->pos.z, itemCell->depth); + batch._glUniform3fv(_drawItemBoundPosLoc, 1, (const float*)&(_itemBounds[i])); + batch._glUniform3fv(_drawItemBoundDimLoc, 1, ((const float*)&(_itemBounds[i])) + VEC3_ADRESS_OFFSET); + glm::ivec4 cellLocation(_itemCells[i].pos, _itemCells[i].depth); batch._glUniform4iv(_drawItemCellLocLoc, 1, ((const int*)(&cellLocation))); - - batch.draw(gpu::LINES, 24, 0); - itemCell++; } } @@ -222,10 +201,10 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, if (_showNetwork) { for (int i = 0; i < nbItems; i++) { - batch._glUniform3fv(_drawItemStatusPosLoc, 1, (const float*) (itemAABox + i)); - batch._glUniform3fv(_drawItemStatusDimLoc, 1, ((const float*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); - batch._glUniform4iv(_drawItemStatusValue0Loc, 1, (const int*)(itemStatus + NUM_STATUS_VEC4_PER_ITEM * i)); - batch._glUniform4iv(_drawItemStatusValue1Loc, 1, (const int*)(itemStatus + NUM_STATUS_VEC4_PER_ITEM * i + 1)); + batch._glUniform3fv(_drawItemStatusPosLoc, 1, (const float*)&(_itemBounds[i])); + batch._glUniform3fv(_drawItemStatusDimLoc, 1, ((const float*)&(_itemBounds[i])) + VEC3_ADRESS_OFFSET); + batch._glUniform4iv(_drawItemStatusValue0Loc, 1, (const int*)&(_itemStatus[i].first)); + batch._glUniform4iv(_drawItemStatusValue1Loc, 1, (const int*)&(_itemStatus[i].second)); batch.draw(gpu::TRIANGLES, 24 * NUM_STATUS_VEC4_PER_ITEM, 0); } } diff --git a/libraries/render/src/render/DrawStatus.h b/libraries/render/src/render/DrawStatus.h index 839a98b373..e60cb58779 100644 --- a/libraries/render/src/render/DrawStatus.h +++ b/libraries/render/src/render/DrawStatus.h @@ -68,9 +68,13 @@ namespace render { gpu::Stream::FormatPointer _drawItemFormat; gpu::PipelinePointer _drawItemBoundsPipeline; gpu::PipelinePointer _drawItemStatusPipeline; - gpu::BufferPointer _itemBounds; - gpu::BufferPointer _itemCells; - gpu::BufferPointer _itemStatus; + + std::vector _itemBounds; + std::vector> _itemStatus; + std::vector _itemCells; + //gpu::BufferPointer _itemBounds; + //gpu::BufferPointer _itemCells; + //gpu::BufferPointer _itemStatus; gpu::TexturePointer _statusIconMap; }; } diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index e091b4842c..0b1d8fb55f 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -157,6 +157,11 @@ void Scene::updateItems(const ItemIDs& ids, UpdateFunctors& functors) { auto updateFunctor = functors.begin(); for (auto updateID : ids) { + if (updateID == Item::INVALID_ITEM_ID) { + updateFunctor++; + continue; + } + // Access the true item auto& item = _items[updateID]; auto oldCell = item.getCell(); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f7ac7894ff..15e896fac4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -62,8 +62,6 @@ #include "MIDIEvent.h" -std::atomic ScriptEngine::_stoppingAllScripts { false }; - static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -138,10 +136,9 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName return false; } -ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString, bool wantSignals) : +ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString) : _scriptContents(scriptContents), _timerFunctionMap(), - _wantSignals(wantSignals), _fileNameString(fileNameString), _arrayBufferClass(new ArrayBufferClass(this)) { @@ -155,7 +152,7 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam } ScriptEngine::~ScriptEngine() { - qCDebug(scriptengine) << "Script Engine shutting down (destructor) for script:" << getFilename(); + qCDebug(scriptengine) << "Script Engine shutting down:" << getFilename(); auto scriptEngines = DependencyManager::get(); if (scriptEngines) { @@ -163,16 +160,15 @@ ScriptEngine::~ScriptEngine() { } else { qCWarning(scriptengine) << "Script destroyed after ScriptEngines!"; } - - waitTillDoneRunning(); } void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); - QThread* receiver; + QThread* workerThread; // Ensure the thread should be running, and does exist - if (_isRunning && _isThreaded && (receiver = thread())) { - connect(this, &ScriptEngine::doneRunning, receiver, &QThread::quit); + if (_isRunning && _isThreaded && (workerThread = thread())) { + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); } } @@ -231,15 +227,14 @@ void ScriptEngine::runDebuggable() { return; } stopAllTimers(); // make sure all our timers are stopped if the script is ending - if (_wantSignals) { - emit scriptEnding(); - emit finished(_fileNameString, this); - } + + emit scriptEnding(); + emit finished(_fileNameString, this); _isRunning = false; - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } + + emit runningStateChanged(); + emit doneRunning(); + timer->deleteLater(); return; } @@ -249,9 +244,7 @@ void ScriptEngine::runDebuggable() { if (_lastUpdate < now) { float deltaTime = (float)(now - _lastUpdate) / (float)USECS_PER_SECOND; if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } + emit update(deltaTime); } } _lastUpdate = now; @@ -272,57 +265,72 @@ void ScriptEngine::runInThread() { } _isThreaded = true; - QThread* workerThread = new QThread(); - QString scriptEngineName = QString("Script Thread:") + getFilename(); - workerThread->setObjectName(scriptEngineName); + // The thread interface cannot live on itself, and we want to move this into the thread, so + // the thread cannot have this as a parent. + QThread* workerThread = new QThread(); + workerThread->setObjectName(QString("Script Thread:") + getFilename()); + moveToThread(workerThread); + // NOTE: If you connect any essential signals for proper shutdown or cleanup of // the script engine, make sure to add code to "reconnect" them to the // disconnectNonEssentialSignals() method - - // when the worker thread is started, call our engine's run.. connect(workerThread, &QThread::started, this, &ScriptEngine::run); - - // tell the thread to stop when the script engine is done connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); - moveToThread(workerThread); - - // Starts an event loop, and emits workerThread->started() workerThread->start(); } void ScriptEngine::waitTillDoneRunning() { - // If the script never started running or finished running before we got here, we don't need to wait for it auto workerThread = thread(); - if (_isThreaded && workerThread) { - QString scriptName = getFilename(); - auto startedWaiting = usecTimestampNow(); + if (_isThreaded && workerThread) { + // We should never be waiting (blocking) on our own thread + assert(workerThread != QThread::currentThread()); + + // Engine should be stopped already, but be defensive + stop(); + + auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { // NOTE: This will be called on the main application thread from stopAllScripts. // The application thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for the main application thread, allowing invokeMethod calls to pass between threads. - QCoreApplication::processEvents(); // thread-safe :) + QCoreApplication::processEvents(); - // If we've been waiting a second or more, then tell the script engine to stop evaluating - static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; + // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; + static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { - qCDebug(scriptengine) << - "Script " << scriptName << " has been running too long [" << elapsedUsecs << " usecs] quitting."; - abortEvaluation(); // to allow the thread to quit workerThread->quit(); - break; + + if (isEvaluating()) { + qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); + abortEvaluation(); + } else { + qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); + auto context = currentContext(); + if (context) { + context->throwError("Timed out during shutdown"); + } + } + + // Wait for the scripting thread to stop running, as + // flooding it with aborts/exceptions will persist it longer + static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; + if (workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { + workerThread->terminate(); + } } // Avoid a pure busy wait QThread::yieldCurrentThread(); } - workerThread->deleteLater(); + qCDebug(scriptengine) << "Script Engine has stopped:" << getFilename(); } } @@ -358,17 +366,13 @@ void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scrip if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) { _debuggable = true; } - if (_wantSignals) { - emit scriptLoaded(url.toString()); - } + emit scriptLoaded(url.toString()); } // FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::errorInLoadingScript(const QUrl& url) { qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__; - if (_wantSignals) { - emit errorLoadingScript(_fileNameString); // ?? - } + emit errorLoadingScript(_fileNameString); // ?? } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -756,7 +760,7 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early } @@ -785,14 +789,12 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi --_evaluatesPending; const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName()); - if (_wantSignals) { - emit evaluationFinished(result, hadUncaughtException); - } + emit evaluationFinished(result, hadUncaughtException); return result; } void ScriptEngine::run() { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { return; // bail early - avoid setting state in init(), as evaluate() will bail too } @@ -801,9 +803,7 @@ void ScriptEngine::run() { } _isRunning = true; - if (_wantSignals) { - emit runningStateChanged(); - } + emit runningStateChanged(); QScriptValue result = evaluate(_scriptContents, _fileNameString); @@ -872,9 +872,7 @@ void ScriptEngine::run() { if (_lastUpdate < now) { float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND; if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } + emit update(deltaTime); } } _lastUpdate = now; @@ -883,10 +881,10 @@ void ScriptEngine::run() { hadUncaughtExceptions(*this, _fileNameString); } + qCDebug(scriptengine) << "Script Engine stopping:" << getFilename(); + stopAllTimers(); // make sure all our timers are stopped if the script is ending - if (_wantSignals) { - emit scriptEnding(); - } + emit scriptEnding(); if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. @@ -904,15 +902,11 @@ void ScriptEngine::run() { } } - if (_wantSignals) { - emit finished(_fileNameString, this); - } + emit finished(_fileNameString, this); _isRunning = false; - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } + emit runningStateChanged(); + emit doneRunning(); } // NOTE: This is private because it must be called on the same thread that created the timers, which is why @@ -945,14 +939,8 @@ void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { void ScriptEngine::stop() { if (!_isFinished) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; - } _isFinished = true; - if (_wantSignals) { - emit runningStateChanged(); - } + emit runningStateChanged(); } } @@ -1025,7 +1013,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { qCDebug(scriptengine) << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } @@ -1034,7 +1022,7 @@ QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { qCDebug(scriptengine) << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } @@ -1076,9 +1064,7 @@ QUrl ScriptEngine::resolvePath(const QString& include) const { } void ScriptEngine::print(const QString& message) { - if (_wantSignals) { - emit printedMessage(message); - } + emit printedMessage(message); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1086,7 +1072,7 @@ void ScriptEngine::print(const QString& message) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored..." << "includeFiles:" << includeFiles << "parent script:" << getFilename(); return; // bail early @@ -1184,7 +1170,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored... " << "includeFile:" << includeFile << "parent script:" << getFilename(); return; // bail early @@ -1199,7 +1185,7 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { - if (_stoppingAllScripts) { + if (DependencyManager::get()->isStopped()) { qCDebug(scriptengine) << "Script.load() while shutting down is ignored... " << "loadFile:" << loadFile << "parent script:" << getFilename(); return; // bail early @@ -1214,13 +1200,9 @@ void ScriptEngine::load(const QString& loadFile) { if (_isReloading) { auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(url.toString()); - if (_wantSignals) { - emit reloadScript(url.toString(), false); - } + emit reloadScript(url.toString(), false); } else { - if (_wantSignals) { - emit loadScript(url.toString(), false); - } + emit loadScript(url.toString(), false); } } @@ -1309,8 +1291,23 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co setParentURL(scriptOrURL); } + const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; QScriptEngine sandbox; - QScriptValue testConstructor = sandbox.evaluate(program); + sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); + QScriptValue testConstructor; + { + QTimer timeout; + timeout.setSingleShot(true); + timeout.start(SANDBOX_TIMEOUT); + connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{ + auto context = sandbox.currentContext(); + if (context) { + // Guard against infinite loops and non-performant code + context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); + } + }); + testConstructor = sandbox.evaluate(program); + } if (hadUncaughtExceptions(sandbox, program.fileName())) { return; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index d37e3eb177..80978e4527 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -67,10 +67,7 @@ public: class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider { Q_OBJECT public: - ScriptEngine(const QString& scriptContents = NO_SCRIPT, - const QString& fileNameString = QString(""), - bool wantSignals = true); - + ScriptEngine(const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("")); ~ScriptEngine(); /// run the script in a dedicated thread. This will have the side effect of evalulating @@ -83,6 +80,15 @@ public: /// run the script in the callers thread, exit when stop() is called. void run(); + QString getFilename() const; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts + Q_INVOKABLE void stop(); + + // Stop any evaluating scripts and wait for the scripting thread to finish. + void waitTillDoneRunning(); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can // properly ensure they are only called on the correct thread @@ -138,10 +144,6 @@ public: Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts - Q_INVOKABLE void stop(); - bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget @@ -191,7 +193,6 @@ protected: bool _isInitialized { false }; QHash _timerFunctionMap; QSet _includedURLs; - bool _wantSignals { true }; QHash _entityScripts; bool _isThreaded { false }; QScriptEngineDebugger* _debugger { nullptr }; @@ -199,8 +200,7 @@ protected: qint64 _lastUpdate; void init(); - QString getFilename() const; - void waitTillDoneRunning(); + bool evaluatePending() const { return _evaluatesPending > 0; } void timerFired(); void stopAllTimers(); @@ -232,9 +232,6 @@ protected: QUrl currentSandboxURL {}; // The toplevel url string for the entity script that loaded the code being executed, else empty. void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function operation); void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args); - - friend class ScriptEngines; - static std::atomic _stoppingAllScripts; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 330a94cf0b..70eb055d22 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -119,26 +119,27 @@ void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) { } void ScriptEngines::addScriptEngine(ScriptEngine* engine) { - _allScriptsMutex.lock(); - _allKnownScriptEngines.insert(engine); - _allScriptsMutex.unlock(); + if (_isStopped) { + engine->deleteLater(); + } else { + QMutexLocker locker(&_allScriptsMutex); + _allKnownScriptEngines.insert(engine); + } } void ScriptEngines::removeScriptEngine(ScriptEngine* engine) { // If we're not already in the middle of stopping all scripts, then we should remove ourselves // from the list of running scripts. We don't do this if we're in the process of stopping all scripts // because that method removes scripts from its list as it iterates them - if (!_stoppingAllScripts) { - _allScriptsMutex.lock(); + if (!_isStopped) { + QMutexLocker locker(&_allScriptsMutex); _allKnownScriptEngines.remove(engine); - _allScriptsMutex.unlock(); } } void ScriptEngines::shutdownScripting() { - _allScriptsMutex.lock(); - _stoppingAllScripts = true; - ScriptEngine::_stoppingAllScripts = true; + _isStopped = true; + QMutexLocker locker(&_allScriptsMutex); qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); QMutableSetIterator i(_allKnownScriptEngines); @@ -149,6 +150,7 @@ void ScriptEngines::shutdownScripting() { // NOTE: typically all script engines are running. But there's at least one known exception to this, the // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using // them. We don't need to stop scripts that aren't running. + // TODO: Scripts could be shut down faster if we spread them across a threadpool. if (scriptEngine->isRunning()) { qCDebug(scriptengine) << "about to shutdown script:" << scriptName; @@ -157,8 +159,7 @@ void ScriptEngines::shutdownScripting() { // and stop. We can safely short circuit this because we know we're in the "quitting" process scriptEngine->disconnect(this); - // Calling stop on the script engine will set it's internal _isFinished state to true, and result - // in the ScriptEngine gracefully ending it's run() method. + // Gracefully stop the engine's scripting thread scriptEngine->stop(); // We need to wait for the engine to be done running before we proceed, because we don't @@ -170,12 +171,10 @@ void ScriptEngines::shutdownScripting() { scriptEngine->deleteLater(); - // If the script is stopped, we can remove it from our set + // Once the script is stopped, we can remove it from our set i.remove(); } } - _stoppingAllScripts = false; - _allScriptsMutex.unlock(); qCDebug(scriptengine) << "DONE Stopping all scripts...."; } @@ -428,7 +427,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL return scriptEngine; } - scriptEngine = new ScriptEngine(NO_SCRIPT, "", true); + scriptEngine = new ScriptEngine(NO_SCRIPT, ""); scriptEngine->setUserLoaded(isUserLoaded); connect(scriptEngine, &ScriptEngine::doneRunning, this, [scriptEngine] { scriptEngine->deleteLater(); @@ -499,7 +498,6 @@ void ScriptEngines::launchScriptEngine(ScriptEngine* scriptEngine) { } } - void ScriptEngines::onScriptFinished(const QString& rawScriptURL, ScriptEngine* engine) { bool removed = false; { diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 6522aa9bb3..72bf7d529e 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -86,16 +86,17 @@ protected: void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); void launchScriptEngine(ScriptEngine* engine); + bool isStopped() const { return _isStopped; } QReadWriteLock _scriptEnginesHashLock; QHash _scriptEnginesHash; QSet _allKnownScriptEngines; QMutex _allScriptsMutex; - std::atomic _stoppingAllScripts { false }; std::list _scriptInitializers; mutable Setting::Handle _scriptsLocationHandle; ScriptsModel _scriptsModel; ScriptsModelFilter _scriptsModelFilter; + std::atomic _isStopped { false }; }; QUrl normalizeScriptURL(const QUrl& rawScriptURL); diff --git a/scripts/system/away.js b/scripts/system/away.js index 932efd6b60..2b2ea8a42b 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -262,6 +262,14 @@ eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(goActive); eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(goActive); eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(goActive); eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(goActive); +eventMapping.from(Controller.Standard.LT).peek().to(goActive); +eventMapping.from(Controller.Standard.LB).peek().to(goActive); +eventMapping.from(Controller.Standard.LS).peek().to(goActive); +eventMapping.from(Controller.Standard.RT).peek().to(goActive); +eventMapping.from(Controller.Standard.RB).peek().to(goActive); +eventMapping.from(Controller.Standard.RS).peek().to(goActive); +eventMapping.from(Controller.Standard.Back).peek().to(goActive); +eventMapping.from(Controller.Standard.Start).peek().to(goActive); Controller.enableMapping(eventMappingName); Script.scriptEnding.connect(function () { @@ -270,4 +278,4 @@ Script.scriptEnding.connect(function () { Controller.disableMapping(eventMappingName); Controller.mousePressEvent.disconnect(goActive); Controller.keyPressEvent.disconnect(maybeGoActive); -}); \ No newline at end of file +}); diff --git a/scripts/system/users.js b/scripts/system/users.js index 9612a19eee..d935dd23ca 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function(properties) { +var PopUpMenu = function (properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -21,9 +21,8 @@ var PopUpMenu = function(properties) { MIN_MAX_BUTTON_SVG_WIDTH = 17.1, MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH; - - MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); + MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH, + MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); function positionDisplayOptions() { var y, @@ -203,7 +202,7 @@ var PopUpMenu = function(properties) { width: MIN_MAX_BUTTON_SVG_WIDTH, height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 }, - color: properties.buttonColor, + //color: properties.buttonColor, alpha: properties.buttonAlpha, visible: properties.visible }); @@ -218,11 +217,10 @@ var PopUpMenu = function(properties) { }; }; -var usersWindow = (function() { +var usersWindow = (function () { - var baseURL = Script.resolvePath("assets/images/tools/"); - - var WINDOW_WIDTH = 260, + var baseURL = Script.resolvePath("assets/images/tools/"), + WINDOW_WIDTH = 260, WINDOW_MARGIN = 12, WINDOW_BASE_MARGIN = 6, // A little less is needed in order look correct WINDOW_FONT = { @@ -248,6 +246,17 @@ var usersWindow = (function() { WINDOW_BACKGROUND_ALPHA = 0.8, windowPane, windowHeading, + + // Window border is similar to that of edit.js. + WINDOW_BORDER_WIDTH = WINDOW_WIDTH + 2 * WINDOW_BASE_MARGIN, + WINDOW_BORDER_TOP_MARGIN = 2 * WINDOW_BASE_MARGIN, + WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, + WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, + WINDOW_BORDER_RADIUS = 4, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, + WINDOW_BORDER_ALPHA = 0.5, + windowBorder, + MIN_MAX_BUTTON_SVG = baseURL + "min-max-toggle.svg", MIN_MAX_BUTTON_SVG_WIDTH = 17.1, MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, @@ -331,6 +340,7 @@ var usersWindow = (function() { visibilityControl, windowHeight, + windowBorderHeight, windowTextHeight, windowLineSpacing, windowLineHeight, // = windowTextHeight + windowLineSpacing @@ -356,14 +366,21 @@ var usersWindow = (function() { MENU_ITEM_AFTER = "Chat...", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", + SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, isMinimized = false, + isBorderVisible = false, - viewportHeight, + viewport, isMirrorDisplay = false, isFullscreenMirror = false, + windowPosition = { }, // Bottom left corner of window pane. + isMovingWindow = false, + movingClickOffset = { x: 0, y: 0 }, + isUsingScrollbars = false, isMovingScrollbar = false, scrollbarBackgroundPosition = {}, @@ -379,19 +396,23 @@ var usersWindow = (function() { if (isMinimized) { windowHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; return; } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; - // Limit window to height of viewport minus VU meter and mirror if displayed + // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; - maxWindowHeight = viewportHeight - AUDIO_METER_HEIGHT; + maxWindowHeight = windowPosition.y - AUDIO_METER_HEIGHT; if (isMirrorDisplay && !isFullscreenMirror) { maxWindowHeight -= MIRROR_HEIGHT; } windowHeight = Math.max(Math.min(windowHeight, maxWindowHeight), nonUsersHeight); + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; // Corresponding number of users to actually display numUsersToDisplay = Math.max(Math.round((windowHeight - nonUsersHeight) / windowLineHeight), 0); @@ -405,38 +426,57 @@ var usersWindow = (function() { } function updateOverlayPositions() { - var y; + // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. + var windowLeft = windowPosition.x, + windowTop = windowPosition.y - windowHeight, + x, + y; + Overlays.editOverlay(windowBorder, { + x: windowPosition.x - WINDOW_BORDER_LEFT_MARGIN, + y: windowTop - WINDOW_BORDER_TOP_MARGIN + }); Overlays.editOverlay(windowPane, { - y: viewportHeight - windowHeight + x: windowLeft, + y: windowTop }); Overlays.editOverlay(windowHeading, { - y: viewportHeight - windowHeight + WINDOW_MARGIN + x: windowLeft + WINDOW_MARGIN, + y: windowTop + WINDOW_MARGIN }); Overlays.editOverlay(minimizeButton, { - y: viewportHeight - windowHeight + WINDOW_MARGIN / 2 + x: windowLeft + WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, + y: windowTop + WINDOW_MARGIN / 2 }); - scrollbarBackgroundPosition.y = viewportHeight - windowHeight + WINDOW_MARGIN + windowTextHeight; + scrollbarBackgroundPosition.x = windowLeft + WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH; + scrollbarBackgroundPosition.y = windowTop + WINDOW_MARGIN + windowTextHeight; Overlays.editOverlay(scrollbarBackground, { + x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { + x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); - y = viewportHeight - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; + x = windowLeft + WINDOW_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + - windowLineHeight - VISIBILITY_SPACER + - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { + x: x, y: y }); y += FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER; - displayControl.updatePosition(WINDOW_MARGIN, y); + displayControl.updatePosition(x, y); y += windowLineHeight + VISIBILITY_SPACER; - visibilityControl.updatePosition(WINDOW_MARGIN, y); + visibilityControl.updatePosition(x, y); } function updateUsersDisplay() { @@ -487,6 +527,10 @@ var usersWindow = (function() { }); } + Overlays.editOverlay(windowBorder, { + height: windowBorderHeight + }); + Overlays.editOverlay(windowPane, { height: windowHeight, text: displayText @@ -512,7 +556,7 @@ var usersWindow = (function() { usersRequest.send(); } - processUsers = function() { + processUsers = function () { var response, myUsername, user, @@ -565,12 +609,15 @@ var usersWindow = (function() { } }; - pollUsersTimedOut = function() { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isVisible && isBorderVisible + }); Overlays.editOverlay(windowPane, { visible: isVisible }); @@ -670,7 +717,7 @@ var usersWindow = (function() { if (clickedOverlay === windowPane) { overlayX = event.x - WINDOW_MARGIN; - overlayY = event.y - viewportHeight + windowHeight - WINDOW_MARGIN - windowLineHeight; + overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; numLinesBefore = Math.round(overlayY / windowLineHeight); minY = numLinesBefore * windowLineHeight; @@ -683,7 +730,8 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX + && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -735,13 +783,29 @@ var usersWindow = (function() { friendsWindow.setURL(FRIENDS_WINDOW_URL); friendsWindow.setVisible(true); friendsWindow.raise(); + return; + } + + if (clickedOverlay === windowBorder) { + movingClickOffset = { + x: event.x - windowPosition.x, + y: event.y - windowPosition.y + }; + + isMovingWindow = true; } } function onMouseMoveEvent(event) { + var isVisible; + if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x + && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN + && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y + && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) + / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -753,35 +817,95 @@ var usersWindow = (function() { isMovingScrollbar = false; } } + + if (isMovingWindow) { + windowPosition = { + x: event.x - movingClickOffset.x, + y: event.y - movingClickOffset.y + }; + calculateWindowHeight(); + updateOverlayPositions(); + updateUsersDisplay(); + + } else { + + isVisible = isBorderVisible; + if (isVisible) { + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x + && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH + && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y + && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + } else { + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH + && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + } + if (isVisible !== isBorderVisible) { + isBorderVisible = isVisible; + Overlays.editOverlay(windowBorder, { + visible: isBorderVisible + }); + } + } } function onMouseReleaseEvent() { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; + var offset = {}; + + if (isMovingScrollbar) { + Overlays.editOverlay(scrollbarBar, { + backgroundAlpha: SCROLLBAR_BAR_ALPHA + }); + isMovingScrollbar = false; + } + + if (isMovingWindow) { + // Save offset of bottom of window to nearest edge of the window. + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; + Settings.setValue(SETINGS_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + isMovingWindow = false; + } } function onScriptUpdate() { - var oldViewportHeight = viewportHeight, + var oldViewport = viewport, oldIsMirrorDisplay = isMirrorDisplay, oldIsFullscreenMirror = isFullscreenMirror, MIRROR_MENU_ITEM = "Mirror", FULLSCREEN_MIRROR_MENU_ITEM = "Fullscreen Mirror"; - viewportHeight = Controller.getViewportDimensions().y; + viewport = Controller.getViewportDimensions(); isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewportHeight !== oldViewportHeight || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay + || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); - updateOverlayPositions(); } + + if (viewport.y !== oldViewport.y) { + if (windowPosition.y > oldViewport.y / 2) { + // Maintain position w.r.t. bottom of window. + windowPosition.y = viewport.y - (oldViewport.y - windowPosition.y); + } + } + + if (viewport.x !== oldViewport.x) { + if (windowPosition.x + (WINDOW_WIDTH / 2) > oldViewport.x / 2) { + // Maintain position w.r.t. right of window. + windowPosition.x = viewport.x - (oldViewport.x - windowPosition.x); + } + } + + updateOverlayPositions(); } function setUp() { - var textSizeOverlay; + var textSizeOverlay, + offsetSetting, + offset = {}, + hmdViewport; textSizeOverlay = Overlays.addOverlay("text", { font: WINDOW_FONT, @@ -792,13 +916,40 @@ var usersWindow = (function() { windowLineHeight = windowTextHeight + windowLineSpacing; Overlays.deleteOverlay(textSizeOverlay); - viewportHeight = Controller.getViewportDimensions().y; + viewport = Controller.getViewportDimensions(); + + offsetSetting = Settings.getValue(SETINGS_USERS_WINDOW_OFFSET); + if (offsetSetting !== "") { + offset = JSON.parse(Settings.getValue(SETINGS_USERS_WINDOW_OFFSET)); + } + if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { + windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; + windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; + + } else { + hmdViewport = Controller.getRecommendedOverlayRect(); + windowPosition = { + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + }; + } calculateWindowHeight(); + windowBorder = Overlays.addOverlay("rectangle", { + x: 0, + y: viewport.y, // Start up off-screen + width: WINDOW_BORDER_WIDTH, + height: windowBorderHeight, + radius: WINDOW_BORDER_RADIUS, + color: WINDOW_BORDER_COLOR, + alpha: WINDOW_BORDER_ALPHA, + visible: isVisible && isBorderVisible + }); + windowPane = Overlays.addOverlay("text", { x: 0, - y: viewportHeight, // Start up off-screen + y: viewport.y, width: WINDOW_WIDTH, height: windowHeight, topMargin: WINDOW_MARGIN + windowLineHeight, @@ -813,8 +964,8 @@ var usersWindow = (function() { }); windowHeading = Overlays.addOverlay("text", { - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 2 * WINDOW_MARGIN, height: windowTextHeight, topMargin: 0, @@ -828,8 +979,8 @@ var usersWindow = (function() { }); minimizeButton = Overlays.addOverlay("image", { - x: WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, - y: viewportHeight, + x: 0, + y: viewport.y, width: MIN_MAX_BUTTON_WIDTH, height: MIN_MAX_BUTTON_HEIGHT, imageURL: MIN_MAX_BUTTON_SVG, @@ -845,11 +996,11 @@ var usersWindow = (function() { }); scrollbarBackgroundPosition = { - x: WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH, - y: viewportHeight + x: 0, + y: viewport.y }; scrollbarBackground = Overlays.addOverlay("text", { - x: scrollbarBackgroundPosition.x, + x: 0, y: scrollbarBackgroundPosition.y, width: SCROLLBAR_BACKGROUND_WIDTH, height: windowTextHeight, @@ -860,11 +1011,11 @@ var usersWindow = (function() { }); scrollbarBarPosition = { - x: WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH + 1, - y: viewportHeight + x: 0, + y: viewport.y }; scrollbarBar = Overlays.addOverlay("text", { - x: scrollbarBarPosition.x, + x: 0, y: scrollbarBarPosition.y, width: SCROLLBAR_BACKGROUND_WIDTH - 2, height: windowTextHeight, @@ -875,8 +1026,8 @@ var usersWindow = (function() { }); friendsButton = Overlays.addOverlay("image", { - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: FRIENDS_BUTTON_WIDTH, height: FRIENDS_BUTTON_HEIGHT, imageURL: FRIENDS_BUTTON_SVG, @@ -895,8 +1046,8 @@ var usersWindow = (function() { value: DISPLAY_VALUES[0], values: DISPLAY_VALUES, displayValues: DISPLAY_DISPLAY_VALUES, - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, promptWidth: DISPLAY_PROMPT_WIDTH, lineHeight: windowLineHeight, @@ -928,8 +1079,8 @@ var usersWindow = (function() { value: myVisibility, values: VISIBILITY_VALUES, displayValues: VISIBILITY_DISPLAY_VALUES, - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, promptWidth: VISIBILITY_PROMPT_WIDTH, lineHeight: windowLineHeight, @@ -979,6 +1130,7 @@ var usersWindow = (function() { Menu.removeMenuItem(MENU_NAME, MENU_ITEM); Script.clearTimeout(usersTimer); + Overlays.deleteOverlay(windowBorder); Overlays.deleteOverlay(windowPane); Overlays.deleteOverlay(windowHeading); Overlays.deleteOverlay(minimizeButton); @@ -991,4 +1143,4 @@ var usersWindow = (function() { setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}());