diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index cc6c4930ff..0f51bd00b1 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -577,15 +578,15 @@ void AudioMixer::domainSettingsRequestComplete() { void AudioMixer::broadcastMixes() { auto nodeList = DependencyManager::get(); - int64_t nextFrame = 0; - QElapsedTimer timer; - timer.start(); - - int64_t usecToSleep = AudioConstants::NETWORK_FRAME_USECS; + auto nextFrameTimestamp = p_high_resolution_clock::now(); + auto timeToSleep = std::chrono::microseconds(0); const int TRAILING_AVERAGE_FRAMES = 100; int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; + int currentFrame { 1 }; + int numFramesPerSecond { (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC) }; + while (!_isFinished) { const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; @@ -595,12 +596,12 @@ void AudioMixer::broadcastMixes() { const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - if (usecToSleep < 0) { - usecToSleep = 0; + if (timeToSleep.count() < 0) { + timeToSleep = std::chrono::microseconds(0); } _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) - + (usecToSleep * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS); + + (timeToSleep.count() * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS); float lastCutoffRatio = _performanceThrottlingRatio; bool hasRatioChanged = false; @@ -694,10 +695,11 @@ void AudioMixer::broadcastMixes() { nodeList->sendPacket(std::move(mixPacket), *node); nodeData->incrementOutgoingMixedAudioSequenceNumber(); - static const int FRAMES_PER_SECOND = int(ceilf(1.0f / AudioConstants::NETWORK_FRAME_SECS)); - // send an audio stream stats packet to the client approximately every second - if (nextFrame % FRAMES_PER_SECOND == 0) { + ++currentFrame; + currentFrame %= numFramesPerSecond; + + if (nodeData->shouldSendStats(currentFrame)) { nodeData->sendAudioStreamStatsPackets(node); } @@ -718,11 +720,14 @@ void AudioMixer::broadcastMixes() { break; } - usecToSleep = (++nextFrame * AudioConstants::NETWORK_FRAME_USECS) - (timer.nsecsElapsed() / 1000); + // push the next frame timestamp to when we should send the next + nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS); - if (usecToSleep > 0) { - usleep(usecToSleep); - } + // sleep as long as we need until next frame, if we can + auto now = p_high_resolution_clock::now(); + timeToSleep = std::chrono::duration_cast(nextFrameTimestamp - now); + + std::this_thread::sleep_for(timeToSleep); } } diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 38cb8a174f..93a51b1df2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include @@ -26,7 +28,14 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : _outgoingMixedAudioSequenceNumber(0), _downstreamAudioStreamStats() { + // of the ~94 blocks in a second of audio sent from the AudioMixer, pick a random one to send out a stats packet on + // this ensures we send out stats to this client around every second + // but do not send all of the stats packets out at the same time + std::random_device randomDevice; + std::mt19937 numberGenerator { randomDevice() }; + std::uniform_int_distribution<> distribution { 1, (int) ceil(1.0f / AudioConstants::NETWORK_FRAME_SECS) }; + _frameToSendStats = distribution(numberGenerator); } AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { @@ -180,6 +189,10 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend() { } } +bool AudioMixerClientData::shouldSendStats(int frameNumber) { + return frameNumber == _frameToSendStats; +} + void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode) { auto nodeList = DependencyManager::get(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 3627a247c0..ff4143cf08 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -58,6 +58,9 @@ public: void incrementOutgoingMixedAudioSequenceNumber() { _outgoingMixedAudioSequenceNumber++; } quint16 getOutgoingSequenceNumber() const { return _outgoingMixedAudioSequenceNumber; } + // uses randomization to have the AudioMixer send a stats packet to this node around every second + bool shouldSendStats(int frameNumber); + signals: void injectorStreamFinished(const QUuid& streamIdentifier); @@ -72,6 +75,8 @@ private: quint16 _outgoingMixedAudioSequenceNumber; AudioStreamStats _downstreamAudioStreamStats; + + int _frameToSendStats { 0 }; }; #endif // hifi_AudioMixerClientData_h diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 759a4956f2..a109934d10 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -36,14 +36,7 @@ const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / (float) AVATAR_MIXE AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message), - _broadcastThread(), - _lastFrameTimestamp(QDateTime::currentMSecsSinceEpoch()), - _trailingSleepRatio(1.0f), - _performanceThrottlingRatio(0.0f), - _sumListeners(0), - _numStatFrames(0), - _sumBillboardPackets(0), - _sumIdentityPackets(0) + _broadcastThread() { // make sure we hear about node kills so we can tell the other nodes connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &AvatarMixer::nodeKilled); @@ -51,7 +44,6 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::AvatarBillboard, this, "handleAvatarBillboardPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); } @@ -66,13 +58,18 @@ AvatarMixer::~AvatarMixer() { // An 80% chance of sending a identity packet within a 5 second interval. // assuming 60 htz update rate. -const float BILLBOARD_AND_IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f; +const float IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f; // NOTE: some additional optimizations to consider. // 1) use the view frustum to cull those avatars that are out of view. Since avatar data doesn't need to be present // if the avatar is not in view or in the keyhole. void AvatarMixer::broadcastAvatarData() { - int idleTime = QDateTime::currentMSecsSinceEpoch() - _lastFrameTimestamp; + int idleTime = AVATAR_DATA_SEND_INTERVAL_MSECS; + + if (_lastFrameTimestamp.time_since_epoch().count() > 0) { + auto idleDuration = p_high_resolution_clock::now() - _lastFrameTimestamp; + idleTime = std::chrono::duration_cast(idleDuration).count(); + } ++_numStatFrames; @@ -245,32 +242,13 @@ void AvatarMixer::broadcastAvatarData() { return; } - // make sure we send out identity and billboard packets to and from new arrivals. + // make sure we send out identity packets to and from new arrivals. bool forceSend = !otherNodeData->checkAndSetHasReceivedFirstPacketsFrom(node->getUUID()); - // we will also force a send of billboard or identity packet - // if either has changed in the last frame - if (otherNodeData->getBillboardChangeTimestamp() > 0 - && (forceSend - || otherNodeData->getBillboardChangeTimestamp() > _lastFrameTimestamp - || distribution(generator) < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) { - - QByteArray rfcUUID = otherNode->getUUID().toRfc4122(); - QByteArray billboard = otherNodeData->getAvatar().getBillboard(); - - auto billboardPacket = NLPacket::create(PacketType::AvatarBillboard, rfcUUID.size() + billboard.size()); - billboardPacket->write(rfcUUID); - billboardPacket->write(billboard); - - nodeList->sendPacket(std::move(billboardPacket), *node); - - ++_sumBillboardPackets; - } - - if (otherNodeData->getIdentityChangeTimestamp() > 0 + if (otherNodeData->getIdentityChangeTimestamp().time_since_epoch().count() > 0 && (forceSend || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp - || distribution(generator) < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) { + || distribution(generator) < IDENTITY_SEND_PROBABILITY)) { QByteArray individualData = otherNodeData->getAvatar().identityByteArray(); @@ -385,7 +363,7 @@ void AvatarMixer::broadcastAvatarData() { otherAvatar.doneEncoding(false); }); - _lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch(); + _lastFrameTimestamp = p_high_resolution_clock::now(); } void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { @@ -438,26 +416,12 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes // parse the identity packet and update the change timestamp if appropriate if (avatar.hasIdentityChangedAfterParsing(message->getMessage())) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); - nodeData->setIdentityChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); + nodeData->flagIdentityChange(); } } } } -void AvatarMixer::handleAvatarBillboardPacket(QSharedPointer message, SharedNodePointer senderNode) { - AvatarMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); - if (nodeData) { - AvatarData& avatar = nodeData->getAvatar(); - - // parse the billboard packet and update the change timestamp if appropriate - if (avatar.hasBillboardChangedAfterParsing(message->getMessage())) { - QMutexLocker nodeDataLocker(&nodeData->getMutex()); - nodeData->setBillboardChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); - } - - } -} - void AvatarMixer::handleKillAvatarPacket(QSharedPointer message) { DependencyManager::get()->processKillNode(*message); } @@ -466,7 +430,6 @@ void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; - statsObject["average_billboard_packets_per_frame"] = (float) _sumBillboardPackets / (float) _numStatFrames; statsObject["average_identity_packets_per_frame"] = (float) _sumIdentityPackets / (float) _numStatFrames; statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100; @@ -507,7 +470,6 @@ void AvatarMixer::sendStatsPacket() { ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject); _sumListeners = 0; - _sumBillboardPackets = 0; _sumIdentityPackets = 0; _numStatFrames = 0; } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 00c53e916b..c7761a2cba 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -15,6 +15,8 @@ #ifndef hifi_AvatarMixer_h #define hifi_AvatarMixer_h +#include + #include /// Handles assignments of type AvatarMixer - distribution of avatar data to various clients @@ -34,7 +36,6 @@ public slots: private slots: void handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleAvatarBillboardPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); void domainSettingsRequestComplete(); @@ -44,15 +45,14 @@ private: QThread _broadcastThread; - quint64 _lastFrameTimestamp; + p_high_resolution_clock::time_point _lastFrameTimestamp; - float _trailingSleepRatio; - float _performanceThrottlingRatio; + float _trailingSleepRatio { 1.0f }; + float _performanceThrottlingRatio { 0.0f }; - int _sumListeners; - int _numStatFrames; - int _sumBillboardPackets; - int _sumIdentityPackets; + int _sumListeners { 0 }; + int _numStatFrames { 0 }; + int _sumIdentityPackets { 0 }; float _maxKbpsPerNode = 0.0f; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index a310d0f16a..4a816291f4 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,8 @@ const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps"; class AvatarMixerClientData : public NodeData { Q_OBJECT public: + using HRCTime = p_high_resolution_clock::time_point; + int parseData(ReceivedMessage& message) override; AvatarData& getAvatar() { return *_avatar; } @@ -45,11 +48,8 @@ public: uint16_t getLastReceivedSequenceNumber() const { return _lastReceivedSequenceNumber; } - quint64 getBillboardChangeTimestamp() const { return _billboardChangeTimestamp; } - void setBillboardChangeTimestamp(quint64 billboardChangeTimestamp) { _billboardChangeTimestamp = billboardChangeTimestamp; } - - quint64 getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } - void setIdentityChangeTimestamp(quint64 identityChangeTimestamp) { _identityChangeTimestamp = identityChangeTimestamp; } + HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } + void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); } void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; } float getFullRateDistance() const { return _fullRateDistance; } @@ -86,8 +86,7 @@ private: std::unordered_map _lastBroadcastSequenceNumbers; std::unordered_set _hasReceivedFirstPacketsFrom; - quint64 _billboardChangeTimestamp = 0; - quint64 _identityChangeTimestamp = 0; + HRCTime _identityChangeTimestamp; float _fullRateDistance = FLT_MAX; float _maxAvatarDistance = FLT_MAX; diff --git a/cmake/templates/CPackProperties.cmake.in b/cmake/templates/CPackProperties.cmake.in index d22ba1f5e1..d7a1278e99 100644 --- a/cmake/templates/CPackProperties.cmake.in +++ b/cmake/templates/CPackProperties.cmake.in @@ -9,16 +9,20 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # +set(INTERFACE_DISPLAY_NAME "Interface") set(INTERFACE_SHORTCUT_NAME "@INTERFACE_SHORTCUT_NAME@") set(INTERFACE_HF_SHORTCUT_NAME "@INTERFACE_HF_SHORTCUT_NAME@") set(INTERFACE_WIN_EXEC_NAME "@INTERFACE_EXEC_PREFIX@.exe") +set(CONSOLE_DISPLAY_NAME "Sandbox") set(CONSOLE_INSTALL_SUBDIR "@CONSOLE_INSTALL_DIR@") set(CONSOLE_SHORTCUT_NAME "@CONSOLE_SHORTCUT_NAME@") set(CONSOLE_HF_SHORTCUT_NAME "@CONSOLE_HF_SHORTCUT_NAME@") set(CONSOLE_WIN_EXEC_NAME "@CONSOLE_EXEC_NAME@") set(PRE_SANDBOX_INTERFACE_SHORTCUT_NAME "@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@") set(PRE_SANDBOX_CONSOLE_SHORTCUT_NAME "@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@") +set(DS_DISPLAY_NAME "Domain Server") set(DS_EXEC_NAME "@DS_EXEC_NAME@") +set(AC_DISPLAY_NAME "Assignment Client") set(AC_EXEC_NAME "@AC_EXEC_NAME@") set(HIGH_FIDELITY_PROTOCOL "@HIGH_FIDELITY_PROTOCOL@") set(PRODUCTION_BUILD "@PRODUCTION_BUILD@") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 0e30d8aa2a..0ea1199c09 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -737,10 +737,10 @@ SectionEnd !macroend !macro CheckForRunningApplications action prompter - !insertmacro PromptForRunningApplication "@INTERFACE_WIN_EXEC_NAME@" "@CONSOLE_SHORTCUT_NAME@" ${action} ${prompter} - !insertmacro PromptForRunningApplication "@CONSOLE_WIN_EXEC_NAME@" "@INTERFACE_SHORTCUT_NAME@" ${action} ${prompter} - !insertmacro PromptForRunningApplication "@DS_EXEC_NAME@" "Domain Server" ${action} ${prompter} - !insertmacro PromptForRunningApplication "@AC_EXEC_NAME@" "Assignment Client" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@INTERFACE_WIN_EXEC_NAME@" "@INTERFACE_DISPLAY_NAME@" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@CONSOLE_WIN_EXEC_NAME@" "@CONSOLE_DISPLAY_NAME@" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@DS_EXEC_NAME@" "@DS_DISPLAY_NAME@" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@AC_EXEC_NAME@" "@AC_DISPLAY_NAME@" ${action} ${prompter} !macroend ;-------------------------------- diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 2c3ee65c92..a4c8c36169 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -14,11 +14,9 @@ Script.load("edit.js"); Script.load("examples.js"); Script.load("selectAudioDevice.js"); Script.load("notifications.js"); -Script.load("users.js"); Script.load("controllers/handControllerGrab.js"); Script.load("controllers/squeezeHands.js"); Script.load("grab.js"); Script.load("directory.js"); Script.load("dialTone.js"); -// Script.load("attachedEntitiesManager.js"); Script.load("depthReticle.js"); diff --git a/examples/disableAvatarAnimations.js b/examples/disableAvatarAnimations.js new file mode 100644 index 0000000000..fc3415fb80 --- /dev/null +++ b/examples/disableAvatarAnimations.js @@ -0,0 +1,50 @@ +// +// disableAvatarAnimations.js +// examples +// +// Copyright 2016 High Fidelity, Inc. +// +// When launched, it will replace all of the avatars animations with a single frame idle pose. +// full body IK and hand grabbing animations will still continue to function, but all other +// animations will be replaced. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var skeletonModelURL = ""; +var jointCount = 0; + +var excludedRoles = ["rightHandGraspOpen", "rightHandGraspClosed", "leftHandGraspOpen", "leftHandGraspClosed"]; +var IDLE_URL = "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/idle.fbx"; + +function overrideAnims() { + var roles = MyAvatar.getAnimationRoles(); + var i, l = roles.length; + for (i = 0; i < l; i++) { + if (excludedRoles.indexOf(roles[i]) == -1) { + MyAvatar.overrideRoleAnimation(roles[i], IDLE_URL, 30, false, 1, 1); + } + } +} + +function restoreAnims() { + var roles = MyAvatar.getAnimationRoles(); + var i, l = roles.length; + for (i = 0; i < l; i++) { + if (excludedRoles.indexOf(roles[i]) == -1) { + MyAvatar.restoreRoleAnimation(roles[i]); + } + } +} + +overrideAnims(); + +MyAvatar.onLoadComplete.connect(function () { + overrideAnims(); +}); + +Script.scriptEnding.connect(function () { + restoreAnims(); +}); + + diff --git a/examples/entityScripts/exampleSelfCallingTimeoutNoCleanup.js b/examples/entityScripts/exampleSelfCallingTimeoutNoCleanup.js new file mode 100644 index 0000000000..a4a66fc785 --- /dev/null +++ b/examples/entityScripts/exampleSelfCallingTimeoutNoCleanup.js @@ -0,0 +1,56 @@ +// +// exampleSelfCallingTimeoutNoCleanup.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 4/18/16. +// Copyright 2016 High Fidelity, Inc. +// +// This is an example of an entity script which hooks the update signal +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + ExampleUpdate = function() { + _this = this; + }; + + ExampleUpdate.prototype = { + + timeOutFunction: function() { + var entityID = _this.entityID; + print("timeOutFunction in entityID:" + entityID); + Script.setTimeout(function() { + _this.timeOutFunction(); + }, 3000); + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + print("preload - entityID:" + entityID); + this.entityID = entityID; + + print("preload - entityID:" + entityID + "-- calling timeOutFunction()...."); + _this.timeOutFunction(); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + print("unload - entityID:" + entityID); + print("NOTE --- WE DID NOT CALL clear our timeout"); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new ExampleUpdate(); +}) diff --git a/examples/entityScripts/exampleTimeoutNoCleanup.js b/examples/entityScripts/exampleTimeoutNoCleanup.js new file mode 100644 index 0000000000..99b8816388 --- /dev/null +++ b/examples/entityScripts/exampleTimeoutNoCleanup.js @@ -0,0 +1,50 @@ +// +// exampleTimeoutNoCleanup.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 4/18/16. +// Copyright 2016 High Fidelity, Inc. +// +// This is an example of an entity script which hooks the update signal +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + ExampleUpdate = function() { + _this = this; + }; + + ExampleUpdate.prototype = { + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + print("preload - entityID:" + entityID); + this.entityID = entityID; + + Script.setInterval(function() { + var entityID = _this.entityID; + print("timer interval in entityID:" + entityID); + }, 3000); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + print("unload - entityID:" + entityID); + print("NOTE --- WE DID NOT CALL clear our timeout"); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new ExampleUpdate(); +}) diff --git a/examples/entityScripts/exampleUpdate.js b/examples/entityScripts/exampleUpdate.js new file mode 100644 index 0000000000..c8a18ac774 --- /dev/null +++ b/examples/entityScripts/exampleUpdate.js @@ -0,0 +1,54 @@ +// +// exampleUpdate.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 4/18/16. +// Copyright 2016 High Fidelity, Inc. +// +// This is an example of an entity script which hooks the update signal +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + ExampleUpdate = function() { + _this = this; + }; + + ExampleUpdate.prototype = { + + // update() will be called regulary, because we've hooked the update signal in our preload() function. + // we will check the avatars hand positions and if either hand is in our bounding box, we will notice that + update: function() { + // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID + var entityID = _this.entityID; + print("update in entityID:" + entityID); + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + print("preload - entityID:" + entityID); + this.entityID = entityID; + Script.update.connect(this.update); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + print("unload - entityID:" + entityID); + Script.update.disconnect(this.update); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new ExampleUpdate(); +}) diff --git a/examples/entityScripts/exampleUpdateNoDisconnect.js b/examples/entityScripts/exampleUpdateNoDisconnect.js new file mode 100644 index 0000000000..81bb742ca2 --- /dev/null +++ b/examples/entityScripts/exampleUpdateNoDisconnect.js @@ -0,0 +1,54 @@ +// +// exampleUpdateNoDisconnect.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 4/18/16. +// Copyright 2016 High Fidelity, Inc. +// +// This is an example of an entity script which hooks the update signal +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + ExampleUpdate = function() { + _this = this; + }; + + ExampleUpdate.prototype = { + + // update() will be called regulary, because we've hooked the update signal in our preload() function. + // we will check the avatars hand positions and if either hand is in our bounding box, we will notice that + update: function() { + // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID + var entityID = _this.entityID; + print("update in entityID:" + entityID); + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + print("preload - entityID:" + entityID); + this.entityID = entityID; + Script.update.connect(this.update); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + print("unload - entityID:" + entityID); + print("NOTE --- WE DID NOT CALL Script.update.disconnect()"); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new ExampleUpdate(); +}) diff --git a/examples/libraries/jasmine/hifi-boot.js b/examples/libraries/jasmine/hifi-boot.js new file mode 100644 index 0000000000..0c66a925af --- /dev/null +++ b/examples/libraries/jasmine/hifi-boot.js @@ -0,0 +1,53 @@ + +(function() { + function ConsoleReporter(options) { + this.jasmineStarted = function (obj) { + print("jasmineStarted: numSpecs = " + obj.totalSpecsDefined); + }; + this.jasmineDone = function (obj) { + print("jasmineDone"); + }; + this.suiteStarted = function(obj) { + print("suiteStarted: \"" + obj.fullName + "\""); + }; + this.suiteDone = function(obj) { + print("suiteDone: \"" + obj.fullName + "\" " + obj.status); + }; + this.specStarted = function(obj) { + print("specStarted: \"" + obj.fullName + "\""); + }; + this.specDone = function(obj) { + print("specDone: \"" + obj.fullName + "\" " + obj.status); + + var i, l = obj.failedExpectations.length; + for (i = 0; i < l; i++) { + print(" " + obj.failedExpectations[i].message); + } + }; + return this; + } + + setTimeout = Script.setTimeout; + setInterval = Script.setInterval; + clearTimeout = Script.clearTimeout; + clearInterval = Script.clearInterval; + + var jasmine = jasmineRequire.core(jasmineRequire); + + var env = jasmine.getEnv(); + + env.addReporter(new ConsoleReporter()); + + var jasmineInterface = jasmineRequire.interface(jasmine, env); + + extend(this, jasmineInterface); + + function extend(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + +}()); + diff --git a/examples/libraries/jasmine/jasmine.js b/examples/libraries/jasmine/jasmine.js new file mode 100644 index 0000000000..32667abf98 --- /dev/null +++ b/examples/libraries/jasmine/jasmine.js @@ -0,0 +1,3458 @@ +/* +Copyright (c) 2008-2015 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +var getJasmineRequireObj = (function (jasmineGlobal) { + var jasmineRequire; + + if (typeof module !== 'undefined' && module.exports) { + if (typeof global !== 'undefined') { + jasmineGlobal = global; + } else { + jasmineGlobal = {}; + } + jasmineRequire = exports; + } else { + if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') { + jasmineGlobal = window; + } + jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {}; + this.jasmineRequire = jasmineRequire; + } + + function getJasmineRequire() { + return jasmineRequire; + } + + getJasmineRequire().core = function(jRequire) { + var j$ = {}; + + jRequire.base(j$, jasmineGlobal); + j$.util = jRequire.util(); + j$.errors = jRequire.errors(); + j$.Any = jRequire.Any(j$); + j$.Anything = jRequire.Anything(j$); + j$.CallTracker = jRequire.CallTracker(); + j$.MockDate = jRequire.MockDate(); + j$.Clock = jRequire.Clock(); + j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(); + j$.Env = jRequire.Env(j$); + j$.ExceptionFormatter = jRequire.ExceptionFormatter(); + j$.Expectation = jRequire.Expectation(); + j$.buildExpectationResult = jRequire.buildExpectationResult(); + j$.JsApiReporter = jRequire.JsApiReporter(); + j$.matchersUtil = jRequire.matchersUtil(j$); + j$.ObjectContaining = jRequire.ObjectContaining(j$); + j$.ArrayContaining = jRequire.ArrayContaining(j$); + j$.pp = jRequire.pp(j$); + j$.QueueRunner = jRequire.QueueRunner(j$); + j$.ReportDispatcher = jRequire.ReportDispatcher(); + j$.Spec = jRequire.Spec(j$); + j$.SpyRegistry = jRequire.SpyRegistry(j$); + j$.SpyStrategy = jRequire.SpyStrategy(); + j$.StringMatching = jRequire.StringMatching(j$); + j$.Suite = jRequire.Suite(j$); + j$.Timer = jRequire.Timer(); + j$.TreeProcessor = jRequire.TreeProcessor(); + j$.version = jRequire.version(); + j$.Order = jRequire.Order(); + + j$.matchers = jRequire.requireMatchers(jRequire, j$); + + return j$; + }; + + return getJasmineRequire; +})(this); + +getJasmineRequireObj().requireMatchers = function(jRequire, j$) { + var availableMatchers = [ + 'toBe', + 'toBeCloseTo', + 'toBeDefined', + 'toBeFalsy', + 'toBeGreaterThan', + 'toBeLessThan', + 'toBeNaN', + 'toBeNull', + 'toBeTruthy', + 'toBeUndefined', + 'toContain', + 'toEqual', + 'toHaveBeenCalled', + 'toHaveBeenCalledWith', + 'toHaveBeenCalledTimes', + 'toMatch', + 'toThrow', + 'toThrowError' + ], + matchers = {}; + + for (var i = 0; i < availableMatchers.length; i++) { + var name = availableMatchers[i]; + matchers[name] = jRequire[name](j$); + } + + return matchers; +}; + +getJasmineRequireObj().base = function(j$, jasmineGlobal) { + j$.unimplementedMethod_ = function() { + throw new Error('unimplemented method'); + }; + + j$.MAX_PRETTY_PRINT_DEPTH = 40; + j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 100; + j$.DEFAULT_TIMEOUT_INTERVAL = 5000; + + j$.getGlobal = function() { + return jasmineGlobal; + }; + + j$.getEnv = function(options) { + var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options); + //jasmine. singletons in here (setTimeout blah blah). + return env; + }; + + j$.isArray_ = function(value) { + return j$.isA_('Array', value); + }; + + j$.isString_ = function(value) { + return j$.isA_('String', value); + }; + + j$.isNumber_ = function(value) { + return j$.isA_('Number', value); + }; + + j$.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; + }; + + j$.isDomNode = function(obj) { + return obj.nodeType > 0; + }; + + j$.fnNameFor = function(func) { + return func.name || func.toString().match(/^\s*function\s*(\w*)\s*\(/)[1]; + }; + + j$.any = function(clazz) { + return new j$.Any(clazz); + }; + + j$.anything = function() { + return new j$.Anything(); + }; + + j$.objectContaining = function(sample) { + return new j$.ObjectContaining(sample); + }; + + j$.stringMatching = function(expected) { + return new j$.StringMatching(expected); + }; + + j$.arrayContaining = function(sample) { + return new j$.ArrayContaining(sample); + }; + + j$.createSpy = function(name, originalFn) { + + var spyStrategy = new j$.SpyStrategy({ + name: name, + fn: originalFn, + getSpy: function() { return spy; } + }), + callTracker = new j$.CallTracker(), + spy = function() { + var callData = { + object: this, + args: Array.prototype.slice.apply(arguments) + }; + + callTracker.track(callData); + var returnValue = spyStrategy.exec.apply(this, arguments); + callData.returnValue = returnValue; + + return returnValue; + }; + + for (var prop in originalFn) { + if (prop === 'and' || prop === 'calls') { + throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); + } + + spy[prop] = originalFn[prop]; + } + + spy.and = spyStrategy; + spy.calls = callTracker; + + return spy; + }; + + j$.isSpy = function(putativeSpy) { + if (!putativeSpy) { + return false; + } + return putativeSpy.and instanceof j$.SpyStrategy && + putativeSpy.calls instanceof j$.CallTracker; + }; + + j$.createSpyObj = function(baseName, methodNames) { + if (j$.isArray_(baseName) && j$.util.isUndefined(methodNames)) { + methodNames = baseName; + baseName = 'unknown'; + } + + if (!j$.isArray_(methodNames) || methodNames.length === 0) { + throw 'createSpyObj requires a non-empty array of method names to create spies for'; + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]); + } + return obj; + }; +}; + +getJasmineRequireObj().util = function() { + + var util = {}; + + util.inherit = function(childClass, parentClass) { + var Subclass = function() { + }; + Subclass.prototype = parentClass.prototype; + childClass.prototype = new Subclass(); + }; + + util.htmlEscape = function(str) { + if (!str) { + return str; + } + return str.replace(/&/g, '&') + .replace(//g, '>'); + }; + + util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) { + arrayOfArgs.push(args[i]); + } + return arrayOfArgs; + }; + + util.isUndefined = function(obj) { + return obj === void 0; + }; + + util.arrayContains = function(array, search) { + var i = array.length; + while (i--) { + if (array[i] === search) { + return true; + } + } + return false; + }; + + util.clone = function(obj) { + if (Object.prototype.toString.apply(obj) === '[object Array]') { + return obj.slice(); + } + + var cloned = {}; + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + cloned[prop] = obj[prop]; + } + } + + return cloned; + }; + + return util; +}; + +getJasmineRequireObj().Spec = function(j$) { + function Spec(attrs) { + this.expectationFactory = attrs.expectationFactory; + this.resultCallback = attrs.resultCallback || function() {}; + this.id = attrs.id; + this.description = attrs.description || ''; + this.queueableFn = attrs.queueableFn; + this.beforeAndAfterFns = attrs.beforeAndAfterFns || function() { return {befores: [], afters: []}; }; + this.userContext = attrs.userContext || function() { return {}; }; + this.onStart = attrs.onStart || function() {}; + this.getSpecName = attrs.getSpecName || function() { return ''; }; + this.expectationResultFactory = attrs.expectationResultFactory || function() { }; + this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; + this.catchingExceptions = attrs.catchingExceptions || function() { return true; }; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + + if (!this.queueableFn.fn) { + this.pend(); + } + + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [], + pendingReason: '' + }; + } + + Spec.prototype.addExpectationResult = function(passed, data, isError) { + var expectationResult = this.expectationResultFactory(data); + if (passed) { + this.result.passedExpectations.push(expectationResult); + } else { + this.result.failedExpectations.push(expectationResult); + + if (this.throwOnExpectationFailure && !isError) { + throw new j$.errors.ExpectationFailed(); + } + } + }; + + Spec.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Spec.prototype.execute = function(onComplete, enabled) { + var self = this; + + this.onStart(this); + + if (!this.isExecutable() || this.markedPending || enabled === false) { + complete(enabled); + return; + } + + var fns = this.beforeAndAfterFns(); + var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters); + + this.queueRunnerFactory({ + queueableFns: allFns, + onException: function() { self.onException.apply(self, arguments); }, + onComplete: complete, + userContext: this.userContext() + }); + + function complete(enabledAgain) { + self.result.status = self.status(enabledAgain); + self.resultCallback(self.result); + + if (onComplete) { + onComplete(); + } + } + }; + + Spec.prototype.onException = function onException(e) { + if (Spec.isPendingSpecException(e)) { + this.pend(extractCustomPendingMessage(e)); + return; + } + + if (e instanceof j$.errors.ExpectationFailed) { + return; + } + + this.addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: e + }, true); + }; + + Spec.prototype.disable = function() { + this.disabled = true; + }; + + Spec.prototype.pend = function(message) { + this.markedPending = true; + if (message) { + this.result.pendingReason = message; + } + }; + + Spec.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; + }; + + Spec.prototype.status = function(enabled) { + if (this.disabled || enabled === false) { + return 'disabled'; + } + + if (this.markedPending) { + return 'pending'; + } + + if (this.result.failedExpectations.length > 0) { + return 'failed'; + } else { + return 'passed'; + } + }; + + Spec.prototype.isExecutable = function() { + return !this.disabled; + }; + + Spec.prototype.getFullName = function() { + return this.getSpecName(this); + }; + + var extractCustomPendingMessage = function(e) { + var fullMessage = e.toString(), + boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage), + boilerplateEnd = boilerplateStart + Spec.pendingSpecExceptionMessage.length; + + return fullMessage.substr(boilerplateEnd); + }; + + Spec.pendingSpecExceptionMessage = '=> marked Pending'; + + Spec.isPendingSpecException = function(e) { + return !!(e && e.toString && e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1); + }; + + return Spec; +}; + +if (typeof window == void 0 && typeof exports == 'object') { + exports.Spec = jasmineRequire.Spec; +} + +/*jshint bitwise: false*/ + +getJasmineRequireObj().Order = function() { + function Order(options) { + this.random = 'random' in options ? options.random : true; + var seed = this.seed = options.seed || generateSeed(); + this.sort = this.random ? randomOrder : naturalOrder; + + function naturalOrder(items) { + return items; + } + + function randomOrder(items) { + var copy = items.slice(); + copy.sort(function(a, b) { + return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id); + }); + return copy; + } + + function generateSeed() { + return String(Math.random()).slice(-5); + } + + // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function + // used to get a different output when the key changes slighly. + // We use your return to sort the children randomly in a consistent way when + // used in conjunction with a seed + + function jenkinsHash(key) { + var hash, i; + for(hash = i = 0; i < key.length; ++i) { + hash += key.charCodeAt(i); + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + + } + + return Order; +}; + +getJasmineRequireObj().Env = function(j$) { + function Env(options) { + options = options || {}; + + var self = this; + var global = options.global || j$.getGlobal(); + + var totalSpecsDefined = 0; + + var catchExceptions = true; + + var realSetTimeout = j$.getGlobal().setTimeout; + var realClearTimeout = j$.getGlobal().clearTimeout; + this.clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); + + var runnableLookupTable = {}; + var runnableResources = {}; + + var currentSpec = null; + var currentlyExecutingSuites = []; + var currentDeclarationSuite = null; + var throwOnExpectationFailure = false; + var random = false; + var seed = null; + + var currentSuite = function() { + return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; + }; + + var currentRunnable = function() { + return currentSpec || currentSuite(); + }; + + var reporter = new j$.ReportDispatcher([ + 'jasmineStarted', + 'jasmineDone', + 'suiteStarted', + 'suiteDone', + 'specStarted', + 'specDone' + ]); + + this.specFilter = function() { + return true; + }; + + this.addCustomEqualityTester = function(tester) { + if(!currentRunnable()) { + throw new Error('Custom Equalities must be added in a before function or a spec'); + } + runnableResources[currentRunnable().id].customEqualityTesters.push(tester); + }; + + this.addMatchers = function(matchersToAdd) { + if(!currentRunnable()) { + throw new Error('Matchers must be added in a before function or a spec'); + } + var customMatchers = runnableResources[currentRunnable().id].customMatchers; + for (var matcherName in matchersToAdd) { + customMatchers[matcherName] = matchersToAdd[matcherName]; + } + }; + + j$.Expectation.addCoreMatchers(j$.matchers); + + var nextSpecId = 0; + var getNextSpecId = function() { + return 'spec' + nextSpecId++; + }; + + var nextSuiteId = 0; + var getNextSuiteId = function() { + return 'suite' + nextSuiteId++; + }; + + var expectationFactory = function(actual, spec) { + return j$.Expectation.Factory({ + util: j$.matchersUtil, + customEqualityTesters: runnableResources[spec.id].customEqualityTesters, + customMatchers: runnableResources[spec.id].customMatchers, + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + return spec.addExpectationResult(passed, result); + } + }; + + var defaultResourcesForRunnable = function(id, parentRunnableId) { + var resources = {spies: [], customEqualityTesters: [], customMatchers: {}}; + + if(runnableResources[parentRunnableId]){ + resources.customEqualityTesters = j$.util.clone(runnableResources[parentRunnableId].customEqualityTesters); + resources.customMatchers = j$.util.clone(runnableResources[parentRunnableId].customMatchers); + } + + runnableResources[id] = resources; + }; + + var clearResourcesForRunnable = function(id) { + spyRegistry.clearSpies(); + delete runnableResources[id]; + }; + + var beforeAndAfterFns = function(suite) { + return function() { + var befores = [], + afters = []; + + while(suite) { + befores = befores.concat(suite.beforeFns); + afters = afters.concat(suite.afterFns); + + suite = suite.parentSuite; + } + + return { + befores: befores.reverse(), + afters: afters + }; + }; + }; + + var getSpecName = function(spec, suite) { + return suite.getFullName() + ' ' + spec.description; + }; + + // TODO: we may just be able to pass in the fn instead of wrapping here + var buildExpectationResult = j$.buildExpectationResult, + exceptionFormatter = new j$.ExceptionFormatter(), + expectationResultFactory = function(attrs) { + attrs.messageFormatter = exceptionFormatter.message; + attrs.stackFormatter = exceptionFormatter.stack; + + return buildExpectationResult(attrs); + }; + + // TODO: fix this naming, and here's where the value comes in + this.catchExceptions = function(value) { + catchExceptions = !!value; + return catchExceptions; + }; + + this.catchingExceptions = function() { + return catchExceptions; + }; + + var maximumSpecCallbackDepth = 20; + var currentSpecCallbackDepth = 0; + + function clearStack(fn) { + currentSpecCallbackDepth++; + if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) { + currentSpecCallbackDepth = 0; + realSetTimeout(fn, 0); + } else { + fn(); + } + } + + var catchException = function(e) { + return j$.Spec.isPendingSpecException(e) || catchExceptions; + }; + + this.throwOnExpectationFailure = function(value) { + throwOnExpectationFailure = !!value; + }; + + this.throwingExpectationFailures = function() { + return throwOnExpectationFailure; + }; + + this.randomizeTests = function(value) { + random = !!value; + }; + + this.randomTests = function() { + return random; + }; + + this.seed = function(value) { + if (value) { + seed = value; + } + return seed; + }; + + var queueRunnerFactory = function(options) { + options.catchException = catchException; + options.clearStack = options.clearStack || clearStack; + options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; + options.fail = self.fail; + + new j$.QueueRunner(options).execute(); + }; + + var topSuite = new j$.Suite({ + env: this, + id: getNextSuiteId(), + description: 'Jasmine__TopLevel__Suite', + queueRunner: queueRunnerFactory + }); + runnableLookupTable[topSuite.id] = topSuite; + defaultResourcesForRunnable(topSuite.id); + currentDeclarationSuite = topSuite; + + this.topSuite = function() { + return topSuite; + }; + + this.execute = function(runnablesToRun) { + if(!runnablesToRun) { + if (focusedRunnables.length) { + runnablesToRun = focusedRunnables; + } else { + runnablesToRun = [topSuite.id]; + } + } + + var order = new j$.Order({ + random: random, + seed: seed + }); + + var processor = new j$.TreeProcessor({ + tree: topSuite, + runnableIds: runnablesToRun, + queueRunnerFactory: queueRunnerFactory, + nodeStart: function(suite) { + currentlyExecutingSuites.push(suite); + defaultResourcesForRunnable(suite.id, suite.parentSuite.id); + reporter.suiteStarted(suite.result); + }, + nodeComplete: function(suite, result) { + if (!suite.disabled) { + clearResourcesForRunnable(suite.id); + } + currentlyExecutingSuites.pop(); + reporter.suiteDone(result); + }, + orderChildren: function(node) { + return order.sort(node.children); + } + }); + + if(!processor.processTree().valid) { + throw new Error('Invalid order: would cause a beforeAll or afterAll to be run multiple times'); + } + + reporter.jasmineStarted({ + totalSpecsDefined: totalSpecsDefined + }); + + processor.execute(function() { + reporter.jasmineDone({ + order: order + }); + }); + }; + + this.addReporter = function(reporterToAdd) { + reporter.addReporter(reporterToAdd); + }; + + var spyRegistry = new j$.SpyRegistry({currentSpies: function() { + if(!currentRunnable()) { + throw new Error('Spies must be created in a before function or a spec'); + } + return runnableResources[currentRunnable().id].spies; + }}); + + this.spyOn = function() { + return spyRegistry.spyOn.apply(spyRegistry, arguments); + }; + + var suiteFactory = function(description) { + var suite = new j$.Suite({ + env: self, + id: getNextSuiteId(), + description: description, + parentSuite: currentDeclarationSuite, + expectationFactory: expectationFactory, + expectationResultFactory: expectationResultFactory, + throwOnExpectationFailure: throwOnExpectationFailure + }); + + runnableLookupTable[suite.id] = suite; + return suite; + }; + + this.describe = function(description, specDefinitions) { + var suite = suiteFactory(description); + if (specDefinitions.length > 0) { + throw new Error('describe does not expect a done parameter'); + } + if (currentDeclarationSuite.markedPending) { + suite.pend(); + } + addSpecsToSuite(suite, specDefinitions); + return suite; + }; + + this.xdescribe = function(description, specDefinitions) { + var suite = suiteFactory(description); + suite.pend(); + addSpecsToSuite(suite, specDefinitions); + return suite; + }; + + var focusedRunnables = []; + + this.fdescribe = function(description, specDefinitions) { + var suite = suiteFactory(description); + suite.isFocused = true; + + focusedRunnables.push(suite.id); + unfocusAncestor(); + addSpecsToSuite(suite, specDefinitions); + + return suite; + }; + + function addSpecsToSuite(suite, specDefinitions) { + var parentSuite = currentDeclarationSuite; + parentSuite.addChild(suite); + currentDeclarationSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch (e) { + declarationError = e; + } + + if (declarationError) { + self.it('encountered a declaration exception', function() { + throw declarationError; + }); + } + + currentDeclarationSuite = parentSuite; + } + + function findFocusedAncestor(suite) { + while (suite) { + if (suite.isFocused) { + return suite.id; + } + suite = suite.parentSuite; + } + + return null; + } + + function unfocusAncestor() { + var focusedAncestor = findFocusedAncestor(currentDeclarationSuite); + if (focusedAncestor) { + for (var i = 0; i < focusedRunnables.length; i++) { + if (focusedRunnables[i] === focusedAncestor) { + focusedRunnables.splice(i, 1); + break; + } + } + } + } + + var specFactory = function(description, fn, suite, timeout) { + totalSpecsDefined++; + var spec = new j$.Spec({ + id: getNextSpecId(), + beforeAndAfterFns: beforeAndAfterFns(suite), + expectationFactory: expectationFactory, + resultCallback: specResultCallback, + getSpecName: function(spec) { + return getSpecName(spec, suite); + }, + onStart: specStarted, + description: description, + expectationResultFactory: expectationResultFactory, + queueRunnerFactory: queueRunnerFactory, + userContext: function() { return suite.clonedSharedUserContext(); }, + queueableFn: { + fn: fn, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }, + throwOnExpectationFailure: throwOnExpectationFailure + }); + + runnableLookupTable[spec.id] = spec; + + if (!self.specFilter(spec)) { + spec.disable(); + } + + return spec; + + function specResultCallback(result) { + clearResourcesForRunnable(spec.id); + currentSpec = null; + reporter.specDone(result); + } + + function specStarted(spec) { + currentSpec = spec; + defaultResourcesForRunnable(spec.id, suite.id); + reporter.specStarted(spec.result); + } + }; + + this.it = function(description, fn, timeout) { + var spec = specFactory(description, fn, currentDeclarationSuite, timeout); + if (currentDeclarationSuite.markedPending) { + spec.pend(); + } + currentDeclarationSuite.addChild(spec); + return spec; + }; + + this.xit = function() { + var spec = this.it.apply(this, arguments); + spec.pend('Temporarily disabled with xit'); + return spec; + }; + + this.fit = function(description, fn, timeout){ + var spec = specFactory(description, fn, currentDeclarationSuite, timeout); + currentDeclarationSuite.addChild(spec); + focusedRunnables.push(spec.id); + unfocusAncestor(); + return spec; + }; + + this.expect = function(actual) { + if (!currentRunnable()) { + throw new Error('\'expect\' was used when there was no current spec, this could be because an asynchronous test timed out'); + } + + return currentRunnable().expect(actual); + }; + + this.beforeEach = function(beforeEachFunction, timeout) { + currentDeclarationSuite.beforeEach({ + fn: beforeEachFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.beforeAll = function(beforeAllFunction, timeout) { + currentDeclarationSuite.beforeAll({ + fn: beforeAllFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.afterEach = function(afterEachFunction, timeout) { + currentDeclarationSuite.afterEach({ + fn: afterEachFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.afterAll = function(afterAllFunction, timeout) { + currentDeclarationSuite.afterAll({ + fn: afterAllFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.pending = function(message) { + var fullMessage = j$.Spec.pendingSpecExceptionMessage; + if(message) { + fullMessage += message; + } + throw fullMessage; + }; + + this.fail = function(error) { + var message = 'Failed'; + if (error) { + message += ': '; + message += error.message || error; + } + + currentRunnable().addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + message: message, + error: error && error.message ? error : null + }); + }; + } + + return Env; +}; + +getJasmineRequireObj().JsApiReporter = function() { + + var noopTimer = { + start: function(){}, + elapsed: function(){ return 0; } + }; + + function JsApiReporter(options) { + var timer = options.timer || noopTimer, + status = 'loaded'; + + this.started = false; + this.finished = false; + this.runDetails = {}; + + this.jasmineStarted = function() { + this.started = true; + status = 'started'; + timer.start(); + }; + + var executionTime; + + this.jasmineDone = function(runDetails) { + this.finished = true; + this.runDetails = runDetails; + executionTime = timer.elapsed(); + status = 'done'; + }; + + this.status = function() { + return status; + }; + + var suites = [], + suites_hash = {}; + + this.suiteStarted = function(result) { + suites_hash[result.id] = result; + }; + + this.suiteDone = function(result) { + storeSuite(result); + }; + + this.suiteResults = function(index, length) { + return suites.slice(index, index + length); + }; + + function storeSuite(result) { + suites.push(result); + suites_hash[result.id] = result; + } + + this.suites = function() { + return suites_hash; + }; + + var specs = []; + + this.specDone = function(result) { + specs.push(result); + }; + + this.specResults = function(index, length) { + return specs.slice(index, index + length); + }; + + this.specs = function() { + return specs; + }; + + this.executionTime = function() { + return executionTime; + }; + + } + + return JsApiReporter; +}; + +getJasmineRequireObj().CallTracker = function() { + + function CallTracker() { + var calls = []; + + this.track = function(context) { + calls.push(context); + }; + + this.any = function() { + return !!calls.length; + }; + + this.count = function() { + return calls.length; + }; + + this.argsFor = function(index) { + var call = calls[index]; + return call ? call.args : []; + }; + + this.all = function() { + return calls; + }; + + this.allArgs = function() { + var callArgs = []; + for(var i = 0; i < calls.length; i++){ + callArgs.push(calls[i].args); + } + + return callArgs; + }; + + this.first = function() { + return calls[0]; + }; + + this.mostRecent = function() { + return calls[calls.length - 1]; + }; + + this.reset = function() { + calls = []; + }; + } + + return CallTracker; +}; + +getJasmineRequireObj().Clock = function() { + function Clock(global, delayedFunctionSchedulerFactory, mockDate) { + var self = this, + realTimingFunctions = { + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout, + setInterval: global.setInterval, + clearInterval: global.clearInterval + }, + fakeTimingFunctions = { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval + }, + installed = false, + delayedFunctionScheduler, + timer; + + + self.install = function() { + if(!originalTimingFunctionsIntact()) { + throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); + } + replace(global, fakeTimingFunctions); + timer = fakeTimingFunctions; + delayedFunctionScheduler = delayedFunctionSchedulerFactory(); + installed = true; + + return self; + }; + + self.uninstall = function() { + delayedFunctionScheduler = null; + mockDate.uninstall(); + replace(global, realTimingFunctions); + + timer = realTimingFunctions; + installed = false; + }; + + self.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; + + self.mockDate = function(initialDate) { + mockDate.install(initialDate); + }; + + self.setTimeout = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); + } + return timer.setTimeout(fn, delay); + } + return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); + }; + + self.setInterval = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); + } + return timer.setInterval(fn, delay); + } + return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); + }; + + self.clearTimeout = function(id) { + return Function.prototype.call.apply(timer.clearTimeout, [global, id]); + }; + + self.clearInterval = function(id) { + return Function.prototype.call.apply(timer.clearInterval, [global, id]); + }; + + self.tick = function(millis) { + if (installed) { + mockDate.tick(millis); + delayedFunctionScheduler.tick(millis); + } else { + throw new Error('Mock clock is not installed, use jasmine.clock().install()'); + } + }; + + return self; + + function originalTimingFunctionsIntact() { + return global.setTimeout === realTimingFunctions.setTimeout && + global.clearTimeout === realTimingFunctions.clearTimeout && + global.setInterval === realTimingFunctions.setInterval && + global.clearInterval === realTimingFunctions.clearInterval; + } + + function legacyIE() { + //if these methods are polyfilled, apply will be present + return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; + } + + function replace(dest, source) { + for (var prop in source) { + dest[prop] = source[prop]; + } + } + + function setTimeout(fn, delay) { + return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + } + + function clearTimeout(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function setInterval(fn, interval) { + return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + } + + function clearInterval(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function argSlice(argsObj, n) { + return Array.prototype.slice.call(argsObj, n); + } + } + + return Clock; +}; + +getJasmineRequireObj().DelayedFunctionScheduler = function() { + function DelayedFunctionScheduler() { + var self = this; + var scheduledLookup = []; + var scheduledFunctions = {}; + var currentTime = 0; + var delayedFnCount = 0; + + self.tick = function(millis) { + millis = millis || 0; + var endTime = currentTime + millis; + + runScheduledFunctions(endTime); + currentTime = endTime; + }; + + self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { + var f; + if (typeof(funcToCall) === 'string') { + /* jshint evil: true */ + f = function() { return eval(funcToCall); }; + /* jshint evil: false */ + } else { + f = funcToCall; + } + + millis = millis || 0; + timeoutKey = timeoutKey || ++delayedFnCount; + runAtMillis = runAtMillis || (currentTime + millis); + + var funcToSchedule = { + runAtMillis: runAtMillis, + funcToCall: f, + recurring: recurring, + params: params, + timeoutKey: timeoutKey, + millis: millis + }; + + if (runAtMillis in scheduledFunctions) { + scheduledFunctions[runAtMillis].push(funcToSchedule); + } else { + scheduledFunctions[runAtMillis] = [funcToSchedule]; + scheduledLookup.push(runAtMillis); + scheduledLookup.sort(function (a, b) { + return a - b; + }); + } + + return timeoutKey; + }; + + self.removeFunctionWithId = function(timeoutKey) { + for (var runAtMillis in scheduledFunctions) { + var funcs = scheduledFunctions[runAtMillis]; + var i = indexOfFirstToPass(funcs, function (func) { + return func.timeoutKey === timeoutKey; + }); + + if (i > -1) { + if (funcs.length === 1) { + delete scheduledFunctions[runAtMillis]; + deleteFromLookup(runAtMillis); + } else { + funcs.splice(i, 1); + } + + // intervals get rescheduled when executed, so there's never more + // than a single scheduled function with a given timeoutKey + break; + } + } + }; + + return self; + + function indexOfFirstToPass(array, testFn) { + var index = -1; + + for (var i = 0; i < array.length; ++i) { + if (testFn(array[i])) { + index = i; + break; + } + } + + return index; + } + + function deleteFromLookup(key) { + var value = Number(key); + var i = indexOfFirstToPass(scheduledLookup, function (millis) { + return millis === value; + }); + + if (i > -1) { + scheduledLookup.splice(i, 1); + } + } + + function reschedule(scheduledFn) { + self.scheduleFunction(scheduledFn.funcToCall, + scheduledFn.millis, + scheduledFn.params, + true, + scheduledFn.timeoutKey, + scheduledFn.runAtMillis + scheduledFn.millis); + } + + function forEachFunction(funcsToRun, callback) { + for (var i = 0; i < funcsToRun.length; ++i) { + callback(funcsToRun[i]); + } + } + + function runScheduledFunctions(endTime) { + if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { + return; + } + + do { + currentTime = scheduledLookup.shift(); + + var funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; + + forEachFunction(funcsToRun, function(funcToRun) { + if (funcToRun.recurring) { + reschedule(funcToRun); + } + }); + + forEachFunction(funcsToRun, function(funcToRun) { + funcToRun.funcToCall.apply(null, funcToRun.params || []); + }); + } while (scheduledLookup.length > 0 && + // checking first if we're out of time prevents setTimeout(0) + // scheduled in a funcToRun from forcing an extra iteration + currentTime !== endTime && + scheduledLookup[0] <= endTime); + } + } + + return DelayedFunctionScheduler; +}; + +getJasmineRequireObj().ExceptionFormatter = function() { + function ExceptionFormatter() { + this.message = function(error) { + var message = ''; + + if (error.name && error.message) { + message += error.name + ': ' + error.message; + } else { + message += error.toString() + ' thrown'; + } + + if (error.fileName || error.sourceURL) { + message += ' in ' + (error.fileName || error.sourceURL); + } + + if (error.line || error.lineNumber) { + message += ' (line ' + (error.line || error.lineNumber) + ')'; + } + + return message; + }; + + this.stack = function(error) { + return error ? error.stack : null; + }; + } + + return ExceptionFormatter; +}; + +getJasmineRequireObj().Expectation = function() { + + function Expectation(options) { + this.util = options.util || { buildFailureMessage: function() {} }; + this.customEqualityTesters = options.customEqualityTesters || []; + this.actual = options.actual; + this.addExpectationResult = options.addExpectationResult || function(){}; + this.isNot = options.isNot; + + var customMatchers = options.customMatchers || {}; + for (var matcherName in customMatchers) { + this[matcherName] = Expectation.prototype.wrapCompare(matcherName, customMatchers[matcherName]); + } + } + + Expectation.prototype.wrapCompare = function(name, matcherFactory) { + return function() { + var args = Array.prototype.slice.call(arguments, 0), + expected = args.slice(0), + message = ''; + + args.unshift(this.actual); + + var matcher = matcherFactory(this.util, this.customEqualityTesters), + matcherCompare = matcher.compare; + + function defaultNegativeCompare() { + var result = matcher.compare.apply(null, args); + result.pass = !result.pass; + return result; + } + + if (this.isNot) { + matcherCompare = matcher.negativeCompare || defaultNegativeCompare; + } + + var result = matcherCompare.apply(null, args); + + if (!result.pass) { + if (!result.message) { + args.unshift(this.isNot); + args.unshift(name); + message = this.util.buildFailureMessage.apply(null, args); + } else { + if (Object.prototype.toString.apply(result.message) === '[object Function]') { + message = result.message(); + } else { + message = result.message; + } + } + } + + if (expected.length == 1) { + expected = expected[0]; + } + + // TODO: how many of these params are needed? + this.addExpectationResult( + result.pass, + { + matcherName: name, + passed: result.pass, + message: message, + actual: this.actual, + expected: expected // TODO: this may need to be arrayified/sliced + } + ); + }; + }; + + Expectation.addCoreMatchers = function(matchers) { + var prototype = Expectation.prototype; + for (var matcherName in matchers) { + var matcher = matchers[matcherName]; + prototype[matcherName] = prototype.wrapCompare(matcherName, matcher); + } + }; + + Expectation.Factory = function(options) { + options = options || {}; + + var expect = new Expectation(options); + + // TODO: this would be nice as its own Object - NegativeExpectation + // TODO: copy instead of mutate options + options.isNot = true; + expect.not = new Expectation(options); + + return expect; + }; + + return Expectation; +}; + +//TODO: expectation result may make more sense as a presentation of an expectation. +getJasmineRequireObj().buildExpectationResult = function() { + function buildExpectationResult(options) { + var messageFormatter = options.messageFormatter || function() {}, + stackFormatter = options.stackFormatter || function() {}; + + var result = { + matcherName: options.matcherName, + message: message(), + stack: stack(), + passed: options.passed + }; + + if(!result.passed) { + result.expected = options.expected; + result.actual = options.actual; + } + + return result; + + function message() { + if (options.passed) { + return 'Passed.'; + } else if (options.message) { + return options.message; + } else if (options.error) { + return messageFormatter(options.error); + } + return ''; + } + + function stack() { + if (options.passed) { + return ''; + } + + var error = options.error; + if (!error) { + try { + throw new Error(message()); + } catch (e) { + error = e; + } + } + return stackFormatter(error); + } + } + + return buildExpectationResult; +}; + +getJasmineRequireObj().MockDate = function() { + function MockDate(global) { + var self = this; + var currentTime = 0; + + if (!global || !global.Date) { + self.install = function() {}; + self.tick = function() {}; + self.uninstall = function() {}; + return self; + } + + var GlobalDate = global.Date; + + self.install = function(mockDate) { + if (mockDate instanceof GlobalDate) { + currentTime = mockDate.getTime(); + } else { + currentTime = new GlobalDate().getTime(); + } + + global.Date = FakeDate; + }; + + self.tick = function(millis) { + millis = millis || 0; + currentTime = currentTime + millis; + }; + + self.uninstall = function() { + currentTime = 0; + global.Date = GlobalDate; + }; + + createDateProperties(); + + return self; + + function FakeDate() { + switch(arguments.length) { + case 0: + return new GlobalDate(currentTime); + case 1: + return new GlobalDate(arguments[0]); + case 2: + return new GlobalDate(arguments[0], arguments[1]); + case 3: + return new GlobalDate(arguments[0], arguments[1], arguments[2]); + case 4: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); + case 5: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4]); + case 6: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5]); + default: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5], arguments[6]); + } + } + + function createDateProperties() { + FakeDate.prototype = GlobalDate.prototype; + + FakeDate.now = function() { + if (GlobalDate.now) { + return currentTime; + } else { + throw new Error('Browser does not support Date.now()'); + } + }; + + FakeDate.toSource = GlobalDate.toSource; + FakeDate.toString = GlobalDate.toString; + FakeDate.parse = GlobalDate.parse; + FakeDate.UTC = GlobalDate.UTC; + } + } + + return MockDate; +}; + +getJasmineRequireObj().pp = function(j$) { + + function PrettyPrinter() { + this.ppNestLevel_ = 0; + this.seen = []; + } + + PrettyPrinter.prototype.format = function(value) { + this.ppNestLevel_++; + try { + if (j$.util.isUndefined(value)) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === 0 && 1/value === -Infinity) { + this.emitScalar('-0'); + } else if (value === j$.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (j$.isSpy(value)) { + this.emitScalar('spy on ' + value.and.identity()); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (value.toString && typeof value === 'object' && !(value instanceof Array) && value.toString !== Object.prototype.toString) { + this.emitScalar(value.toString()); + } else if (j$.util.arrayContains(this.seen, value)) { + this.emitScalar(''); + } else if (j$.isArray_(value) || j$.isA_('Object', value)) { + this.seen.push(value); + if (j$.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + this.seen.pop(); + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } + }; + + PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, property)) { continue; } + fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) && + obj.__lookupGetter__(property) !== null) : false); + } + }; + + PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitString = j$.unimplementedMethod_; + + function StringPrettyPrinter() { + PrettyPrinter.call(this); + + this.string = ''; + } + + j$.util.inherit(StringPrettyPrinter, PrettyPrinter); + + StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); + }; + + StringPrettyPrinter.prototype.emitString = function(value) { + this.append('\'' + value + '\''); + }; + + StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Array'); + return; + } + var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + this.append('[ '); + for (var i = 0; i < length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + if(array.length > length){ + this.append(', ...'); + } + + var self = this; + var first = array.length === 0; + this.iterateObject(array, function(property, isGetter) { + if (property.match(/^\d+$/)) { + return; + } + + if (first) { + first = false; + } else { + self.append(', '); + } + + self.formatProperty(array, property, isGetter); + }); + + this.append(' ]'); + }; + + StringPrettyPrinter.prototype.emitObject = function(obj) { + var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null'; + this.append(constructorName); + + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + return; + } + + var self = this; + this.append('({ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.formatProperty(obj, property, isGetter); + }); + + this.append(' })'); + }; + + StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { + this.append(property); + this.append(': '); + if (isGetter) { + this.append(''); + } else { + this.format(obj[property]); + } + }; + + StringPrettyPrinter.prototype.append = function(value) { + this.string += value; + }; + + return function(value) { + var stringPrettyPrinter = new StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; + }; +}; + +getJasmineRequireObj().QueueRunner = function(j$) { + + function once(fn) { + var called = false; + return function() { + if (!called) { + called = true; + fn(); + } + }; + } + + function QueueRunner(attrs) { + this.queueableFns = attrs.queueableFns || []; + this.onComplete = attrs.onComplete || function() {}; + this.clearStack = attrs.clearStack || function(fn) {fn();}; + this.onException = attrs.onException || function() {}; + this.catchException = attrs.catchException || function() { return true; }; + this.userContext = attrs.userContext || {}; + this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; + this.fail = attrs.fail || function() {}; + } + + QueueRunner.prototype.execute = function() { + this.run(this.queueableFns, 0); + }; + + QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { + var length = queueableFns.length, + self = this, + iterativeIndex; + + + for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { + var queueableFn = queueableFns[iterativeIndex]; + if (queueableFn.fn.length > 0) { + attemptAsync(queueableFn); + return; + } else { + attemptSync(queueableFn); + } + } + + var runnerDone = iterativeIndex >= length; + + if (runnerDone) { + this.clearStack(this.onComplete); + } + + function attemptSync(queueableFn) { + try { + queueableFn.fn.call(self.userContext); + } catch (e) { + handleException(e, queueableFn); + } + } + + function attemptAsync(queueableFn) { + var clearTimeout = function () { + if (timeoutId) { + Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); + } + }, + next = once(function () { + clearTimeout(timeoutId); + self.run(queueableFns, iterativeIndex + 1); + }), + timeoutId; + + next.fail = function() { + self.fail.apply(null, arguments); + next(); + }; + + if (queueableFn.timeout) { + timeoutId = Function.prototype.apply.apply(self.timeout.setTimeout, [j$.getGlobal(), [function() { + var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); + onException(error); + next(); + }, queueableFn.timeout()]]); + } + + try { + queueableFn.fn.call(self.userContext, next); + } catch (e) { + handleException(e, queueableFn); + next(); + } + } + + function onException(e) { + self.onException(e); + } + + function handleException(e, queueableFn) { + onException(e); + if (!self.catchException(e)) { + //TODO: set a var when we catch an exception and + //use a finally block to close the loop in a nice way.. + throw e; + } + } + }; + + return QueueRunner; +}; + +getJasmineRequireObj().ReportDispatcher = function() { + function ReportDispatcher(methods) { + + var dispatchedMethods = methods || []; + + for (var i = 0; i < dispatchedMethods.length; i++) { + var method = dispatchedMethods[i]; + this[method] = (function(m) { + return function() { + dispatch(m, arguments); + }; + }(method)); + } + + var reporters = []; + + this.addReporter = function(reporter) { + reporters.push(reporter); + }; + + return this; + + function dispatch(method, args) { + for (var i = 0; i < reporters.length; i++) { + var reporter = reporters[i]; + if (reporter[method]) { + reporter[method].apply(reporter, args); + } + } + } + } + + return ReportDispatcher; +}; + + +getJasmineRequireObj().SpyRegistry = function(j$) { + + function SpyRegistry(options) { + options = options || {}; + var currentSpies = options.currentSpies || function() { return []; }; + + this.spyOn = function(obj, methodName) { + if (j$.util.isUndefined(obj)) { + throw new Error('spyOn could not find an object to spy upon for ' + methodName + '()'); + } + + if (j$.util.isUndefined(methodName)) { + throw new Error('No method name supplied'); + } + + if (j$.util.isUndefined(obj[methodName])) { + throw new Error(methodName + '() method does not exist'); + } + + if (obj[methodName] && j$.isSpy(obj[methodName])) { + //TODO?: should this return the current spy? Downside: may cause user confusion about spy state + throw new Error(methodName + ' has already been spied upon'); + } + + var descriptor; + try { + descriptor = Object.getOwnPropertyDescriptor(obj, methodName); + } catch(e) { + // IE 8 doesn't support `definePropery` on non-DOM nodes + } + + if (descriptor && !(descriptor.writable || descriptor.set)) { + throw new Error(methodName + ' is not declared writable or has no setter'); + } + + var spy = j$.createSpy(methodName, obj[methodName]); + + currentSpies().push({ + spy: spy, + baseObj: obj, + methodName: methodName, + originalValue: obj[methodName] + }); + + obj[methodName] = spy; + + return spy; + }; + + this.clearSpies = function() { + var spies = currentSpies(); + for (var i = 0; i < spies.length; i++) { + var spyEntry = spies[i]; + spyEntry.baseObj[spyEntry.methodName] = spyEntry.originalValue; + } + }; + } + + return SpyRegistry; +}; + +getJasmineRequireObj().SpyStrategy = function() { + + function SpyStrategy(options) { + options = options || {}; + + var identity = options.name || 'unknown', + originalFn = options.fn || function() {}, + getSpy = options.getSpy || function() {}, + plan = function() {}; + + this.identity = function() { + return identity; + }; + + this.exec = function() { + return plan.apply(this, arguments); + }; + + this.callThrough = function() { + plan = originalFn; + return getSpy(); + }; + + this.returnValue = function(value) { + plan = function() { + return value; + }; + return getSpy(); + }; + + this.returnValues = function() { + var values = Array.prototype.slice.call(arguments); + plan = function () { + return values.shift(); + }; + return getSpy(); + }; + + this.throwError = function(something) { + var error = (something instanceof Error) ? something : new Error(something); + plan = function() { + throw error; + }; + return getSpy(); + }; + + this.callFake = function(fn) { + plan = fn; + return getSpy(); + }; + + this.stub = function(fn) { + plan = function() {}; + return getSpy(); + }; + } + + return SpyStrategy; +}; + +getJasmineRequireObj().Suite = function(j$) { + function Suite(attrs) { + this.env = attrs.env; + this.id = attrs.id; + this.parentSuite = attrs.parentSuite; + this.description = attrs.description; + this.expectationFactory = attrs.expectationFactory; + this.expectationResultFactory = attrs.expectationResultFactory; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + + this.beforeFns = []; + this.afterFns = []; + this.beforeAllFns = []; + this.afterAllFns = []; + this.disabled = false; + + this.children = []; + + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [] + }; + } + + Suite.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + if (parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + } + return fullName; + }; + + Suite.prototype.disable = function() { + this.disabled = true; + }; + + Suite.prototype.pend = function(message) { + this.markedPending = true; + }; + + Suite.prototype.beforeEach = function(fn) { + this.beforeFns.unshift(fn); + }; + + Suite.prototype.beforeAll = function(fn) { + this.beforeAllFns.push(fn); + }; + + Suite.prototype.afterEach = function(fn) { + this.afterFns.unshift(fn); + }; + + Suite.prototype.afterAll = function(fn) { + this.afterAllFns.push(fn); + }; + + Suite.prototype.addChild = function(child) { + this.children.push(child); + }; + + Suite.prototype.status = function() { + if (this.disabled) { + return 'disabled'; + } + + if (this.markedPending) { + return 'pending'; + } + + if (this.result.failedExpectations.length > 0) { + return 'failed'; + } else { + return 'finished'; + } + }; + + Suite.prototype.isExecutable = function() { + return !this.disabled; + }; + + Suite.prototype.canBeReentered = function() { + return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0; + }; + + Suite.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; + }; + + Suite.prototype.sharedUserContext = function() { + if (!this.sharedContext) { + this.sharedContext = this.parentSuite ? clone(this.parentSuite.sharedUserContext()) : {}; + } + + return this.sharedContext; + }; + + Suite.prototype.clonedSharedUserContext = function() { + return clone(this.sharedUserContext()); + }; + + Suite.prototype.onException = function() { + if (arguments[0] instanceof j$.errors.ExpectationFailed) { + return; + } + + if(isAfterAll(this.children)) { + var data = { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: arguments[0] + }; + this.result.failedExpectations.push(this.expectationResultFactory(data)); + } else { + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + child.onException.apply(child, arguments); + } + } + }; + + Suite.prototype.addExpectationResult = function () { + if(isAfterAll(this.children) && isFailure(arguments)){ + var data = arguments[1]; + this.result.failedExpectations.push(this.expectationResultFactory(data)); + if(this.throwOnExpectationFailure) { + throw new j$.errors.ExpectationFailed(); + } + } else { + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + try { + child.addExpectationResult.apply(child, arguments); + } catch(e) { + // keep going + } + } + } + }; + + function isAfterAll(children) { + return children && children[0].result.status; + } + + function isFailure(args) { + return !args[0]; + } + + function clone(obj) { + var clonedObj = {}; + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + clonedObj[prop] = obj[prop]; + } + } + + return clonedObj; + } + + return Suite; +}; + +if (typeof window == void 0 && typeof exports == 'object') { + exports.Suite = jasmineRequire.Suite; +} + +getJasmineRequireObj().Timer = function() { + var defaultNow = (function(Date) { + return function() { return new Date().getTime(); }; + })(Date); + + function Timer(options) { + options = options || {}; + + var now = options.now || defaultNow, + startTime; + + this.start = function() { + startTime = now(); + }; + + this.elapsed = function() { + return now() - startTime; + }; + } + + return Timer; +}; + +getJasmineRequireObj().TreeProcessor = function() { + function TreeProcessor(attrs) { + var tree = attrs.tree, + runnableIds = attrs.runnableIds, + queueRunnerFactory = attrs.queueRunnerFactory, + nodeStart = attrs.nodeStart || function() {}, + nodeComplete = attrs.nodeComplete || function() {}, + orderChildren = attrs.orderChildren || function(node) { return node.children; }, + stats = { valid: true }, + processed = false, + defaultMin = Infinity, + defaultMax = 1 - Infinity; + + this.processTree = function() { + processNode(tree, false); + processed = true; + return stats; + }; + + this.execute = function(done) { + if (!processed) { + this.processTree(); + } + + if (!stats.valid) { + throw 'invalid order'; + } + + var childFns = wrapChildren(tree, 0); + + queueRunnerFactory({ + queueableFns: childFns, + userContext: tree.sharedUserContext(), + onException: function() { + tree.onException.apply(tree, arguments); + }, + onComplete: done + }); + }; + + function runnableIndex(id) { + for (var i = 0; i < runnableIds.length; i++) { + if (runnableIds[i] === id) { + return i; + } + } + } + + function processNode(node, parentEnabled) { + var executableIndex = runnableIndex(node.id); + + if (executableIndex !== undefined) { + parentEnabled = true; + } + + parentEnabled = parentEnabled && node.isExecutable(); + + if (!node.children) { + stats[node.id] = { + executable: parentEnabled && node.isExecutable(), + segments: [{ + index: 0, + owner: node, + nodes: [node], + min: startingMin(executableIndex), + max: startingMax(executableIndex) + }] + }; + } else { + var hasExecutableChild = false; + + var orderedChildren = orderChildren(node); + + for (var i = 0; i < orderedChildren.length; i++) { + var child = orderedChildren[i]; + + processNode(child, parentEnabled); + + if (!stats.valid) { + return; + } + + var childStats = stats[child.id]; + + hasExecutableChild = hasExecutableChild || childStats.executable; + } + + stats[node.id] = { + executable: hasExecutableChild + }; + + segmentChildren(node, orderedChildren, stats[node.id], executableIndex); + + if (!node.canBeReentered() && stats[node.id].segments.length > 1) { + stats = { valid: false }; + } + } + } + + function startingMin(executableIndex) { + return executableIndex === undefined ? defaultMin : executableIndex; + } + + function startingMax(executableIndex) { + return executableIndex === undefined ? defaultMax : executableIndex; + } + + function segmentChildren(node, orderedChildren, nodeStats, executableIndex) { + var currentSegment = { index: 0, owner: node, nodes: [], min: startingMin(executableIndex), max: startingMax(executableIndex) }, + result = [currentSegment], + lastMax = defaultMax, + orderedChildSegments = orderChildSegments(orderedChildren); + + function isSegmentBoundary(minIndex) { + return lastMax !== defaultMax && minIndex !== defaultMin && lastMax < minIndex - 1; + } + + for (var i = 0; i < orderedChildSegments.length; i++) { + var childSegment = orderedChildSegments[i], + maxIndex = childSegment.max, + minIndex = childSegment.min; + + if (isSegmentBoundary(minIndex)) { + currentSegment = {index: result.length, owner: node, nodes: [], min: defaultMin, max: defaultMax}; + result.push(currentSegment); + } + + currentSegment.nodes.push(childSegment); + currentSegment.min = Math.min(currentSegment.min, minIndex); + currentSegment.max = Math.max(currentSegment.max, maxIndex); + lastMax = maxIndex; + } + + nodeStats.segments = result; + } + + function orderChildSegments(children) { + var specifiedOrder = [], + unspecifiedOrder = []; + + for (var i = 0; i < children.length; i++) { + var child = children[i], + segments = stats[child.id].segments; + + for (var j = 0; j < segments.length; j++) { + var seg = segments[j]; + + if (seg.min === defaultMin) { + unspecifiedOrder.push(seg); + } else { + specifiedOrder.push(seg); + } + } + } + + specifiedOrder.sort(function(a, b) { + return a.min - b.min; + }); + + return specifiedOrder.concat(unspecifiedOrder); + } + + function executeNode(node, segmentNumber) { + if (node.children) { + return { + fn: function(done) { + nodeStart(node); + + queueRunnerFactory({ + onComplete: function() { + nodeComplete(node, node.getResult()); + done(); + }, + queueableFns: wrapChildren(node, segmentNumber), + userContext: node.sharedUserContext(), + onException: function() { + node.onException.apply(node, arguments); + } + }); + } + }; + } else { + return { + fn: function(done) { node.execute(done, stats[node.id].executable); } + }; + } + } + + function wrapChildren(node, segmentNumber) { + var result = [], + segmentChildren = stats[node.id].segments[segmentNumber].nodes; + + for (var i = 0; i < segmentChildren.length; i++) { + result.push(executeNode(segmentChildren[i].owner, segmentChildren[i].index)); + } + + if (!stats[node.id].executable) { + return result; + } + + return node.beforeAllFns.concat(result).concat(node.afterAllFns); + } + } + + return TreeProcessor; +}; + +getJasmineRequireObj().Any = function(j$) { + + function Any(expectedObject) { + if (typeof expectedObject === 'undefined') { + throw new TypeError( + 'jasmine.any() expects to be passed a constructor function. ' + + 'Please pass one or use jasmine.anything() to match any object.' + ); + } + this.expectedObject = expectedObject; + } + + Any.prototype.asymmetricMatch = function(other) { + if (this.expectedObject == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedObject == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedObject == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedObject == Object) { + return typeof other == 'object'; + } + + if (this.expectedObject == Boolean) { + return typeof other == 'boolean'; + } + + return other instanceof this.expectedObject; + }; + + Any.prototype.jasmineToString = function() { + return ''; + }; + + return Any; +}; + +getJasmineRequireObj().Anything = function(j$) { + + function Anything() {} + + Anything.prototype.asymmetricMatch = function(other) { + return !j$.util.isUndefined(other) && other !== null; + }; + + Anything.prototype.jasmineToString = function() { + return ''; + }; + + return Anything; +}; + +getJasmineRequireObj().ArrayContaining = function(j$) { + function ArrayContaining(sample) { + this.sample = sample; + } + + ArrayContaining.prototype.asymmetricMatch = function(other) { + var className = Object.prototype.toString.call(this.sample); + if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not \'' + this.sample + '\'.'); } + + for (var i = 0; i < this.sample.length; i++) { + var item = this.sample[i]; + if (!j$.matchersUtil.contains(other, item)) { + return false; + } + } + + return true; + }; + + ArrayContaining.prototype.jasmineToString = function () { + return ''; + }; + + return ArrayContaining; +}; + +getJasmineRequireObj().ObjectContaining = function(j$) { + + function ObjectContaining(sample) { + this.sample = sample; + } + + function getPrototype(obj) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(obj); + } + + if (obj.constructor.prototype == obj) { + return null; + } + + return obj.constructor.prototype; + } + + function hasProperty(obj, property) { + if (!obj) { + return false; + } + + if (Object.prototype.hasOwnProperty.call(obj, property)) { + return true; + } + + return hasProperty(getPrototype(obj), property); + } + + ObjectContaining.prototype.asymmetricMatch = function(other) { + if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } + + for (var property in this.sample) { + if (!hasProperty(other, property) || + !j$.matchersUtil.equals(this.sample[property], other[property])) { + return false; + } + } + + return true; + }; + + ObjectContaining.prototype.jasmineToString = function() { + return ''; + }; + + return ObjectContaining; +}; + +getJasmineRequireObj().StringMatching = function(j$) { + + function StringMatching(expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error('Expected is not a String or a RegExp'); + } + + this.regexp = new RegExp(expected); + } + + StringMatching.prototype.asymmetricMatch = function(other) { + return this.regexp.test(other); + }; + + StringMatching.prototype.jasmineToString = function() { + return ''; + }; + + return StringMatching; +}; + +getJasmineRequireObj().errors = function() { + function ExpectationFailed() {} + + ExpectationFailed.prototype = new Error(); + ExpectationFailed.prototype.constructor = ExpectationFailed; + + return { + ExpectationFailed: ExpectationFailed + }; +}; +getJasmineRequireObj().matchersUtil = function(j$) { + // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? + + return { + equals: function(a, b, customTesters) { + customTesters = customTesters || []; + + return eq(a, b, [], [], customTesters); + }, + + contains: function(haystack, needle, customTesters) { + customTesters = customTesters || []; + + if ((Object.prototype.toString.apply(haystack) === '[object Array]') || + (!!haystack && !haystack.indexOf)) + { + for (var i = 0; i < haystack.length; i++) { + if (eq(haystack[i], needle, [], [], customTesters)) { + return true; + } + } + return false; + } + + return !!haystack && haystack.indexOf(needle) >= 0; + }, + + buildFailureMessage: function() { + var args = Array.prototype.slice.call(arguments, 0), + matcherName = args[0], + isNot = args[1], + actual = args[2], + expected = args.slice(3), + englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + + var message = 'Expected ' + + j$.pp(actual) + + (isNot ? ' not ' : ' ') + + englishyPredicate; + + if (expected.length > 0) { + for (var i = 0; i < expected.length; i++) { + if (i > 0) { + message += ','; + } + message += ' ' + j$.pp(expected[i]); + } + } + + return message + '.'; + } + }; + + function isAsymmetric(obj) { + return obj && j$.isA_('Function', obj.asymmetricMatch); + } + + function asymmetricMatch(a, b) { + var asymmetricA = isAsymmetric(a), + asymmetricB = isAsymmetric(b); + + if (asymmetricA && asymmetricB) { + return undefined; + } + + if (asymmetricA) { + return a.asymmetricMatch(b); + } + + if (asymmetricB) { + return b.asymmetricMatch(a); + } + } + + // Equality function lovingly adapted from isEqual in + // [Underscore](http://underscorejs.org) + function eq(a, b, aStack, bStack, customTesters) { + var result = true; + + var asymmetricResult = asymmetricMatch(a, b); + if (!j$.util.isUndefined(asymmetricResult)) { + return asymmetricResult; + } + + for (var i = 0; i < customTesters.length; i++) { + var customTesterResult = customTesters[i](a, b); + if (!j$.util.isUndefined(customTesterResult)) { + return customTesterResult; + } + } + + if (a instanceof Error && b instanceof Error) { + return a.message == b.message; + } + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { return a !== 0 || 1 / a == 1 / b; } + // A strict comparison is necessary because `null == undefined`. + if (a === null || b === null) { return a === b; } + var className = Object.prototype.toString.call(a); + if (className != Object.prototype.toString.call(b)) { return false; } + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') { return false; } + + var aIsDomNode = j$.isDomNode(a); + var bIsDomNode = j$.isDomNode(b); + if (aIsDomNode && bIsDomNode) { + // At first try to use DOM3 method isEqualNode + if (a.isEqualNode) { + return a.isEqualNode(b); + } + // IE8 doesn't support isEqualNode, try to use outerHTML && innerText + var aIsElement = a instanceof Element; + var bIsElement = b instanceof Element; + if (aIsElement && bIsElement) { + return a.outerHTML == b.outerHTML; + } + if (aIsElement || bIsElement) { + return false; + } + return a.innerText == b.innerText && a.textContent == b.textContent; + } + if (aIsDomNode || bIsDomNode) { + return false; + } + + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) { return bStack[length] == b; } + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0; + // Recursively compare objects and arrays. + // Compare array lengths to determine if a deep comparison is necessary. + if (className == '[object Array]' && a.length !== b.length) { + result = false; + } + + if (result) { + // Objects with different constructors are not equivalent, but `Object`s + // or `Array`s from different frames are. + if (className !== '[object Array]') { + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && + isFunction(bCtor) && bCtor instanceof bCtor)) { + return false; + } + } + // Deep compare objects. + for (var key in a) { + if (has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) { break; } + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (has(b, key) && !(size--)) { break; } + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + + return result; + + function has(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + } + + function isFunction(obj) { + return typeof obj === 'function'; + } + } +}; + +getJasmineRequireObj().toBe = function() { + function toBe() { + return { + compare: function(actual, expected) { + return { + pass: actual === expected + }; + } + }; + } + + return toBe; +}; + +getJasmineRequireObj().toBeCloseTo = function() { + + function toBeCloseTo() { + return { + compare: function(actual, expected, precision) { + if (precision !== 0) { + precision = precision || 2; + } + + return { + pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2) + }; + } + }; + } + + return toBeCloseTo; +}; + +getJasmineRequireObj().toBeDefined = function() { + function toBeDefined() { + return { + compare: function(actual) { + return { + pass: (void 0 !== actual) + }; + } + }; + } + + return toBeDefined; +}; + +getJasmineRequireObj().toBeFalsy = function() { + function toBeFalsy() { + return { + compare: function(actual) { + return { + pass: !!!actual + }; + } + }; + } + + return toBeFalsy; +}; + +getJasmineRequireObj().toBeGreaterThan = function() { + + function toBeGreaterThan() { + return { + compare: function(actual, expected) { + return { + pass: actual > expected + }; + } + }; + } + + return toBeGreaterThan; +}; + + +getJasmineRequireObj().toBeLessThan = function() { + function toBeLessThan() { + return { + + compare: function(actual, expected) { + return { + pass: actual < expected + }; + } + }; + } + + return toBeLessThan; +}; +getJasmineRequireObj().toBeNaN = function(j$) { + + function toBeNaN() { + return { + compare: function(actual) { + var result = { + pass: (actual !== actual) + }; + + if (result.pass) { + result.message = 'Expected actual not to be NaN.'; + } else { + result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; }; + } + + return result; + } + }; + } + + return toBeNaN; +}; + +getJasmineRequireObj().toBeNull = function() { + + function toBeNull() { + return { + compare: function(actual) { + return { + pass: actual === null + }; + } + }; + } + + return toBeNull; +}; + +getJasmineRequireObj().toBeTruthy = function() { + + function toBeTruthy() { + return { + compare: function(actual) { + return { + pass: !!actual + }; + } + }; + } + + return toBeTruthy; +}; + +getJasmineRequireObj().toBeUndefined = function() { + + function toBeUndefined() { + return { + compare: function(actual) { + return { + pass: void 0 === actual + }; + } + }; + } + + return toBeUndefined; +}; + +getJasmineRequireObj().toContain = function() { + function toContain(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + + return { + pass: util.contains(actual, expected, customEqualityTesters) + }; + } + }; + } + + return toContain; +}; + +getJasmineRequireObj().toEqual = function() { + + function toEqual(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + var result = { + pass: false + }; + + result.pass = util.equals(actual, expected, customEqualityTesters); + + return result; + } + }; + } + + return toEqual; +}; + +getJasmineRequireObj().toHaveBeenCalled = function(j$) { + + function toHaveBeenCalled() { + return { + compare: function(actual) { + var result = {}; + + if (!j$.isSpy(actual)) { + throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.'); + } + + if (arguments.length > 1) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + result.pass = actual.calls.any(); + + result.message = result.pass ? + 'Expected spy ' + actual.and.identity() + ' not to have been called.' : + 'Expected spy ' + actual.and.identity() + ' to have been called.'; + + return result; + } + }; + } + + return toHaveBeenCalled; +}; + +getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { + + function toHaveBeenCalledTimes() { + return { + compare: function(actual, expected) { + if (!j$.isSpy(actual)) { + throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.'); + } + + var args = Array.prototype.slice.call(arguments, 0), + result = { pass: false }; + + if(!expected){ + throw new Error('Expected times failed is required as an argument.'); + } + + actual = args[0]; + var calls = actual.calls.count(); + var timesMessage = expected === 1 ? 'once' : expected + ' times'; + result.pass = calls === expected; + result.message = result.pass ? + 'Expected spy ' + actual.and.identity() + ' not to have been called ' + timesMessage + '. It was called ' + calls + ' times.' : + 'Expected spy ' + actual.and.identity() + ' to have been called ' + timesMessage + '. It was called ' + calls + ' times.'; + return result; + } + }; + } + + return toHaveBeenCalledTimes; +}; + +getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { + + function toHaveBeenCalledWith(util, customEqualityTesters) { + return { + compare: function() { + var args = Array.prototype.slice.call(arguments, 0), + actual = args[0], + expectedArgs = args.slice(1), + result = { pass: false }; + + if (!j$.isSpy(actual)) { + throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.'); + } + + if (!actual.calls.any()) { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; }; + return result; + } + + if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) { + result.pass = true; + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; }; + } else { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; }; + } + + return result; + } + }; + } + + return toHaveBeenCalledWith; +}; + +getJasmineRequireObj().toMatch = function(j$) { + + function toMatch() { + return { + compare: function(actual, expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error('Expected is not a String or a RegExp'); + } + + var regexp = new RegExp(expected); + + return { + pass: regexp.test(actual) + }; + } + }; + } + + return toMatch; +}; + +getJasmineRequireObj().toThrow = function(j$) { + + function toThrow(util) { + return { + compare: function(actual, expected) { + var result = { pass: false }, + threw = false, + thrown; + + if (typeof actual != 'function') { + throw new Error('Actual is not a Function'); + } + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + result.message = 'Expected function to throw an exception.'; + return result; + } + + if (arguments.length == 1) { + result.pass = true; + result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; }; + + return result; + } + + if (util.equals(thrown, expected)) { + result.pass = true; + result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; }; + } else { + result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' + j$.pp(thrown) + '.'; }; + } + + return result; + } + }; + } + + return toThrow; +}; + +getJasmineRequireObj().toThrowError = function(j$) { + function toThrowError () { + return { + compare: function(actual) { + var threw = false, + pass = {pass: true}, + fail = {pass: false}, + thrown; + + if (typeof actual != 'function') { + throw new Error('Actual is not a Function'); + } + + var errorMatcher = getMatcher.apply(null, arguments); + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + fail.message = 'Expected function to throw an Error.'; + return fail; + } + + if (!(thrown instanceof Error)) { + fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; }; + return fail; + } + + if (errorMatcher.hasNoSpecifics()) { + pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + '.'; + return pass; + } + + if (errorMatcher.matches(thrown)) { + pass.message = function() { + return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.'; + }; + return pass; + } else { + fail.message = function() { + return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + + ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.'; + }; + return fail; + } + } + }; + + function getMatcher() { + var expected = null, + errorType = null; + + if (arguments.length == 2) { + expected = arguments[1]; + if (isAnErrorType(expected)) { + errorType = expected; + expected = null; + } + } else if (arguments.length > 2) { + errorType = arguments[1]; + expected = arguments[2]; + if (!isAnErrorType(errorType)) { + throw new Error('Expected error type is not an Error.'); + } + } + + if (expected && !isStringOrRegExp(expected)) { + if (errorType) { + throw new Error('Expected error message is not a string or RegExp.'); + } else { + throw new Error('Expected is not an Error, string, or RegExp.'); + } + } + + function messageMatch(message) { + if (typeof expected == 'string') { + return expected == message; + } else { + return expected.test(message); + } + } + + return { + errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception', + thrownDescription: function(thrown) { + var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception', + thrownMessage = ''; + + if (expected) { + thrownMessage = ' with message ' + j$.pp(thrown.message); + } + + return thrownName + thrownMessage; + }, + messageDescription: function() { + if (expected === null) { + return ''; + } else if (expected instanceof RegExp) { + return ' with a message matching ' + j$.pp(expected); + } else { + return ' with message ' + j$.pp(expected); + } + }, + hasNoSpecifics: function() { + return expected === null && errorType === null; + }, + matches: function(error) { + return (errorType === null || error instanceof errorType) && + (expected === null || messageMatch(error.message)); + } + }; + } + + function isStringOrRegExp(potential) { + return potential instanceof RegExp || (typeof potential == 'string'); + } + + function isAnErrorType(type) { + if (typeof type !== 'function') { + return false; + } + + var Surrogate = function() {}; + Surrogate.prototype = type.prototype; + return (new Surrogate()) instanceof Error; + } + } + + return toThrowError; +}; + +getJasmineRequireObj().interface = function(jasmine, env) { + var jasmineInterface = { + describe: function(description, specDefinitions) { + return env.describe(description, specDefinitions); + }, + + xdescribe: function(description, specDefinitions) { + return env.xdescribe(description, specDefinitions); + }, + + fdescribe: function(description, specDefinitions) { + return env.fdescribe(description, specDefinitions); + }, + + it: function() { + return env.it.apply(env, arguments); + }, + + xit: function() { + return env.xit.apply(env, arguments); + }, + + fit: function() { + return env.fit.apply(env, arguments); + }, + + beforeEach: function() { + return env.beforeEach.apply(env, arguments); + }, + + afterEach: function() { + return env.afterEach.apply(env, arguments); + }, + + beforeAll: function() { + return env.beforeAll.apply(env, arguments); + }, + + afterAll: function() { + return env.afterAll.apply(env, arguments); + }, + + expect: function(actual) { + return env.expect(actual); + }, + + pending: function() { + return env.pending.apply(env, arguments); + }, + + fail: function() { + return env.fail.apply(env, arguments); + }, + + spyOn: function(obj, methodName) { + return env.spyOn(obj, methodName); + }, + + jsApiReporter: new jasmine.JsApiReporter({ + timer: new jasmine.Timer() + }), + + jasmine: jasmine + }; + + jasmine.addCustomEqualityTester = function(tester) { + env.addCustomEqualityTester(tester); + }; + + jasmine.addMatchers = function(matchers) { + return env.addMatchers(matchers); + }; + + jasmine.clock = function() { + return env.clock; + }; + + return jasmineInterface; +}; + +getJasmineRequireObj().version = function() { + return '2.4.1'; +}; diff --git a/examples/libraries/toolBars.js b/examples/libraries/toolBars.js index 3b712e2ea0..6e54c0276c 100644 --- a/examples/libraries/toolBars.js +++ b/examples/libraries/toolBars.js @@ -355,20 +355,36 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit }); } }; - that.windowDimensions = Controller.getViewportDimensions(); + + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + var recommendedRect = Controller.getRecommendedOverlayRect(); + var recommendedDimmensions = { x: recommendedRect.width, y: recommendedRect.height }; + that.windowDimensions = recommendedDimmensions; // Controller.getViewportDimensions(); + that.origin = { x: recommendedRect.x, y: recommendedRect.y }; // Maybe fixme: Keeping the same percent of the window size isn't always the right thing. // For example, maybe we want "keep the same percentage to whatever two edges are closest to the edge of screen". // If we change that, the places to do so are onResizeViewport, save (maybe), and the initial move based on Settings, below. that.onResizeViewport = function (newSize) { // Can be overridden or extended by clients. - var fractionX = that.x / that.windowDimensions.x; - var fractionY = that.y / that.windowDimensions.y; - that.windowDimensions = newSize || Controller.getViewportDimensions(); - that.move(fractionX * that.windowDimensions.x, fractionY * that.windowDimensions.y); + var recommendedRect = Controller.getRecommendedOverlayRect(); + var recommendedDimmensions = { x: recommendedRect.width, y: recommendedRect.height }; + var originRelativeX = (that.x - that.origin.x); + var originRelativeY = (that.y - that.origin.y); + var fractionX = clamp(originRelativeX / that.windowDimensions.x, 0, 1); + var fractionY = clamp(originRelativeY / that.windowDimensions.y, 0, 1); + that.windowDimensions = newSize || recommendedDimmensions; + that.origin = { x: recommendedRect.x, y: recommendedRect.y }; + var newX = (fractionX * that.windowDimensions.x) + recommendedRect.x; + var newY = (fractionY * that.windowDimensions.y) + recommendedRect.y; + that.move(newX, newY); }; if (optionalPersistenceKey) { this.fractionKey = optionalPersistenceKey + '.fraction'; this.save = function () { - var screenSize = Controller.getViewportDimensions(); + var recommendedRect = Controller.getRecommendedOverlayRect(); + var screenSize = { x: recommendedRect.width, y: recommendedRect.height }; if (screenSize.x > 0 && screenSize.y > 0) { // Guard against invalid screen size that can occur at shut-down. var fraction = {x: that.x / screenSize.x, y: that.y / screenSize.y}; @@ -411,7 +427,9 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit that.move(that.dragOffsetX + event.x, that.dragOffsetY + event.y); }; that.checkResize = function () { // Can be overriden or extended, but usually not. See onResizeViewport. - var currentWindowSize = Controller.getViewportDimensions(); + var recommendedRect = Controller.getRecommendedOverlayRect(); + var currentWindowSize = { x: recommendedRect.width, y: recommendedRect.height }; + if ((currentWindowSize.x !== that.windowDimensions.x) || (currentWindowSize.y !== that.windowDimensions.y)) { that.onResizeViewport(currentWindowSize); } @@ -434,7 +452,8 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit } if (this.fractionKey || optionalInitialPositionFunction) { var savedFraction = JSON.parse(Settings.getValue(this.fractionKey) || '0'); // getValue can answer empty string - var screenSize = Controller.getViewportDimensions(); + var recommendedRect = Controller.getRecommendedOverlayRect(); + var screenSize = { x: recommendedRect.width, y: recommendedRect.height }; if (savedFraction) { // If we have saved data, keep the toolbar at the same proportion of the screen width/height. that.move(savedFraction.x * screenSize.x, savedFraction.y * screenSize.y); diff --git a/examples/tests/avatarUnitTests.js b/examples/tests/avatarUnitTests.js new file mode 100644 index 0000000000..29e3ad0588 --- /dev/null +++ b/examples/tests/avatarUnitTests.js @@ -0,0 +1,59 @@ + +Script.include("../libraries/jasmine/jasmine.js"); +Script.include("../libraries/jasmine/hifi-boot.js"); + +// Art3mis +var DEFAULT_AVATAR_URL = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/contents/e76946cc-c272-4adf-9bb6-02cde0a4b57d/8fd984ea6fe1495147a3303f87fa6e23.fst?1460131758"; + +var ORIGIN = {x: 0, y: 0, z: 0}; +var ONE_HUNDRED = {x: 100, y: 100, z: 100}; +var ROT_IDENT = {x: 0, y: 0, z: 0, w: 1}; + +describe("MyAvatar", function () { + + // reload the avatar from scratch before each test. + beforeEach(function (done) { + MyAvatar.skeletonModelURL = DEFAULT_AVATAR_URL; + + // wait until we are finished loading + var id = Script.setInterval(function () { + if (MyAvatar.jointNames.length == 72) { + // assume we are finished loading. + Script.clearInterval(id); + MyAvatar.position = ORIGIN; + MyAvatar.orientation = ROT_IDENT; + // give the avatar 1/2 a second to settle on the ground in the idle pose. + Script.setTimeout(function () { + done(); + }, 500); + } + }, 500); + }); + + // makes the assumption that there is solid ground somewhat underneath the avatar. + it("position and orientation getters", function () { + var pos = MyAvatar.position; + + expect(Math.abs(pos.x)).toBeLessThan(0.1); + expect(Math.abs(pos.y)).toBeLessThan(1.0); + expect(Math.abs(pos.z)).toBeLessThan(0.1); + + var rot = MyAvatar.orientation; + expect(Math.abs(rot.x)).toBeLessThan(0.01); + expect(Math.abs(rot.y)).toBeLessThan(0.01); + expect(Math.abs(rot.z)).toBeLessThan(0.01); + expect(Math.abs(1 - rot.w)).toBeLessThan(0.01); + }); + + it("position and orientation setters", function (done) { + MyAvatar.position = ONE_HUNDRED; + Script.setTimeout(function () { + expect(Vec3.length(Vec3.subtract(MyAvatar.position, ONE_HUNDRED))).toBeLessThan(0.1); + done(); + }, 100); + }); + +}); + +jasmine.getEnv().execute(); + diff --git a/examples/utilities/render/framebuffer.qml b/examples/utilities/render/framebuffer.qml index d6f4ea7915..f13018142e 100644 --- a/examples/utilities/render/framebuffer.qml +++ b/examples/utilities/render/framebuffer.qml @@ -39,6 +39,7 @@ Column { "Shadow", "Pyramid Depth", "Ambient Occlusion", + "Ambient Occlusion Blurred", "Custom Shader" ] RadioButton { diff --git a/examples/utilities/render/stats.qml b/examples/utilities/render/stats.qml index e6cd03b3c3..6bea341c98 100644 --- a/examples/utilities/render/stats.qml +++ b/examples/utilities/render/stats.qml @@ -80,11 +80,6 @@ Item { label: "GPU", color: "#1AC567" }, - { - prop: "frameTextureCount", - label: "Frame", - color: "#E2334D" - }, { prop: "textureGPUTransferCount", label: "Transfer", @@ -114,13 +109,7 @@ Item { prop: "textureGPUVirtualMemoryUsage", label: "GPU Virtual", color: "#9495FF" - }, - { - prop: "frameTextureMemoryUsage", - label: "Frame", - color: "#E2334D" } - ] } @@ -170,6 +159,24 @@ Item { ] } + PlotPerf { + title: "State Changes" + height: parent.evalEvenHeight() + object: stats.config + plots: [ + { + prop: "frameTextureCount", + label: "Textures", + color: "#00B4EF" + }, + { + prop: "frameSetPipelineCount", + label: "Pipelines", + color: "#E2334D" + } + ] + } + property var drawOpaqueConfig: Render.getConfig("DrawOpaqueDeferred") property var drawTransparentConfig: Render.getConfig("DrawTransparentDeferred") property var drawLightConfig: Render.getConfig("DrawLight") @@ -178,9 +185,10 @@ Item { title: "Items" height: parent.evalEvenHeight() object: parent.drawOpaqueConfig + plots: [ { - object: Render.getConfig("DrawOpaqueDeferred"), + object: parent.drawOpaqueConfig, prop: "numDrawn", label: "Opaques", color: "#1AC567" @@ -198,7 +206,42 @@ Item { color: "#FED959" } ] - } + } + + PlotPerf { + title: "Timing" + height: parent.evalEvenHeight() + object: parent.drawOpaqueConfig + valueUnit: "ms" + valueScale: 1000 + valueNumDigits: "1" + plots: [ + { + object: Render.getConfig("DrawOpaqueDeferred"), + prop: "cpuRunTime", + label: "Opaques", + color: "#1AC567" + }, + { + object: Render.getConfig("DrawTransparentDeferred"), + prop: "cpuRunTime", + label: "Translucents", + color: "#00B4EF" + }, + { + object: Render.getConfig("RenderDeferred"), + prop: "cpuRunTime", + label: "Lighting", + color: "#FED959" + }, + { + object: Render.getConfig("RenderDeferredTask"), + prop: "cpuRunTime", + label: "RenderFrame", + color: "#E2334D" + } + ] + } } } diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index fb989dd174..75f379a425 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -23,6 +23,8 @@ Hifi.AvatarInputs { readonly property int mirrorWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 + + readonly property bool shouldReposition: true Settings { category: "Overlay.AvatarInputs" diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index beea0d8c64..c0804a967d 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -21,8 +21,11 @@ FocusScope { objectName: "desktop" anchors.fill: parent - onHeightChanged: d.repositionAll(); - onWidthChanged: d.repositionAll(); + property rect recommendedRect: rect(0,0,0,0); + + onHeightChanged: d.handleSizeChanged(); + + onWidthChanged: d.handleSizeChanged(); // Controls and windows can trigger this signal to ensure the desktop becomes visible // when they're opened. @@ -50,6 +53,20 @@ FocusScope { QtObject { id: d + function handleSizeChanged() { + var oldRecommendedRect = recommendedRect; + var newRecommendedRectJS = Controller.getRecommendedOverlayRect(); + var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, + newRecommendedRectJS.width, + newRecommendedRectJS.height); + + if (oldRecommendedRect != Qt.rect(0,0,0,0) + && oldRecommendedRect != newRecommendedRect) { + d.repositionAll(); + } + recommendedRect = newRecommendedRect; + } + function findChild(item, name) { for (var i = 0; i < item.children.length; ++i) { if (item.children[i].objectName === name) { @@ -202,12 +219,42 @@ FocusScope { // } } + function getRepositionChildren(predicate) { + var currentWindows = []; + if (!desktop) { + console.log("Could not find desktop"); + return currentWindows; + } + + for (var i = 0; i < desktop.children.length; ++i) { + var child = desktop.children[i]; + if (child.shouldReposition === true && (!predicate || predicate(child))) { + currentWindows.push(child) + } + } + return currentWindows; + } function repositionAll() { + var oldRecommendedRect = recommendedRect; + var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; + var newRecommendedRect = Controller.getRecommendedOverlayRect(); + var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { - reposition(windows[i]); + var targetWindow = windows[i]; + if (targetWindow.visible) { + repositionWindow(targetWindow, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); + } } + + // also reposition the other children that aren't top level windows but want to be repositioned + var otherChildren = d.getRepositionChildren(); + for (var i = 0; i < otherChildren.length; ++i) { + var child = otherChildren[i]; + repositionWindow(child, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); + } + } } @@ -232,38 +279,56 @@ FocusScope { targetWindow.focus = true; } - reposition(targetWindow); + var oldRecommendedRect = recommendedRect; + var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; + var newRecommendedRect = Controller.getRecommendedOverlayRect(); + var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; + repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); showDesktop(); } - function reposition(item) { + function repositionWindow(targetWindow, forceReposition, + oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions) { + if (desktop.width === 0 || desktop.height === 0) { return; } - var targetWindow = d.getDesktopWindow(item); if (!targetWindow) { console.warn("Could not find top level window for " + item); return; } + var recommended = Controller.getRecommendedOverlayRect(); + var maxX = recommended.x + recommended.width; + var maxY = recommended.y + recommended.height; var newPosition = Qt.vector2d(targetWindow.x, targetWindow.y); - // If the window is completely offscreen, reposition it - if ((targetWindow.x > desktop.width || (targetWindow.x + targetWindow.width) < 0) || - (targetWindow.y > desktop.height || (targetWindow.y + targetWindow.height) < 0)) { + + // if we asked to force reposition, or if the window is completely outside of the recommended rectangle, reposition it + if (forceReposition || (targetWindow.x > maxX || (targetWindow.x + targetWindow.width) < recommended.x) || + (targetWindow.y > maxY || (targetWindow.y + targetWindow.height) < recommended.y)) { newPosition.x = -1 newPosition.y = -1 } + if (newPosition.x === -1 && newPosition.y === -1) { - // Set initial window position - // var minPosition = Qt.vector2d(-windowRect.x, -windowRect.y); - // var maxPosition = Qt.vector2d(desktop.width - windowRect.width, desktop.height - windowRect.height); - // newPosition = Utils.clampVector(newPosition, minPosition, maxPosition); - // newPosition = Utils.randomPosition(minPosition, maxPosition); - newPosition = Qt.vector2d(desktop.width / 2 - targetWindow.width / 2, - desktop.height / 2 - targetWindow.height / 2); + var originRelativeX = (targetWindow.x - oldRecommendedRect.x); + var originRelativeY = (targetWindow.y - oldRecommendedRect.y); + if (isNaN(originRelativeX)) { + originRelativeX = 0; + } + if (isNaN(originRelativeY)) { + originRelativeY = 0; + } + var fractionX = Utils.clamp(originRelativeX / oldRecommendedDimmensions.x, 0, 1); + var fractionY = Utils.clamp(originRelativeY / oldRecommendedDimmensions.y, 0, 1); + + var newX = (fractionX * newRecommendedDimmensions.x) + newRecommendedRect.x; + var newY = (fractionY * newRecommendedDimmensions.y) + newRecommendedRect.y; + + newPosition = Qt.vector2d(newX, newY); } targetWindow.x = newPosition.x; targetWindow.y = newPosition.y; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 41eb7a305d..4e01fe85c3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -195,12 +195,7 @@ static const uint32_t INVALID_FRAME = UINT32_MAX; static const float PHYSICS_READY_RANGE = 3.0f; // how far from avatar to check for entities that aren't ready for simulation -#ifndef __APPLE__ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); -#else -// Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475 -static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).append("/script.js"); -#endif Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); @@ -1183,8 +1178,6 @@ void Application::cleanupBeforeQuit() { } _keyboardFocusHighlight = nullptr; - getEntities()->clear(); // this will allow entity scripts to properly shutdown - auto nodeList = DependencyManager::get(); // send the domain a disconnect packet, force stoppage of domain-server check-ins @@ -1195,6 +1188,7 @@ void Application::cleanupBeforeQuit() { nodeList->getPacketReceiver().setShouldDropPackets(true); getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + DependencyManager::get()->saveScripts(); DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts DependencyManager::destroy(); @@ -2686,8 +2680,6 @@ void Application::idle(uint64_t now) { _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); } - - // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. auto offscreenUi = DependencyManager::get(); if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { @@ -4191,8 +4183,13 @@ void Application::updateWindowTitle() const { } void Application::clearDomainOctreeDetails() { + + // if we're about to quit, we really don't need to do any of these things... + if (_aboutToQuit) { + return; + } + qCDebug(interfaceapp) << "Clearing domain octree details..."; - // reset the environment so that we don't erroneously end up with multiple resetPhysicsReadyInformation(); @@ -4216,7 +4213,6 @@ void Application::clearDomainOctreeDetails() { void Application::domainChanged(const QString& domainHostname) { updateWindowTitle(); - clearDomainOctreeDetails(); // disable physics until we have enough information about our new location to not cause craziness. resetPhysicsReadyInformation(); } @@ -4899,19 +4895,44 @@ QRect Application::getRenderingGeometry() const { } glm::uvec2 Application::getUiSize() const { - return getActiveDisplayPlugin()->getRecommendedUiSize(); + static const uint MIN_SIZE = 1; + glm::uvec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedUiSize(); + } + return result; +} + +QRect Application::getRecommendedOverlayRect() const { + auto uiSize = getUiSize(); + QRect result(0, 0, uiSize.x, uiSize.y); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedOverlayRect(); + } + return result; } QSize Application::getDeviceSize() const { - return fromGlm(getActiveDisplayPlugin()->getRecommendedRenderSize()); + static const int MIN_SIZE = 1; + QSize result(MIN_SIZE, MIN_SIZE); + if (_displayPlugin) { + result = fromGlm(getActiveDisplayPlugin()->getRecommendedRenderSize()); + } + return result; } bool Application::isThrottleRendering() const { - return getActiveDisplayPlugin()->isThrottled(); + if (_displayPlugin) { + return getActiveDisplayPlugin()->isThrottled(); + } + return false; } bool Application::hasFocus() const { - return getActiveDisplayPlugin()->hasFocus(); + if (_displayPlugin) { + return getActiveDisplayPlugin()->hasFocus(); + } + return (QApplication::activeWindow() != nullptr); } glm::vec2 Application::getViewportDimensions() const { diff --git a/interface/src/Application.h b/interface/src/Application.h index 6bfad21525..2911d42b65 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -117,6 +117,7 @@ public: QRect getRenderingGeometry() const; glm::uvec2 getUiSize() const; + QRect getRecommendedOverlayRect() const; QSize getDeviceSize() const; bool hasFocus() const; diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index a0283f0efd..7e2deef494 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -42,26 +41,11 @@ static const float TAU = 6.28318530717958f; //static const float MILKY_WAY_RATIO = 0.4f; static const char* UNIFORM_TIME_NAME = "iGlobalTime"; - - -Stars::Stars() { -} - -Stars::~Stars() { -} - // Produce a random float value between 0 and 1 static float frand() { return (float)rand() / (float)RAND_MAX; } -// Produce a random radian value between 0 and 2 PI (TAU) -/* -static float rrand() { - return frand() * TAU; -} - */ - // http://mathworld.wolfram.com/SpherePointPicking.html static vec2 randPolar() { vec2 result(frand(), frand()); @@ -115,59 +99,56 @@ struct StarVertex { vec4 colorAndSize; }; -// FIXME star colors -void Stars::render(RenderArgs* renderArgs, float alpha) { - static gpu::BufferPointer vertexBuffer; - static gpu::Stream::FormatPointer streamFormat; - static gpu::Element positionElement, colorElement; - static gpu::PipelinePointer _gridPipeline; - static gpu::PipelinePointer _starsPipeline; - static int32_t _timeSlot{ -1 }; - static std::once_flag once; +static const int STARS_VERTICES_SLOT{ 0 }; +static const int STARS_COLOR_SLOT{ 1 }; - const int VERTICES_SLOT = 0; - const int COLOR_SLOT = 1; +gpu::PipelinePointer Stars::_gridPipeline{}; +gpu::PipelinePointer Stars::_starsPipeline{}; +int32_t Stars::_timeSlot{ -1 }; - std::call_once(once, [&] { - { - auto vs = gpu::Shader::createVertex(std::string(standardTransformPNTC_vert)); - auto ps = gpu::Shader::createPixel(std::string(starsGrid_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - _timeSlot = program->getBuffers().findLocation(UNIFORM_TIME_NAME); - if (_timeSlot == gpu::Shader::INVALID_LOCATION) { - _timeSlot = program->getUniforms().findLocation(UNIFORM_TIME_NAME); - } - auto state = gpu::StatePointer(new gpu::State()); - // enable decal blend - state->setDepthTest(gpu::State::DepthTest(false)); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - _gridPipeline = gpu::Pipeline::create(program, state); - } - { - auto vs = gpu::Shader::createVertex(std::string(stars_vert)); - auto ps = gpu::Shader::createPixel(std::string(stars_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - auto state = gpu::StatePointer(new gpu::State()); - // enable decal blend - state->setDepthTest(gpu::State::DepthTest(false)); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - state->setAntialiasedLineEnable(true); // line smoothing also smooth points - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - _starsPipeline = gpu::Pipeline::create(program, state); - +void Stars::init() { + if (!_gridPipeline) { + auto vs = gpu::Shader::createVertex(std::string(standardTransformPNTC_vert)); + auto ps = gpu::Shader::createPixel(std::string(starsGrid_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + _timeSlot = program->getBuffers().findLocation(UNIFORM_TIME_NAME); + if (_timeSlot == gpu::Shader::INVALID_LOCATION) { + _timeSlot = program->getUniforms().findLocation(UNIFORM_TIME_NAME); } + auto state = gpu::StatePointer(new gpu::State()); + // enable decal blend + state->setDepthTest(gpu::State::DepthTest(false)); + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + _gridPipeline = gpu::Pipeline::create(program, state); + } + if (!_starsPipeline) { + auto vs = gpu::Shader::createVertex(std::string(stars_vert)); + auto ps = gpu::Shader::createPixel(std::string(stars_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + auto state = gpu::StatePointer(new gpu::State()); + // enable decal blend + state->setDepthTest(gpu::State::DepthTest(false)); + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + state->setAntialiasedLineEnable(true); // line smoothing also smooth points + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + _starsPipeline = gpu::Pipeline::create(program, state); + } + + unsigned limit = STARFIELD_NUM_STARS; + std::vector points; + points.resize(limit); + + { // generate stars QElapsedTimer startTime; startTime.start(); + vertexBuffer.reset(new gpu::Buffer); srand(STARFIELD_SEED); - unsigned limit = STARFIELD_NUM_STARS; - std::vector points; - points.resize(limit); for (size_t star = 0; star < limit; ++star) { points[star].position = vec4(fromPolar(randPolar()), 1); float size = frand() * 2.5f + 0.5f; @@ -179,16 +160,32 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { points[star].colorAndSize = vec4(color, size); } } + double timeDiff = (double)startTime.nsecsElapsed() / 1000000.0; // ns to ms qDebug() << "Total time to generate stars: " << timeDiff << " msec"; + } + + gpu::Element positionElement, colorElement; + const size_t VERTEX_STRIDE = sizeof(StarVertex); + + vertexBuffer->append(VERTEX_STRIDE * limit, (const gpu::Byte*)&points[0]); + streamFormat.reset(new gpu::Stream::Format()); // 1 for everyone + streamFormat->setAttribute(gpu::Stream::POSITION, STARS_VERTICES_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW), 0); + streamFormat->setAttribute(gpu::Stream::COLOR, STARS_COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA)); + positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; + colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; + + size_t offset = offsetof(StarVertex, position); + positionView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, positionElement); + + offset = offsetof(StarVertex, colorAndSize); + colorView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, colorElement); +} + +// FIXME star colors +void Stars::render(RenderArgs* renderArgs, float alpha) { + std::call_once(once, [&]{ init(); }); - vertexBuffer->append(sizeof(StarVertex) * limit, (const gpu::Byte*)&points[0]); - streamFormat.reset(new gpu::Stream::Format()); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW), 0); - streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA)); - positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; - colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; - }); auto modelCache = DependencyManager::get(); auto textureCache = DependencyManager::get(); @@ -210,17 +207,10 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { batch._glUniform1f(_timeSlot, secs); geometryCache->renderCube(batch); - static const size_t VERTEX_STRIDE = sizeof(StarVertex); - size_t offset = offsetof(StarVertex, position); - gpu::BufferView posView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, positionElement); - offset = offsetof(StarVertex, colorAndSize); - gpu::BufferView colView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, colorElement); - // Render the stars batch.setPipeline(_starsPipeline); - batch.setInputFormat(streamFormat); - batch.setInputBuffer(VERTICES_SLOT, posView); - batch.setInputBuffer(COLOR_SLOT, colView); + batch.setInputBuffer(STARS_VERTICES_SLOT, positionView); + batch.setInputBuffer(STARS_COLOR_SLOT, colorView); batch.draw(gpu::Primitive::POINTS, STARFIELD_NUM_STARS); } diff --git a/interface/src/Stars.h b/interface/src/Stars.h index 73f04755ab..f07caff770 100644 --- a/interface/src/Stars.h +++ b/interface/src/Stars.h @@ -12,21 +12,37 @@ #ifndef hifi_Stars_h #define hifi_Stars_h +#include + class RenderArgs; // Starfield rendering component. class Stars { public: - Stars(); - ~Stars(); + Stars() = default; + ~Stars() = default; + + Stars(Stars const&) = delete; + Stars& operator=(Stars const&) = delete; // Renders the starfield from a local viewer's perspective. // The parameters specifiy the field of view. void render(RenderArgs* args, float alpha); + private: - // don't copy/assign - Stars(Stars const&); // = delete; - Stars& operator=(Stars const&); // delete; + // Pipelines + static gpu::PipelinePointer _gridPipeline; + static gpu::PipelinePointer _starsPipeline; + static int32_t _timeSlot; + + // Buffers + gpu::BufferPointer vertexBuffer; + gpu::Stream::FormatPointer streamFormat; + gpu::BufferView positionView; + gpu::BufferView colorView; + std::once_flag once; + + void init(); }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4472abe6c3..ea713e2d96 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -198,6 +198,8 @@ MyAvatar::MyAvatar(RigPointer rig) : _headData->setLookAtPosition(headData->getLookAtPosition()); } }); + + connect(rig.get(), SIGNAL(onLoadComplete()), this, SIGNAL(onLoadComplete())); } MyAvatar::~MyAvatar() { @@ -349,6 +351,10 @@ void MyAvatar::simulate(float deltaTime) { _skeletonModel->simulate(deltaTime); } + // we've achived our final adjusted position and rotation for the avatar + // and all of its joints, now update our attachements. + Avatar::simulateAttachments(deltaTime); + if (!_skeletonModel->hasSkeleton()) { // All the simulation that can be done has been done return; @@ -1169,9 +1175,6 @@ void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) { _bodySensorMatrix = _follow.postPhysicsUpdate(*this, _bodySensorMatrix); setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity()); - - // now that physics has adjusted our position, we can update attachements. - Avatar::simulateAttachments(deltaTime); } QString MyAvatar::getScriptedMotorFrame() const { @@ -1575,6 +1578,7 @@ glm::vec3 MyAvatar::applyKeyboardMotor(float deltaTime, const glm::vec3& localVe float speedIncreaseFactor = 1.8f; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor; const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED; + if (motorSpeed < maxBoostSpeed) { // an active keyboard motor should never be slower than this float boostCoefficient = (maxBoostSpeed - motorSpeed) / maxBoostSpeed; @@ -2089,7 +2093,7 @@ float MyAvatar::getAccelerationEnergy() { int changeInVelocity = abs(velocity.length() - priorVelocity.length()); float changeInEnergy = priorVelocity.length() * changeInVelocity * AVATAR_MOVEMENT_ENERGY_CONSTANT; priorVelocity = velocity; - + return changeInEnergy; } @@ -2111,4 +2115,3 @@ bool MyAvatar::didTeleport() { lastPosition = pos; return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME); } - diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index b15f812197..35caabe0f7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -299,7 +299,7 @@ signals: void collisionWithEntity(const Collision& collision); void energyChanged(float newEnergy); void positionGoneTo(); - + void onLoadComplete(); private: diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index f6e5b6364f..d28c209a52 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -80,6 +80,11 @@ glm::vec2 ControllerScriptingInterface::getViewportDimensions() const { return qApp->getUiSize(); } +QVariant ControllerScriptingInterface::getRecommendedOverlayRect() const { + auto rect = qApp->getRecommendedOverlayRect(); + return qRectToVariant(rect); +} + controller::InputController* ControllerScriptingInterface::createInputController(const QString& deviceName, const QString& tracker) { // This is where we retrieve the Device Tracker category and then the sub tracker within it auto icIt = _inputControllers.find(0); diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 43bb6987db..fc8d125839 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -96,6 +96,7 @@ public slots: virtual void releaseJoystick(int joystickIndex); virtual glm::vec2 getViewportDimensions() const; + virtual QVariant getRecommendedOverlayRect() const; /// Factory to create an InputController virtual controller::InputController* createInputController(const QString& deviceName, const QString& tracker); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 9e2ba46ddb..b3f5e30d40 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1161,6 +1161,7 @@ void Rig::initAnimGraph(const QUrl& url) { overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); } + emit onLoadComplete(); }); connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index cbf4696723..897fa358e8 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -32,6 +32,7 @@ typedef std::shared_ptr RigPointer; // However only specific methods thread-safe. Noted below. class Rig : public QObject, public std::enable_shared_from_this { + Q_OBJECT public: struct StateHandler { AnimVariantMap results; @@ -223,7 +224,10 @@ public: const glm::mat4& getGeometryToRigTransform() const { return _geometryToRigTransform; } - protected: +signals: + void onLoadComplete(); + +protected: bool isIndexValid(int index) const { return _animSkeleton && index >= 0 && index < _animSkeleton->getNumJoints(); } void updateAnimationStateHandlers(); void applyOverridePoses(); diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index 3d0e4e7c5f..12d7e618e5 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -388,7 +388,7 @@ void SwingTwistConstraint::dynamicallyAdjustLimits(const glm::quat& rotation) { glm::vec3 swungY = swingRotation * Vectors::UNIT_Y; glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungY); float theta = atan2f(-swingAxis.z, swingAxis.x); - if (isnan(theta)) { + if (glm::isnan(theta)) { // atan2f() will only return NaN if either of its arguments is NaN, which can only // happen if we've been given a bad rotation. Since a NaN value here could potentially // cause a crash (we use the value of theta to compute indices into a std::vector) diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index 67a3f8c6ff..38fead87f1 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -29,6 +29,7 @@ namespace AudioConstants { const int NETWORK_FRAME_SAMPLES_PER_CHANNEL = NETWORK_FRAME_BYTES_PER_CHANNEL / sizeof(AudioSample); const float NETWORK_FRAME_SECS = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL / float(AudioConstants::SAMPLE_RATE)); const float NETWORK_FRAME_MSECS = NETWORK_FRAME_SECS * 1000.0f; + const float NETWORK_FRAMES_PER_SEC = 1.0f / NETWORK_FRAME_SECS; // be careful with overflows when using this constant const int NETWORK_FRAME_USECS = static_cast(NETWORK_FRAME_MSECS * 1000.0f); diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index b39fd8861d..4648fc8957 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -34,6 +34,7 @@ static const float reticleSize = TWO_PI / 100.0f; static QString _tooltipId; const uvec2 CompositorHelper::VIRTUAL_SCREEN_SIZE = uvec2(3960, 1188); // ~10% more pixel density than old version, 72dx240d FOV +const QRect CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT = QRect(956, 0, 2048, 1188); // don't include entire width only center 2048 const float CompositorHelper::VIRTUAL_UI_ASPECT_RATIO = (float)VIRTUAL_SCREEN_SIZE.x / (float)VIRTUAL_SCREEN_SIZE.y; const vec2 CompositorHelper::VIRTUAL_UI_TARGET_FOV = vec2(PI * 3.0f / 2.0f, PI * 3.0f / 2.0f / VIRTUAL_UI_ASPECT_RATIO); const vec2 CompositorHelper::MOUSE_EXTENTS_ANGULAR_SIZE = vec2(PI * 2.0f, PI * 0.95f); // horizontal: full sphere, vertical: ~5deg from poles diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index 062e5c1319..c0b53b329e 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -42,6 +42,7 @@ class CompositorHelper : public QObject, public Dependency { Q_PROPERTY(bool reticleOverDesktop READ getReticleOverDesktop WRITE setReticleOverDesktop) public: static const uvec2 VIRTUAL_SCREEN_SIZE; + static const QRect VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; static const float VIRTUAL_UI_ASPECT_RATIO; static const vec2 VIRTUAL_UI_TARGET_FOV; static const vec2 MOUSE_EXTENTS_ANGULAR_SIZE; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 34e484a988..79b50a7f88 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -34,6 +34,11 @@ glm::uvec2 HmdDisplayPlugin::getRecommendedUiSize() const { return CompositorHelper::VIRTUAL_SCREEN_SIZE; } +QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { + return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; +} + + bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index d16f03112f..e6ceb7e376 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -26,6 +26,8 @@ public: void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) override final; bool isDisplayVisible() const override { return isHmdMounted(); } + QRect getRecommendedOverlayRect() const override final; + virtual glm::mat4 getHeadPose() const override; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index eab28f500a..09f50c5de3 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -75,9 +75,29 @@ EntityTreeRenderer::~EntityTreeRenderer() { // it is registered with ScriptEngines, which will call deleteLater for us. } +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); + _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data()); + _entitiesScriptEngine->runInThread(); + DependencyManager::get()->setEntitiesScriptEngine(_entitiesScriptEngine.data()); +} + void EntityTreeRenderer::clear() { leaveAllEntities(); - _entitiesScriptEngine->unloadAllEntityScripts(); + if (_entitiesScriptEngine) { + _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(); + } auto scene = _viewState->getMain3DScene(); render::PendingChanges pendingChanges; @@ -94,7 +114,7 @@ void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { - _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); + ScriptEngine::loadEntityScript(_entitiesScriptEngine, entity->getEntityItemID(), entity->getScript(), true); } } } @@ -105,10 +125,7 @@ void EntityTreeRenderer::init() { entityTree->setFBXService(this); if (_wantScripts) { - _entitiesScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities"); - _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); - _entitiesScriptEngine->runInThread(); - DependencyManager::get()->setEntitiesScriptEngine(_entitiesScriptEngine); + setupEntitiesScriptEngine(); } forceRecheckEntities(); // setup our state to force checking our inside/outsideness of entities @@ -122,6 +139,8 @@ void EntityTreeRenderer::init() { void EntityTreeRenderer::shutdown() { _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential _shuttingDown = true; + + clear(); // always clear() on shutdown } void EntityTreeRenderer::setTree(OctreePointer newTree) { @@ -763,7 +782,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const if (entity && entity->shouldPreloadScript()) { QString scriptUrl = entity->getScript(); scriptUrl = ResourceManager::normalizeURL(scriptUrl); - _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload); + ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); entity->scriptHasPreloaded(); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 0fd5adc3f0..a6fc58e5f1 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -126,6 +126,8 @@ protected: } private: + void setupEntitiesScriptEngine(); + void addEntityToScene(EntityItemPointer entity); bool findBestZoneAndMaybeContainingEntities(const glm::vec3& avatarPosition, QVector* entitiesContainingAvatar); @@ -155,7 +157,7 @@ private: NetworkTexturePointer _ambientTexture; bool _wantScripts; - ScriptEngine* _entitiesScriptEngine; + QSharedPointer _entitiesScriptEngine; bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id, const Collision& collision); @@ -196,6 +198,8 @@ private: QHash _entitiesInScene; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; + + static int _entitiesScriptEngineCount; }; diff --git a/libraries/entities-renderer/src/RenderableProceduralItemShader.h b/libraries/entities-renderer/src/RenderableProceduralItemShader.h index a01a6b8571..1afd3bc608 100644 --- a/libraries/entities-renderer/src/RenderableProceduralItemShader.h +++ b/libraries/entities-renderer/src/RenderableProceduralItemShader.h @@ -18,7 +18,7 @@ // -const QString SHADER_COMMON = R"SHADER(#version 410 core +const QString SHADER_COMMON = R"SHADER( layout(location = 0) out vec4 _fragColor0; layout(location = 1) out vec4 _fragColor1; layout(location = 2) out vec4 _fragColor2; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540cc68689..8213316b7b 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -414,7 +414,13 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { } } +void EntityScriptingInterface::setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine) { + std::lock_guard lock(_entitiesScriptEngineLock); + _entitiesScriptEngine = engine; +} + void EntityScriptingInterface::callEntityMethod(QUuid id, const QString& method, const QStringList& params) { + std::lock_guard lock(_entitiesScriptEngineLock); if (_entitiesScriptEngine) { EntityItemID entityID{ id }; _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index b65bc55dbb..ca6e6fd9ac 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -71,7 +71,7 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } - void setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine) { _entitiesScriptEngine = engine; } + void setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine); float calculateCost(float mass, float oldVelocity, float newVelocity); public slots: @@ -214,6 +214,8 @@ private: bool precisionPicking, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard); EntityTreePointer _entityTree; + + std::mutex _entitiesScriptEngineLock; EntitiesScriptEngineProvider* _entitiesScriptEngine { nullptr }; bool _bidOnSimulationOwnership { false }; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 9dc475ab27..e90ef9e782 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -83,11 +83,18 @@ protected: }; friend class OffscreenQmlSurface; + + QJsonObject getGLContextData(); + Queue _queue; QMutex _mutex; QWaitCondition _waitCondition; std::atomic _rendering { false }; + QJsonObject _glData; + QMutex _glMutex; + QWaitCondition _glWait; + private: // Event-driven methods void init(); @@ -211,22 +218,31 @@ void OffscreenQmlRenderThread::setupFbo() { } } +QJsonObject OffscreenQmlRenderThread::getGLContextData() { + _glMutex.lock(); + if (_glData.isEmpty()) { + _glWait.wait(&_glMutex); + } + _glMutex.unlock(); + return _glData; +} + void OffscreenQmlRenderThread::init() { qDebug() << "Initializing QML Renderer"; - connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender); - connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate); - if (!_canvas.makeCurrent()) { qWarning("Failed to make context current on QML Renderer Thread"); _quit = true; return; } - // Expose GL data to QML - auto glData = getGLContextData(); - auto setGL = [=]{ _surface->getRootContext()->setContextProperty("GL", glData); }; - _surface->executeOnUiThread(setGL); + _glMutex.lock(); + _glData = ::getGLContextData(); + _glMutex.unlock(); + _glWait.wakeAll(); + + connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender); + connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate); _renderControl->initialize(_canvas.getContext()); setupFbo(); @@ -386,14 +402,16 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _qmlEngine->setIncubationController(_renderer->_quickWindow->incubationController()); } + _qmlEngine->rootContext()->setContextProperty("GL", _renderer->getGLContextData()); + _qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); + _qmlComponent = new QQmlComponent(_qmlEngine); + // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. - _updateTimer.setInterval(MIN_TIMER_MS); QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); QObject::connect(qApp, &QCoreApplication::aboutToQuit, this, &OffscreenQmlSurface::onAboutToQuit); + _updateTimer.setInterval(MIN_TIMER_MS); _updateTimer.start(); - _qmlComponent = new QQmlComponent(_qmlEngine); - _qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); } void OffscreenQmlSurface::resize(const QSize& newSize_) { diff --git a/libraries/gpu/src/gpu/Config.slh b/libraries/gpu/src/gpu/Config.slh index 3da92df7c1..ae556a3828 100644 --- a/libraries/gpu/src/gpu/Config.slh +++ b/libraries/gpu/src/gpu/Config.slh @@ -13,11 +13,11 @@ <@if GLPROFILE == PC_GL @> <@def GPU_FEATURE_PROFILE GPU_CORE@> - <@def VERSION_HEADER #version 410 core@> + <@def VERSION_HEADER //PC 410 core@> <@elif GLPROFILE == MAC_GL @> <@def GPU_FEATURE_PROFILE GPU_CORE@> - <@def VERSION_HEADER #version 410 core@> + <@def VERSION_HEADER //MAC 410 core@> <@else@> <@def GPU_FEATURE_PROFILE GPU_CORE@> - <@def VERSION_HEADER #version 410 core@> + <@def VERSION_HEADER //410 core@> <@endif@> diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 869db97be7..5325d600f4 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -39,6 +39,8 @@ public: int _DSNumAPIDrawcalls = 0; int _DSNumDrawcalls = 0; int _DSNumTriangles = 0; + + int _PSNumSetPipelines = 0; ContextStats() {} ContextStats(const ContextStats& stats) = default; diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index f51448f8fd..bd7b86adfd 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -461,8 +461,10 @@ void GLBackend::resetStages() { #define ADD_COMMAND_GL(call) _commands.push_back(COMMAND_##call); _commandOffsets.push_back(_params.size()); -//#define DO_IT_NOW(call, offset) runLastCommand(); -#define DO_IT_NOW(call, offset) +#define GET_UNIFORM_LOCATION(shaderUniformLoc) shaderUniformLoc +// THis will be used in the next PR +// #define GET_UNIFORM_LOCATION(shaderUniformLoc) _pipeline._programShader->getUniformLocation(shaderUniformLoc) + void Batch::_glActiveBindTexture(GLenum unit, GLenum target, GLuint texture) { // clean the cache on the texture unit we are going to use so the next call to setResourceTexture() at the same slot works fine @@ -472,14 +474,11 @@ void Batch::_glActiveBindTexture(GLenum unit, GLenum target, GLuint texture) { _params.push_back(texture); _params.push_back(target); _params.push_back(unit); - - - DO_IT_NOW(_glActiveBindTexture, 3); } void GLBackend::do_glActiveBindTexture(Batch& batch, size_t paramOffset) { glActiveTexture(batch._params[paramOffset + 2]._uint); glBindTexture( - batch._params[paramOffset + 1]._uint, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._uint), batch._params[paramOffset + 0]._uint); (void) CHECK_GL_ERROR(); @@ -492,8 +491,6 @@ void Batch::_glUniform1i(GLint location, GLint v0) { ADD_COMMAND_GL(glUniform1i); _params.push_back(v0); _params.push_back(location); - - DO_IT_NOW(_glUniform1i, 1); } void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -503,7 +500,7 @@ void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform1f( - batch._params[paramOffset + 1]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), batch._params[paramOffset + 0]._int); (void) CHECK_GL_ERROR(); } @@ -515,8 +512,6 @@ void Batch::_glUniform1f(GLint location, GLfloat v0) { ADD_COMMAND_GL(glUniform1f); _params.push_back(v0); _params.push_back(location); - - DO_IT_NOW(_glUniform1f, 1); } void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -527,7 +522,7 @@ void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { updatePipeline(); glUniform1f( - batch._params[paramOffset + 1]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), batch._params[paramOffset + 0]._float); (void) CHECK_GL_ERROR(); } @@ -538,8 +533,6 @@ void Batch::_glUniform2f(GLint location, GLfloat v0, GLfloat v1) { _params.push_back(v1); _params.push_back(v0); _params.push_back(location); - - DO_IT_NOW(_glUniform2f, 1); } void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { @@ -550,7 +543,7 @@ void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform2f( - batch._params[paramOffset + 2]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); (void) CHECK_GL_ERROR(); @@ -563,8 +556,6 @@ void Batch::_glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) { _params.push_back(v1); _params.push_back(v0); _params.push_back(location); - - DO_IT_NOW(_glUniform3f, 1); } void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { @@ -575,7 +566,7 @@ void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform3f( - batch._params[paramOffset + 3]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), batch._params[paramOffset + 2]._float, batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); @@ -591,8 +582,6 @@ void Batch::_glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLf _params.push_back(v1); _params.push_back(v0); _params.push_back(location); - - DO_IT_NOW(_glUniform4f, 1); } @@ -604,7 +593,7 @@ void GLBackend::do_glUniform4f(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform4f( - batch._params[paramOffset + 4]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 4]._int), batch._params[paramOffset + 3]._float, batch._params[paramOffset + 2]._float, batch._params[paramOffset + 1]._float, @@ -619,8 +608,6 @@ void Batch::_glUniform3fv(GLint location, GLsizei count, const GLfloat* value) { _params.push_back(cacheData(count * VEC3_SIZE, value)); _params.push_back(count); _params.push_back(location); - - DO_IT_NOW(_glUniform3fv, 3); } void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -630,7 +617,7 @@ void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform3fv( - batch._params[paramOffset + 2]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._uint, (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); @@ -645,8 +632,6 @@ void Batch::_glUniform4fv(GLint location, GLsizei count, const GLfloat* value) { _params.push_back(cacheData(count * VEC4_SIZE, value)); _params.push_back(count); _params.push_back(location); - - DO_IT_NOW(_glUniform4fv, 3); } void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -656,7 +641,7 @@ void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { } updatePipeline(); - GLint location = batch._params[paramOffset + 2]._int; + GLint location = GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int); GLsizei count = batch._params[paramOffset + 1]._uint; const GLfloat* value = (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint); glUniform4fv(location, count, value); @@ -671,8 +656,6 @@ void Batch::_glUniform4iv(GLint location, GLsizei count, const GLint* value) { _params.push_back(cacheData(count * VEC4_SIZE, value)); _params.push_back(count); _params.push_back(location); - - DO_IT_NOW(_glUniform4iv, 3); } void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -682,7 +665,7 @@ void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniform4iv( - batch._params[paramOffset + 2]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._uint, (const GLint*)batch.editData(batch._params[paramOffset + 0]._uint)); @@ -697,8 +680,6 @@ void Batch::_glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpo _params.push_back(transpose); _params.push_back(count); _params.push_back(location); - - DO_IT_NOW(_glUniformMatrix4fv, 4); } void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { @@ -708,7 +689,7 @@ void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { } updatePipeline(); glUniformMatrix4fv( - batch._params[paramOffset + 3]._int, + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), batch._params[paramOffset + 2]._uint, batch._params[paramOffset + 1]._uint, (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); @@ -722,8 +703,6 @@ void Batch::_glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) _params.push_back(blue); _params.push_back(green); _params.push_back(red); - - DO_IT_NOW(_glColor4f, 4); } void GLBackend::do_glColor4f(Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 9806c17db4..05afb28bf1 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -153,17 +153,42 @@ public: class GLShader : public GPUObject { public: - GLuint _shader; - GLuint _program; + enum Version { + Mono = 0, + + NumVersions + }; + + struct ShaderObject { + GLuint glshader{ 0 }; + GLuint glprogram{ 0 }; + GLint transformCameraSlot{ -1 }; + GLint transformObjectSlot{ -1 }; + }; + + using ShaderObjects = std::array< ShaderObject, NumVersions >; + using UniformMapping = std::map; + using UniformMappingVersions = std::vector; - GLint _transformCameraSlot = -1; - GLint _transformObjectSlot = -1; GLShader(); ~GLShader(); + + ShaderObjects _shaderObjects; + UniformMappingVersions _uniformMappings; + + GLuint getProgram() const { + return _shaderObjects[Mono].glprogram; + } + + GLint getUniformLocation(GLint srcLoc) { + return srcLoc; + // THIS will be used in the next PR + // return _uniformMappings[Mono][srcLoc]; + } + }; static GLShader* syncGPUObject(const Shader& shader); - static GLuint getShaderID(const ShaderPointer& shader); class GLState : public GPUObject { public: @@ -464,6 +489,7 @@ protected: PipelinePointer _pipeline; GLuint _program; + GLShader* _programShader; bool _invalidProgram; State::Data _stateCache; @@ -475,6 +501,7 @@ protected: PipelineStageState() : _pipeline(), _program(0), + _programShader(nullptr), _invalidProgram(false), _stateCache(State::DEFAULT), _stateSignatureCache(0), diff --git a/libraries/gpu/src/gpu/GLBackendPipeline.cpp b/libraries/gpu/src/gpu/GLBackendPipeline.cpp index 60ec478a8e..17ca67a5d3 100755 --- a/libraries/gpu/src/gpu/GLBackendPipeline.cpp +++ b/libraries/gpu/src/gpu/GLBackendPipeline.cpp @@ -64,11 +64,15 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { return; } + // A true new Pipeline + _stats._PSNumSetPipelines++; + // null pipeline == reset if (!pipeline) { _pipeline._pipeline.reset(); _pipeline._program = 0; + _pipeline._programShader = nullptr; _pipeline._invalidProgram = true; _pipeline._state = nullptr; @@ -80,8 +84,10 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { } // check the program cache - if (_pipeline._program != pipelineObject->_program->_program) { - _pipeline._program = pipelineObject->_program->_program; + GLuint glprogram = pipelineObject->_program->getProgram(); + if (_pipeline._program != glprogram) { + _pipeline._program = glprogram; + _pipeline._programShader = pipelineObject->_program; _pipeline._invalidProgram = true; } @@ -142,6 +148,7 @@ void GLBackend::resetPipelineStage() { // Second the shader side _pipeline._invalidProgram = false; _pipeline._program = 0; + _pipeline._programShader = nullptr; _pipeline._pipeline.reset(); glUseProgram(0); } diff --git a/libraries/gpu/src/gpu/GLBackendShader.cpp b/libraries/gpu/src/gpu/GLBackendShader.cpp index c27c5dd97d..86149c3f07 100755 --- a/libraries/gpu/src/gpu/GLBackendShader.cpp +++ b/libraries/gpu/src/gpu/GLBackendShader.cpp @@ -13,27 +13,205 @@ using namespace gpu; -GLBackend::GLShader::GLShader() : - _shader(0), - _program(0) -{} +GLBackend::GLShader::GLShader() +{ +} GLBackend::GLShader::~GLShader() { - if (_shader != 0) { - glDeleteShader(_shader); - } - if (_program != 0) { - glDeleteProgram(_program); + for (auto& so : _shaderObjects) { + if (so.glshader != 0) { + glDeleteShader(so.glshader); + } + if (so.glprogram != 0) { + glDeleteProgram(so.glprogram); + } } } -void makeBindings(GLBackend::GLShader* shader) { - if(!shader || !shader->_program) { +bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject) { + if (shaderSource.empty()) { + qCDebug(gpulogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create"; + return false; + } + + // Create the shader object + GLuint glshader = glCreateShader(shaderDomain); + if (!glshader) { + qCDebug(gpulogging) << "GLShader::compileShader - failed to create the gl shader object"; + return nullptr; + } + + // Assign the source + const int NUM_SOURCE_STRINGS = 2; + const GLchar* srcstr[] = { defines.c_str(), shaderSource.c_str() }; + glShaderSource(glshader, NUM_SOURCE_STRINGS, srcstr, NULL); + + // Compile ! + glCompileShader(glshader); + + // check if shader compiled + GLint compiled = 0; + glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled); + + // if compilation fails + if (!compiled) { + // save the source code to a temp file so we can debug easily + /* std::ofstream filestream; + filestream.open("debugshader.glsl"); + if (filestream.is_open()) { + filestream << shaderSource->source; + filestream.close(); + } + */ + + GLint infoLength = 0; + glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); + + char* temp = new char[infoLength]; + glGetShaderInfoLog(glshader, infoLength, NULL, temp); + + + /* + filestream.open("debugshader.glsl.info.txt"); + if (filestream.is_open()) { + filestream << std::string(temp); + filestream.close(); + } + */ + + qCWarning(gpulogging) << "GLShader::compileShader - failed to compile the gl shader object:"; + for (auto s : srcstr) { + qCWarning(gpulogging) << s; + } + qCWarning(gpulogging) << "GLShader::compileShader - errors:"; + qCWarning(gpulogging) << temp; + delete[] temp; + + glDeleteShader(glshader); + return false; + } + + GLuint glprogram = 0; +#ifdef SEPARATE_PROGRAM + // so far so good, program is almost done, need to link: + GLuint glprogram = glCreateProgram(); + if (!glprogram) { + qCDebug(gpulogging) << "GLShader::compileShader - failed to create the gl shader & gl program object"; + return false; + } + + glProgramParameteri(glprogram, GL_PROGRAM_SEPARABLE, GL_TRUE); + glAttachShader(glprogram, glshader); + glLinkProgram(glprogram); + + GLint linked = 0; + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + + if (!linked) { + /* + // save the source code to a temp file so we can debug easily + std::ofstream filestream; + filestream.open("debugshader.glsl"); + if (filestream.is_open()) { + filestream << shaderSource->source; + filestream.close(); + } + */ + + GLint infoLength = 0; + glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); + + char* temp = new char[infoLength]; + glGetProgramInfoLog(glprogram, infoLength, NULL, temp); + + qCDebug(gpulogging) << "GLShader::compileShader - failed to LINK the gl program object :"; + qCDebug(gpulogging) << temp; + + /* + filestream.open("debugshader.glsl.info.txt"); + if (filestream.is_open()) { + filestream << String(temp); + filestream.close(); + } + */ + delete[] temp; + + glDeleteShader(glshader); + glDeleteProgram(glprogram); + return false; + } +#endif + + shaderObject = glshader; + programObject = glprogram; + + return true; +} + +GLuint compileProgram(const std::vector& glshaders) { + // A brand new program: + GLuint glprogram = glCreateProgram(); + if (!glprogram) { + qCDebug(gpulogging) << "GLShader::compileProgram - failed to create the gl program object"; + return 0; + } + + // glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE); + // Create the program from the sub shaders + for (auto so : glshaders) { + glAttachShader(glprogram, so); + } + + // Link! + glLinkProgram(glprogram); + + GLint linked = 0; + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + + if (!linked) { + /* + // save the source code to a temp file so we can debug easily + std::ofstream filestream; + filestream.open("debugshader.glsl"); + if (filestream.is_open()) { + filestream << shaderSource->source; + filestream.close(); + } + */ + + GLint infoLength = 0; + glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); + + char* temp = new char[infoLength]; + glGetProgramInfoLog(glprogram, infoLength, NULL, temp); + + qCDebug(gpulogging) << "GLShader::compileProgram - failed to LINK the gl program object :"; + qCDebug(gpulogging) << temp; + + /* + filestream.open("debugshader.glsl.info.txt"); + if (filestream.is_open()) { + filestream << std::string(temp); + filestream.close(); + } + */ + delete[] temp; + + glDeleteProgram(glprogram); + return 0; + } + + return glprogram; +} + + +void makeProgramBindings(GLBackend::GLShader::ShaderObject& shaderObject) { + if (!shaderObject.glprogram) { return; } - GLuint glprogram = shader->_program; + GLuint glprogram = shaderObject.glprogram; GLint loc = -1; - + //Check for gpu specific attribute slotBindings loc = glGetAttribLocation(glprogram, "inPosition"); if (loc >= 0 && loc != gpu::Stream::POSITION) { @@ -96,226 +274,111 @@ void makeBindings(GLBackend::GLShader* shader) { loc = glGetProgramResourceIndex(glprogram, GL_SHADER_STORAGE_BLOCK, "transformObjectBuffer"); if (loc >= 0) { glShaderStorageBlockBinding(glprogram, loc, gpu::TRANSFORM_OBJECT_SLOT); - shader->_transformObjectSlot = gpu::TRANSFORM_OBJECT_SLOT; + shaderObject.transformObjectSlot = gpu::TRANSFORM_OBJECT_SLOT; } #else loc = glGetUniformLocation(glprogram, "transformObjectBuffer"); if (loc >= 0) { glProgramUniform1i(glprogram, loc, gpu::TRANSFORM_OBJECT_SLOT); - shader->_transformObjectSlot = gpu::TRANSFORM_OBJECT_SLOT; + shaderObject.transformObjectSlot = gpu::TRANSFORM_OBJECT_SLOT; } #endif loc = glGetUniformBlockIndex(glprogram, "transformCameraBuffer"); if (loc >= 0) { glUniformBlockBinding(glprogram, loc, gpu::TRANSFORM_CAMERA_SLOT); - shader->_transformCameraSlot = gpu::TRANSFORM_CAMERA_SLOT; + shaderObject.transformCameraSlot = gpu::TRANSFORM_CAMERA_SLOT; } (void)CHECK_GL_ERROR(); } -GLBackend::GLShader* compileShader(const Shader& shader) { +GLBackend::GLShader* compileBackendShader(const Shader& shader) { // Any GLSLprogram ? normally yes... const std::string& shaderSource = shader.getSource().getCode(); - if (shaderSource.empty()) { - qCDebug(gpulogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create"; - return nullptr; - } + + // GLSL version + const std::string glslVersion = { + "#version 410 core" + }; // Shader domain - const GLenum SHADER_DOMAINS[2] = { GL_VERTEX_SHADER, GL_FRAGMENT_SHADER }; + const int NUM_SHADER_DOMAINS = 2; + const GLenum SHADER_DOMAINS[NUM_SHADER_DOMAINS] = { + GL_VERTEX_SHADER, + GL_FRAGMENT_SHADER + }; GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; - // Create the shader object - GLuint glshader = glCreateShader(shaderDomain); - if (!glshader) { - qCDebug(gpulogging) << "GLShader::compileShader - failed to create the gl shader object"; - return nullptr; - } + // Domain specific defines + const std::string domainDefines[NUM_SHADER_DOMAINS] = { + "#define VERTEX_SHADER", + "#define PIXEL_SHADER" + }; - // Assign the source - const GLchar* srcstr = shaderSource.c_str(); - glShaderSource(glshader, 1, &srcstr, NULL); - // Compile ! - glCompileShader(glshader); - // check if shader compiled - GLint compiled = 0; - glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled); + // Versions specific of the shader + const std::string versionDefines[GLBackend::GLShader::NumVersions] = { + "" + }; - // if compilation fails - if (!compiled) { - // save the source code to a temp file so we can debug easily - /* std::ofstream filestream; - filestream.open("debugshader.glsl"); - if (filestream.is_open()) { - filestream << shaderSource->source; - filestream.close(); + GLBackend::GLShader::ShaderObjects shaderObjects; + + for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { + auto& shaderObject = shaderObjects[version]; + + std::string shaderDefines = glslVersion + "\n" + domainDefines[shader.getType()] + "\n" + versionDefines[version]; + + bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); + if (!result) { + return nullptr; } - */ - - GLint infoLength = 0; - glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); - - char* temp = new char[infoLength] ; - glGetShaderInfoLog(glshader, infoLength, NULL, temp); - - - /* - filestream.open("debugshader.glsl.info.txt"); - if (filestream.is_open()) { - filestream << std::string(temp); - filestream.close(); - } - */ - - qCWarning(gpulogging) << "GLShader::compileShader - failed to compile the gl shader object:"; - qCWarning(gpulogging) << srcstr; - qCWarning(gpulogging) << "GLShader::compileShader - errors:"; - qCWarning(gpulogging) << temp; - delete[] temp; - - glDeleteShader(glshader); - return nullptr; } - GLuint glprogram = 0; -#ifdef SEPARATE_PROGRAM - // so far so good, program is almost done, need to link: - GLuint glprogram = glCreateProgram(); - if (!glprogram) { - qCDebug(gpulogging) << "GLShader::compileShader - failed to create the gl shader & gl program object"; - return nullptr; - } - - glProgramParameteri(glprogram, GL_PROGRAM_SEPARABLE, GL_TRUE); - glAttachShader(glprogram, glshader); - glLinkProgram(glprogram); - - GLint linked = 0; - glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); - - if (!linked) { - /* - // save the source code to a temp file so we can debug easily - std::ofstream filestream; - filestream.open("debugshader.glsl"); - if (filestream.is_open()) { - filestream << shaderSource->source; - filestream.close(); - } - */ - - GLint infoLength = 0; - glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); - - char* temp = new char[infoLength] ; - glGetProgramInfoLog(glprogram, infoLength, NULL, temp); - - qCDebug(gpulogging) << "GLShader::compileShader - failed to LINK the gl program object :"; - qCDebug(gpulogging) << temp; - - /* - filestream.open("debugshader.glsl.info.txt"); - if (filestream.is_open()) { - filestream << String(temp); - filestream.close(); - } - */ - delete[] temp; - - glDeleteShader(glshader); - glDeleteProgram(glprogram); - return nullptr; - } -#endif - // So far so good, the shader is created successfully GLBackend::GLShader* object = new GLBackend::GLShader(); - object->_shader = glshader; - object->_program = glprogram; - - makeBindings(object); + object->_shaderObjects = shaderObjects; return object; } -GLBackend::GLShader* compileProgram(const Shader& program) { - if(!program.isProgram()) { +GLBackend::GLShader* compileBackendProgram(const Shader& program) { + if (!program.isProgram()) { return nullptr; } - // Let's go through every shaders and make sure they are ready to go - std::vector< GLuint > shaderObjects; - for (auto subShader : program.getShaders()) { - GLuint so = GLBackend::getShaderID(subShader); - if (!so) { - qCDebug(gpulogging) << "GLShader::compileProgram - One of the shaders of the program is not compiled?"; + GLBackend::GLShader::ShaderObjects programObjects; + + for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { + auto& programObject = programObjects[version]; + + // Let's go through every shaders and make sure they are ready to go + std::vector< GLuint > shaderGLObjects; + for (auto subShader : program.getShaders()) { + auto object = GLBackend::syncGPUObject(*subShader); + if (object) { + shaderGLObjects.push_back(object->_shaderObjects[version].glshader); + } else { + qCDebug(gpulogging) << "GLShader::compileBackendProgram - One of the shaders of the program is not compiled?"; + return nullptr; + } + } + + GLuint glprogram = compileProgram(shaderGLObjects); + if (glprogram == 0) { return nullptr; } - shaderObjects.push_back(so); + + programObject.glprogram = glprogram; + + makeProgramBindings(programObject); } - // so far so good, program is almost done, need to link: - GLuint glprogram = glCreateProgram(); - if (!glprogram) { - qCDebug(gpulogging) << "GLShader::compileProgram - failed to create the gl program object"; - return nullptr; - } - // glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE); - // Create the program from the sub shaders - for (auto so : shaderObjects) { - glAttachShader(glprogram, so); - } - - // Link! - glLinkProgram(glprogram); - - GLint linked = 0; - glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); - - if (!linked) { - /* - // save the source code to a temp file so we can debug easily - std::ofstream filestream; - filestream.open("debugshader.glsl"); - if (filestream.is_open()) { - filestream << shaderSource->source; - filestream.close(); - } - */ - - GLint infoLength = 0; - glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); - - char* temp = new char[infoLength] ; - glGetProgramInfoLog(glprogram, infoLength, NULL, temp); - - qCDebug(gpulogging) << "GLShader::compileProgram - failed to LINK the gl program object :"; - qCDebug(gpulogging) << temp; - - /* - filestream.open("debugshader.glsl.info.txt"); - if (filestream.is_open()) { - filestream << std::string(temp); - filestream.close(); - } - */ - delete[] temp; - - glDeleteProgram(glprogram); - return nullptr; - } - - // So far so good, the program is created successfully + // So far so good, the program versions have all been created successfully GLBackend::GLShader* object = new GLBackend::GLShader(); - object->_shader = 0; - object->_program = glprogram; - - makeBindings(object); + object->_shaderObjects = programObjects; return object; } @@ -329,14 +392,14 @@ GLBackend::GLShader* GLBackend::syncGPUObject(const Shader& shader) { } // need to have a gpu object? if (shader.isProgram()) { - GLShader* tempObject = compileProgram(shader); - if (tempObject) { + GLShader* tempObject = compileBackendProgram(shader); + if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); } } else if (shader.isDomain()) { - GLShader* tempObject = compileShader(shader); - if (tempObject) { + GLShader* tempObject = compileBackendShader(shader); + if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); } @@ -345,23 +408,6 @@ GLBackend::GLShader* GLBackend::syncGPUObject(const Shader& shader) { return object; } - -GLuint GLBackend::getShaderID(const ShaderPointer& shader) { - if (!shader) { - return 0; - } - GLShader* object = GLBackend::syncGPUObject(*shader); - if (object) { - if (shader->isProgram()) { - return object->_program; - } else { - return object->_shader; - } - } else { - return 0; - } -} - class ElementResource { public: gpu::Element _element; @@ -714,27 +760,38 @@ bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindin return false; } - if (object->_program) { - Shader::SlotSet buffers; - makeUniformBlockSlots(object->_program, slotBindings, buffers); + // Apply bindings to all program versions and generate list of slots from default version + for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { + auto& shaderObject = object->_shaderObjects[version]; + if (shaderObject.glprogram) { + Shader::SlotSet buffers; + makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); - Shader::SlotSet uniforms; - Shader::SlotSet textures; - Shader::SlotSet samplers; - makeUniformSlots(object->_program, slotBindings, uniforms, textures, samplers); - - Shader::SlotSet inputs; - makeInputSlots(object->_program, slotBindings, inputs); + Shader::SlotSet uniforms; + Shader::SlotSet textures; + Shader::SlotSet samplers; + makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); - Shader::SlotSet outputs; - makeOutputSlots(object->_program, slotBindings, outputs); + Shader::SlotSet inputs; + makeInputSlots(shaderObject.glprogram, slotBindings, inputs); - shader.defineSlots(uniforms, buffers, textures, samplers, inputs, outputs); - - } else if (object->_shader) { + Shader::SlotSet outputs; + makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); + // Define the public slots only from the default version + if (version == 0) { + shader.defineSlots(uniforms, buffers, textures, samplers, inputs, outputs); + } else { + GLShader::UniformMapping mapping; + for (auto srcUniform : shader.getUniforms()) { + mapping[srcUniform._location] = uniforms.findLocation(srcUniform._name); + } + object->_uniformMappings.push_back(mapping); + } + } } + return true; } diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index c793a92b72..deb17300c3 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -325,3 +325,26 @@ Buffer::Size Buffer::append(Size size, const Byte* data) { return newSize; } +const Element BufferView::DEFAULT_ELEMENT = Element( gpu::SCALAR, gpu::UINT8, gpu::RAW ); + +BufferView::BufferView() : +BufferView(DEFAULT_ELEMENT) {} + +BufferView::BufferView(const Element& element) : + BufferView(BufferPointer(), element) {} + +BufferView::BufferView(Buffer* newBuffer, const Element& element) : + BufferView(BufferPointer(newBuffer), element) {} + +BufferView::BufferView(const BufferPointer& buffer, const Element& element) : + BufferView(buffer, DEFAULT_OFFSET, buffer ? buffer->getSize() : 0, element.getSize(), element) {} + +BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element) : + BufferView(buffer, offset, size, element.getSize(), element) {} + +BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element) : + _buffer(buffer), + _offset(offset), + _size(size), + _element(element), + _stride(stride) {} diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 98ad0a2d28..570aff00fc 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -174,73 +174,30 @@ typedef std::vector< BufferPointer > Buffers; class BufferView { -protected: - void initFromBuffer(const BufferPointer& buffer) { - _buffer = (buffer); - if (_buffer) { - _size = (buffer->getSize()); - } - } +protected: + static const Resource::Size DEFAULT_OFFSET{ 0 }; + static const Element DEFAULT_ELEMENT; + public: - typedef Resource::Size Size; - typedef int Index; + using Size = Resource::Size; + using Index = int; BufferPointer _buffer; - Size _offset{ 0 }; - Size _size{ 0 }; + Size _offset; + Size _size; Element _element; - uint16 _stride{ 1 }; + uint16 _stride; - BufferView() : - _buffer(NULL), - _offset(0), - _size(0), - _element(gpu::SCALAR, gpu::UINT8, gpu::RAW), - _stride(1) - {}; - - BufferView(const Element& element) : - _buffer(NULL), - _offset(0), - _size(0), - _element(element), - _stride(uint16(element.getSize())) - {}; - - // create the BufferView and own the Buffer - BufferView(Buffer* newBuffer, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _offset(0), - _element(element), - _stride(uint16(element.getSize())) - { - initFromBuffer(BufferPointer(newBuffer)); - }; - BufferView(const BufferPointer& buffer, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _offset(0), - _element(element), - _stride(uint16(element.getSize())) - { - initFromBuffer(buffer); - }; - BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _buffer(buffer), - _offset(offset), - _size(size), - _element(element), - _stride(uint16(element.getSize())) - {}; - BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _buffer(buffer), - _offset(offset), - _size(size), - _element(element), - _stride(stride) - {}; - - ~BufferView() {} BufferView(const BufferView& view) = default; BufferView& operator=(const BufferView& view) = default; + BufferView(); + BufferView(const Element& element); + BufferView(Buffer* newBuffer, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); + Size getNumElements() const { return _size / _element.getSize(); } //Template iterator with random access on the buffer sysmem diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 8f2bbcbb85..68d9a61226 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -155,9 +155,12 @@ void ResourceCache::clearATPAssets() { } { QWriteLocker locker(&_resourcesToBeGottenLock); - for (auto& url : _resourcesToBeGotten) { - if (url.scheme() == URL_SCHEME_ATP) { - _resourcesToBeGotten.removeAll(url); + auto it = _resourcesToBeGotten.begin(); + while (it != _resourcesToBeGotten.end()) { + if (it->scheme() == URL_SCHEME_ATP) { + it = _resourcesToBeGotten.erase(it); + } else { + ++it; } } } diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index d9cd329bdd..ba43076b31 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -329,10 +329,21 @@ void SendQueue::run() { auto nextPacketDelta = (newPacketCount == 2 ? 2 : 1) * _packetSendPeriod; nextPacketTimestamp += std::chrono::microseconds(nextPacketDelta); - // sleep as long as we need until next packet send, if we can + // sleep as long as we need for next packet send, if we can auto now = p_high_resolution_clock::now(); + auto timeToSleep = duration_cast(nextPacketTimestamp - now); + // we use nextPacketTimestamp so that we don't fall behind, not to force long sleeps + // we'll never allow nextPacketTimestamp to force us to sleep for more than nextPacketDelta + // so cap it to that value + if (timeToSleep > std::chrono::microseconds(nextPacketDelta)) { + // reset the nextPacketTimestamp so that it is correct next time we come around + nextPacketTimestamp = now + std::chrono::microseconds(nextPacketDelta); + + timeToSleep = std::chrono::microseconds(nextPacketDelta); + } + // we're seeing SendQueues sleep for a long period of time here, // which can lock the NodeList if it's attempting to clear connections // for now we guard this by capping the time this thread and sleep for diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 9dd40a35a7..f685aee748 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -72,6 +72,17 @@ CharacterController::CharacterController() { _pendingFlags = PENDING_FLAG_UPDATE_SHAPE; } +CharacterController::~CharacterController() { + if (_rigidBody) { + btCollisionShape* shape = _rigidBody->getCollisionShape(); + if (shape) { + delete shape; + } + delete _rigidBody; + _rigidBody = nullptr; + } +} + bool CharacterController::needsRemoval() const { return ((_pendingFlags & PENDING_FLAG_REMOVE_FROM_SIMULATION) == PENDING_FLAG_REMOVE_FROM_SIMULATION); } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index d3c7405353..d810e904a7 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -36,7 +36,7 @@ class btDynamicsWorld; class CharacterController : public btCharacterControllerInterface { public: CharacterController(); - virtual ~CharacterController() {} + virtual ~CharacterController(); bool needsRemoval() const; bool needsAddition() const; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index de6fe0c839..64a73ab12a 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -105,6 +105,12 @@ public: return aspect(getRecommendedRenderSize()); } + // The recommended bounds for primary overlay placement + virtual QRect getRecommendedOverlayRect() const { + auto recommendedSize = getRecommendedUiSize(); + return QRect(0, 0, recommendedSize.x, recommendedSize.y); + } + // Stereo specific methods virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { return baseProjection; diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 2a2c2883d3..24a2f0d8a5 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -46,6 +46,7 @@ const float DEFAULT_METALLIC = 0; const vec3 DEFAULT_SPECULAR = vec3(0.1); const vec3 DEFAULT_EMISSIVE = vec3(0.0); const float DEFAULT_OCCLUSION = 1.0; +const vec3 DEFAULT_FRESNEL = DEFAULT_EMISSIVE; void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 71bc2dc6d0..c87aa1cee2 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -90,6 +90,7 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu // Specular highlight from ambient vec3 direction = -reflect(fragEyeDir, fragNormal); + float levels = getLightAmbientMapNumMips(light); float lod = min(floor((roughness) * levels), levels); vec4 skyboxLight = evalSkyboxLight(direction, lod); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index ba5b2a59d5..c09cd45bff 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -96,7 +96,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("PrepareDeferred"); // Render opaque objects in DeferredBuffer - addJob("DrawOpaqueDeferred", opaques, shapePlumber); + addJob("DrawOpaqueDeferred", opaques, shapePlumber); // Once opaque is all rendered create stencil background addJob("DrawOpaqueStencil"); @@ -185,9 +185,6 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); - config->setNumDrawn((int)inItems.size()); - emit config->numDrawnChanged(); - glm::mat4 projMat; Transform viewMat; args->_viewFrustum->evalProjectionMatrix(projMat); @@ -199,6 +196,40 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); args->_batch = nullptr; }); + + config->setNumDrawn((int)inItems.size()); +} + +void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems) { + assert(renderContext->args); + assert(renderContext->args->_viewFrustum); + + auto config = std::static_pointer_cast(renderContext->jobConfig); + + RenderArgs* args = renderContext->args; + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + glm::mat4 projMat; + Transform viewMat; + args->_viewFrustum->evalProjectionMatrix(projMat); + args->_viewFrustum->evalViewTransform(viewMat); + + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + + if (_stateSort) { + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + } else { + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + } + args->_batch = nullptr; + }); + + config->setNumDrawn((int)inItems.size()); } DrawOverlay3D::DrawOverlay3D(bool opaque) : diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 9fb6802992..727340f50d 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -22,6 +22,7 @@ public: using JobModel = render::Job::Model; }; + class PrepareDeferred { public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); @@ -29,32 +30,31 @@ public: using JobModel = render::Job::Model; }; - class RenderDeferred { public: using JobModel = render::Job::Model; void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - }; class DrawConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY numDrawnChanged) + Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) + Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) public: - int getNumDrawn() { return numDrawn; } - void setNumDrawn(int num) { numDrawn = num; emit numDrawnChanged(); } + int getNumDrawn() { return _numDrawn; } + void setNumDrawn(int numDrawn) { _numDrawn = numDrawn; emit newStats(); } int maxDrawn{ -1 }; signals: - void numDrawnChanged(); + void newStats(); void dirty(); protected: - int numDrawn{ 0 }; + int _numDrawn{ 0 }; }; class DrawDeferred { @@ -72,6 +72,44 @@ protected: int _maxDrawn; // initialized by Config }; +class DrawStateSortConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY numDrawnChanged) + Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) + Q_PROPERTY(bool stateSort MEMBER stateSort NOTIFY dirty) +public: + + int getNumDrawn() { return numDrawn; } + void setNumDrawn(int num) { numDrawn = num; emit numDrawnChanged(); } + + int maxDrawn{ -1 }; + bool stateSort{ true }; + +signals: + void numDrawnChanged(); + void dirty(); + +protected: + int numDrawn{ 0 }; +}; + +class DrawStateSortDeferred { +public: + using Config = DrawStateSortConfig; + using JobModel = render::Job::ModelI; + + DrawStateSortDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} + + void configure(const Config& config) { _maxDrawn = config.maxDrawn; _stateSort = config.stateSort; } + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems); + +protected: + render::ShapePlumberPointer _shapePlumber; + int _maxDrawn; // initialized by Config + bool _stateSort; +}; + + class DrawStencilDeferred { public: using JobModel = render::Job::Model; diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index da11507965..b28f271f9d 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -27,7 +27,7 @@ ToneMappingEffect::ToneMappingEffect() { } void ToneMappingEffect::init() { - const char BlitTextureGamma_frag[] = R"SCRIBE(#version 410 core + const char BlitTextureGamma_frag[] = R"SCRIBE( // Generated on Sat Oct 24 09:34:37 2015 // // Draw texture 0 fetched at texcoord.xy diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index 854221a5cf..f5385c23b7 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -41,20 +41,15 @@ void main() { float w = clamp( s, 0.0, 0.5); float a = smoothstep(0.5 - w, 0.5 + w, sdf); - // gamma correction for linear attenuation - a = pow(a, 1.0 / gamma); - // discard if unvisible if (a < 0.01) { discard; } - packDeferredFragmentLightmap( - normalize(_normal), - 1.0, - vec3(1.0), - DEFAULT_ROUGHNESS, - DEFAULT_METALLIC, - DEFAULT_SPECULAR, - Color.rgb); + packDeferredFragmentTranslucent( + normalize(_normal), + a, + Color.rgb, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); } \ No newline at end of file diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 08ff97fd17..34dfe53e34 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -65,6 +65,57 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } } +void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + auto& scene = sceneContext->_scene; + RenderArgs* args = renderContext->args; + + int numItemsToDraw = (int)inItems.size(); + if (maxDrawnItems != -1) { + numItemsToDraw = glm::min(numItemsToDraw, maxDrawnItems); + } + + using SortedPipelines = std::vector; + using SortedShapes = std::unordered_map, render::ShapeKey::Hash, render::ShapeKey::KeyEqual>; + SortedPipelines sortedPipelines; + SortedShapes sortedShapes; + std::vector ownPipelineBucket; + + + for (auto i = 0; i < numItemsToDraw; ++i) { + auto item = scene->getItem(inItems[i].id); + + { + assert(item.getKey().isShape()); + const auto& key = item.getShapeKey(); + if (key.isValid() && !key.hasOwnPipeline()) { + auto& bucket = sortedShapes[key]; + if (bucket.empty()) { + sortedPipelines.push_back(key); + } + bucket.push_back(item); + } else if (key.hasOwnPipeline()) { + ownPipelineBucket.push_back(item); + } else { + qDebug() << "Item could not be rendered: invalid key ?" << key; + } + } + } + + // Then render + for (auto& pipelineKey : sortedPipelines) { + auto& bucket = sortedShapes[pipelineKey]; + args->_pipeline = shapeContext->pickPipeline(args, pipelineKey); + for (auto& item : bucket) { + item.render(args); + } + } + args->_pipeline = nullptr; + for (auto& item : ownPipelineBucket) { + item.render(args); + } +} + void DrawLight::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inLights) { assert(renderContext->args); assert(renderContext->args->_viewFrustum); diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 8a0f951028..aa564980c4 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -18,6 +18,7 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); diff --git a/libraries/render/src/render/EngineStats.cpp b/libraries/render/src/render/EngineStats.cpp index 4b0936d326..a17845134a 100644 --- a/libraries/render/src/render/EngineStats.cpp +++ b/libraries/render/src/render/EngineStats.cpp @@ -49,5 +49,7 @@ void EngineStats::run(const SceneContextPointer& sceneContext, const RenderConte config->frameTextureRate = config->frameTextureCount * frequency; config->frameTextureMemoryUsage = _gpuStats._RSAmountTextureMemoryBounded - gpuStats._RSAmountTextureMemoryBounded; + config->frameSetPipelineCount = _gpuStats._PSNumSetPipelines - gpuStats._PSNumSetPipelines; + config->emitDirty(); } diff --git a/libraries/render/src/render/EngineStats.h b/libraries/render/src/render/EngineStats.h index ab58d2af38..e777e60a4e 100644 --- a/libraries/render/src/render/EngineStats.h +++ b/libraries/render/src/render/EngineStats.h @@ -47,6 +47,9 @@ namespace render { Q_PROPERTY(quint32 frameTextureRate MEMBER frameTextureRate NOTIFY dirty) Q_PROPERTY(quint32 frameTextureMemoryUsage MEMBER frameTextureMemoryUsage NOTIFY dirty) + Q_PROPERTY(quint32 frameSetPipelineCount MEMBER frameSetPipelineCount NOTIFY dirty) + + public: EngineStatsConfig() : Job::Config(true) {} @@ -73,6 +76,10 @@ namespace render { quint32 frameTextureRate{ 0 }; qint64 frameTextureMemoryUsage{ 0 }; + quint32 frameSetPipelineCount{ 0 }; + + + void emitDirty() { emit dirty(); } signals: diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index eabdc99338..300c0efd56 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -127,6 +127,9 @@ protected: // A default Config is always on; to create an enableable Config, use the ctor JobConfig(bool enabled) class JobConfig : public QObject { Q_OBJECT + Q_PROPERTY(quint64 cpuRunTime READ getCPUTRunTime NOTIFY newStats()) + + quint64 _CPURunTime{ 0 }; public: using Persistent = PersistentConfig; @@ -151,11 +154,17 @@ public: Q_INVOKABLE QString toJSON() { return QJsonDocument(toJsonValue(*this).toObject()).toJson(QJsonDocument::Compact); } Q_INVOKABLE void load(const QVariantMap& map) { qObjectFromJsonValue(QJsonObject::fromVariantMap(map), *this); emit loaded(); } + // Running Time measurement + // The new stats signal is emitted once per run time of a job when stats (cpu runtime) are updated + void setCPURunTime(quint64 ustime) { _CPURunTime = ustime; emit newStats(); } + quint64 getCPUTRunTime() const { return _CPURunTime; } + public slots: void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); emit loaded(); } signals: void loaded(); + void newStats(); }; class TaskConfig : public JobConfig { @@ -223,7 +232,11 @@ public: virtual void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) = 0; protected: + void setCPURunTime(quint64 ustime) { std::static_pointer_cast(_config)->setCPURunTime(ustime); } + QConfigPointer _config; + + friend class Job; }; using ConceptPointer = std::shared_ptr; @@ -278,8 +291,11 @@ public: void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { PerformanceTimer perfTimer(_name.c_str()); PROFILE_RANGE(_name.c_str()); + auto start = usecTimestampNow(); _concept->run(sceneContext, renderContext); + + _concept->setCPURunTime(usecTimestampNow() - start); } protected: diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 506bdb9463..15f3ebb985 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -143,7 +144,6 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam ScriptEngine::~ScriptEngine() { qCDebug(scriptengine) << "Script Engine shutting down (destructor) for script:" << getFilename(); - auto scriptEngines = DependencyManager::get(); if (scriptEngines) { scriptEngines->removeScriptEngine(this); @@ -1047,39 +1047,25 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin // since all of these operations can be asynch we will always do the actual work in the response handler // for the download -void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { - if (QThread::currentThread() != thread()) { +void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { + // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread + // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means + // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over + // to the "Entities" ScriptEngine thread. + DependencyManager::get()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + QSharedPointer strongEngine = theEngine.toStrongRef(); + if (strongEngine) { #ifdef THREAD_DEBUGGING - qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" - << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " - "entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload; + qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" + << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]"; #endif - - QMetaObject::invokeMethod(this, "loadEntityScript", - Q_ARG(const EntityItemID&, entityID), - Q_ARG(const QString&, entityScript), - Q_ARG(bool, forceRedownload)); - return; - } -#ifdef THREAD_DEBUGGING - qDebug() << "ScriptEngine::loadEntityScript() called on correct thread [" << thread() << "] " - "entityID:" << entityID << "entityScript:" << entityScript << "forceRedownload:" << forceRedownload; -#endif - - // If we've been called our known entityScripts should not know about us.. - assert(!_entityScripts.contains(entityID)); - -#ifdef THREAD_DEBUGGING - qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" - << QThread::currentThread() << "] expected thread [" << thread() << "]"; -#endif - DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { -#ifdef THREAD_DEBUGGING - qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" - << QThread::currentThread() << "] expected thread [" << thread() << "]"; -#endif - - this->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); + strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); + } else { + // FIXME - I'm leaving this in for testing, so that QA can confirm that sometimes the script contents + // returns after the ScriptEngine has been deleted, we can remove this after QA verifies the + // repro case. + qDebug() << "ScriptCache::getScriptContents() returned after our ScriptEngine was deleted... script:" << scriptOrURL; + } }, forceRedownload); } @@ -1213,6 +1199,16 @@ void ScriptEngine::unloadAllEntityScripts() { callEntityScriptMethod(entityID, "unload"); } _entityScripts.clear(); + +#ifdef DEBUG_ENGINE_STATE + qDebug() << "---- CURRENT STATE OF ENGINE: --------------------------"; + QScriptValueIterator it(globalObject()); + while (it.hasNext()) { + it.next(); + qDebug() << it.name() << ":" << it.value().toString(); + } + qDebug() << "--------------------------------------------------------"; +#endif // DEBUG_ENGINE_STATE } void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index a6a623e751..e8ce00c66c 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -124,7 +124,7 @@ public: Q_INVOKABLE QUrl resolvePath(const QString& path) const; // Entity Script Related methods - Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload = false); // will call the preload method once loaded + static void loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 78141188ef..8312de50fb 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -22,12 +22,7 @@ #define __STR1__(x) __STR2__(x) #define __LOC__ __FILE__ "(" __STR1__(__LINE__) ") : Warning Msg: " -#ifndef __APPLE__ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); -#else -// Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475 -static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).append("/script.js"); -#endif ScriptsModel& getScriptsModel() { static ScriptsModel scriptsModel; diff --git a/libraries/shared/src/Debug.h b/libraries/shared/src/Debug.h index d5568749d4..55d6d7975d 100644 --- a/libraries/shared/src/Debug.h +++ b/libraries/shared/src/Debug.h @@ -55,8 +55,11 @@ public: } ~Counter() { log(); } - // Increase the count for key. - void add(const K& key); + // Increase the count for key (by inc). + void add(const K& key, size_t inc = 1); + + // Decrease the count for key (by dec). + void sub(const K& key, size_t dec = 1); // Log current counts (called on destruction). void log(); @@ -123,20 +126,25 @@ private: }; template -void Counter::add(const K& k) { +void Counter::add(const K& k, size_t inc) { std::lock_guard lock(_mutex); auto& it = _map.find(k); if (it == _map.end()) { // No entry for k; add it - _map.insert(std::pair(k, 1)); + _map.insert(std::pair(k, inc)); } else { // Entry for k; update it - it->second++; + it->second += inc; } } +template +void Counter::sub(const K& k, size_t dec) { + add(k, -dec); +} + template void Counter::log() { // Avoid logging nothing diff --git a/libraries/shared/src/PIDController.cpp b/libraries/shared/src/PIDController.cpp index 78aa31e4c7..0d1d267dea 100644 --- a/libraries/shared/src/PIDController.cpp +++ b/libraries/shared/src/PIDController.cpp @@ -34,7 +34,7 @@ float PIDController::update(float measuredValue, float dt, bool resetAccumulator if (getIsLogging()) { // if logging/reporting updateHistory(measuredValue, dt, error, accumulatedError, changeInError, p, i, d, computedValue); } - Q_ASSERT(!isnan(computedValue)); + Q_ASSERT(!glm::isnan(computedValue)); // update state for next time _lastError = error; @@ -75,4 +75,4 @@ void PIDController::reportHistory() { qCDebug(shared) << "Limits: setpoint" << getMeasuredValueSetpoint() << "accumulate" << getAccumulatedValueLowLimit() << getAccumulatedValueHighLimit() << "controlled" << getControlledValueLowLimit() << getControlledValueHighLimit() << "kp/ki/kd" << getKP() << getKI() << getKD(); -} \ No newline at end of file +} diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 953fdb3582..53fa8b30cf 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -128,7 +128,7 @@ void vec3FromScriptValue(const QScriptValue &object, glm::vec3 &vec3) { vec3.z = object.property("z").toVariant().toFloat(); } -QVariant vec3toVariant(const glm::vec3 &vec3) { +QVariant vec3toVariant(const glm::vec3& vec3) { if (vec3.x != vec3.x || vec3.y != vec3.y || vec3.z != vec3.z) { // if vec3 contains a NaN don't try to convert it return QVariant(); @@ -140,6 +140,18 @@ QVariant vec3toVariant(const glm::vec3 &vec3) { return result; } +QVariant vec4toVariant(const glm::vec4& vec4) { + if (isNaN(vec4.x) || isNaN(vec4.y) || isNaN(vec4.z) || isNaN(vec4.w)) { + // if vec4 contains a NaN don't try to convert it + return QVariant(); + } + QVariantMap result; + result["x"] = vec4.x; + result["y"] = vec4.y; + result["z"] = vec4.z; + result["w"] = vec4.w; + return result; +} QScriptValue qVectorVec3ToScriptValue(QScriptEngine* engine, const QVector& vector) { QScriptValue array = engine->newArray(); @@ -150,7 +162,7 @@ QScriptValue qVectorVec3ToScriptValue(QScriptEngine* engine, const QVector()) { + v = glm::vec4(object.toFloat()); + valid = true; + } else if (object.canConvert()) { + auto qvec4 = qvariant_cast(object); + v.x = qvec4.x(); + v.y = qvec4.y(); + v.z = qvec4.z(); + v.w = qvec4.w(); + valid = true; + } else { + auto map = object.toMap(); + auto x = map["x"]; + auto y = map["y"]; + auto z = map["z"]; + auto w = map["w"]; + if (x.canConvert() && y.canConvert() && z.canConvert() && w.canConvert()) { + v.x = x.toFloat(); + v.y = y.toFloat(); + v.z = z.toFloat(); + v.w = w.toFloat(); + valid = true; + } + } + return v; +} + +glm::vec4 vec4FromVariant(const QVariant& object) { + bool valid = false; + return vec4FromVariant(object, valid); +} + +QScriptValue quatToScriptValue(QScriptEngine* engine, const glm::quat& quat) { QScriptValue obj = engine->newObject(); if (quat.x != quat.x || quat.y != quat.y || quat.z != quat.z || quat.w != quat.w) { // if quat contains a NaN don't try to convert it @@ -207,7 +256,7 @@ QScriptValue quatToScriptValue(QScriptEngine* engine, const glm::quat &quat) { return obj; } -void quatFromScriptValue(const QScriptValue &object, glm::quat &quat) { +void quatFromScriptValue(const QScriptValue& object, glm::quat &quat) { quat.x = object.property("x").toVariant().toFloat(); quat.y = object.property("y").toVariant().toFloat(); quat.z = object.property("z").toVariant().toFloat(); @@ -245,12 +294,12 @@ glm::quat quatFromVariant(const QVariant &object, bool& isValid) { return q; } -glm::quat quatFromVariant(const QVariant &object) { +glm::quat quatFromVariant(const QVariant& object) { bool valid = false; return quatFromVariant(object, valid); } -QVariant quatToVariant(const glm::quat &quat) { +QVariant quatToVariant(const glm::quat& quat) { if (quat.x != quat.x || quat.y != quat.y || quat.z != quat.z) { // if vec3 contains a NaN don't try to convert it return QVariant(); diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 652ec26fe7..2aefd3aa47 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -43,12 +43,15 @@ void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4); // Vec4 QScriptValue vec4toScriptValue(QScriptEngine* engine, const glm::vec4& vec4); void vec4FromScriptValue(const QScriptValue& object, glm::vec4& vec4); +QVariant vec4toVariant(const glm::vec4& vec4); +glm::vec4 vec4FromVariant(const QVariant &object, bool& valid); +glm::vec4 vec4FromVariant(const QVariant &object); // Vec3 QScriptValue vec3toScriptValue(QScriptEngine* engine, const glm::vec3 &vec3); void vec3FromScriptValue(const QScriptValue &object, glm::vec3 &vec3); -QVariant vec3toVariant(const glm::vec3 &vec3); +QVariant vec3toVariant(const glm::vec3& vec3); glm::vec3 vec3FromVariant(const QVariant &object, bool& valid); glm::vec3 vec3FromVariant(const QVariant &object); @@ -71,9 +74,10 @@ glm::quat quatFromVariant(const QVariant &object); // Rect QScriptValue qRectToScriptValue(QScriptEngine* engine, const QRect& rect); void qRectFromScriptValue(const QScriptValue& object, QRect& rect); - -QVariant qRectToVariant(const QRect& rect); QRect qRectFromVariant(const QVariant& object, bool& isValid); +QRect qRectFromVariant(const QVariant& object); +QVariant qRectToVariant(const QRect& rect); + // xColor QScriptValue xColorToScriptValue(QScriptEngine* engine, const xColor& color); diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index 4a2c0cf04b..951d004318 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -18,7 +18,7 @@ void Settings::getFloatValueIfValid(const QString& name, float& floatValue) { const QVariant badDefaultValue = NAN; bool ok = true; float tempFloat = value(name, badDefaultValue).toFloat(&ok); - if (ok && !isnan(tempFloat)) { + if (ok && !glm::isnan(tempFloat)) { floatValue = tempFloat; } } @@ -47,7 +47,7 @@ void Settings::getVec3ValueIfValid(const QString& name, glm::vec3& vecValue) { float x = value(QString("x"), badDefaultValue).toFloat(&ok); float y = value(QString("y"), badDefaultValue).toFloat(&ok); float z = value(QString("z"), badDefaultValue).toFloat(&ok); - if (ok && (!isnan(x) && !isnan(y) && !isnan(z))) { + if (ok && (!glm::isnan(x) && !glm::isnan(y) && !glm::isnan(z))) { vecValue = glm::vec3(x, y, z); } } @@ -74,7 +74,7 @@ void Settings::getQuatValueIfValid(const QString& name, glm::quat& quatValue) { float y = value(QString("y"), badDefaultValue).toFloat(&ok); float z = value(QString("z"), badDefaultValue).toFloat(&ok); float w = value(QString("w"), badDefaultValue).toFloat(&ok); - if (ok && (!isnan(x) && !isnan(y) && !isnan(z) && !isnan(w))) { + if (ok && (!glm::isnan(x) && !glm::isnan(y) && !glm::isnan(z) && !glm::isnan(w))) { quatValue = glm::quat(w, x, y, z); } }