diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9e6ec1209c..4156619bb2 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -88,6 +88,10 @@ void Agent::playAvatarSound(SharedSoundPointer sound) { QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound)); return; } else { + // TODO: seems to add occasional artifact in tests. I believe it is + // correct to do this, but need to figure out for sure, so commenting this + // out until I verify. + // _numAvatarSoundSentBytes = 0; setAvatarSound(sound); } } @@ -404,8 +408,37 @@ QUuid Agent::getSessionUUID() const { return DependencyManager::get()->getSessionUUID(); } +void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { + // this must happen on Agent's main thread + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setIsListeningToAudioStream", Q_ARG(bool, isListeningToAudioStream)); + return; + } + if (_isListeningToAudioStream) { + // have to tell just the audio mixer to KillAvatar. + + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode( + [&](const SharedNodePointer& node)->bool { + return (node->getType() == NodeType::AudioMixer) && node->getActiveSocket(); + }, + [&](const SharedNodePointer& node) { + qDebug() << "sending KillAvatar message to Audio Mixers"; + auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true); + packet->write(getSessionUUID().toRfc4122()); + nodeList->sendPacket(std::move(packet), *node); + }); + + } + _isListeningToAudioStream = isListeningToAudioStream; +} void Agent::setIsAvatar(bool isAvatar) { + // this must happen on Agent's main thread + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setIsAvatar", Q_ARG(bool, isAvatar)); + return; + } _isAvatar = isAvatar; if (_isAvatar && !_avatarIdentityTimer) { @@ -435,14 +468,16 @@ void Agent::setIsAvatar(bool isAvatar) { // when we stop sending identity, but then get woken up again by the mixer itself, which sends // identity packets to everyone. Here we explicitly tell the mixer to kill the entry for us. auto nodeList = DependencyManager::get(); - auto packetList = NLPacketList::create(PacketType::KillAvatar, QByteArray(), true, true); - packetList->write(getSessionUUID().toRfc4122()); nodeList->eachMatchingNode( [&](const SharedNodePointer& node)->bool { - return node->getType() == NodeType::AvatarMixer && node->getActiveSocket(); + return (node->getType() == NodeType::AvatarMixer || node->getType() == NodeType::AudioMixer) + && node->getActiveSocket(); }, [&](const SharedNodePointer& node) { - nodeList->sendPacketList(std::move(packetList), *node); + qDebug() << "sending KillAvatar message to Avatar and Audio Mixers"; + auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true); + packet->write(getSessionUUID().toRfc4122()); + nodeList->sendPacket(std::move(packet), *node); }); } emit stopAvatarAudioTimer(); @@ -474,24 +509,18 @@ void Agent::processAgentAvatar() { nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); } } -void Agent::flushEncoder() { +void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { _flushEncoder = false; - static QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); - static QByteArray encodedZeros; + static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); if (_encoder) { _encoder->encode(zeros, encodedZeros); + } else { + encodedZeros = zeros; } } void Agent::processAgentAvatarAudio() { if (_isAvatar && (_isListeningToAudioStream || _avatarSound)) { - // after sound is done playing, encoder has a bit of state in it, - // and needs some 0s to forget or you get a little click next time - // you play something - if (_flushEncoder) { - flushEncoder(); - } - // if we have an avatar audio stream then send it out to our audio-mixer auto scriptedAvatar = DependencyManager::get(); bool silentFrame = true; @@ -528,7 +557,7 @@ void Agent::processAgentAvatarAudio() { } } - auto audioPacket = NLPacket::create(silentFrame + auto audioPacket = NLPacket::create(silentFrame && !_flushEncoder ? PacketType::SilentAudioFrame : PacketType::MicrophoneAudioNoEcho); @@ -564,13 +593,17 @@ void Agent::processAgentAvatarAudio() { glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); audioPacket->writePrimitive(headOrientation); - QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); QByteArray encodedBuffer; - // encode it - if(_encoder) { - _encoder->encode(decodedBuffer, encodedBuffer); + if (_flushEncoder) { + encodeFrameOfZeros(encodedBuffer); } else { - encodedBuffer = decodedBuffer; + QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + if (_encoder) { + // encode it + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } } audioPacket->write(encodedBuffer.constData(), encodedBuffer.size()); } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index b882ac3125..c9b1707101 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -49,7 +49,7 @@ public: bool isPlayingAvatarSound() const { return _avatarSound != NULL; } bool isListeningToAudioStream() const { return _isListeningToAudioStream; } - void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } + void setIsListeningToAudioStream(bool isListeningToAudioStream); float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -81,7 +81,7 @@ signals: private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); - void flushEncoder(); + void encodeFrameOfZeros(QByteArray& encodedZeros); std::unique_ptr _scriptEngine; EntityEditPacketSender _entityEditSender; diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp index 857209df7c..77dd61043e 100644 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -15,19 +15,23 @@ // this should send a signal every 10ms, with pretty good precision. Hardcoding // to 10ms since that's what you'd want for audio. void AvatarAudioTimer::start() { - qDebug() << "AvatarAudioTimer::start called"; + qDebug() << __FUNCTION__; auto startTime = usecTimestampNow(); quint64 frameCounter = 0; const int TARGET_INTERVAL_USEC = 10000; // 10ms while (!_quit) { - frameCounter++; - // simplest possible timer + ++frameCounter; + + // tick every 10ms from startTime quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; - quint64 interval = std::max((quint64)0, targetTime - usecTimestampNow()); - usleep(interval); + quint64 now = usecTimestampNow(); + + // avoid quint64 underflow + if (now < targetTime) { + usleep(targetTime - now); + } + emit avatarTick(); } qDebug() << "AvatarAudioTimer is finished"; } - - diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 0252c037bf..d785579c38 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -90,9 +90,10 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::InjectAudio, PacketType::SilentAudioFrame, PacketType::AudioStreamStats }, this, "handleNodeAudioPacket"); - packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); + packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); + packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -481,6 +482,7 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) { } void AudioMixer::handleNodeAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { + getOrCreateClientData(sendingNode.data()); DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); } @@ -579,18 +581,8 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess } } - auto clientData = dynamic_cast(sendingNode->getLinkedData()); - - // FIXME - why would we not have client data at this point?? - if (!clientData) { - qDebug() << "UNEXPECTED -- didn't have node linked data in " << __FUNCTION__; - sendingNode->setLinkedData(std::unique_ptr { new AudioMixerClientData(sendingNode->getUUID()) }); - clientData = dynamic_cast(sendingNode->getLinkedData()); - connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); - } - + auto clientData = getOrCreateClientData(sendingNode.data()); clientData->setupCodec(selectedCodec, selectedCodecName); - qDebug() << "selectedCodecName:" << selectedCodecName; clientData->sendSelectAudioFormat(sendingNode, selectedCodecName); } @@ -599,14 +591,29 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { // enumerate the connected listeners to remove HRTF objects for the disconnected node auto nodeList = DependencyManager::get(); - nodeList->eachNode([](const SharedNodePointer& node) { + nodeList->eachNode([&killedNode](const SharedNodePointer& node) { auto clientData = dynamic_cast(node->getLinkedData()); if (clientData) { - clientData->removeHRTFsForNode(node->getUUID()); + clientData->removeHRTFsForNode(killedNode->getUUID()); } }); } +void AudioMixer::handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + auto clientData = dynamic_cast(sendingNode->getLinkedData()); + if (clientData) { + clientData->removeAgentAvatarAudioStream(); + auto nodeList = DependencyManager::get(); + nodeList->eachNode([sendingNode](const SharedNodePointer& node){ + auto listenerClientData = dynamic_cast(node->getLinkedData()); + if (listenerClientData) { + listenerClientData->removeHRTFForStream(sendingNode->getUUID()); + } + }); + } +} + + void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { sendingNode->parseIgnoreRequestMessage(packet); } @@ -646,7 +653,8 @@ void AudioMixer::sendStatsPacket() { statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100.0f; statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio; - statsObject["avg_listeners_per_frame"] = (float) _sumListeners / (float) _numStatFrames; + statsObject["avg_streams_per_frame"] = (float)_sumStreams / (float)_numStatFrames; + statsObject["avg_listeners_per_frame"] = (float)_sumListeners / (float)_numStatFrames; QJsonObject mixStats; mixStats["%_hrtf_mixes"] = percentageForMixStats(_hrtfRenders); @@ -660,6 +668,7 @@ void AudioMixer::sendStatsPacket() { statsObject["mix_stats"] = mixStats; + _sumStreams = 0; _sumListeners = 0; _hrtfRenders = 0; _hrtfSilentRenders = 0; @@ -707,17 +716,24 @@ void AudioMixer::run() { ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer); } +AudioMixerClientData* AudioMixer::getOrCreateClientData(Node* node) { + auto clientData = dynamic_cast(node->getLinkedData()); + + if (!clientData) { + node->setLinkedData(std::unique_ptr { new AudioMixerClientData(node->getUUID()) }); + clientData = dynamic_cast(node->getLinkedData()); + connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); + } + + return clientData; +} + void AudioMixer::domainSettingsRequestComplete() { auto nodeList = DependencyManager::get(); nodeList->addNodeTypeToInterestSet(NodeType::Agent); - nodeList->linkedDataCreateCallback = [&](Node* node) { - node->setLinkedData(std::unique_ptr { new AudioMixerClientData(node->getUUID()) }); - auto clientData = dynamic_cast(node->getLinkedData()); - - connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); - }; + nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); }; DomainHandler& domainHandler = nodeList->getDomainHandler(); const QJsonObject& settingsObject = domainHandler.getSettingsObject(); @@ -730,79 +746,71 @@ void AudioMixer::domainSettingsRequestComplete() { } void AudioMixer::broadcastMixes() { + const int TRAILING_AVERAGE_FRAMES = 100; + const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; + const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; + + const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; + const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; + + const float RATIO_BACK_OFF = 0.02f; + auto nodeList = DependencyManager::get(); auto nextFrameTimestamp = p_high_resolution_clock::now(); auto timeToSleep = std::chrono::microseconds(0); - const int TRAILING_AVERAGE_FRAMES = 100; + int currentFrame = 1; + int numFramesPerSecond = (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC); 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; + // manage mixer load + { + _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) + + // ratio of frame spent sleeping / total frame time + ((CURRENT_FRAME_RATIO * timeToSleep.count()) / (float) AudioConstants::NETWORK_FRAME_USECS); - const float RATIO_BACK_OFF = 0.02f; + bool hasRatioChanged = false; - const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; - const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - - if (timeToSleep.count() < 0) { - timeToSleep = std::chrono::microseconds(0); - } - - _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) - + (timeToSleep.count() * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS); - - float lastCutoffRatio = _performanceThrottlingRatio; - bool hasRatioChanged = false; - - if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { - if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { - // we're struggling - change our min required loudness to reduce some load - _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); - - qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; - } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { - // we've recovered and can back off the required loudness - _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; - - if (_performanceThrottlingRatio < 0) { - _performanceThrottlingRatio = 0; + if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { + if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { + qDebug() << "Mixer is struggling"; + // change our min required loudness to reduce some load + _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); + hasRatioChanged = true; + } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { + qDebug() << "Mixer is recovering"; + // back off the required loudness + _performanceThrottlingRatio = std::max(0.0f, _performanceThrottlingRatio - RATIO_BACK_OFF); + hasRatioChanged = true; } - qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; + if (hasRatioChanged) { + // set out min audability threshold from the new ratio + _minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio)); + framesSinceCutoffEvent = 0; + + qDebug() << "Sleeping" << _trailingSleepRatio << "of frame"; + qDebug() << "Cutoff is" << _performanceThrottlingRatio; + qDebug() << "Minimum audibility to be mixed is" << _minAudibilityThreshold; + } } - if (hasRatioChanged) { - // set out min audability threshold from the new ratio - _minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio)); - qDebug() << "Minimum audability required to be mixed is now" << _minAudibilityThreshold; - - framesSinceCutoffEvent = 0; + if (!hasRatioChanged) { + ++framesSinceCutoffEvent; } } - if (!hasRatioChanged) { - ++framesSinceCutoffEvent; - } - + // mix nodeList->eachNode([&](const SharedNodePointer& node) { - if (node->getLinkedData()) { AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); // this function will attempt to pop a frame from each audio stream. // a pointer to the popped data is stored as a member in InboundAudioStream. // That's how the popped audio data will be read for mixing (but only if the pop was successful) - nodeData->checkBuffersBeforeFrameSend(); + _sumStreams += nodeData->checkBuffersBeforeFrameSend(); // if the stream should be muted, send mute packet if (nodeData->getAvatarAudioStream() @@ -818,7 +826,8 @@ void AudioMixer::broadcastMixes() { std::unique_ptr mixPacket; - if (mixHasAudio) { + if (mixHasAudio || nodeData->shouldFlushEncoder()) { + int mixPacketBytes = sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + AudioConstants::NETWORK_FRAME_BYTES_STEREO; mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes); @@ -831,12 +840,17 @@ void AudioMixer::broadcastMixes() { QString codecInPacket = nodeData->getCodecName(); mixPacket->writeString(codecInPacket); - QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); QByteArray encodedBuffer; - nodeData->encode(decodedBuffer, encodedBuffer); - + if (mixHasAudio) { + QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); + nodeData->encode(decodedBuffer, encodedBuffer); + } else { + // time to flush, which resets the shouldFlush until next time we encode something + nodeData->encodeFrameOfZeros(encodedBuffer); + } // pack mixed audio samples mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); + } else { int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE; mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); @@ -876,24 +890,32 @@ void AudioMixer::broadcastMixes() { ++_numStatFrames; - // since we're a while loop we need to help Qt's event processing - QCoreApplication::processEvents(); + // play nice with qt event-looping + { + // since we're a while loop we need to help qt's event processing + QCoreApplication::processEvents(); - if (_isFinished) { - // at this point the audio-mixer is done - // check if we have a deferred delete event to process (which we should once finished) - QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); - break; + if (_isFinished) { + // alert qt that this is finished + QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); + break; + } } - // push the next frame timestamp to when we should send the next - nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS); + // sleep until the next frame, if necessary + { + nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS); - // 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); + auto now = p_high_resolution_clock::now(); + timeToSleep = std::chrono::duration_cast(nextFrameTimestamp - now); - std::this_thread::sleep_for(timeToSleep); + if (timeToSleep.count() < 0) { + nextFrameTimestamp = now; + timeToSleep = std::chrono::microseconds(0); + } + + std::this_thread::sleep_for(timeToSleep); + } } } diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index bccac529c1..3c68e4c6af 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -48,10 +48,12 @@ private slots: void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); void removeHRTFsForFinishedInjector(const QUuid& streamID); private: + AudioMixerClientData* getOrCreateClientData(Node* node); void domainSettingsRequestComplete(); /// adds one stream to the mix for a listening node @@ -85,6 +87,7 @@ private: float _attenuationPerDoublingInDistance; float _noiseMutingThreshold; int _numStatFrames { 0 }; + int _sumStreams { 0 }; int _sumListeners { 0 }; int _hrtfRenders { 0 }; int _hrtfSilentRenders { 0 }; diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 1eb36cd8a7..5b8c4aa105 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -73,11 +73,19 @@ void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid& } } +void AudioMixerClientData::removeAgentAvatarAudioStream() { + QWriteLocker writeLocker { &_streamsLock }; + auto it = _audioStreams.find(QUuid()); + if (it != _audioStreams.end()) { + _audioStreams.erase(it); + } + writeLocker.unlock(); +} + int AudioMixerClientData::parseData(ReceivedMessage& message) { PacketType packetType = message.getType(); if (packetType == PacketType::AudioStreamStats) { - // skip over header, appendFlag, and num stats packed message.seek(sizeof(quint8) + sizeof(quint16)); @@ -180,7 +188,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { return 0; } -void AudioMixerClientData::checkBuffersBeforeFrameSend() { +int AudioMixerClientData::checkBuffersBeforeFrameSend() { QWriteLocker writeLocker { &_streamsLock }; auto it = _audioStreams.begin(); @@ -208,6 +216,8 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend() { ++it; } } + + return (int)_audioStreams.size(); } bool AudioMixerClientData::shouldSendStats(int frameNumber) { @@ -355,7 +365,10 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { } void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; + qDebug() << __FUNCTION__ << + "sendingNode:" << *node << + "currentCodec:" << currentCodec << + "receivedCodec:" << recievedCodec; sendSelectAudioFormat(node, currentCodec); } @@ -366,6 +379,17 @@ void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const Q nodeList->sendPacket(std::move(replyPacket), *node); } +void AudioMixerClientData::encodeFrameOfZeros(QByteArray& encodedZeros) { + static QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_STEREO, 0); + if (_shouldFlushEncoder) { + if (_encoder) { + _encoder->encode(zeros, encodedZeros); + } else { + encodedZeros = zeros; + } + } + _shouldFlushEncoder = false; +} void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& codecName) { cleanupCodec(); // cleanup any previously allocated coders first diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 2f8ff4d049..52c659c240 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -50,9 +50,12 @@ public: // removes an AudioHRTF object for a given stream void removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()); + void removeAgentAvatarAudioStream(); + int parseData(ReceivedMessage& message) override; - void checkBuffersBeforeFrameSend(); + // attempt to pop a frame from each audio stream, and return the number of streams from this client + int checkBuffersBeforeFrameSend(); void removeDeadInjectedStreams(); @@ -76,7 +79,11 @@ public: } else { encodedBuffer = decodedBuffer; } + // once you have encoded, you need to flush eventually. + _shouldFlushEncoder = true; } + void encodeFrameOfZeros(QByteArray& encodedZeros); + bool shouldFlushEncoder() { return _shouldFlushEncoder; } QString getCodecName() { return _selectedCodecName; } @@ -105,6 +112,8 @@ private: QString _selectedCodecName; Encoder* _encoder{ nullptr }; // for outbound mixed stream Decoder* _decoder{ nullptr }; // for mic stream + + bool _shouldFlushEncoder { false }; }; #endif // hifi_AudioMixerClientData_h diff --git a/cmake/externals/openvr/CMakeLists.txt b/cmake/externals/openvr/CMakeLists.txt index 1cd4c071f1..19a9dd1f15 100644 --- a/cmake/externals/openvr/CMakeLists.txt +++ b/cmake/externals/openvr/CMakeLists.txt @@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://github.com/ValveSoftware/openvr/archive/v1.0.2.zip - URL_MD5 0d1cf5f579cf092e33f34759967b7046 + URL https://github.com/ValveSoftware/openvr/archive/v1.0.3.zip + URL_MD5 b484b12901917cc739e40389583c8b0d CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/macros/MemoryDebugger.cmake b/cmake/macros/MemoryDebugger.cmake index cb907efa96..7808812493 100644 --- a/cmake/macros/MemoryDebugger.cmake +++ b/cmake/macros/MemoryDebugger.cmake @@ -14,7 +14,7 @@ endif () if (HIFI_MEMORY_DEBUGGING) if (UNIX) - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -U_FORTIFY_SOURCE -fno-stack-protector -fno-omit-frame-pointer") SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=address") endif (UNIX) endif () diff --git a/cmake/macros/SetupHifiPlugin.cmake b/cmake/macros/SetupHifiPlugin.cmake index e9c8688590..0db91cb9e6 100644 --- a/cmake/macros/SetupHifiPlugin.cmake +++ b/cmake/macros/SetupHifiPlugin.cmake @@ -17,6 +17,12 @@ macro(SETUP_HIFI_PLUGIN) set(PLUGIN_PATH "plugins") endif() + if (WIN32) + # produce PDB files for plugins as well + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /DEBUG") + endif() + if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles") set(PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${PLUGIN_PATH}/") else() diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 568418afe1..d65612351c 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -571,7 +571,9 @@ Function HandlePostInstallOptions ; both launches use the explorer trick in case the user has elevated permissions for the installer ; it won't be possible to use this approach if either application should be launched with a command line param ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' + ; create shortcut with ARGUMENTS + CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface" + Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"' ${Else} Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' ${EndIf} diff --git a/cmake/templates/VersionInfo.rc.in b/cmake/templates/VersionInfo.rc.in new file mode 100644 index 0000000000..ad192ed87d --- /dev/null +++ b/cmake/templates/VersionInfo.rc.in @@ -0,0 +1,22 @@ +// Language and character set information as described at +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa381049(v=vs.85).aspx +#define US_ENGLISH_UNICODE "040904B0" + +// More information about the format of this file can be found at +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa381058(v=vs.85).aspx +1 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK US_ENGLISH_UNICODE + BEGIN + VALUE "CompanyName", "@BUILD_ORGANIZATION@" + VALUE "FileDescription", "@APP_FULL_NAME@" + VALUE "FileVersion", "@BUILD_VERSION@" + VALUE "InternalName", "@TARGET_NAME@" + VALUE "OriginalFilename", "@TARGET_NAME@.exe" + VALUE "ProductName", "@APP_FULL_NAME@" + VALUE "ProductVersion", "@BUILD_VERSION@" + END + END +END diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index bf9d7d04a6..abe7ed176a 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -237,6 +237,7 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; + userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; } else { // this node is an agent const QHostAddress& addr = node->getLocalSocket().getAddress(); @@ -312,6 +313,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; + userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; newNode->setPermissions(userPerms); return newNode; } diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index b43376c374..131c4ee509 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -133,8 +133,12 @@ elseif (WIN32) set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc") configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT}) + set(APP_FULL_NAME "High Fidelity Interface") + set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc") + configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) + # add an executable that also has the icon itself and the configured rc file as resources - add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT}) + add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) if (NOT DEV_BUILD) add_custom_command( diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 3edad8be55..236a200841 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -23,7 +23,7 @@ { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, { "from": "OculusTouch.LeftGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.LeftGrip" }, - { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, + { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, { "from": "OculusTouch.RY", "to": "Standard.RY", "filters": [ @@ -39,7 +39,7 @@ { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, { "from": "OculusTouch.RightGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.RightGrip" }, - { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, + { "from": "OculusTouch.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, { "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" }, @@ -58,4 +58,3 @@ { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" } ] } - diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index dce3e9660c..4fbdb37abf 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -34,7 +34,7 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, - { "from": "Vive.RightHand", "to": "Standard.RightHand" } + { "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, + { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] } ] -} \ No newline at end of file +} diff --git a/interface/resources/html/help.html b/interface/resources/html/help.html index 6cc4dab6af..422f5c6b46 100644 --- a/interface/resources/html/help.html +++ b/interface/resources/html/help.html @@ -50,21 +50,33 @@ function showKbm() { document.getElementById("main_image").setAttribute("src", "img/controls-help-keyboard.png"); } - function showHandControllers() { + function showViveControllers() { document.getElementById("main_image").setAttribute("src", "img/controls-help-vive.png"); } - function showGameController() { + function showXboxController() { document.getElementById("main_image").setAttribute("src", "img/controls-help-gamepad.png"); } + function load() { + console.log("In help.html: ", window.location.href); + parts = window.location.href.split("?"); + if (parts.length > 0) { + var defaultTab = parts[1]; + if (defaultTab == "xbox") { + showXboxController(); + } else if (defaultTab == "vive") { + showViveControllers(); + } + } + } - +
- - + +
diff --git a/interface/resources/html/img/controls-help-gamepad.png b/interface/resources/html/img/controls-help-gamepad.png index e0c8c8901d..cb77dbdabd 100644 Binary files a/interface/resources/html/img/controls-help-gamepad.png and b/interface/resources/html/img/controls-help-gamepad.png differ diff --git a/interface/resources/images/steam-min-spec-failed.png b/interface/resources/images/steam-min-spec-failed.png new file mode 100644 index 0000000000..99abac9e1c Binary files /dev/null and b/interface/resources/images/steam-min-spec-failed.png differ diff --git a/interface/resources/meshes/being_of_light/being_of_light.fbx b/interface/resources/meshes/being_of_light/being_of_light.fbx index 57505ca80d..20e71abd6d 100644 Binary files a/interface/resources/meshes/being_of_light/being_of_light.fbx and b/interface/resources/meshes/being_of_light/being_of_light.fbx differ diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 4150979cd4..384504aaa0 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -95,46 +95,10 @@ Hifi.AvatarInputs { anchors.fill: parent color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" - Image { - id: faceMute - width: root.iconSize - height: root.iconSize - visible: root.cameraEnabled - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - anchors.verticalCenter: parent.verticalCenter - source: root.cameraMuted ? "../images/face-mute.svg" : "../images/face.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleCameraMute() - } - onDoubleClicked: { - root.resetSensors(); - } - } - } - - Image { - id: micMute - width: root.iconSize - height: root.iconSize - anchors.left: root.cameraEnabled ? faceMute.right : parent.left - anchors.leftMargin: root.iconPadding - anchors.verticalCenter: parent.verticalCenter - source: root.audioMuted ? "../images/mic-mute.svg" : "../images/mic.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleAudioMute() - } - } - } - Item { id: audioMeter anchors.verticalCenter: parent.verticalCenter - anchors.left: micMute.right + anchors.left: parent.left anchors.leftMargin: root.iconPadding anchors.right: parent.right anchors.rightMargin: root.iconPadding diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index ca3a2da577..5e05601ce4 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -20,10 +20,10 @@ ScrollingWindow { anchors.centerIn: parent UpdateDialog { id: updateDialog - + implicitWidth: backgroundRectangle.width implicitHeight: backgroundRectangle.height - + readonly property int contentWidth: 500 readonly property int logoSize: 60 readonly property int borderWidth: 30 @@ -36,7 +36,7 @@ ScrollingWindow { signal triggerBuildDownload signal closeUpdateDialog - + Rectangle { id: backgroundRectangle color: "#ffffff" @@ -47,7 +47,7 @@ ScrollingWindow { Image { id: logo - source: "../images/interface-logo.svg" + source: "../images/hifi-logo.svg" width: updateDialog.logoSize height: updateDialog.logoSize anchors { @@ -65,7 +65,7 @@ ScrollingWindow { topMargin: updateDialog.borderWidth top: parent.top } - + Rectangle { id: header width: parent.width - updateDialog.logoSize - updateDialog.inputSpacing diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index abaf11a8e2..f388e5c120 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -27,6 +27,7 @@ Item { WebEngineView { id: root + objectName: "webEngineView" x: 0 y: 0 width: parent.width diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index b207087be0..66b59f0aea 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -312,6 +312,7 @@ FocusScope { onPinnedChanged: { if (pinned) { + d.raiseWindow(desktop); desktop.focus = true; desktop.forceActiveFocus(); diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 8c1b78af79..70eab82910 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -113,9 +113,8 @@ Rectangle { } FiraSansRegular { id: users; - visible: action === 'concurrency'; - text: onlineUsers; - size: textSize; + text: (action === 'concurrency') ? onlineUsers : 'snapshot'; + size: (action === 'concurrency') ? textSize : textSizeSmall; color: hifi.colors.white; anchors { verticalCenter: usersImage.verticalCenter; diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 7f1fbcb174..db0c1ba724 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -51,27 +51,41 @@ OriginalDesktop.Desktop { Toolbar { id: sysToolbar; objectName: "com.highfidelity.interface.toolbar.system"; - // These values will be overridden by sysToolbar.x/y if there is a saved position in Settings - // On exit, the sysToolbar position is saved to settings - x: 30 + anchors.horizontalCenter: settings.constrainToolbarToCenterX ? desktop.horizontalCenter : undefined; + // Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained. + x: sysToolbar.x y: 50 } + Settings { + id: settings; + category: "toolbar"; + property bool constrainToolbarToCenterX: true; + } + function setConstrainToolbarToCenterX(constrain) { // Learn about c++ preference change. + settings.constrainToolbarToCenterX = constrain; + } property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar map[sysToolbar.objectName] = sysToolbar; return map; })({}); + Component.onCompleted: { WebEngine.settings.javascriptCanOpenWindows = true; WebEngine.settings.javascriptCanAccessClipboard = false; WebEngine.settings.spatialNavigationEnabled = false; WebEngine.settings.localContentCanAccessRemoteUrls = true; - var toggleHudButton = sysToolbar.addButton({ - objectName: "hudToggle", - imageURL: "../../../icons/hud.svg", - visible: true, - pinned: true, + [ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts. + "hmdToggle", "mute", "mod", "help", + "hudToggle", + "com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto" + ].forEach(function (name) { + sysToolbar.addButton({objectName: name}); }); + var toggleHudButton = sysToolbar.findButton("hudToggle"); + toggleHudButton.imageURL = "../../../icons/hud.svg"; + toggleHudButton.pinned = true; + sysToolbar.updatePinned(); // automatic when adding buttons only IFF button is pinned at creation. toggleHudButton.buttonState = Qt.binding(function(){ return desktop.pinned ? 1 : 0 diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index 95f55f504b..9e46d86ecd 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -17,7 +17,7 @@ PreferencesDialog { id: root objectName: "GeneralPreferencesDialog" title: "General Settings" - showCategories: ["Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers"] + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers"] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 30989be688..01ce74cf6e 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -114,6 +114,9 @@ Window { // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded var result = findButton(properties.objectName); if (result) { + for (var property in properties) { + result[property] = properties[property]; + } return result; } properties.toolbar = this; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85c371d2b5..8cb81ae97f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -523,6 +523,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), + _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _scaleMirror(1.0f), _rotateMirror(0.0f), _raiseMirror(0.0f), @@ -534,6 +535,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { + setProperty("com.highfidelity.launchedFromSteam", SteamClient::isRunning()); + _runningMarker.startRunningMarker(); PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care @@ -569,6 +572,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _deadlockWatchdogThread = new DeadlockWatchdogThread(); _deadlockWatchdogThread->start(); + qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << SteamClient::getSteamVRBuildID(); qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; @@ -1142,7 +1146,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); // If the user clicks somewhere where there is NO entity at all, we will release focus - connect(getEntities(), &EntityTreeRenderer::mousePressOffEntity, [=]() { + connect(getEntities().data(), &EntityTreeRenderer::mousePressOffEntity, [=]() { setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); }); @@ -1191,6 +1195,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); properties["sim_rate"] = getAverageSimsPerSecond(); properties["avatar_sim_rate"] = getAvatarSimrate(); + properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); auto bandwidthRecorder = DependencyManager::get(); properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); @@ -1234,6 +1239,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto glInfo = getGLContextData(); properties["gl_info"] = glInfo; properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory()); + properties["ideal_thread_count"] = QThread::idealThreadCount(); auto hmdHeadPose = getHMDSensorPose(); properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); @@ -2145,12 +2151,27 @@ void Application::setFieldOfView(float fov) { } } +void Application::setSettingConstrainToolbarPosition(bool setting) { + _constrainToolbarPosition.set(setting); + DependencyManager::get()->setConstrainToolbarToCenterX(setting); +} + void Application::aboutApp() { InfoView::show(INFO_WELCOME_PATH); } void Application::showHelp() { - InfoView::show(INFO_HELP_PATH); + static const QString QUERY_STRING_XBOX = "xbox"; + static const QString QUERY_STRING_VIVE = "vive"; + + QString queryString = ""; + if (PluginUtils::isViveControllerAvailable()) { + queryString = QUERY_STRING_VIVE; + } else if (PluginUtils::isXboxControllerAvailable()) { + queryString = QUERY_STRING_XBOX; + } + + InfoView::show(INFO_HELP_PATH, false, queryString); } void Application::resizeEvent(QResizeEvent* event) { @@ -3467,7 +3488,7 @@ void Application::init() { // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts connect(_entitySimulation.get(), &EntitySimulation::entityCollisionWithEntity, - getEntities(), &EntityTreeRenderer::entityCollisionWithEntity); + getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing // of events related clicking, hovering over, and entering entities diff --git a/interface/src/Application.h b/interface/src/Application.h index 1a0041223e..4c98be9c2d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -180,7 +180,7 @@ public: void copyDisplayViewFrustum(ViewFrustum& viewOut) const; void copyShadowViewFrustum(ViewFrustum& viewOut) const override; const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } - EntityTreeRenderer* getEntities() const { return DependencyManager::get().data(); } + QSharedPointer getEntities() const { return DependencyManager::get(); } QUndoStack* getUndoStack() { return &_undoStack; } MainWindow* getWindow() const { return _window; } EntityTreePointer getEntityClipboard() const { return _entityClipboard; } @@ -206,6 +206,9 @@ public: float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); + float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } + void setSettingConstrainToolbarPosition(bool setting); + NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; } virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; } @@ -229,7 +232,7 @@ public: qint64 getCurrentSessionRuntime() const { return _sessionRunTimer.elapsed(); } - bool isAboutToQuit() const { return _aboutToQuit; } + bool isAboutToQuit() const override { return _aboutToQuit; } bool isPhysicsEnabled() const { return _physicsEnabled; } // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display @@ -506,6 +509,7 @@ private: Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; + Setting::Handle _constrainToolbarPosition; float _scaleMirror; float _rotateMirror; diff --git a/interface/src/InterfaceParentFinder.cpp b/interface/src/InterfaceParentFinder.cpp index 1979f8344b..824e81b6d8 100644 --- a/interface/src/InterfaceParentFinder.cpp +++ b/interface/src/InterfaceParentFinder.cpp @@ -29,7 +29,7 @@ SpatiallyNestableWeakPointer InterfaceParentFinder::find(QUuid parentID, bool& s if (entityTree) { parent = entityTree->findByID(parentID); } else { - EntityTreeRenderer* treeRenderer = qApp->getEntities(); + auto treeRenderer = qApp->getEntities(); EntityTreePointer tree = treeRenderer ? treeRenderer->getTree() : nullptr; parent = tree ? tree->findEntityByEntityItemID(parentID) : nullptr; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 75fcf67c47..f3ba977c03 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -338,6 +338,9 @@ Menu::Menu() { // Developer > Render > Throttle FPS If Not Focus addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true); + // Developer > Render > OpenVR threaded submit + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::OpenVrThreadedSubmit, 0, true); + // Developer > Render > Resolution MenuWrapper* resolutionMenu = renderOptionsMenu->addMenu(MenuOption::RenderResolution); QActionGroup* resolutionGroup = new QActionGroup(resolutionMenu); @@ -631,6 +634,14 @@ Menu::Menu() { // Developer > Audio >>> MenuWrapper* audioDebugMenu = developerMenu->addMenu("Audio"); + action = addActionToQMenuAndActionHash(audioDebugMenu, "Stats..."); + connect(action, &QAction::triggered, [] { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/audio/stats.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); + }); + action = addActionToQMenuAndActionHash(audioDebugMenu, "Buffers..."); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a75e5109a0..b859136be3 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -139,6 +139,7 @@ namespace MenuOption { const QString OctreeStats = "Entity Statistics"; const QString OnePointCalibration = "1 Point Calibration"; const QString OnlyDisplayTopTen = "Only Display Top Ten"; + const QString OpenVrThreadedSubmit = "OpenVR Threaded Submit"; const QString OutputMenu = "Display"; const QString Overlays = "Overlays"; const QString PackageModel = "Package Model..."; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 7968a3b6ea..0366d566d2 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -109,7 +109,7 @@ Avatar::Avatar(RigPointer rig) : Avatar::~Avatar() { assert(isDead()); // mark dead before calling the dtor - EntityTreeRenderer* treeRenderer = qApp->getEntities(); + auto treeRenderer = qApp->getEntities(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { @@ -199,7 +199,7 @@ void Avatar::updateAvatarEntities() { return; // wait until MyAvatar gets an ID before doing this. } - EntityTreeRenderer* treeRenderer = qApp->getEntities(); + auto treeRenderer = qApp->getEntities(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (!entityTree) { return; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dabffb4307..2d6411de60 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -505,7 +505,7 @@ void MyAvatar::simulate(float deltaTime) { locationChanged(); // if a entity-child of this avatar has moved outside of its queryAACube, update the cube and tell the entity server. - EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + auto entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { bool flyingAllowed = true; @@ -2133,7 +2133,7 @@ void MyAvatar::setAvatarCollisionsEnabled(bool enabled) { } bool ghostingAllowed = true; - EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + auto entityTreeRenderer = qApp->getEntities(); if (entityTreeRenderer) { std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); if (zone) { @@ -2467,7 +2467,7 @@ void MyAvatar::removeHoldAction(AvatarActionHold* holdAction) { } void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose) { - EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + auto entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { // lateAvatarUpdate will modify entity position & orientation, so we need an entity write lock diff --git a/interface/src/main.cpp b/interface/src/main.cpp index ab4ab689f7..a88388050b 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -37,7 +37,11 @@ #include #endif - +#ifdef Q_OS_WIN +extern "C" { + typedef int(__stdcall * CHECKMINSPECPROC) (); +} +#endif int main(int argc, const char* argv[]) { #if HAS_BUGSPLAT @@ -155,15 +159,33 @@ int main(int argc, const char* argv[]) { SteamClient::init(); +#ifdef Q_OS_WIN + // If we're running in steam mode, we need to do an explicit check to ensure we're up to the required min spec + if (SteamClient::isRunning()) { + QString appPath; + { + char filename[MAX_PATH]; + GetModuleFileName(NULL, filename, MAX_PATH); + QFileInfo appInfo(filename); + appPath = appInfo.absolutePath(); + } + QString openvrDllPath = appPath + "/plugins/openvr.dll"; + HMODULE openvrDll; + CHECKMINSPECPROC checkMinSpecPtr; + if ((openvrDll = LoadLibrary(openvrDllPath.toLocal8Bit().data())) && + (checkMinSpecPtr = (CHECKMINSPECPROC)GetProcAddress(openvrDll, "CheckMinSpec"))) { + if (!checkMinSpecPtr()) { + return -1; + } + } + } +#endif int exitCode; { QSettings::setDefaultFormat(QSettings::IniFormat); Application app(argc, const_cast(argv), startupTime, runServer, serverContentPathOptionValue); - bool launchedFromSteam = SteamClient::isRunning(); - app.setProperty("com.highfidelity.launchedFromSteam", launchedFromSteam); - // If we failed the OpenGLVersion check, log it. if (override) { auto accountManager = DependencyManager::get(); diff --git a/interface/src/octree/OctreePacketProcessor.cpp b/interface/src/octree/OctreePacketProcessor.cpp index 0254157b17..c792834d9c 100644 --- a/interface/src/octree/OctreePacketProcessor.cpp +++ b/interface/src/octree/OctreePacketProcessor.cpp @@ -92,13 +92,19 @@ void OctreePacketProcessor::processPacket(QSharedPointer messag switch(packetType) { case PacketType::EntityErase: { if (DependencyManager::get()->shouldRenderEntities()) { - qApp->getEntities()->processEraseMessage(*message, sendingNode); + auto renderer = qApp->getEntities(); + if (renderer) { + renderer->processEraseMessage(*message, sendingNode); + } } } break; case PacketType::EntityData: { if (DependencyManager::get()->shouldRenderEntities()) { - qApp->getEntities()->processDatagram(*message, sendingNode); + auto renderer = qApp->getEntities(); + if (renderer) { + renderer->processDatagram(*message, sendingNode); + } } } break; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 8e65b2fb57..7d3261aa78 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -68,6 +68,13 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } + // UI + { + auto getter = []()->bool { return qApp->getSettingConstrainToolbarPosition(); }; + auto setter = [](bool value) { qApp->setSettingConstrainToolbarPosition(value); }; + preferences->addPreference(new CheckPreference("UI", "Constrain Toolbar Position to Horizontal Center", getter, setter)); + } + // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 3334b0301b..1bf5f5de4e 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -89,7 +89,7 @@ QTemporaryFile* Snapshot::saveTempSnapshot(QImage image) { QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { // adding URL to snapshot - QUrl currentURL = DependencyManager::get()->currentAddress(); + QUrl currentURL = DependencyManager::get()->currentShareableAddress(); shot.setText(URL, currentURL.toString()); QString username = DependencyManager::get()->getAccountInfo().getUsername(); @@ -146,7 +146,10 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { void Snapshot::uploadSnapshot(const QString& filename) { const QString SNAPSHOT_UPLOAD_URL = "/api/v1/snapshots"; - static SnapshotUploader uploader; + // Alternatively to parseSnapshotData, we could pass the inWorldLocation through the call chain. This way is less disruptive to existing code. + SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(filename); + SnapshotUploader* uploader = new SnapshotUploader(snapshotData->getURL(), filename); + delete snapshotData; QFile* file = new QFile(filename); Q_ASSERT(file->exists()); @@ -163,7 +166,7 @@ void Snapshot::uploadSnapshot(const QString& filename) { multiPart->append(imagePart); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams(&uploader, "uploadSuccess", &uploader, "uploadFailure"); + JSONCallbackParameters callbackParams(uploader, "uploadSuccess", uploader, "uploadFailure"); accountManager->sendRequest(SNAPSHOT_UPLOAD_URL, AccountManagerAuth::Required, diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 5bc9bb386c..c36efddc14 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -15,9 +15,13 @@ #include "scripting/WindowScriptingInterface.h" #include "SnapshotUploader.h" +SnapshotUploader::SnapshotUploader(QUrl inWorldLocation, QString pathname) : + _inWorldLocation(inWorldLocation), + _pathname(pathname) { +} + void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { const QString STORY_UPLOAD_URL = "/api/v1/user_stories"; - static SnapshotUploader uploader; // parse the reply for the thumbnail_url QByteArray contents = reply.readAll(); @@ -28,11 +32,8 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QString thumbnailUrl = dataObject.value("thumbnail_url").toString(); QString imageUrl = dataObject.value("image_url").toString(); auto addressManager = DependencyManager::get(); - QString placeName = addressManager->getPlaceName(); - if (placeName.isEmpty()) { - placeName = addressManager->getHost(); - } - QString currentPath = addressManager->currentPath(true); + QString placeName = _inWorldLocation.authority(); // We currently only upload shareable places, in which case this is just host. + QString currentPath = _inWorldLocation.path(); // create json post data QJsonObject rootObject; @@ -48,7 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams(&uploader, "createStorySuccess", &uploader, "createStoryFailure"); + JSONCallbackParameters callbackParams(this, "createStorySuccess", this, "createStoryFailure"); accountManager->sendRequest(STORY_UPLOAD_URL, AccountManagerAuth::Required, @@ -56,20 +57,23 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { callbackParams, QJsonDocument(rootObject).toJson()); - } - else { + } else { emit DependencyManager::get()->snapshotShared(contents); + delete this; } } void SnapshotUploader::uploadFailure(QNetworkReply& reply) { - emit DependencyManager::get()->snapshotShared(reply.readAll()); + emit DependencyManager::get()->snapshotShared(reply.readAll()); // maybe someday include _inWorldLocation, _filename? + delete this; } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { emit DependencyManager::get()->snapshotShared(QString()); + delete this; } void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { - emit DependencyManager::get()->snapshotShared(reply.readAll()); + emit DependencyManager::get()->snapshotShared(reply.readAll()); + delete this; } \ No newline at end of file diff --git a/interface/src/ui/SnapshotUploader.h b/interface/src/ui/SnapshotUploader.h index d4a5f86431..ae6d5d55ca 100644 --- a/interface/src/ui/SnapshotUploader.h +++ b/interface/src/ui/SnapshotUploader.h @@ -14,13 +14,19 @@ #include #include +#include class SnapshotUploader : public QObject { Q_OBJECT - public slots: +public: + SnapshotUploader(QUrl inWorldLocation, QString pathname); +public slots: void uploadSuccess(QNetworkReply& reply); void uploadFailure(QNetworkReply& reply); void createStorySuccess(QNetworkReply& reply); void createStoryFailure(QNetworkReply& reply); +private: + QUrl _inWorldLocation; + QString _pathname; }; #endif // hifi_SnapshotUploader_h \ No newline at end of file diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 11660a332d..05632cb1e6 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -302,7 +302,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); - STAT_UPDATE(gpuSparseTextureEnabled, gpu::Texture::getEnableSparseTextures() ? 1 : 0); + STAT_UPDATE(gpuSparseTextureEnabled, qApp->getGPUContext()->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); STAT_UPDATE(decimatedTextureCount, (int)DECIMATED_TEXTURE_COUNT.load()); diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 351c09beee..a379ebd80a 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -118,11 +118,26 @@ void AnimSkeleton::convertAbsoluteRotationsToRelative(std::vector& ro } } +void AnimSkeleton::saveNonMirroredPoses(const AnimPoseVec& poses) const { + _nonMirroredPoses.clear(); + for (int i = 0; i < (int)_nonMirroredIndices.size(); ++i) { + _nonMirroredPoses.push_back(poses[_nonMirroredIndices[i]]); + } +} + +void AnimSkeleton::restoreNonMirroredPoses(AnimPoseVec& poses) const { + for (int i = 0; i < (int)_nonMirroredIndices.size(); ++i) { + int index = _nonMirroredIndices[i]; + poses[index] = _nonMirroredPoses[i]; + } +} void AnimSkeleton::mirrorRelativePoses(AnimPoseVec& poses) const { + saveNonMirroredPoses(poses); convertRelativePosesToAbsolute(poses); mirrorAbsolutePoses(poses); convertAbsolutePosesToRelative(poses); + restoreNonMirroredPoses(poses); } void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { @@ -189,8 +204,14 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } // build mirror map. + _nonMirroredIndices.clear(); _mirrorMap.reserve(_joints.size()); for (int i = 0; i < (int)joints.size(); i++) { + if (_joints[i].name.endsWith("tEye")) { + // HACK: we don't want to mirror some joints so we remember their indices + // so we can restore them after a future mirror operation + _nonMirroredIndices.push_back(i); + } int mirrorJointIndex = -1; if (_joints[i].name.startsWith("Left")) { QString mirrorJointName = QString(_joints[i].name).replace(0, 4, "Right"); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 68cce11326..e1c6ae95c8 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -57,6 +57,9 @@ public: void convertAbsoluteRotationsToRelative(std::vector& rotations) const; + void saveNonMirroredPoses(const AnimPoseVec& poses) const; + void restoreNonMirroredPoses(AnimPoseVec& poses) const; + void mirrorRelativePoses(AnimPoseVec& poses) const; void mirrorAbsolutePoses(AnimPoseVec& poses) const; @@ -75,6 +78,8 @@ protected: AnimPoseVec _absoluteDefaultPoses; AnimPoseVec _relativePreRotationPoses; AnimPoseVec _relativePostRotationPoses; + mutable AnimPoseVec _nonMirroredPoses; + std::vector _nonMirroredIndices; std::vector _mirrorMap; // no copies diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 5208b893ac..062991c187 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -85,18 +85,26 @@ public: } void beforeAboutToQuit() { + Lock lock(_checkDevicesMutex); _quit = true; } void run() override { - while (!_quit) { + while (true) { + { + Lock lock(_checkDevicesMutex); + if (_quit) { + break; + } + _audioClient->checkDevices(); + } QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); - _audioClient->checkDevices(); } } private: AudioClient* _audioClient { nullptr }; + Mutex _checkDevicesMutex; bool _quit { false }; }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c842a78dc5..2bd022d240 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -520,7 +520,7 @@ const FBXGeometry* EntityTreeRenderer::getGeometryForEntity(EntityItemPointer en std::shared_ptr modelEntityItem = std::dynamic_pointer_cast(entityItem); assert(modelEntityItem); // we need this!!! - ModelPointer model = modelEntityItem->getModel(this); + ModelPointer model = modelEntityItem->getModel(getSharedFromThis()); if (model && model->isLoaded()) { result = &model->getFBXGeometry(); } @@ -533,7 +533,7 @@ ModelPointer EntityTreeRenderer::getModelForEntityItem(EntityItemPointer entityI if (entityItem->getType() == EntityTypes::Model) { std::shared_ptr modelEntityItem = std::dynamic_pointer_cast(entityItem); - result = modelEntityItem->getModel(this); + result = modelEntityItem->getModel(getSharedFromThis()); } return result; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 36e52e6f46..7890ae8275 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -45,6 +45,10 @@ public: AbstractScriptingServicesInterface* scriptingServices); virtual ~EntityTreeRenderer(); + QSharedPointer getSharedFromThis() { + return qSharedPointerCast(sharedFromThis()); + } + virtual char getMyNodeType() const override { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const override { return PacketType::EntityQuery; } virtual PacketType getExpectedPacketType() const override { return PacketType::EntityData; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index f2e938ece3..332a88e499 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -55,7 +55,10 @@ void RenderableModelEntityItem::setModelURL(const QString& url) { auto& currentURL = getParsedModelURL(); ModelEntityItem::setModelURL(url); - if (currentURL != getParsedModelURL() || !_model) { + if (currentURL != getParsedModelURL()) { + _needsModelReload = true; + } + if (_needsModelReload || !_model) { EntityTreePointer tree = getTree(); if (tree) { QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); @@ -65,7 +68,7 @@ void RenderableModelEntityItem::setModelURL(const QString& url) { void RenderableModelEntityItem::loader() { _needsModelReload = true; - EntityTreeRenderer* renderer = DependencyManager::get().data(); + auto renderer = DependencyManager::get(); assert(renderer); { PerformanceTimer perfTimer("getModel"); @@ -368,7 +371,7 @@ void RenderableModelEntityItem::render(RenderArgs* args) { if (!_model || _needsModelReload) { // TODO: this getModel() appears to be about 3% of model render time. We should optimize PerformanceTimer perfTimer("getModel"); - EntityTreeRenderer* renderer = static_cast(args->_renderer); + auto renderer = qSharedPointerCast(args->_renderer); getModel(renderer); // Remap textures immediately after loading to avoid flicker @@ -470,7 +473,7 @@ void RenderableModelEntityItem::render(RenderArgs* args) { } } -ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { +ModelPointer RenderableModelEntityItem::getModel(QSharedPointer renderer) { if (!renderer) { return nullptr; } @@ -495,7 +498,7 @@ ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if (QUrl(getModelURL()) != _model->getURL()) { - QMetaObject::invokeMethod(_myRenderer, "updateModel", Qt::QueuedConnection, + QMetaObject::invokeMethod(_myRenderer.data(), "updateModel", Qt::QueuedConnection, Q_ARG(ModelPointer, _model), Q_ARG(const QString&, getModelURL())); _needsInitialSimulation = true; @@ -523,17 +526,24 @@ bool RenderableModelEntityItem::needsToCallUpdate() const { } void RenderableModelEntityItem::update(const quint64& now) { - if (!_dimensionsInitialized && _model && _model->isActive()) { - if (_model->isLoaded()) { - EntityItemProperties properties; - properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it - auto extents = _model->getMeshExtents(); - properties.setDimensions(extents.maximum - extents.minimum); - qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL()); - QMetaObject::invokeMethod(DependencyManager::get().data(), "editEntity", - Qt::QueuedConnection, - Q_ARG(QUuid, getEntityItemID()), - Q_ARG(EntityItemProperties, properties)); + if (!_dimensionsInitialized) { + if (_model) { + if (_model->isActive() && _model->isLoaded()) { + EntityItemProperties properties; + properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it + auto extents = _model->getMeshExtents(); + properties.setDimensions(extents.maximum - extents.minimum); + qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL()); + QMetaObject::invokeMethod(DependencyManager::get().data(), "editEntity", + Qt::QueuedConnection, + Q_ARG(QUuid, getEntityItemID()), + Q_ARG(EntityItemProperties, properties)); + } + } else if (_needsModelReload) { + EntityTreePointer tree = getTree(); + if (tree) { + QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); + } } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index e785e61d22..a52b0b0041 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -51,7 +51,7 @@ public: bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const override; - ModelPointer getModel(EntityTreeRenderer* renderer); + ModelPointer getModel(QSharedPointer renderer); virtual bool needsToCallUpdate() const override; virtual void update(const quint64& now) override; @@ -105,7 +105,7 @@ private: ModelPointer _model = nullptr; bool _needsInitialSimulation = true; bool _needsModelReload = true; - EntityTreeRenderer* _myRenderer = nullptr; + QSharedPointer _myRenderer; QString _lastTextures; QVariantMap _currentTextures; QVariantMap _originalTextures; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index a2ca2a7cfe..d1dd5cce8e 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1035,50 +1035,53 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { return; } - EntityItemPointer currentXPNeighbor = _xPNeighbor.lock(); - EntityItemPointer currentYPNeighbor = _yPNeighbor.lock(); - EntityItemPointer currentZPNeighbor = _zPNeighbor.lock(); + auto currentXPNeighbor = getXPNeighbor(); + auto currentYPNeighbor = getYPNeighbor(); + auto currentZPNeighbor = getZPNeighbor(); - if (currentXPNeighbor) { - auto polyVoxXPNeighbor = std::dynamic_pointer_cast(currentXPNeighbor); - if (polyVoxXPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { - withWriteLock([&] { + if (currentXPNeighbor && currentXPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + withWriteLock([&] { + for (int y = 0; y < _volData->getHeight(); y++) { + for (int z = 0; z < _volData->getDepth(); z++) { + uint8_t neighborValue = currentXPNeighbor->getVoxel(0, y, z); + if ((y == 0 || z == 0) && _volData->getVoxelAt(_volData->getWidth() - 1, y, z) != neighborValue) { + bonkNeighbors(); + } + _volData->setVoxelAt(_volData->getWidth() - 1, y, z, neighborValue); + } + } + }); + } + + + if (currentYPNeighbor && currentYPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + withWriteLock([&] { + for (int x = 0; x < _volData->getWidth(); x++) { + for (int z = 0; z < _volData->getDepth(); z++) { + uint8_t neighborValue = currentYPNeighbor->getVoxel(x, 0, z); + if ((x == 0 || z == 0) && _volData->getVoxelAt(x, _volData->getHeight() - 1, z) != neighborValue) { + bonkNeighbors(); + } + _volData->setVoxelAt(x, _volData->getHeight() - 1, z, neighborValue); + } + } + }); + } + + + if (currentZPNeighbor && currentZPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + withWriteLock([&] { + for (int x = 0; x < _volData->getWidth(); x++) { for (int y = 0; y < _volData->getHeight(); y++) { - for (int z = 0; z < _volData->getDepth(); z++) { - uint8_t neighborValue = polyVoxXPNeighbor->getVoxel(0, y, z); - _volData->setVoxelAt(_volData->getWidth() - 1, y, z, neighborValue); + uint8_t neighborValue = currentZPNeighbor->getVoxel(x, y, 0); + _volData->setVoxelAt(x, y, _volData->getDepth() - 1, neighborValue); + if ((x == 0 || y == 0) && _volData->getVoxelAt(x, y, _volData->getDepth() - 1) != neighborValue) { + bonkNeighbors(); } + _volData->setVoxelAt(x, y, _volData->getDepth() - 1, neighborValue); } - }); - } - } - - if (currentYPNeighbor) { - auto polyVoxYPNeighbor = std::dynamic_pointer_cast(currentYPNeighbor); - if (polyVoxYPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { - withWriteLock([&] { - for (int x = 0; x < _volData->getWidth(); x++) { - for (int z = 0; z < _volData->getDepth(); z++) { - uint8_t neighborValue = polyVoxYPNeighbor->getVoxel(x, 0, z); - _volData->setVoxelAt(x, _volData->getWidth() - 1, z, neighborValue); - } - } - }); - } - } - - if (currentZPNeighbor) { - auto polyVoxZPNeighbor = std::dynamic_pointer_cast(currentZPNeighbor); - if (polyVoxZPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { - withWriteLock([&] { - for (int x = 0; x < _volData->getWidth(); x++) { - for (int y = 0; y < _volData->getHeight(); y++) { - uint8_t neighborValue = polyVoxZPNeighbor->getVoxel(x, y, 0); - _volData->setVoxelAt(x, y, _volData->getDepth() - 1, neighborValue); - } - } - }); - } + } + }); } } @@ -1393,25 +1396,46 @@ void RenderablePolyVoxEntityItem::setZPNeighborID(const EntityItemID& zPNeighbor } } +std::shared_ptr RenderablePolyVoxEntityItem::getXNNeighbor() { + return std::dynamic_pointer_cast(_xNNeighbor.lock()); +} + +std::shared_ptr RenderablePolyVoxEntityItem::getYNNeighbor() { + return std::dynamic_pointer_cast(_yNNeighbor.lock()); +} + +std::shared_ptr RenderablePolyVoxEntityItem::getZNNeighbor() { + return std::dynamic_pointer_cast(_zNNeighbor.lock()); +} + +std::shared_ptr RenderablePolyVoxEntityItem::getXPNeighbor() { + return std::dynamic_pointer_cast(_xPNeighbor.lock()); +} + +std::shared_ptr RenderablePolyVoxEntityItem::getYPNeighbor() { + return std::dynamic_pointer_cast(_yPNeighbor.lock()); +} + +std::shared_ptr RenderablePolyVoxEntityItem::getZPNeighbor() { + return std::dynamic_pointer_cast(_zPNeighbor.lock()); +} + void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); - EntityItemPointer currentXNNeighbor = _xNNeighbor.lock(); - EntityItemPointer currentYNNeighbor = _yNNeighbor.lock(); - EntityItemPointer currentZNNeighbor = _zNNeighbor.lock(); + auto currentXNNeighbor = getXNNeighbor(); + auto currentYNNeighbor = getYNNeighbor(); + auto currentZNNeighbor = getZNNeighbor(); - if (currentXNNeighbor && currentXNNeighbor->getType() == EntityTypes::PolyVox) { - auto polyVoxXNNeighbor = std::dynamic_pointer_cast(currentXNNeighbor); - polyVoxXNNeighbor->setVolDataDirty(); + if (currentXNNeighbor) { + currentXNNeighbor->setVolDataDirty(); } - if (currentYNNeighbor && currentYNNeighbor->getType() == EntityTypes::PolyVox) { - auto polyVoxYNNeighbor = std::dynamic_pointer_cast(currentYNNeighbor); - polyVoxYNNeighbor->setVolDataDirty(); + if (currentYNNeighbor) { + currentYNNeighbor->setVolDataDirty(); } - if (currentZNNeighbor && currentZNNeighbor->getType() == EntityTypes::PolyVox) { - auto polyVoxZNNeighbor = std::dynamic_pointer_cast(currentZNNeighbor); - polyVoxZNNeighbor->setVolDataDirty(); + if (currentZNNeighbor) { + currentZNNeighbor->setVolDataDirty(); } } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 44186073b2..f84637ec95 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -116,6 +116,13 @@ public: virtual void setYPNeighborID(const EntityItemID& yPNeighborID) override; virtual void setZPNeighborID(const EntityItemID& zPNeighborID) override; + std::shared_ptr getXNNeighbor(); + std::shared_ptr getYNNeighbor(); + std::shared_ptr getZNNeighbor(); + std::shared_ptr getXPNeighbor(); + std::shared_ptr getYPNeighbor(); + std::shared_ptr getZPNeighbor(); + virtual void updateRegistrationPoint(const glm::vec3& value) override; void setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index b71fab9439..f426f4a816 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -64,7 +64,7 @@ RenderableWebEntityItem::~RenderableWebEntityItem() { } } -bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { +bool RenderableWebEntityItem::buildWebSurface(QSharedPointer renderer) { if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) { qWarning() << "Too many concurrent web views to create new view"; return false; @@ -95,7 +95,13 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { auto deleter = [](OffscreenQmlSurface* webSurface) { AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { - webSurface->deleteLater(); + if (AbstractViewStateInterface::instance()->isAboutToQuit()) { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + } else { + webSurface->deleteLater(); + } }); }; _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); @@ -133,10 +139,11 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { handlePointerEvent(event); } }; - _mousePressConnection = QObject::connect(renderer, &EntityTreeRenderer::mousePressOnEntity, forwardPointerEvent); - _mouseReleaseConnection = QObject::connect(renderer, &EntityTreeRenderer::mouseReleaseOnEntity, forwardPointerEvent); - _mouseMoveConnection = QObject::connect(renderer, &EntityTreeRenderer::mouseMoveOnEntity, forwardPointerEvent); - _hoverLeaveConnection = QObject::connect(renderer, &EntityTreeRenderer::hoverLeaveEntity, [=](const EntityItemID& entityItemID, const PointerEvent& event) { + _mousePressConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mousePressOnEntity, forwardPointerEvent); + _mouseReleaseConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseReleaseOnEntity, forwardPointerEvent); + _mouseMoveConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseMoveOnEntity, forwardPointerEvent); + _hoverLeaveConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::hoverLeaveEntity, + [=](const EntityItemID& entityItemID, const PointerEvent& event) { if (this->_pressed && this->getID() == entityItemID) { // If the user mouses off the entity while the button is down, simulate a touch end. QTouchEvent::TouchPoint point; @@ -184,7 +191,8 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - if (!buildWebSurface(static_cast(args->_renderer))) { + auto renderer = qSharedPointerCast(args->_renderer); + if (!buildWebSurface(renderer)) { return; } _fadeStartTime = usecTimestampNow(); @@ -326,7 +334,18 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) { void RenderableWebEntityItem::destroyWebSurface() { if (_webSurface) { --_currentWebCount; + + QQuickItem* rootItem = _webSurface->getRootItem(); + if (rootItem) { + QObject* obj = rootItem->findChild("webEngineView"); + if (obj) { + // stop loading + QMetaObject::invokeMethod(obj, "stop"); + } + } + _webSurface->pause(); + _webSurface->disconnect(_connection); QObject::disconnect(_mousePressConnection); _mousePressConnection = QMetaObject::Connection(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 33039e50f1..c2e3ae7f9f 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -52,7 +52,7 @@ public: virtual bool isTransparent() override; private: - bool buildWebSurface(EntityTreeRenderer* renderer); + bool buildWebSurface(QSharedPointer renderer); void destroyWebSurface(); glm::vec2 getWindowSize() const; diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 02b0afdf26..03e24c9046 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -123,7 +123,9 @@ glm::vec3 OBJTokenizer::getVec3() { return v; } glm::vec2 OBJTokenizer::getVec2() { - auto v = glm::vec2(getFloat(), 1.0f - getFloat()); // OBJ has an odd sense of u, v. Also N.B.: getFloat() has side-effect + float uCoord = getFloat(); + float vCoord = 1.0f - getFloat(); + auto v = glm::vec2(uCoord, vCoord); while (isNextTokenFloat()) { // there can be a w, but we don't handle that nextToken(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index c82c2b4a32..06f755c1dd 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -344,7 +344,6 @@ bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) { OffscreenQmlSurface::OffscreenQmlSurface() { } -static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2; OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(&_updateTimer); QObject::disconnect(qApp); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 2e1084e581..3513d7a05b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -56,6 +56,7 @@ BackendPointer GLBackend::createBackend() { } result->initInput(); result->initTransform(); + result->initTextureManagementStage(); INSTANCE = result.get(); void* voidInstance = &(*result); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index f99d34393c..1be279b375 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -176,6 +176,9 @@ public: virtual void releaseQuery(GLuint id) const; virtual void queueLambda(const std::function lambda) const; + bool isTextureManagementSparseEnabled() const override { return (_textureManagement._sparseCapable && Texture::getEnableSparseTextures()); } + bool isTextureManagementIncrementalTransferEnabled() const override { return (_textureManagement._incrementalTransferCapable && Texture::getEnableIncrementalTextureTransfers()); } + protected: void recycle() const override; @@ -364,6 +367,12 @@ protected: void resetStages(); + struct TextureManagementStageState { + bool _sparseCapable { false }; + bool _incrementalTransferCapable { false }; + } _textureManagement; + virtual void initTextureManagementStage() {} + typedef void (GLBackend::*CommandCall)(const Batch&, size_t); static CommandCall _commandCalls[Batch::NUM_COMMANDS]; friend class GLState; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 61a76c2d0b..3ce1c8e5c4 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -111,7 +111,7 @@ float GLTexture::getMemoryPressure() { } #else // Hardcode texture limit for sparse textures at 1 GB for now - availableTextureMemory = GPU_MEMORY_RESERVE_BYTES; + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; #endif } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 059156b4a3..643d54af6a 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -32,6 +32,7 @@ public: static GLuint allocate(const Texture& texture); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; + public: GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); @@ -132,6 +133,9 @@ protected: // Output stage void do_blit(const Batch& batch, size_t paramOffset) override; + + // Texture Management Stage + void initTextureManagementStage() override; }; } } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index a11e486f2b..ac9a84513e 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -148,6 +148,24 @@ uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { return pageCounts.x * pageCounts.y * pageCounts.z; } + + +void GL45Backend::initTextureManagementStage() { + + // enable the Sparse Texture on gl45 + _textureManagement._sparseCapable = true; + _textureManagement._incrementalTransferCapable = true; + + // But now let s refine the behavior based on vendor + std::string vendor { (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos) || (vendor.find("INTEL") != std::string::npos)) { + qCDebug(gpugllogging) << "GPU is sparse capable but force it off, vendor = " << vendor.c_str(); + _textureManagement._sparseCapable = false; + } else { + qCDebug(gpugllogging) << "GPU is sparse capable, vendor = " << vendor.c_str(); + } +} + using TransferState = GL45Backend::GL45Texture::TransferState; TransferState::TransferState(GL45Texture& texture) : texture(texture) { @@ -250,7 +268,8 @@ GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this), _transferState(*this) { - if (_transferrable && Texture::getEnableSparseTextures()) { + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { _sparseInfo.maybeMakeSparse(); if (_sparseInfo.sparse) { Backend::incrementTextureGPUSparseCount(); @@ -322,7 +341,9 @@ void GL45Texture::withPreservedTexture(std::function f) const { } void GL45Texture::generateMips() const { - qCDebug(gpugl45logging) << "Generating mipmaps for " << _source.c_str(); + if (_transferrable) { + qCDebug(gpugl45logging) << "Generating mipmaps for " << _source.c_str(); + } glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } @@ -360,7 +381,8 @@ void GL45Texture::startTransfer() { } bool GL45Texture::continueTransfer() { - if (!Texture::getEnableIncrementalTextureTransfers()) { + auto backend = _backend.lock(); + if (!backend || !backend->isTextureManagementIncrementalTransferEnabled()) { size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; for (uint8_t face = 0; face < maxFace; ++face) { for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 763e91b3e4..e174e9d728 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -85,7 +85,8 @@ public: void getStats(ContextStats& stats) const { stats = _stats; } - + virtual bool isTextureManagementSparseEnabled() const = 0; + virtual bool isTextureManagementIncrementalTransferEnabled() const = 0; // These should only be accessed by Backend implementation to repport the buffer and texture allocations, // they are NOT public calls @@ -125,6 +126,7 @@ protected: friend class Context; ContextStats _stats; StereoState _stereo; + }; class Context { @@ -270,7 +272,6 @@ protected: static std::atomic _textureGPUFramebufferMemoryUsage; static std::atomic _textureGPUTransferCount; - friend class Backend; }; typedef std::shared_ptr ContextPointer; diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 0f1022a0c9..1eacb46d77 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -147,6 +147,7 @@ class Texture : public Resource { static std::atomic _enableSparseTextures; static std::atomic _enableIncrementalTextureTransfers; + public: static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 248811b86a..fe5a42bb85 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -24,8 +24,11 @@ void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { logAction("toggled_away", { { "is_away", isAway } }); } -void UserActivityLoggerScriptingInterface::tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime) { +void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete, + float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion) { logAction("tutorial_progress", { + { "tutorial_run_id", tutorialRunID }, + { "tutorial_version", tutorialVersion }, { "step", stepName }, { "step_number", stepNumber }, { "seconds_to_complete", secondsToComplete }, diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index bf3e20a2d7..52101e3e53 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -23,7 +23,8 @@ public: Q_INVOKABLE void enabledEdit(); Q_INVOKABLE void openedMarketplace(); Q_INVOKABLE void toggledAway(bool isAway); - Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime); + Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, + float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0); private: void logAction(QString action, QJsonObject details = {}); diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 43bd7dd973..98cc62bdee 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -49,7 +49,7 @@ Socket::Socket(QObject* parent, bool shouldChangeSocketOptions) : connect(&_udpSocket, &QAbstractSocket::stateChanged, this, &Socket::handleStateChanged); // in order to help track down the zombie server bug, add a timer to check if we missed a readyRead - const int READY_READ_BACKUP_CHECK_MSECS = 10 * 1000; + const int READY_READ_BACKUP_CHECK_MSECS = 2 * 1000; connect(_readyReadBackupTimer, &QTimer::timeout, this, &Socket::checkForReadyReadBackup); _readyReadBackupTimer->start(READY_READ_BACKUP_CHECK_MSECS); } @@ -306,6 +306,13 @@ void Socket::checkForReadyReadBackup() { if (_udpSocket.hasPendingDatagrams()) { qCDebug(networking) << "Socket::checkForReadyReadBackup() detected blocked readyRead signal. Flushing pending datagrams."; + // so that birarda can possibly figure out how the heck we get into this state in the first place + // output the sequence number and socket address of the last processed packet + qCDebug(networking) << "Socket::checkForReadyReadyBackup() last sequence number" + << (uint32_t) _lastReceivedSequenceNumber << "from" << _lastPacketSockAddr << "-" + << _lastPacketSizeRead << "bytes"; + + // drop all of the pending datagrams on the floor while (_udpSocket.hasPendingDatagrams()) { _udpSocket.readDatagram(nullptr, 0); @@ -334,6 +341,10 @@ void Socket::readPendingDatagrams() { auto sizeRead = _udpSocket.readDatagram(buffer.get(), packetSizeWithHeader, senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); + // save information for this packet, in case it is the one that sticks readyRead + _lastPacketSizeRead = sizeRead; + _lastPacketSockAddr = senderSockAddr; + if (sizeRead <= 0) { // we either didn't pull anything for this packet or there was an error reading (this seems to trigger // on windows even if there's not a packet available) @@ -373,6 +384,9 @@ void Socket::readPendingDatagrams() { auto packet = Packet::fromReceivedPacket(std::move(buffer), packetSizeWithHeader, senderSockAddr); packet->setReceiveTime(receiveTime); + // save the sequence number in case this is the packet that sticks readyRead + _lastReceivedSequenceNumber = packet->getSequenceNumber(); + // call our verification operator to see if this packet is verified if (!_packetFilterOperator || _packetFilterOperator(*packet)) { if (packet->isReliable()) { @@ -494,12 +508,16 @@ std::vector Socket::getConnectionSockAddrs() { } void Socket::handleSocketError(QAbstractSocket::SocketError socketError) { - qCWarning(networking) << "udt::Socket error -" << socketError; + static const QString SOCKET_REGEX = "udt::Socket error - "; + static QString repeatedMessage + = LogHandler::getInstance().addRepeatedMessageRegex(SOCKET_REGEX); + + qCDebug(networking) << "udt::Socket error - " << socketError; } void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { if (socketState != QAbstractSocket::BoundState) { - qCWarning(networking) << "udt::Socket state changed - state is now" << socketState; + qCDebug(networking) << "udt::Socket state changed - state is now" << socketState; } } diff --git a/libraries/networking/src/udt/Socket.h b/libraries/networking/src/udt/Socket.h index a811d78958..1919e00b41 100644 --- a/libraries/networking/src/udt/Socket.h +++ b/libraries/networking/src/udt/Socket.h @@ -144,6 +144,10 @@ private: std::unique_ptr _ccFactory { new CongestionControlFactory() }; bool _shouldChangeSocketOptions { true }; + + int _lastPacketSizeRead { 0 }; + SequenceNumber _lastReceivedSequenceNumber; + HifiSockAddr _lastPacketSockAddr; friend UDTTest; }; diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index 5380aaa6ce..5fd7e4dba3 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -451,6 +451,9 @@ bool OctreePacketData::appendValue(const QVector& value) { bit = 0; } } + if (bit != 0) { + destinationBuffer++; + } int boolsSize = destinationBuffer - start; success = append(start, boolsSize); if (success) { @@ -683,6 +686,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); + if (length * sizeof(glm::vec3) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { + result.resize(0); + return sizeof(uint16_t); + } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(glm::vec3)); return sizeof(uint16_t) + length * sizeof(glm::vec3); @@ -692,6 +699,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); + if (length * sizeof(glm::quat) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { + result.resize(0); + return sizeof(uint16_t); + } result.resize(length); const unsigned char *start = dataBytes; @@ -706,6 +717,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); + if (length * sizeof(float) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { + result.resize(0); + return sizeof(uint16_t); + } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(float)); return sizeof(uint16_t) + length * sizeof(float); @@ -715,6 +730,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); + if (length / 8 > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { + result.resize(0); + return sizeof(uint16_t); + } result.resize(length); int bit = 0; diff --git a/libraries/octree/src/OctreeRenderer.cpp b/libraries/octree/src/OctreeRenderer.cpp index a81f946680..06c0ff1f12 100644 --- a/libraries/octree/src/OctreeRenderer.cpp +++ b/libraries/octree/src/OctreeRenderer.cpp @@ -216,7 +216,7 @@ bool OctreeRenderer::renderOperation(OctreeElementPointer element, void* extraDa void OctreeRenderer::render(RenderArgs* renderArgs) { if (_tree) { - renderArgs->_renderer = this; + renderArgs->_renderer = sharedFromThis(); _tree->withReadLock([&] { _tree->recurseTreeWithOperation(renderOperation, renderArgs); }); diff --git a/libraries/octree/src/OctreeRenderer.h b/libraries/octree/src/OctreeRenderer.h index cd4ddc4801..c18464b7ea 100644 --- a/libraries/octree/src/OctreeRenderer.h +++ b/libraries/octree/src/OctreeRenderer.h @@ -29,7 +29,7 @@ class OctreeRenderer; // Generic client side Octree renderer class. -class OctreeRenderer : public QObject { +class OctreeRenderer : public QObject, public QEnableSharedFromThis { Q_OBJECT public: OctreeRenderer(); diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 9b9ee0e299..100dab0fd1 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -256,8 +256,18 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) } break; case SHAPE_TYPE_SPHERE: { - float radius = info.getHalfExtents().x; - shape = new btSphereShape(radius); + glm::vec3 halfExtents = info.getHalfExtents(); + float radius = halfExtents.x; + if (radius == halfExtents.y && radius == halfExtents.z) { + shape = new btSphereShape(radius); + } else { + ShapeInfo::PointList points; + points.reserve(NUM_UNIT_SPHERE_DIRECTIONS); + for (uint32_t i = 0; i < NUM_UNIT_SPHERE_DIRECTIONS; ++i) { + points.push_back(bulletToGLM(_unitSphereDirections[i]) * halfExtents); + } + shape = createConvexHull(points); + } } break; case SHAPE_TYPE_CAPSULE_Y: { diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index c025b03075..3a9107390a 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -139,6 +139,7 @@ public: virtual bool isStereo() const { return isHmd(); } virtual bool isThrottled() const { return false; } virtual float getTargetFrameRate() const { return 0.0f; } + virtual bool hasAsyncReprojection() const { return false; } /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, diff --git a/libraries/plugins/src/plugins/InputPlugin.h b/libraries/plugins/src/plugins/InputPlugin.h index f68be3edf6..0db0b24420 100644 --- a/libraries/plugins/src/plugins/InputPlugin.h +++ b/libraries/plugins/src/plugins/InputPlugin.h @@ -21,6 +21,9 @@ public: virtual void pluginFocusOutEvent() = 0; virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) = 0; + // Some input plugins are comprised of multiple subdevices (SDL2, for instance). + // If an input plugin is only a single device, it will only return it's primary name. + virtual QStringList getSubdeviceNames() { return { getName() }; }; virtual bool isHandController() const = 0; }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 21b80e2370..21e652dc40 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -86,6 +86,7 @@ const LoaderList& getLoadedPlugins() { QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins/"; #endif QDir pluginDir(pluginPath); + pluginDir.setSorting(QDir::Name); pluginDir.setFilter(QDir::Files); if (pluginDir.exists()) { qInfo() << "Loading runtime plugins from " << pluginPath; diff --git a/libraries/plugins/src/plugins/PluginUtils.cpp b/libraries/plugins/src/plugins/PluginUtils.cpp index bc53e8166a..48530bfe8c 100644 --- a/libraries/plugins/src/plugins/PluginUtils.cpp +++ b/libraries/plugins/src/plugins/PluginUtils.cpp @@ -32,3 +32,26 @@ bool PluginUtils::isHandControllerAvailable() { } return false; }; + +bool isSubdeviceContainingNameAvailable(QString name) { + for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->isActive()) { + auto subdeviceNames = inputPlugin->getSubdeviceNames(); + for (auto& subdeviceName : subdeviceNames) { + if (subdeviceName.contains(name)) { + return true; + } + } + } + } + return false; +}; + +bool PluginUtils::isViveControllerAvailable() { + return isSubdeviceContainingNameAvailable("OpenVR"); +}; + +bool PluginUtils::isXboxControllerAvailable() { + return isSubdeviceContainingNameAvailable("X360 Controller"); +}; + diff --git a/libraries/plugins/src/plugins/PluginUtils.h b/libraries/plugins/src/plugins/PluginUtils.h index 727677ccd3..f1449bc3af 100644 --- a/libraries/plugins/src/plugins/PluginUtils.h +++ b/libraries/plugins/src/plugins/PluginUtils.h @@ -16,4 +16,6 @@ class PluginUtils { public: static bool isHMDAvailable(const QString& pluginName = ""); static bool isHandControllerAvailable(); + static bool isViveControllerAvailable(); + static bool isXboxControllerAvailable(); }; diff --git a/libraries/render-utils/src/AbstractViewStateInterface.h b/libraries/render-utils/src/AbstractViewStateInterface.h index 362c0cc1bf..4570ead9e1 100644 --- a/libraries/render-utils/src/AbstractViewStateInterface.h +++ b/libraries/render-utils/src/AbstractViewStateInterface.h @@ -40,7 +40,9 @@ public: virtual glm::vec3 getAvatarPosition() const = 0; + virtual bool isAboutToQuit() const = 0; virtual void postLambdaEvent(std::function f) = 0; + virtual qreal getDevicePixelRatio() = 0; virtual render::ScenePointer getMain3DScene() = 0; diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index 605d7e95bd..d191c89f09 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -44,33 +44,42 @@ void BatchLoader::start() { return; } + for (const auto& rawURL : _urls) { QUrl url = expandScriptUrl(normalizeScriptURL(rawURL)); qCDebug(scriptengine) << "Loading script at " << url; - QPointer self = this; - DependencyManager::get()->getScriptContents(url.toString(), [this, self](const QString& url, const QString& contents, bool isURL, bool success) { - if (!self) { - return; - } + auto scriptCache = DependencyManager::get(); - // Because the ScriptCache may call this callback from differents threads, - // we need to make sure this is thread-safe. - std::lock_guard lock(_dataLock); + // Use a proxy callback to handle the call and emit the signal in a thread-safe way. + // If BatchLoader is deleted before the callback is called, the subsequent "emit" call will not do + // anything. + ScriptCacheSignalProxy* proxy = new ScriptCacheSignalProxy(); + connect(scriptCache.data(), &ScriptCache::destroyed, proxy, &ScriptCacheSignalProxy::deleteLater); + connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success) { if (isURL && success) { _data.insert(url, contents); qCDebug(scriptengine) << "Loaded: " << url; } else { _data.insert(url, QString()); - qCDebug(scriptengine) << "Could not load" << url; + qCDebug(scriptengine) << "Could not load: " << url; } if (!_finished && _urls.size() == _data.size()) { _finished = true; emit finished(_data); } + }); + + scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) { + proxy->receivedContent(url, contents, isURL, success); + proxy->deleteLater(); }, false); } } + +void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success) { + emit contentAvailable(url, contents, isURL, success); +} diff --git a/libraries/script-engine/src/BatchLoader.h b/libraries/script-engine/src/BatchLoader.h index 40b43d23b6..046e17ff63 100644 --- a/libraries/script-engine/src/BatchLoader.h +++ b/libraries/script-engine/src/BatchLoader.h @@ -21,10 +21,19 @@ #include +class ScriptCacheSignalProxy : public QObject { + Q_OBJECT +public: + void receivedContent(const QString& url, const QString& contents, bool isURL, bool success); + +signals: + void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success); +}; + class BatchLoader : public QObject { Q_OBJECT public: - BatchLoader(const QList& urls) ; + BatchLoader(const QList& urls); void start(); bool isFinished() const { return _finished; }; @@ -39,7 +48,6 @@ private: bool _finished; QSet _urls; QMap _data; - std::mutex _dataLock; }; #endif // hifi_BatchLoader_h diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index c0b87c9a5c..6c2e7a349e 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -37,29 +37,30 @@ glm::quat Quat::lookAt(const glm::vec3& eye, const glm::vec3& center, const glm: glm::quat Quat::lookAtSimple(const glm::vec3& eye, const glm::vec3& center) { auto dir = glm::normalize(center - eye); // if the direction is nearly aligned with the Y axis, then use the X axis for 'up' - if (dir.x < 0.001f && dir.z < 0.001f) { + const float MAX_ABS_Y_COMPONENT = 0.9999991f; + if (fabsf(dir.y) > MAX_ABS_Y_COMPONENT) { return lookAt(eye, center, Vectors::UNIT_X); } return lookAt(eye, center, Vectors::UNIT_Y); } -glm::quat Quat::multiply(const glm::quat& q1, const glm::quat& q2) { - return q1 * q2; +glm::quat Quat::multiply(const glm::quat& q1, const glm::quat& q2) { + return q1 * q2; } -glm::quat Quat::fromVec3Degrees(const glm::vec3& eulerAngles) { - return glm::quat(glm::radians(eulerAngles)); +glm::quat Quat::fromVec3Degrees(const glm::vec3& eulerAngles) { + return glm::quat(glm::radians(eulerAngles)); } -glm::quat Quat::fromVec3Radians(const glm::vec3& eulerAngles) { - return glm::quat(eulerAngles); +glm::quat Quat::fromVec3Radians(const glm::vec3& eulerAngles) { + return glm::quat(eulerAngles); } -glm::quat Quat::fromPitchYawRollDegrees(float pitch, float yaw, float roll) { +glm::quat Quat::fromPitchYawRollDegrees(float pitch, float yaw, float roll) { return glm::quat(glm::radians(glm::vec3(pitch, yaw, roll))); } -glm::quat Quat::fromPitchYawRollRadians(float pitch, float yaw, float roll) { +glm::quat Quat::fromPitchYawRollRadians(float pitch, float yaw, float roll) { return glm::quat(glm::vec3(pitch, yaw, roll)); } diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 96e3d7e914..025447a5b8 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -222,6 +222,9 @@ void ScriptCache::scriptContentAvailable() { }); } else { // Dubious, but retained here because it matches the behavior before fixing the threading + + allCallbacks = scriptRequest.scriptUsers; + scriptContent = _scriptCache[url]; finished = true; qCWarning(scriptengine) << "Error loading script from URL " << url; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 9fc3de1c9e..01088660ff 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -146,6 +146,8 @@ public: Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } + Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } + bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget diff --git a/libraries/shared/src/GPUIdent.cpp b/libraries/shared/src/GPUIdent.cpp index 02f92d87e7..fb6f291e19 100644 --- a/libraries/shared/src/GPUIdent.cpp +++ b/libraries/shared/src/GPUIdent.cpp @@ -10,9 +10,15 @@ #include + #ifdef Q_OS_WIN -#include -#include +#include + +//#include +//#include + +#include +#pragma comment(lib, "dxgi.lib") #elif defined(Q_OS_MAC) #include @@ -53,9 +59,101 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) CGLDestroyRendererInfo(rendererInfo); #elif defined(Q_OS_WIN) + + struct ConvertLargeIntegerToQString { + QString convert(const LARGE_INTEGER& version) { + QString value; + value.append(QString::number(uint32_t(((version.HighPart & 0xFFFF0000) >> 16) & 0x0000FFFF))); + value.append("."); + value.append(QString::number(uint32_t((version.HighPart) & 0x0000FFFF))); + value.append("."); + value.append(QString::number(uint32_t(((version.LowPart & 0xFFFF0000) >> 16) & 0x0000FFFF))); + value.append("."); + value.append(QString::number(uint32_t((version.LowPart) & 0x0000FFFF))); + return value; + } + } convertDriverVersionToString; + + // Create the DXGI factory + // Let s get into DXGI land: + HRESULT hr = S_OK; + + IDXGIFactory1* pFactory = nullptr; + hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)(&pFactory) ); + if (hr != S_OK || pFactory == nullptr) { + qCDebug(shared) << "Unable to create DXGI"; + return this; + } + + std::vector validAdapterList; + using AdapterEntry = std::pair, std::vector>; + std::vector adapterToOutputs; + // Enumerate adapters and outputs + { + UINT adapterNum = 0; + IDXGIAdapter1* pAdapter = nullptr; + while (pFactory->EnumAdapters1(adapterNum, &pAdapter) != DXGI_ERROR_NOT_FOUND) { + + // Found an adapter, get descriptor + DXGI_ADAPTER_DESC1 adapterDesc; + pAdapter->GetDesc1(&adapterDesc); + + LARGE_INTEGER version; + hr = pAdapter->CheckInterfaceSupport(__uuidof(IDXGIDevice), &version); + + std::wstring wDescription (adapterDesc.Description); + std::string description(wDescription.begin(), wDescription.end()); + qCDebug(shared) << "Found adapter: " << description.c_str() + << " Driver version: " << convertDriverVersionToString.convert(version); + + AdapterEntry adapterEntry; + adapterEntry.first.first = adapterDesc; + adapterEntry.first.second = version; + + + + UINT outputNum = 0; + IDXGIOutput * pOutput; + bool hasOutputConnectedToDesktop = false; + while (pAdapter->EnumOutputs(outputNum, &pOutput) != DXGI_ERROR_NOT_FOUND) { + + // FOund an output attached to the adapter, get descriptor + DXGI_OUTPUT_DESC outputDesc; + pOutput->GetDesc(&outputDesc); + + adapterEntry.second.push_back(outputDesc); + + std::wstring wDeviceName(outputDesc.DeviceName); + std::string deviceName(wDeviceName.begin(), wDeviceName.end()); + qCDebug(shared) << " Found output: " << deviceName.c_str() << " desktop: " << (outputDesc.AttachedToDesktop ? "true" : "false") + << " Rect [ l=" << outputDesc.DesktopCoordinates.left << " r=" << outputDesc.DesktopCoordinates.right + << " b=" << outputDesc.DesktopCoordinates.bottom << " t=" << outputDesc.DesktopCoordinates.top << " ]"; + + hasOutputConnectedToDesktop |= (bool) outputDesc.AttachedToDesktop; + + pOutput->Release(); + outputNum++; + } + + adapterToOutputs.push_back(adapterEntry); + + // add this adapter to the valid list if has output + if (hasOutputConnectedToDesktop && !adapterEntry.second.empty()) { + validAdapterList.push_back(adapterNum); + } + + pAdapter->Release(); + adapterNum++; + } + } + pFactory->Release(); + + + // THis was the previous technique used to detect the platform we are running on on windows. + /* // COM must be initialized already using CoInitialize. E.g., by the audio subsystem. CComPtr spLoc = NULL; - HRESULT hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_SERVER, IID_IWbemLocator, (LPVOID *)&spLoc); + hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_SERVER, IID_IWbemLocator, (LPVOID *)&spLoc); if (hr != S_OK || spLoc == NULL) { qCDebug(shared) << "Unable to connect to WMI"; return this; @@ -139,7 +237,7 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) var.ChangeType(CIM_UINT64); // We're going to receive some integral type, but it might not be uint. // We might be hosed here. The parameter is documented to be UINT32, but that's only 4 GB! const ULONGLONG BYTES_PER_MEGABYTE = 1024 * 1024; - _dedicatedMemoryMB = (uint) (var.ullVal / BYTES_PER_MEGABYTE); + _dedicatedMemoryMB = (uint64_t) (var.ullVal / BYTES_PER_MEGABYTE); } else { qCDebug(shared) << "Unable to get video AdapterRAM"; @@ -149,6 +247,22 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) } hr = spEnumInst->Next(WBEM_INFINITE, 1, &spInstance.p, &uNumOfInstances); } + */ + + if (!validAdapterList.empty()) { + auto& adapterEntry = adapterToOutputs[validAdapterList.front()]; + + std::wstring wDescription(adapterEntry.first.first.Description); + std::string description(wDescription.begin(), wDescription.end()); + _name = QString(description.c_str()); + + _driver = convertDriverVersionToString.convert(adapterEntry.first.second); + + const ULONGLONG BYTES_PER_MEGABYTE = 1024 * 1024; + _dedicatedMemoryMB = (uint64_t)(adapterEntry.first.first.DedicatedVideoMemory / BYTES_PER_MEGABYTE); + _isValid = true; + } + #endif return this; } diff --git a/libraries/shared/src/GPUIdent.h b/libraries/shared/src/GPUIdent.h index 4e844b0e54..8615e61b08 100644 --- a/libraries/shared/src/GPUIdent.h +++ b/libraries/shared/src/GPUIdent.h @@ -14,17 +14,19 @@ #ifndef hifi_GPUIdent_h #define hifi_GPUIdent_h +#include + class GPUIdent { public: - unsigned int getMemory() { return _dedicatedMemoryMB; } + uint64_t getMemory() { return _dedicatedMemoryMB; } QString getName() { return _name; } QString getDriver() { return _driver; } bool isValid() { return _isValid; } // E.g., GPUIdent::getInstance()->getMemory(); static GPUIdent* getInstance(const QString& vendor = "", const QString& renderer = "") { return _instance.ensureQuery(vendor, renderer); } private: - uint _dedicatedMemoryMB { 0 }; + uint64_t _dedicatedMemoryMB { 0 }; QString _name { "" }; QString _driver { "" }; bool _isQueried { false }; diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 171b58de17..984529c4ba 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -263,6 +263,14 @@ void quatFromScriptValue(const QScriptValue& object, glm::quat &quat) { quat.y = object.property("y").toVariant().toFloat(); quat.z = object.property("z").toVariant().toFloat(); quat.w = object.property("w").toVariant().toFloat(); + + // enforce normalized quaternion + float length = glm::length(quat); + if (length > FLT_EPSILON) { + quat /= length; + } else { + quat = glm::quat(); + } } glm::quat quatFromVariant(const QVariant &object, bool& isValid) { @@ -273,6 +281,14 @@ glm::quat quatFromVariant(const QVariant &object, bool& isValid) { q.y = qvec3.y(); q.z = qvec3.z(); q.w = qvec3.scalar(); + + // enforce normalized quaternion + float length = glm::length(q); + if (length > FLT_EPSILON) { + q /= length; + } else { + q = glm::quat(); + } isValid = true; } else { auto map = object.toMap(); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index e8f0002fed..851e065f20 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -79,7 +79,7 @@ public: }; RenderArgs(std::shared_ptr context = nullptr, - OctreeRenderer* renderer = nullptr, + QSharedPointer renderer = QSharedPointer(nullptr), float sizeScale = 1.0f, int boundaryLevelAdjust = 0, RenderMode renderMode = DEFAULT_RENDER_MODE, @@ -110,7 +110,7 @@ public: std::shared_ptr _context = nullptr; std::shared_ptr _blitFramebuffer = nullptr; std::shared_ptr _pipeline = nullptr; - OctreeRenderer* _renderer = nullptr; + QSharedPointer _renderer; std::stack _viewFrustums; glm::ivec4 _viewport{ 0.0f, 0.0f, 1.0f, 1.0f }; glm::vec3 _boomOffset{ 0.0f, 0.0f, 1.0f }; diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 424c2bfa22..b8ea3a4272 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -33,13 +33,8 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString _halfExtents = glm::vec3(0.0f); break; case SHAPE_TYPE_BOX: + case SHAPE_TYPE_SPHERE: break; - case SHAPE_TYPE_SPHERE: { - // sphere radius is max of halfExtents - float radius = glm::max(glm::max(halfExtents.x, halfExtents.y), halfExtents.z); - _halfExtents = glm::vec3(radius); - break; - } case SHAPE_TYPE_COMPOUND: case SHAPE_TYPE_STATIC_MESH: _url = QUrl(url); @@ -119,8 +114,7 @@ float ShapeInfo::computeVolume() const { break; } case SHAPE_TYPE_SPHERE: { - float radius = _halfExtents.x; - volume = 4.0f * PI * radius * radius * radius / 3.0f; + volume = 4.0f * PI * _halfExtents.x * _halfExtents.y * _halfExtents.z / 3.0f; break; } case SHAPE_TYPE_CYLINDER_Y: { diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 235d258d21..9936027302 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -245,6 +245,32 @@ void SteamClient::shutdown() { steamCallbackManager.getTicketRequests().stopAll(); } +int SteamClient::getSteamVRBuildID() { + if (initialized) { + static const int MAX_PATH_SIZE = 512; + static const int STEAMVR_APPID = 250820; + char rawPath[MAX_PATH_SIZE]; + SteamApps()->GetAppInstallDir(STEAMVR_APPID, rawPath, MAX_PATH_SIZE); + + QString path(rawPath); + path += "\\bin\\version.txt"; + qDebug() << "SteamVR version file path:" << path; + + QFile file(path); + if (file.open(QIODevice::ReadOnly)) { + QString buildIDString = file.readLine(); + + bool ok = false; + int buildID = buildIDString.toInt(&ok); + if (ok) { + return buildID; + } + } + } + return 0; +} + + void SteamClient::runCallbacks() { if (!initialized) { return; diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 5bf0d4db56..a191adee97 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -37,6 +37,7 @@ public: static void openInviteOverlay(); static void joinLobby(QString lobbyId); + static int getSteamVRBuildID(); }; class SteamScriptingInterface : public QObject { diff --git a/libraries/ui/src/InfoView.cpp b/libraries/ui/src/InfoView.cpp index 6b6d6645f5..d2c72bf5f2 100644 --- a/libraries/ui/src/InfoView.cpp +++ b/libraries/ui/src/InfoView.cpp @@ -37,7 +37,7 @@ QString fetchVersion(const QUrl& url) { return r.trimmed(); } -void InfoView::show(const QString& path, bool firstOrChangedOnly) { +void InfoView::show(const QString& path, bool firstOrChangedOnly, QString urlQuery) { static bool registered{ false }; if (!registered) { registerType(); @@ -49,6 +49,8 @@ void InfoView::show(const QString& path, bool firstOrChangedOnly) { } else { url = QUrl::fromLocalFile(path); } + url.setQuery(urlQuery); + if (firstOrChangedOnly) { const QString lastVersion = infoVersion.get(); const QString version = fetchVersion(url); diff --git a/libraries/ui/src/InfoView.h b/libraries/ui/src/InfoView.h index 275effbfa5..ea6150a4d8 100644 --- a/libraries/ui/src/InfoView.h +++ b/libraries/ui/src/InfoView.h @@ -22,7 +22,7 @@ class InfoView : public QQuickItem { static const QString NAME; public: static void registerType(); - static void show(const QString& path, bool firstOrChangedOnly = false); + static void show(const QString& path, bool firstOrChangedOnly = false, QString urlQuery = ""); InfoView(QQuickItem* parent = nullptr); QUrl url(); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index ca7d3f7c17..d9b15eebe0 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -371,6 +371,13 @@ void OffscreenUi::setPinned(bool pinned) { } } +void OffscreenUi::setConstrainToolbarToCenterX(bool constrained) { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "setConstrainToolbarToCenterX", Q_ARG(QVariant, constrained)); + if (!invokeResult) { + qWarning() << "Failed to set toolbar constraint"; + } +} + void OffscreenUi::addMenuInitializer(std::function f) { if (!_vrMenu) { _queuedMenuInitializers.push_back(f); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 2e6e853336..3ab4fa0758 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -52,6 +52,7 @@ public: void setPinned(bool pinned = true); void togglePinned(); + void setConstrainToolbarToCenterX(bool constrained); bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); diff --git a/plugins/hifiSdl2/src/Joystick.h b/plugins/hifiSdl2/src/Joystick.h index 25381d545a..a10e02d325 100644 --- a/plugins/hifiSdl2/src/Joystick.h +++ b/plugins/hifiSdl2/src/Joystick.h @@ -31,6 +31,8 @@ public: const QString& getName() const { return _name; } + SDL_GameController* getGameController() { return _sdlGameController; } + // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index b9a19658e2..b6fa567aee 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -65,8 +65,10 @@ void SDL2Manager::init() { _openJoysticks[id] = joystick; auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(joystick); + auto name = SDL_GameControllerName(controller); + _subdeviceNames << name; emit joystickAdded(joystick.get()); - emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); + emit subdeviceConnected(getName(), name); } } } @@ -78,6 +80,10 @@ void SDL2Manager::init() { } } +QStringList SDL2Manager::getSubdeviceNames() { + return _subdeviceNames; +} + void SDL2Manager::deinit() { _openJoysticks.clear(); @@ -157,15 +163,19 @@ void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrati Joystick::Pointer joystick = std::make_shared(id, controller); _openJoysticks[id] = joystick; userInputMapper->registerDevice(joystick); + QString name = SDL_GameControllerName(controller); emit joystickAdded(joystick.get()); - emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); + emit subdeviceConnected(getName(), name); + _subdeviceNames << name; } } else if (event.type == SDL_CONTROLLERDEVICEREMOVED) { if (_openJoysticks.contains(event.cdevice.which)) { Joystick::Pointer joystick = _openJoysticks[event.cdevice.which]; _openJoysticks.remove(event.cdevice.which); userInputMapper->removeDevice(joystick->getDeviceID()); + QString name = SDL_GameControllerName(joystick->getGameController()); emit joystickRemoved(joystick.get()); + _subdeviceNames.removeOne(name); } } } diff --git a/plugins/hifiSdl2/src/SDL2Manager.h b/plugins/hifiSdl2/src/SDL2Manager.h index 44b75abd2f..fc1654bce1 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.h +++ b/plugins/hifiSdl2/src/SDL2Manager.h @@ -26,6 +26,7 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + QStringList getSubdeviceNames() override; bool isHandController() const override { return false; } void init() override; @@ -79,6 +80,7 @@ private: QMap _openJoysticks; bool _isInitialized { false } ; static const QString NAME; + QStringList _subdeviceNames; }; #endif // hifi__SDL2Manager_h diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 023f933acf..e5dc75095d 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -19,6 +19,9 @@ public: ~OculusBaseDisplayPlugin(); bool isSupported() const override; + bool hasAsyncReprojection() const override { return true; } + + // Stereo specific methods void resetSensors() override final; bool beginFrameRender(uint32_t frameIndex) override; diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 5d493f4c9d..f0edc5a465 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -117,6 +117,17 @@ void OculusControllerManager::stopHapticPulse(bool leftHand) { } } +QStringList OculusControllerManager::getSubdeviceNames() { + QStringList devices; + if (_touch) { + devices << _touch->getName(); + } + if (_remote) { + devices << _remote->getName(); + } + return devices; +} + using namespace controller; static const std::vector> BUTTON_MAP { { diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 234acd7db2..1ca9e0f47e 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -27,6 +27,7 @@ public: const QString& getName() const override { return NAME; } bool isHandController() const override { return _touch != nullptr; } + QStringList getSubdeviceNames() override; bool activate() override; void deactivate() override; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index b9a491a8a2..1a4067a847 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -35,6 +35,7 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here +const QString OpenVrThreadedSubmit = "OpenVR Threaded Submit"; // this probably shouldn't be hardcoded here PoseData _nextRenderPoseData; PoseData _nextSimPoseData; @@ -42,15 +43,12 @@ PoseData _nextSimPoseData; #define MIN_CORES_FOR_NORMAL_RENDER 5 bool forceInterleavedReprojection = (QThread::idealThreadCount() < MIN_CORES_FOR_NORMAL_RENDER); - static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; // Flip y-axis since GL UV coords are backwards. static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_LEFT{ 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_RIGHT{ 0.5f, 0, 1, 1 }; -#if OPENVR_THREADED_SUBMIT - #define REPROJECTION_BINDING 1 static const char* HMD_REPROJECTION_VERT = R"SHADER( @@ -351,12 +349,17 @@ public: OpenVrDisplayPlugin& _plugin; }; -#endif - bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } +float OpenVrDisplayPlugin::getTargetFrameRate() const { + if (forceInterleavedReprojection && !_asyncReprojectionActive) { + return TARGET_RATE_OpenVr / 2.0f; + } + return TARGET_RATE_OpenVr; +} + void OpenVrDisplayPlugin::init() { Plugin::init(); @@ -394,6 +397,16 @@ bool OpenVrDisplayPlugin::internalActivate() { return false; } + vr::Compositor_FrameTiming timing; + memset(&timing, 0, sizeof(timing)); + timing.m_nSize = sizeof(vr::Compositor_FrameTiming); + vr::VRCompositor()->GetFrameTiming(&timing); + _asyncReprojectionActive = timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync; + + _threadedSubmit = !_asyncReprojectionActive; + qDebug() << "OpenVR Async Reprojection active: " << _asyncReprojectionActive; + qDebug() << "OpenVR Threaded submit enabled: " << _threadedSubmit; + _openVrDisplayActive = true; _container->setIsOptionChecked(StandingHMDSensorMode, true); @@ -434,16 +447,16 @@ bool OpenVrDisplayPlugin::internalActivate() { #endif } -#if OPENVR_THREADED_SUBMIT - _submitThread = std::make_shared(*this); - if (!_submitCanvas) { - withMainThreadContext([&] { - _submitCanvas = std::make_shared(); - _submitCanvas->create(); - _submitCanvas->doneCurrent(); - }); + if (_threadedSubmit) { + _submitThread = std::make_shared(*this); + if (!_submitCanvas) { + withMainThreadContext([&] { + _submitCanvas = std::make_shared(); + _submitCanvas->create(); + _submitCanvas->doneCurrent(); + }); + } } -#endif return Parent::internalActivate(); } @@ -473,27 +486,27 @@ void OpenVrDisplayPlugin::customizeContext() { Parent::customizeContext(); -#if OPENVR_THREADED_SUBMIT - _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); - for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { - if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + if (_threadedSubmit) { + _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); + for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { + if (0 != i) { + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + } + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); + _submitThread->_canvas = _submitCanvas; + _submitThread->start(QThread::HighPriority); } - _submitThread->_canvas = _submitCanvas; - _submitThread->start(QThread::HighPriority); -#endif } void OpenVrDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); -#if OPENVR_THREADED_SUBMIT - _submitThread->_quit = true; - _submitThread->wait(); - _submitThread.reset(); -#endif + if (_threadedSubmit) { + _submitThread->_quit = true; + _submitThread->wait(); + _submitThread.reset(); + } } void OpenVrDisplayPlugin::resetSensors() { @@ -582,75 +595,76 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } void OpenVrDisplayPlugin::compositeLayers() { -#if OPENVR_THREADED_SUBMIT - ++_renderingIndex; - _renderingIndex %= COMPOSITING_BUFFER_SIZE; + if (_threadedSubmit) { + ++_renderingIndex; + _renderingIndex %= COMPOSITING_BUFFER_SIZE; - auto& newComposite = _compositeInfos[_renderingIndex]; - newComposite.pose = _currentPresentFrameInfo.presentPose; - _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); -#endif + auto& newComposite = _compositeInfos[_renderingIndex]; + newComposite.pose = _currentPresentFrameInfo.presentPose; + _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); + } Parent::compositeLayers(); -#if OPENVR_THREADED_SUBMIT - newComposite.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - // https://www.opengl.org/registry/specs/ARB/sync.txt: - // > The simple flushing behavior defined by - // > SYNC_FLUSH_COMMANDS_BIT will not help when waiting for a fence - // > command issued in another context's command stream to complete. - // > Applications which block on a fence sync object must take - // > additional steps to assure that the context from which the - // > corresponding fence command was issued has flushed that command - // > to the graphics pipeline. - glFlush(); + if (_threadedSubmit) { + auto& newComposite = _compositeInfos[_renderingIndex]; + newComposite.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + // https://www.opengl.org/registry/specs/ARB/sync.txt: + // > The simple flushing behavior defined by + // > SYNC_FLUSH_COMMANDS_BIT will not help when waiting for a fence + // > command issued in another context's command stream to complete. + // > Applications which block on a fence sync object must take + // > additional steps to assure that the context from which the + // > corresponding fence command was issued has flushed that command + // > to the graphics pipeline. + glFlush(); - if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + if (!newComposite.textureID) { + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + } + withPresentThreadLock([&] { + _submitThread->update(newComposite); + }); } - withPresentThreadLock([&] { - _submitThread->update(newComposite); - }); -#endif } void OpenVrDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) -#if OPENVR_THREADED_SUBMIT - _submitThread->waitForPresent(); -#else - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); - vr::Texture_t vrTexture{ (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; - vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); - vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); - vr::VRCompositor()->PostPresentHandoff(); -#endif + if (_threadedSubmit) { + _submitThread->waitForPresent(); + } else { + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; + vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); + vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); + vr::VRCompositor()->PostPresentHandoff(); + _presentRate.increment(); + } } void OpenVrDisplayPlugin::postPreview() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) PoseData nextRender, nextSim; nextRender.frameIndex = presentCount(); -#if !OPENVR_THREADED_SUBMIT - vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); - glm::mat4 resetMat; - withPresentThreadLock([&] { - resetMat = _sensorResetMat; - }); - nextRender.update(resetMat); - nextSim.update(resetMat); - withPresentThreadLock([&] { - _nextSimPoseData = nextSim; - }); - _nextRenderPoseData = nextRender; - - // FIXME - this looks wrong! - _hmdActivityLevel = vr::k_EDeviceActivityLevel_UserInteraction; // _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); -#else _hmdActivityLevel = _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); -#endif + + if (!_threadedSubmit) { + vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); + + glm::mat4 resetMat; + withPresentThreadLock([&] { + resetMat = _sensorResetMat; + }); + nextRender.update(resetMat); + nextSim.update(resetMat); + withPresentThreadLock([&] { + _nextSimPoseData = nextSim; + }); + _nextRenderPoseData = nextRender; + + } } bool OpenVrDisplayPlugin::isHmdMounted() const { @@ -684,3 +698,7 @@ void OpenVrDisplayPlugin::unsuppressKeyboard() { bool OpenVrDisplayPlugin::isKeyboardVisible() { return isOpenVrKeyboardShown(); } + +int OpenVrDisplayPlugin::getRequiredThreadCount() const { + return Parent::getRequiredThreadCount() + (_threadedSubmit ? 1 : 0); +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 025f879d84..3403bae27c 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -15,9 +15,6 @@ const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device property? This number is vive-only. -#define OPENVR_THREADED_SUBMIT 1 - -#if OPENVR_THREADED_SUBMIT namespace gl { class OffscreenContext; } @@ -34,7 +31,6 @@ struct CompositeInfo { glm::mat4 pose; GLsync fence{ 0 }; }; -#endif class OpenVrDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; @@ -44,7 +40,8 @@ public: void init() override; - float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } + float getTargetFrameRate() const override; + bool hasAsyncReprojection() const override { return _asyncReprojectionActive; } void customizeContext() override; void uncustomizeContext() override; @@ -58,8 +55,8 @@ public: void unsuppressKeyboard() override; bool isKeyboardVisible() override; - // Needs an additional thread for VR submission - int getRequiredThreadCount() const override { return Parent::getRequiredThreadCount() + 1; } + // Possibly needs an additional thread for VR submission + int getRequiredThreadCount() const override; protected: bool internalActivate() override; @@ -71,7 +68,6 @@ protected: bool isHmdMounted() const override; void postPreview() override; - private: vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; @@ -80,12 +76,13 @@ private: vr::HmdMatrix34_t _lastGoodHMDPose; mat4 _sensorResetMat; + bool _threadedSubmit { true }; -#if OPENVR_THREADED_SUBMIT CompositeInfo::Array _compositeInfos; size_t _renderingIndex { 0 }; std::shared_ptr _submitThread; std::shared_ptr _submitCanvas; friend class OpenVrSubmitThread; -#endif + + bool _asyncReprojectionActive { false }; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 91cad67d46..2803ca424e 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -324,3 +326,107 @@ controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat result.angularVelocity = angularVelocity; return result; } + +#define FAILED_MIN_SPEC_OVERLAY_NAME "FailedMinSpecOverlay" +#define FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME "Minimum specifications for SteamVR not met" +#define FAILED_MIN_SPEC_UPDATE_INTERVAL_MS 10 +#define FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS (MSECS_PER_SECOND * 30) +#define MIN_CORES_SPEC 5 + +void showMinSpecWarning() { + auto vrSystem = acquireOpenVrSystem(); + auto vrOverlay = vr::VROverlay(); + if (!vrOverlay) { + qFatal("Unable to initialize SteamVR overlay manager"); + } + + vr::VROverlayHandle_t minSpecFailedOverlay = 0; + if (vr::VROverlayError_None != vrOverlay->CreateOverlay(FAILED_MIN_SPEC_OVERLAY_NAME, FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME, &minSpecFailedOverlay)) { + qFatal("Unable to create overlay"); + } + + // Needed here for PathUtils + QCoreApplication miniApp(__argc, __argv); + + vrSystem->ResetSeatedZeroPose(); + QString imagePath = PathUtils::resourcesPath() + "/images/steam-min-spec-failed.png"; + vrOverlay->SetOverlayFromFile(minSpecFailedOverlay, imagePath.toLocal8Bit().toStdString().c_str()); + vrOverlay->SetHighQualityOverlay(minSpecFailedOverlay); + vrOverlay->SetOverlayWidthInMeters(minSpecFailedOverlay, 1.4f); + vrOverlay->SetOverlayInputMethod(minSpecFailedOverlay, vr::VROverlayInputMethod_Mouse); + vrOverlay->ShowOverlay(minSpecFailedOverlay); + + QTimer* timer = new QTimer(&miniApp); + timer->setInterval(FAILED_MIN_SPEC_UPDATE_INTERVAL_MS); + QObject::connect(timer, &QTimer::timeout, [&] { + vr::TrackedDevicePose_t vrPoses[vr::k_unMaxTrackedDeviceCount]; + vrSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0, vrPoses, vr::k_unMaxTrackedDeviceCount); + auto headPose = toGlm(vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); + auto overlayPose = toOpenVr(headPose * glm::translate(glm::mat4(), vec3(0, 0, -1))); + vrOverlay->SetOverlayTransformAbsolute(minSpecFailedOverlay, vr::TrackingUniverseSeated, &overlayPose); + + vr::VREvent_t event; + while (vrSystem->PollNextEvent(&event, sizeof(event))) { + switch (event.eventType) { + case vr::VREvent_Quit: + vrSystem->AcknowledgeQuit_Exiting(); + QCoreApplication::quit(); + break; + + case vr::VREvent_ButtonPress: + // Quit on any button press except for 'putting on the headset' + if (event.data.controller.button != vr::k_EButton_ProximitySensor) { + QCoreApplication::quit(); + } + break; + + default: + break; + } + } + + }); + timer->start(); + + QTimer::singleShot(FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS, &miniApp, &QCoreApplication::quit); + miniApp.exec(); +} + + +bool checkMinSpecImpl() { + // If OpenVR isn't supported, we have no min spec, so pass + if (!openVrSupported()) { + return true; + } + + // If we have at least 5 cores, pass + auto coreCount = QThread::idealThreadCount(); + if (coreCount >= MIN_CORES_SPEC) { + return true; + } + + // Even if we have too few cores... if the compositor is using async reprojection, pass + auto system = acquireOpenVrSystem(); + auto compositor = vr::VRCompositor(); + if (system && compositor) { + vr::Compositor_FrameTiming timing; + memset(&timing, 0, sizeof(timing)); + timing.m_nSize = sizeof(vr::Compositor_FrameTiming); + compositor->GetFrameTiming(&timing); + releaseOpenVrSystem(); + if (timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync) { + return true; + } + } + + // We're using OpenVR and we don't have enough cores... + showMinSpecWarning(); + + return false; +} + +extern "C" { + __declspec(dllexport) int __stdcall CheckMinSpec() { + return checkMinSpecImpl() ? 1 : 0; + } +} diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 4279e6a6ac..be79dd1155 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -82,5 +82,7 @@ struct PoseData { } }; +// FIXME remove once OpenVR header is updated +#define VRCompositor_ReprojectionAsync 0x04 controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 2d2720e388..ff8fc64474 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -210,6 +210,11 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + + if (!_system) { + return; + } + auto userInputMapper = DependencyManager::get(); handleOpenVrEvents(); if (openVrQuitRequested()) { diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 718b5f3d3e..4376960ea5 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS = [ "system/controllers/toggleAdvancedMovementForHandControllers.js", "system/dialTone.js", "system/firstPersonHMD.js", - "system/snapshot.js" + "system/snapshot.js", + "system/help.js" ]; // add a menu item for debugging diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js index 6768b41318..b87d418643 100644 --- a/scripts/developer/tests/performance/crowd-agent.js +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -16,7 +16,7 @@ var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; -print('crowd-agent version 3'); +print('crowd-agent version 4'); /* Observations: - File urls for AC scripts silently fail. Use a local server (e.g., python SimpleHTTPServer) for development. @@ -34,19 +34,56 @@ function getSound(data, callback) { // callback(sound) when downloaded (which ma if (sound.downloaded) { return callback(sound); } - sound.ready.connect(function () { callback(sound); }); + function onDownloaded() { + sound.ready.disconnect(onDownloaded); + callback(sound); + } + sound.ready.connect(onDownloaded); } function onFinishedPlaying() { messageSend({key: 'finishedSound'}); } var attachment; +var stopper; +function clearStopper() { + if (!stopper) { + return; + } + Script.clearTimeout(stopper); + stopper = null; +} +function stopAgent(parameters) { + function stop() { + clearStopper(); + if (attachment) { + Avatar.detachOne(attachment.modelURL, attachment.jointName); + attachment = undefined; + } + Agent.isListeningToAudioStream = false; + Agent.isAvatar = false; + print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); + } + // Shutting down lots of agents at once can be hard on other parts of the system. (See fogbugz 2095.) + // For now, accept a parameter to delay for the given number of milliseconds before stopping. + // (We cannot count on summoning scripts to spread out the STOP messages, because they might be doing so + // on scriptEnding, in which case they are not allowed to create new delays.) + if (parameters.delay) { + if (!stopper) { // Let the first stopper do the deed. + stopper = Script.setTimeout(stop, parameters.delay); + } + } else { + stop(); + } +} + var MILLISECONDS_IN_SECOND = 1000; function startAgent(parameters) { // Can also be used to update. print('crowd-agent starting params', JSON.stringify(parameters), JSON.stringify(Agent)); + clearStopper(); + var wasOff = !Agent.isAvatar; Agent.isAvatar = true; - Agent.isListeningToAudioStream = true; // Send silence when not chattering. if (parameters.position) { Avatar.position = parameters.position; } @@ -56,6 +93,11 @@ function startAgent(parameters) { // Can also be used to update. if (parameters.skeletonModelURL) { Avatar.skeletonModelURL = parameters.skeletonModelURL; } + if (parameters.listen != undefined) { + Agent.isListeningToAudioStream = parameters.listen; // Send silence when not chattering. + } else if (wasOff) { + Agent.isListeningToAudioStream = true; + } if (parameters.soundData) { getSound(parameters.soundData, function (sound) { Script.setTimeout(onFinishedPlaying, sound.duration * MILLISECONDS_IN_SECOND); @@ -74,14 +116,6 @@ function startAgent(parameters) { // Can also be used to update. } print('crowd-agent avatars started'); } -function stopAgent(parameters) { - if (attachment) { - Avatar.detachOne(attachment.modelURL, attachment.jointName); - attachment = undefined; - } - Agent.isAvatar = false; - print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); -} function messageHandler(channel, messageString, senderID) { if (channel !== MESSAGE_CHANNEL) { diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index 806b58dbf6..398bc4fd0a 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -21,6 +21,10 @@ var NOMINAL_LOAD_TIME = 30; // seconds var MAXIMUM_LOAD_TIME = NOMINAL_LOAD_TIME * 2; var MINIMUM_AVATARS = 25; // changeable by prompt +// If we add or remove things too quickly, we get problems (e.g., audio, fogbugz 2095). +// For now, spread them out this timing apart. +var SPREAD_TIME_MS = 500; + var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2). var SOUND_DATA = {url: "http://hifi-content.s3.amazonaws.com/howard/sounds/piano1.wav"}; var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND at once. @@ -34,7 +38,7 @@ var ANIMATION_DATA = { "loopFlag": true }; -var version = 3; +var version = 4; function debug() { print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify))); } @@ -86,6 +90,7 @@ function nextAfter(array, id) { // Wrapping next element in array after id. var summonedAgents = []; var chattering = []; +var accumulatedDelay = 0; var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; function messageSend(message) { Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); @@ -106,25 +111,29 @@ function messageHandler(channel, messageString, senderID) { } switch (message.key) { case "hello": - // There can be avatars we've summoned that do not yet appear in the AvatarList. - avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); - debug('present', avatarIdentifiers, summonedAgents); - if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS) { - var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; - if (chatter) { - chattering.push(senderID); + Script.setTimeout(function () { + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS) { + var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; + if (chatter) { + chattering.push(senderID); + } + summonedAgents.push(senderID); + messageSend({ + key: 'SUMMON', + rcpt: senderID, + position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), + orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), + soundData: chatter && SOUND_DATA, + listen: true, + skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", + animationData: ANIMATION_DATA + }); } - summonedAgents.push(senderID); - messageSend({ - key: 'SUMMON', - rcpt: senderID, - position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), - orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), - soundData: chatter && SOUND_DATA, - skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", - animationData: ANIMATION_DATA - }); - } + }, accumulatedDelay); + accumulatedDelay += SPREAD_TIME_MS; // assume we'll get all the hello respsponses more or less together. break; case "finishedSound": // Give someone else a chance. chattering = without(chattering, [senderID]); @@ -147,13 +156,15 @@ Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); Script.scriptEnding.connect(function () { debug('stopping agents', summonedAgents); - summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); }); + Messages.messageReceived.disconnect(messageHandler); // don't respond to any messages during shutdown + accumulatedDelay = 0; + summonedAgents.forEach(function (id) { + messageSend({key: 'STOP', rcpt: id, delay: accumulatedDelay}); + accumulatedDelay += SPREAD_TIME_MS; + }); debug('agents stopped'); - Script.setTimeout(function () { - Messages.messageReceived.disconnect(messageHandler); - Messages.unsubscribe(MESSAGE_CHANNEL); - debug('unsubscribed'); - }, 500); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); }); var fail = false, results = ""; @@ -257,7 +268,7 @@ function doRender(continuation) { } config.newStats.connect(onNewStats); - startTwirl(720, 1, 15, 0.08, function () { + startTwirl(720, 1, 20, 0.08, function () { var end = Date.now(); config.newStats.disconnect(onNewStats); addResult('frame rate', 1000 * frames / (end - start), @@ -270,7 +281,7 @@ function doRender(continuation) { http://hifi-content.s3.amazonaws.com/howard/scripts/tests/performance/crowd-agent.js?v=3\n\ on your domain server."; } else if (total < MINIMUM_AVATARS) { - fail = "FAIL: Only " + summonedAgents.length + " avatars reported. Missing " + (MINIMUM_AVATARS - total) + "."; + fail = "FAIL: Only " + summonedAgents.length + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total."; } } continuation(); diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js index 8b67859b3a..69bf0860ae 100644 --- a/scripts/developer/tests/performance/summon.js +++ b/scripts/developer/tests/performance/summon.js @@ -13,19 +13,26 @@ // // See crowd-agent.js -var version = 1; +var version = 2; var label = "summon"; function debug() { print.apply(null, [].concat.apply([label, version], [].map.call(arguments, JSON.stringify))); } + var MINIMUM_AVATARS = 25; // We will summon agents to produce this many total. (Of course, there might not be enough agents.) +var N_LISTENING = MINIMUM_AVATARS - 1; +var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND_DATA at once. + +// If we add or remove things too quickly, we get problems (e.g., audio, fogbugz 2095). +// For now, spread them out this timing apart. +var SPREAD_TIME_MS = 500; + var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2). -var SOUND_DATA = {url: "http://howard-stearns.github.io/models/sounds/piano1.wav"}; -var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND at once. +var SOUND_DATA = {url: "http://hifi-content.s3.amazonaws.com/howard/sounds/piano1.wav"}; var NEXT_SOUND_SPREAD = 500; // millisecond range of how long to wait after one sound finishes, before playing the next var ANIMATION_DATA = { - "url": "http://howard-stearns.github.io/models/resources/avatar/animations/idle.fbx", - // "url": "http://howard-stearns.github.io/models/resources/avatar/animations/walk_fwd.fbx", // alternative example + "url": "http://hifi-content.s3.amazonaws.com/howard/resources/avatar/animations/idle.fbx", + // "url": "http://hifi-content.s3.amazonaws.com/howard/resources/avatar/animations/walk_fwd.fbx", // alternative example "startFrame": 0.0, "endFrame": 300.0, "timeScale": 1.0, @@ -45,6 +52,8 @@ function nextAfter(array, id) { // Wrapping next element in array after id. var summonedAgents = []; var chattering = []; +var nListening = 0; +var accumulatedDelay = 0; var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; function messageSend(message) { Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); @@ -65,25 +74,33 @@ function messageHandler(channel, messageString, senderID) { } switch (message.key) { case "hello": - // There can be avatars we've summoned that do not yet appear in the AvatarList. - avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); - debug('present', avatarIdentifiers, summonedAgents); - if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { - var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; - if (chatter) { - chattering.push(senderID); + Script.setTimeout(function () { + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { + var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; + var listen = nListening < N_LISTENING; + if (chatter) { + chattering.push(senderID); + } + if (listen) { + nListening++; + } + summonedAgents.push(senderID); + messageSend({ + key: 'SUMMON', + rcpt: senderID, + position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), + orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), + soundData: chatter && SOUND_DATA, + listen: listen, + skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", + animationData: ANIMATION_DATA + }); } - summonedAgents.push(senderID); - messageSend({ - key: 'SUMMON', - rcpt: senderID, - position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), - orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), - soundData: chatter && SOUND_DATA, - skeletonModelURL: "http://howard-stearns.github.io/models/resources/meshes/defaultAvatar_full.fst", - animationData: ANIMATION_DATA - }); - } + }, accumulatedDelay); + accumulatedDelay += SPREAD_TIME_MS; // assume we'll get all the hello respsponses more or less together. break; case "finishedSound": // Give someone else a chance. chattering = without(chattering, [senderID]); @@ -99,20 +116,22 @@ function messageHandler(channel, messageString, senderID) { Window.alert("Someone else is summoning avatars."); break; default: - print("crowd-agent received unrecognized message:", messageString); + print("crowd summon.js received unrecognized message:", messageString); } } Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); Script.scriptEnding.connect(function () { debug('stopping agents', summonedAgents); - summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); }); + Messages.messageReceived.disconnect(messageHandler); // don't respond to any messages during shutdown + accumulatedDelay = 0; + summonedAgents.forEach(function (id) { + messageSend({key: 'STOP', rcpt: id, delay: accumulatedDelay}); + accumulatedDelay += SPREAD_TIME_MS; + }); debug('agents stopped'); - Script.setTimeout(function () { - Messages.messageReceived.disconnect(messageHandler); - Messages.unsubscribe(MESSAGE_CHANNEL); - debug('unsubscribed'); - }, 500); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); }); messageSend({key: 'HELO'}); // Ask agents to report in now. @@ -120,9 +139,9 @@ Script.setTimeout(function () { var total = AvatarList.getAvatarIdentifiers().length; if (0 === summonedAgents.length) { Window.alert("No agents reported.\n\Please run " + MINIMUM_AVATARS + " instances of\n\ -http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/crowd-agent.js\n\ +http://hifi-content.s3.amazonaws.com/howard/scripts/tests/performance/crowd-agent.js\n\ on your domain server."); } else if (total < MINIMUM_AVATARS) { - Window.alert("Only " + summonedAgents.length + " of the expected " + (MINIMUM_AVATARS - total) + " agents reported in."); + Window.alert("Only " + summonedAgents.length + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total."); } -}, 5000); +}, MINIMUM_AVATARS * SPREAD_TIME_MS ) diff --git a/scripts/system/assets/images/progress-bar-2k.svg b/scripts/system/assets/images/progress-bar-2k.svg new file mode 100644 index 0000000000..45758c7c68 --- /dev/null +++ b/scripts/system/assets/images/progress-bar-2k.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar-4k.svg b/scripts/system/assets/images/progress-bar-4k.svg new file mode 100644 index 0000000000..609ab9610b --- /dev/null +++ b/scripts/system/assets/images/progress-bar-4k.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar-background.svg b/scripts/system/assets/images/progress-bar-background.svg deleted file mode 100644 index a8b4e1aab5..0000000000 --- a/scripts/system/assets/images/progress-bar-background.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/scripts/system/assets/images/progress-bar-text.svg b/scripts/system/assets/images/progress-bar-text.svg new file mode 100644 index 0000000000..05ebb3f637 --- /dev/null +++ b/scripts/system/assets/images/progress-bar-text.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar.svg b/scripts/system/assets/images/progress-bar.svg deleted file mode 100644 index e24a2cbff4..0000000000 --- a/scripts/system/assets/images/progress-bar.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/scripts/system/assets/images/tools/help.svg b/scripts/system/assets/images/tools/help.svg new file mode 100644 index 0000000000..b7fa8ca5cd --- /dev/null +++ b/scripts/system/assets/images/tools/help.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/away.js b/scripts/system/away.js index 79ec22967d..96813031f1 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -304,7 +304,7 @@ function setEnabled(value) { var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; var handleMessage = function(channel, message, sender) { - if (channel === CHANNEL_AWAY_ENABLE) { + if (channel === CHANNEL_AWAY_ENABLE && sender === MyAvatar.sessionUUID) { print("away.js | Got message on Hifi-Away-Enable: ", message); setEnabled(message === 'enable'); } @@ -344,6 +344,7 @@ Script.scriptEnding.connect(function () { Controller.mousePressEvent.disconnect(goActive); Controller.keyPressEvent.disconnect(maybeGoActive); Messages.messageReceived.disconnect(handleMessage); + Messages.unsubscribe(CHANNEL_AWAY_ENABLE); }); if (HMD.active && !HMD.mounted) { diff --git a/scripts/system/controllers/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js index f42ac3aeda..6135f18426 100644 --- a/scripts/system/controllers/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -31,14 +31,24 @@ function resolveHardware(path) { return resolveInner(Controller.Hardware, parts, 0); } +var DEBUG = true; +function debug() { + if (DEBUG) { + var args = Array.prototype.slice.call(arguments); + args.unshift("controllerDisplay.js | "); + print.apply(this, args); + } +} + createControllerDisplay = function(config) { var controllerDisplay = { overlays: [], partOverlays: {}, parts: {}, - mappingName: "mapping-display", + mappingName: "mapping-display-" + Math.random(), setVisible: function(visible) { + debug("Setting visible", this.overlays.length); for (var i = 0; i < this.overlays.length; ++i) { Overlays.editOverlay(this.overlays[i], { visible: visible @@ -166,7 +176,7 @@ createControllerDisplay = function(config) { } else if (part.type === "static") { // do nothing } else { - print("TYPE NOT SUPPORTED: ", part.type); + debug("TYPE NOT SUPPORTED: ", part.type); } controllerDisplay.overlays.push(overlayID); diff --git a/scripts/system/controllers/controllerDisplayManager.js b/scripts/system/controllers/controllerDisplayManager.js index 2c621f7e90..550357d659 100644 --- a/scripts/system/controllers/controllerDisplayManager.js +++ b/scripts/system/controllers/controllerDisplayManager.js @@ -69,7 +69,6 @@ ControllerDisplayManager = function() { } } - Messages.subscribe('Controller-Display'); var handleMessages = function(channel, message, sender) { var i, data, name, visible; if (!controllerLeft && !controllerRight) { diff --git a/scripts/system/controllers/viveControllerConfiguration.js b/scripts/system/controllers/viveControllerConfiguration.js index 341b8256f7..b49c3e1d04 100644 --- a/scripts/system/controllers/viveControllerConfiguration.js +++ b/scripts/system/controllers/viveControllerConfiguration.js @@ -62,6 +62,7 @@ var TIP_TEXTURE_BASE_URL = BASE_URL + "meshes/controller/vive_tips.fbm/"; var viveModelURL = BASE_URL + "meshes/controller/vive_body.fbx"; var viveTipsModelURL = BASE_URL + "meshes/controller/vive_tips.fbx"; +var viveTriggerModelURL = "meshes/controller/vive_trigger.fbx" VIVE_CONTROLLER_CONFIGURATION_LEFT = { name: "Vive", @@ -86,7 +87,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { defaultTextureLayer: "blank", textureLayers: { blank: { - defaultTextureURL: TIP_TEXTURE_BASE_URL + "Blank.png" + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" }, trigger: { defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" @@ -141,7 +142,18 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { minValue: 0.0, maxValue: 1.0, axis: { x: -1, y: 0, z: 0 }, - maxAngle: 20 + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg", + } + } }, l_grip: { @@ -262,7 +274,18 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { minValue: 0.0, maxValue: 1.0, axis: { x: -1, y: 0, z: 0 }, - maxAngle: 25 + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg", + } + } }, l_grip: { diff --git a/scripts/system/help.js b/scripts/system/help.js new file mode 100644 index 0000000000..e79ed0444c --- /dev/null +++ b/scripts/system/help.js @@ -0,0 +1,41 @@ +"use strict"; + +// +// help.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Nov 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + + var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + var buttonName = "help"; // matching location reserved in Desktop.qml + var button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/help.svg"), + visible: true, + hoverState: 2, + defaultState: 1, + buttonState: 1, + alpha: 0.9 + }); + + // TODO: make button state reflect whether the window is opened or closed (independently from us). + + function onClicked(){ + Menu.triggerOption('Help...') + } + + button.clicked.connect(onClicked); + + Script.scriptEnding.connect(function () { + toolBar.removeButton(buttonName); + button.clicked.disconnect(onClicked); + }); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index a7dfb048b8..d6d9098e21 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1492,7 +1492,7 @@ function loaded() { var lis = dropdown.parentNode.getElementsByTagName("li"); var text = ""; for (var i = 0; i < lis.length; i++) { - if (lis[i].getAttribute("value") === dropdown.value) { + if (String(lis[i].getAttribute("value")) === String(dropdown.value)) { text = lis[i].textContent; } } diff --git a/scripts/system/progress.js b/scripts/system/progress.js index d7f6713c69..92853c9ada 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -13,18 +13,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE function debug() { - return; - print.apply(null, arguments); + //print.apply(null, arguments); } var rawProgress = 100, // % raw value. displayProgress = 100, // % smoothed value to display. - DISPLAY_PROGRESS_MINOR_MAXIMUM = 8, // % displayed progress bar goes up to while 0% raw progress. - DISPLAY_PROGRESS_MINOR_INCREMENT = 0.1, // % amount to increment display value each update when 0% raw progress. - DISPLAY_PROGRESS_MAJOR_INCREMENT = 5, // % maximum amount to increment display value when >0% raw progress. alpha = 0.0, alphaDelta = 0.0, // > 0 if fading in; < 0 if fading out. ALPHA_DELTA_IN = 0.15, @@ -34,19 +30,60 @@ fadeWaitTimer = null, FADE_OUT_WAIT = 1000, // Wait before starting to fade out after progress 100%. visible = false, - BAR_WIDTH = 480, // Dimension of SVG in pixels of visible portion (half) of the bar. - BAR_HEIGHT = 10, - BAR_Y_OFFSET_2D = -10, // Offset of progress bar while in desktop mode - BAR_Y_OFFSET_HMD = -300, // Offset of progress bar while in HMD - BAR_URL = Script.resolvePath("assets/images/progress-bar.svg"), - BACKGROUND_WIDTH = 520, - BACKGROUND_HEIGHT = 50, - BACKGROUND_URL = Script.resolvePath("assets/images/progress-bar-background.svg"), + + BAR_DESKTOP_2K_WIDTH = 2240, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats. + BAR_DESKTOP_2K_REPEAT = 320, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_2K_HEIGHT = 3, // Display height of SVG + BAR_DESKTOP_2K_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 4096 x 1920 display with 6 visible repeats. + BAR_DESKTOP_4K_REPEAT = 640, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_4K_HEIGHT = 6, // Display height of SVG + BAR_DESKTOP_4K_URL = Script.resolvePath("assets/images/progress-bar-4k.svg"), + + BAR_HMD_WIDTH = 2240, // Desktop image works with HMD well. + BAR_HMD_REPEAT = 320, + BAR_HMD_HEIGHT = 3, + BAR_HMD_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_Y_OFFSET_DESKTOP = 0, // Offset of progress bar while in desktop mode + BAR_Y_OFFSET_HMD = -100, // Offset of progress bar while in HMD + + ANIMATION_SECONDS_PER_REPEAT = 4, // Speed of bar animation + + TEXT_HEIGHT = 32, + TEXT_WIDTH = 256, + TEXT_URL = Script.resolvePath("assets/images/progress-bar-text.svg"), windowWidth = 0, windowHeight = 0, - background2D = {}, - bar2D = {}, - SCALE_2D = 0.35; // Scale the SVGs for 2D display. + barDesktop = {}, + barHMD = {}, + textDesktop = {}, // Separate desktop and HMD overlays because can't change text size after overlay created. + textHMD = {}, + SCALE_TEXT_DESKTOP = 0.6, + SCALE_TEXT_HMD = 1.0, + isHMD = false, + + // Max seen since downloads started. This is reset when all downloads have completed. + maxSeen = 0, + + // Progress is defined as: (pending_downloads + active_downloads) / max_seen + // We keep track of both the current progress (rawProgress) and the + // best progress we've seen (bestRawProgress). As you are downloading, you may + // encounter new assets that require downloads, increasing the number of + // pending downloads and thus decreasing your overall progress. + bestRawProgress = 0, + + // True if we have known active downloads + isDownloading = false, + + // Entities are streamed to users, so you don't receive them all at once; instead, you + // receive them over a period of time. In many cases we end up in a situation where + // + // The initial delay cooldown keeps us from tracking progress before the allotted time + // has passed. + INITIAL_DELAY_COOLDOWN_TIME = 1000, + initialDelayCooldown = 0; function fade() { @@ -67,45 +104,32 @@ visible = false; } - Overlays.editOverlay(background2D.overlay, { + Overlays.editOverlay(barDesktop.overlay, { alpha: alpha, - visible: visible + visible: visible && !isHMD }); - Overlays.editOverlay(bar2D.overlay, { + Overlays.editOverlay(barHMD.overlay, { alpha: alpha, - visible: visible + visible: visible && isHMD + }); + Overlays.editOverlay(textDesktop.overlay, { + alpha: alpha, + visible: visible && !isHMD + }); + Overlays.editOverlay(textHMD.overlay, { + alpha: alpha, + visible: visible && isHMD }); } - Window.domainChanged.connect(function() { + Window.domainChanged.connect(function () { isDownloading = false; bestRawProgress = 100; rawProgress = 100; displayProgress = 100; }); - // Max seen since downloads started. This is reset when all downloads have completed. - var maxSeen = 0; - - // Progress is defined as: (pending_downloads + active_downloads) / max_seen - // We keep track of both the current progress (rawProgress) and the - // best progress we've seen (bestRawProgress). As you are downloading, you may - // encounter new assets that require downloads, increasing the number of - // pending downloads and thus decreasing your overall progress. - var bestRawProgress = 0; - - // True if we have known active downloads - var isDownloading = false; - - // Entities are streamed to users, so you don't receive them all at once; instead, you - // receive them over a period of time. In many cases we end up in a situation where - // - // The initial delay cooldown keeps us from tracking progress before the allotted time - // has passed. - var INITIAL_DELAY_COOLDOWN_TIME = 1000; - var initialDelayCooldown = 0; function onDownloadInfoChanged(info) { - var i; debug("PROGRESS: Download info changed ", info.downloading.length, info.pending, maxSeen); @@ -140,43 +164,96 @@ } function createOverlays() { - background2D.overlay = Overlays.addOverlay("image", { - imageURL: BACKGROUND_URL, - width: background2D.width, - height: background2D.height, - visible: false, - alpha: 0.0 - }); - bar2D.overlay = Overlays.addOverlay("image", { - imageURL: BAR_URL, + barDesktop.overlay = Overlays.addOverlay("image", { + imageURL: barDesktop.url, subImage: { x: 0, y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height }, - width: bar2D.width, - height: bar2D.height, + width: barDesktop.width, + height: barDesktop.height, + visible: false, + alpha: 0.0 + }); + barHMD.overlay = Overlays.addOverlay("image", { + imageURL: BAR_HMD_URL, + subImage: { + x: 0, + y: 0, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + }, + width: barHMD.width, + height: barHMD.height, + visible: false, + alpha: 0.0 + }); + textDesktop.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textDesktop.width, + height: textDesktop.height, + visible: false, + alpha: 0.0 + }); + textHMD.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textHMD.width, + height: textHMD.height, visible: false, alpha: 0.0 }); } function deleteOverlays() { - Overlays.deleteOverlay(background2D.overlay); - Overlays.deleteOverlay(bar2D.overlay); + Overlays.deleteOverlay(barDesktop.overlay); + Overlays.deleteOverlay(barHMD.overlay); + Overlays.deleteOverlay(textDesktop.overlay); + Overlays.deleteOverlay(textHMD.overlay); + } + + function updateProgressBarLocation() { + var viewport = Controller.getViewportDimensions(); + + windowWidth = viewport.x; + windowHeight = viewport.y; + isHMD = HMD.active; + + if (isHMD) { + + Overlays.editOverlay(barHMD.overlay, { + x: windowWidth / 2 - barHMD.width / 2, + y: windowHeight - 2 * barHMD.height + BAR_Y_OFFSET_HMD + }); + + Overlays.editOverlay(textHMD.overlay, { + x: windowWidth / 2 - textHMD.width / 2, + y: windowHeight - 2 * barHMD.height - textHMD.height + BAR_Y_OFFSET_HMD + }); + + } else { + + Overlays.editOverlay(barDesktop.overlay, { + x: windowWidth / 2 - barDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height + BAR_Y_OFFSET_DESKTOP, + width: barDesktop.width + }); + + Overlays.editOverlay(textDesktop.overlay, { + x: windowWidth / 2 - textDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height - textDesktop.height + BAR_Y_OFFSET_DESKTOP + }); + } } - var b = 0; - var currentOrientation = null; function update() { + var viewport, diff, x; + initialDelayCooldown -= 30; - var viewport, - eyePosition, - avatarOrientation; if (displayProgress < rawProgress) { - var diff = rawProgress - displayProgress; + diff = rawProgress - displayProgress; if (diff < 0.5) { displayProgress = rawProgress; } else { @@ -204,7 +281,7 @@ } else { // Fully visible because downloading or recently so if (fadeWaitTimer === null) { if (rawProgress === 100) { // Was downloading but have finished so fade out soon - fadeWaitTimer = Script.setTimeout(function() { + fadeWaitTimer = Script.setTimeout(function () { alphaDelta = ALPHA_DELTA_OUT; fadeTimer = Script.setInterval(fade, FADE_INTERVAL); fadeWaitTimer = null; @@ -219,59 +296,67 @@ } if (visible) { + x = ((Date.now() / 1000) % ANIMATION_SECONDS_PER_REPEAT) / ANIMATION_SECONDS_PER_REPEAT; + if (isHMD) { + x = x * barDesktop.repeat; + } else { + x = x * BAR_HMD_REPEAT; + } // Update progress bar - Overlays.editOverlay(bar2D.overlay, { - visible: true, + Overlays.editOverlay(barDesktop.overlay, { + visible: !isHMD, subImage: { - x: BAR_WIDTH * (1 - displayProgress / 100), + x: barDesktop.repeat - x, y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT - }, + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height + } }); - Overlays.editOverlay(background2D.overlay, { - visible: true, + Overlays.editOverlay(barHMD.overlay, { + visible: isHMD, + subImage: { + x: BAR_HMD_REPEAT - x, + y: 0, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + } + }); + + Overlays.editOverlay(textDesktop.overlay, { + visible: !isHMD + }); + + Overlays.editOverlay(textHMD.overlay, { + visible: isHMD }); // Update 2D overlays to maintain positions at bottom middle of window viewport = Controller.getViewportDimensions(); - if (viewport.x !== windowWidth || viewport.y !== windowHeight) { + if (viewport.x !== windowWidth || viewport.y !== windowHeight || isHMD !== HMD.active) { updateProgressBarLocation(); } } } - function updateProgressBarLocation() { - var viewport = Controller.getViewportDimensions(); - windowWidth = viewport.x; - windowHeight = viewport.y; - - var yOffset = HMD.active ? BAR_Y_OFFSET_HMD : BAR_Y_OFFSET_2D; - - background2D.width = SCALE_2D * BACKGROUND_WIDTH; - background2D.height = SCALE_2D * BACKGROUND_HEIGHT; - bar2D.width = SCALE_2D * BAR_WIDTH; - bar2D.height = SCALE_2D * BAR_HEIGHT; - - Overlays.editOverlay(background2D.overlay, { - x: windowWidth / 2 - background2D.width / 2, - y: windowHeight - background2D.height - bar2D.height + yOffset - }); - - Overlays.editOverlay(bar2D.overlay, { - x: windowWidth / 2 - bar2D.width / 2, - y: windowHeight - background2D.height - bar2D.height + (background2D.height - bar2D.height) / 2 + yOffset - }); - } - function setUp() { - background2D.width = SCALE_2D * BACKGROUND_WIDTH; - background2D.height = SCALE_2D * BACKGROUND_HEIGHT; - bar2D.width = SCALE_2D * BAR_WIDTH; - bar2D.height = SCALE_2D * BAR_HEIGHT; + var is4k = Window.innerWidth > 3000; + + isHMD = HMD.active; + + barDesktop.width = is4k ? BAR_DESKTOP_4K_WIDTH - BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_WIDTH - BAR_DESKTOP_2K_REPEAT; + barDesktop.height = is4k ? BAR_DESKTOP_4K_HEIGHT : BAR_DESKTOP_2K_HEIGHT; + barDesktop.repeat = is4k ? BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_REPEAT; + barDesktop.url = is4k ? BAR_DESKTOP_4K_URL : BAR_DESKTOP_2K_URL; + barHMD.width = BAR_HMD_WIDTH - BAR_HMD_REPEAT; + barHMD.height = BAR_HMD_HEIGHT; + + textDesktop.width = SCALE_TEXT_DESKTOP * TEXT_WIDTH; + textDesktop.height = SCALE_TEXT_DESKTOP * TEXT_HEIGHT; + textHMD.width = SCALE_TEXT_HMD * TEXT_WIDTH; + textHMD.height = SCALE_TEXT_HMD * TEXT_HEIGHT; createOverlays(); } @@ -283,7 +368,7 @@ setUp(); GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged); GlobalServices.updateDownloadInfo(); - Script.setInterval(update, 1000/60); + Script.setInterval(update, 1000 / 60); Script.scriptEnding.connect(tearDown); }()); // END LOCAL_SCOPE diff --git a/server-console/packager.js b/server-console/packager.js index bc3b8989d2..89bcd7cb71 100644 --- a/server-console/packager.js +++ b/server-console/packager.js @@ -37,7 +37,7 @@ if (osType == "Darwin") { } else if (osType == "Windows_NT") { options["version-string"] = { CompanyName: "High Fidelity, Inc.", - FileDescription: SHORT_NAME, + FileDescription: FULL_NAME, ProductName: FULL_NAME, OriginalFilename: EXEC_NAME + ".exe" } diff --git a/server-console/src/main.js b/server-console/src/main.js index 7b328c0b6e..6c82230601 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -163,17 +163,19 @@ function shutdownCallback(idx) { if (homeServer.state == ProcessGroupStates.STOPPED) { // if the home server is already down, take down the server console now log.debug("Quitting."); - app.quit(); + app.exit(0); } else { // if the home server is still running, wait until we get a state change or timeout // before quitting the app log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(app.quit, 5000); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); homeServer.on('state-update', function(processGroup) { if (processGroup.state == ProcessGroupStates.STOPPED) { clearTimeout(timeoutID); log.debug("Quitting."); - app.quit(); + app.exit(0); } }); } @@ -240,7 +242,7 @@ var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) if (shouldQuit) { log.warn("Another instance of the Sandbox is already running - this instance will quit."); - app.quit(); + app.exit(0); return; } @@ -288,12 +290,12 @@ function binaryMissingMessage(displayName, executableName, required) { if (!dsPath) { dialog.showErrorBox("Domain Server Not Found", binaryMissingMessage("domain-server", "domain-server", true)); - app.quit(); + app.exit(0); } if (!acPath) { dialog.showErrorBox("Assignment Client Not Found", binaryMissingMessage("assignment-client", "assignment-client", true)); - app.quit(); + app.exit(0); } function openFileBrowser(path) { @@ -812,7 +814,8 @@ for (var key in trayIcons) { const notificationIcon = path.join(__dirname, '../resources/console-notification.png'); function onContentLoaded() { - maybeShowSplash(); + // Disable splash window for now. + // maybeShowSplash(); if (buildInfo.releaseType == 'PRODUCTION') { var currentVersion = null; @@ -865,6 +868,12 @@ function onContentLoaded() { homeServer.start(); } + // If we were launched with the launchInterface option, then we need to launch interface now + if (argv.launchInterface) { + log.debug("Interface launch requested... argv.launchInterface:", argv.launchInterface); + startInterface(); + } + // If we were launched with the shutdownWatcher option, then we need to watch for the interface app // shutting down. The interface app will regularly update a running state file which we will check. // If the file doesn't exist or stops updating for a significant amount of time, we will shut down. diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 21ae9c5a99..7da4a1a925 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils) # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui OpenGL Script Widgets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") -link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils ) +link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils octree ) package_libraries_for_deployment() target_nsight() diff --git a/tests/gpu-test/src/TestFbx.cpp b/tests/gpu-test/src/TestFbx.cpp index 538bb0a973..11cd60c3f7 100644 --- a/tests/gpu-test/src/TestFbx.cpp +++ b/tests/gpu-test/src/TestFbx.cpp @@ -13,6 +13,7 @@ #include #include +#include struct MyVertex { vec3 position; diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 791bed773e..a9f5216991 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef DEFERRED_LIGHTING extern void initDeferredPipelines(render::ShapePlumber& plumber); diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index 9608576a6f..975dbf175c 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 3df36c0edf..d4a8322f8a 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -425,6 +425,7 @@ protected: return vec3(); } + bool isAboutToQuit() const override { return false; } void postLambdaEvent(std::function f) override {} qreal getDevicePixelRatio() override { @@ -641,7 +642,7 @@ private: _renderCount = _renderThread._presentCount.load(); update(); - RenderArgs renderArgs(_renderThread._gpuContext, _octree.data(), DEFAULT_OCTREE_SIZE_SCALE, + RenderArgs renderArgs(_renderThread._gpuContext, _octree, DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); diff --git a/tools/atp-extract.py b/tools/atp-extract.py new file mode 100644 index 0000000000..10c31ab0d3 --- /dev/null +++ b/tools/atp-extract.py @@ -0,0 +1,44 @@ +# +# Tool to extract atp files from the asset server cache. +# Usage: python2 atp-extract.py -[lxa] [filename] +# +# cd into the c:\Users\BettySpaghetti\AppData\Roaming\High Fidelity\assignment-client\assets dir +# run 'python2 atp-extract.py -l' to list all files +# run 'python2 atp-extract.py -x file' to extract that particular file to the current directory. +# run 'python2 atp-extract.py -a' to extract all files. +# + +import os, json, sys, shutil + +def loadMapFile(filename): + with open(filename, 'r') as f: + return json.load(f) + +def extractFile(assetMap, filename): + if filename != None: + assetFilename = assetMap.get("/" + filename) + if assetFilename != None: + dir = os.path.dirname(filename) + if dir != "" and not os.path.exists(dir): + os.makedirs(dir) + shutil.copy("files/" + assetFilename, filename) + return True + return False + +option = sys.argv[1] +if option == '-l': + assetMap = loadMapFile("map.json") + for key, value in assetMap.iteritems(): + print key[1:] +elif option == '-x': + assetMap = loadMapFile("map.json") + outputFilename = sys.argv[2] + if not extractFile(assetMap, outputFilename): + print("Error could not extract file: \"" + outputFilename + "\"") +elif option == '-a': + assetMap = loadMapFile("map.json") + for key, value in assetMap.iteritems(): + print("Extracting " + key[1:]) + extractFile(assetMap, key[1:]) +else: + print("unsuported option \"" + option + "\"") diff --git a/tutorial/entityData.js b/tutorial/entityData.js index 76eb4d98ed..b14185e78f 100644 --- a/tutorial/entityData.js +++ b/tutorial/entityData.js @@ -1,12 +1,13 @@ -birdFirework1 = { - "clientOnly": 0, - "collisionsWillMove": 1, - "created": "2016-09-13T23:05:08Z", - "dimensions": { - "x": 0.10120716691017151, - "y": 0.12002291530370712, - "z": 0.18833979964256287 - }, +fireworkURLs = [ + "atp:/tutorial_models/bomb1.fbx", + "atp:/tutorial_models/bomb2.fbx", + "atp:/tutorial_models/bomb3.fbx", + "atp:/tutorial_models/bomb4.fbx", + "atp:/tutorial_models/bomb5.fbx", + "atp:/tutorial_models/bomb6.fbx", +]; + +fireworkBaseProps = { "collisionsWillMove": 1, velocity: { x: 0, @@ -20,7 +21,7 @@ birdFirework1 = { "z": 0 }, "id": "{1c4061bc-b2e7-4435-bc47-3fcc39ae6624}", - "modelURL": "atp:/tutorial_models/birdStatue15.fbx", + "modelURL": "atp:/tutorial_models/bomb1.fbx", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "position": { "x": 0.11612319946289062, @@ -44,15 +45,8 @@ birdFirework1 = { "userData": "{\n \"hifiHomeKey\": {\n \"reset\": true\n }\n}" } ; + birdFirework2 = { - "clientOnly": 0, - "collisionsWillMove": 1, - "created": "2016-09-12T22:56:48Z", - "dimensions": { - "x": 0.098819166421890259, - "y": 0.11143554747104645, - "z": 0.18833979964256287 - }, "collisionsWillMove": 1, velocity: { x: 0, @@ -66,7 +60,7 @@ birdFirework2 = { "z": 0 }, "id": "{ba067084-8d0f-4eeb-a8a1-c6814527c1bb}", - "modelURL": "atp:/tutorial_models/statuebird4.fbx", + "modelURL": "atp:/tutorial_models/bomb2.fbx", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "position": { "x": 0, diff --git a/tutorial/firePit/fire.js b/tutorial/firePit/fire.js index 077d79a42a..4565975351 100644 --- a/tutorial/firePit/fire.js +++ b/tutorial/firePit/fire.js @@ -4,6 +4,12 @@ (function() { + function debug() { + var args = Array.prototype.slice.call(arguments); + args.unshift("fire.js | "); + print.apply(this, args); + } + var _this = this; function Fire() { @@ -54,30 +60,45 @@ var colors = [RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET]; + var firePitSoundURL = Script.resolvePath("fire_burst.wav"); + debug("Firepit burst sound url is: ", firePitSoundURL); + + var explodeTextureURL = Script.resolvePath("explode.png"); + debug("Firepit explode texture url is: ", explodeTextureURL); + Fire.prototype = { preload: function(entityID) { + debug("Preload"); this.entityID = entityID; - this.EXPLOSION_SOUND = SoundCache.getSound("atp:/firepit/fire_burst.wav"); + this.EXPLOSION_SOUND = SoundCache.getSound(firePitSoundURL); }, collisionWithEntity: function(myID, otherID, collisionInfo) { + debug("Collided with entity: ", myID, otherID); var otherProps = Entities.getEntityProperties(otherID); var data = null; try { - data = JSON.parse(otherProps.userData) + data = JSON.parse(otherProps.userData); } catch (err) { - print('ERROR GETTING USERDATA!'); + debug('ERROR GETTING USERDATA!'); } if (data === null || "") { + debug("Data is null or empty", data); return; } else { + debug("Got data", data); if (data.hasOwnProperty('hifiHomeKey')) { + debug("Has hifiHomeKey"); if (data.hifiHomeKey.reset === true) { + debug("Reset is true"); _this.playSoundAtCurrentPosition(); _this.explodeWithColor(); Entities.deleteEntity(otherID) + debug("Sending local message"); Messages.sendLocalMessage('Entity-Exploded', JSON.stringify({ entityID: otherID, + position: Entities.getEntityProperties(this.entityID).position })); + debug("Done sending local message"); } } } @@ -137,7 +158,7 @@ "alphaStart": -0.2, "alphaFinish": 0.5, "emitterShouldTrail": 0, - "textures": "atp:/firepit/explode.png", + "textures": explodeTextureURL, "type": "ParticleEffect", lifetime: 1, position: myProps.position diff --git a/tutorial/fuse.js b/tutorial/fuse.js index 842695d85c..59306f4113 100644 --- a/tutorial/fuse.js +++ b/tutorial/fuse.js @@ -11,13 +11,17 @@ (function() { Script.include('utils.js'); - var DEBUG = false; + var DEBUG = true; function debug() { if (DEBUG) { - print.apply(self, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("fuse.js | "); + print.apply(this, args); } } + var active = false; + var fuseSound = SoundCache.getSound("atp:/tutorial_sounds/fuse.wav"); function getChildProperties(entityID, propertyNames) { var childEntityIDs = Entities.getChildrenIDs(entityID); @@ -33,12 +37,20 @@ }; Fuse.prototype = { light: function() { - debug("LIT", this.entityID); - var anim = Entities.getEntityProperties(this.entityID, ['animation']).animation; + debug("Received light()", this.entityID); - if (anim.currentFrame < 140) { + var visible = Entities.getEntityProperties(this.entityID, ['visible']).visible; + if (!visible) { + debug("Fuse is not visible, returning"); return; } + + if (active) { + debug("Fuse is active, returning"); + return; + } + active = true; + Entities.editEntity(this.entityID, { animation: { currentFrame: 1, @@ -56,6 +68,7 @@ var childrenProps = getChildProperties(this.entityID, ['type']); for (var childEntityID in childrenProps) { + debug("Updating: ", childEntityID); var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { Entities.editEntity(childEntityID, { @@ -70,13 +83,14 @@ var self = this; Script.setTimeout(function() { - debug("BLOW UP"); - var spinnerID = Utils.findEntity({ name: "tutorial/equip/spinner" }, 20); + debug("Setting off fireworks"); + var spinnerID = "{dd13fcd5-616f-4749-ab28-2e1e8bc512e9}"; Entities.callEntityMethod(spinnerID, "onLit"); injector.stop(); var childrenProps = getChildProperties(self.entityID, ['type']); for (var childEntityID in childrenProps) { + debug("Updating: ", childEntityID); var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { Entities.editEntity(childEntityID, { @@ -90,8 +104,14 @@ } }, 4900); + + Script.setTimeout(function() { + debug("Setting fuse to inactive"); + active = false; + }, 14000); }, preload: function(entityID) { + debug("Preload"); this.entityID = entityID; }, }; diff --git a/tutorial/fuseCollider.js b/tutorial/fuseCollider.js index 0ad5cfb371..953fcd316d 100644 --- a/tutorial/fuseCollider.js +++ b/tutorial/fuseCollider.js @@ -5,11 +5,12 @@ }; Fuse.prototype = { onLit: function() { - print("LIT", this.entityID); - var fuseID = Utils.findEntity({ name: "tutorial/equip/fuse" }, 20); + print("fuseCollider.js | Lit", this.entityID); + var fuseID = "{c8944a13-9acb-4d77-b1ee-851845e98357}" Entities.callEntityMethod(fuseID, "light"); }, preload: function(entityID) { + print("fuseCollider.js | preload"); this.entityID = entityID; }, }; diff --git a/tutorial/lighter/createButaneLighter.js b/tutorial/lighter/createButaneLighter.js index caf3188b14..1a6b94d0f6 100644 --- a/tutorial/lighter/createButaneLighter.js +++ b/tutorial/lighter/createButaneLighter.js @@ -53,33 +53,39 @@ createButaneLighter = function(transform) { shapeType: 'simple-compound', type: 'Model', userData: JSON.stringify({ - tag: "equip-temporary", - grabbableKey: { - invertSolidWhileHeld: true + "tag": "equip-temporary", + "grabbableKey": { + "invertSolidWhileHeld": true }, - wearable: { - joints: { - RightHand: [{ - x: 0.029085848480463028, - y: 0.09807153046131134, - z: 0.03062543272972107 - }, { - x: 0.5929139256477356, - y: 0.3207578659057617, - z: 0.7151655554771423, - w: -0.18468326330184937 - }], - LeftHand: [{ - x: -0.029085848480463028, - y: 0.09807153046131134, - z: 0.03062543272972107 - }, { - x: -0.5929139256477356, - y: 0.3207578659057617, - z: 0.7151655554771423, - w: -0.18468326330184937 - }] - } + "wearable": { + "joints": { + "RightHand": [ + { + "x": 0.049671292304992676, + "y": 0.09825992584228516, + "z": 0.03760027885437012 + }, + { + "x": 0.6562752723693848, + "y": 0.27598991990089417, + "z": 0.6638742685317993, + "w": -0.22890058159828186 + } + ], + "LeftHand": [ + { + "x": -0.028073370456695557, + "y": 0.09609812498092651, + "z": 0.039550721645355225 + }, + { + "x": -0.6697965264320374, + "y": 0.22050897777080536, + "z": 0.6544681191444397, + "w": 0.27283111214637756 + } + ] + } } }), script: SCRIPT_URL diff --git a/tutorial/ownershipToken.js b/tutorial/ownershipToken.js index 745eee44e4..4a970af66d 100644 --- a/tutorial/ownershipToken.js +++ b/tutorial/ownershipToken.js @@ -81,7 +81,6 @@ var TOKEN_STATE_OWNED = 2; OwnershipToken = function(name, parentEntityID, options) { this.name = MyAvatar.sessionUUID + "-" + Math.floor(Math.random() * 10000000); - this.name = Math.floor(Math.random() * 10000000); this.parentEntityID = parentEntityID; // How often to check whether the token is available if we don't currently own it @@ -160,7 +159,7 @@ OwnershipToken.prototype = { var ownerID = getOwnershipTokenID(this.parentEntityID); if (ownerID !== null) { // Already owned, return - debug(this.name, "Token already owned by another client, return"); + debug(this.name, "Token already owned by another client, returning. Owner: " + owenerID + ", Us: " + this.name); return; } @@ -185,3 +184,5 @@ OwnershipToken.prototype = { Script.setTimeout(checkOwnershipRequest.bind(this), 2000); }, }; + +debug("Returning from ownershipToken"); diff --git a/tutorial/spinner.js b/tutorial/spinner.js index b50db2704e..2edbb43700 100644 --- a/tutorial/spinner.js +++ b/tutorial/spinner.js @@ -9,10 +9,10 @@ // (function() { - var DEBUG = false; + var DEBUG = true; function debug() { if (DEBUG) { - print.apply(self, arguments); + print.apply(this, arguments); } } @@ -31,7 +31,7 @@ } Spinner.prototype = { onLit: function() { - debug("LIT SPINNER", this.entityID); + debug("spinner.js | Spinner lit"); Entities.editEntity(this.entityID, { "angularDamping": 0.1, "angularVelocity": { @@ -50,6 +50,7 @@ for (var childEntityID in childrenProps) { var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { + debug("spinner.js | Modifying: ", childEntityID); Entities.editEntity(childEntityID, { emitRate: 35, }); @@ -59,13 +60,14 @@ var self = this; Script.setTimeout(function() { - debug("BLOW UP"); + debug("spinner.js | Finishing spinner"); injector.stop(); var childrenProps = getChildProperties(self.entityID, ['type']); for (var childEntityID in childrenProps) { var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { + debug("spinner.js | Modifying: ", childEntityID); Entities.editEntity(childEntityID, { emitRate: 0, }); @@ -74,6 +76,7 @@ }, 4900); }, preload: function(entityID) { + debug("spinner.js | Preload"); this.entityID = entityID; }, }; diff --git a/tutorial/tutorial.js b/tutorial/tutorial.js index 8c74bddc29..3d2b4ce36b 100644 --- a/tutorial/tutorial.js +++ b/tutorial/tutorial.js @@ -32,7 +32,7 @@ if (!Function.prototype.bind) { if (this.prototype) { // Function.prototype doesn't have a prototype property - fNOP.prototype = this.prototype; + fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); @@ -40,26 +40,36 @@ if (!Function.prototype.bind) { }; } -var DEBUG = false; +var DEBUG = true; function debug() { if (DEBUG) { - print.apply(this, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("tutorial.js | "); + print.apply(this, args); } } var INFO = true; function info() { if (INFO) { - print.apply(this, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("tutorial.js | "); + print.apply(this, args); } } +// Return a number between min (inclusive) and max (exclusive) +function randomInt(min, max) { + return min + Math.floor(Math.random() * (max - min)) +} + var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn"; var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn"; var GUN_SPAWN_NAME = "tutorial/gun_spawn"; var TELEPORT_PAD_NAME = "tutorial/teleport/pad" var successSound = SoundCache.getSound("atp:/tutorial_sounds/good_one.L.wav"); +var firecrackerSound = SoundCache.getSound("atp:/tutorial_sounds/Pops_Firecracker.wav"); var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; @@ -106,14 +116,6 @@ findEntities = function(properties, searchRadius, filterFn) { return matchedEntities; } -function setControllerVisible(name, visible) { - return; - Messages.sendLocalMessage('Controller-Display', JSON.stringify({ - name: name, - visible: visible, - })); -} - function setControllerPartsVisible(parts) { Messages.sendLocalMessage('Controller-Display-Parts', JSON.stringify(parts)); } @@ -191,12 +193,17 @@ function deleteEntitiesWithTag(tag) { } } function editEntitiesWithTag(tag, propertiesOrFn) { - var entityIDs = findEntitiesWithTag(tag); - for (var i = 0; i < entityIDs.length; ++i) { - if (isFunction(propertiesOrFn)) { - Entities.editEntity(entityIDs[i], propertiesOrFn(entityIDs[i])); - } else { - Entities.editEntity(entityIDs[i], propertiesOrFn); + var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; + + debug("Editing tag: ", tag); + if (entities) { + for (entityID in entities) { + debug("Editing: ", entityID, ", ", propertiesOrFn, ", Is in local tree: ", isEntityInLocalTree(entityID)); + if (isFunction(propertiesOrFn)) { + Entities.editEntity(entityID, propertiesOrFn(entityIDs[i])); + } else { + Entities.editEntity(entityID, propertiesOrFn); + } } } } @@ -205,7 +212,7 @@ function findEntitiesWithTag(tag) { return findEntities({ userData: "" }, 10000, function(properties, key, value) { data = parseJSON(value); return data.tag == tag; - }); + }); } // From http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type @@ -222,21 +229,30 @@ function playSuccessSound() { }); } + +function playFirecrackerSound(position) { + Audio.playSound(firecrackerSound, { + position: position, + volume: 0.7, + loop: false + }); +} + /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // // // STEP: DISABLE CONTROLLERS // // // /////////////////////////////////////////////////////////////////////////////// -var stepDisableControllers = function(name) { +var stepStart = function(name) { this.tag = name; - this.shouldLog = false; } -stepDisableControllers.prototype = { +stepStart.prototype = { start: function(onFinish) { - HMD.requestShowHandControllers(); disableEverything(); + HMD.requestShowHandControllers(); + onFinish(); }, cleanup: function() { @@ -258,6 +274,7 @@ function disableEverything() { setControllerPartLayer('tips', 'blank'); hideEntitiesWithTag('finish'); + setAwayEnabled(false); } @@ -275,7 +292,6 @@ function reenableEverything() { setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); MyAvatar.shouldRenderLocally = true; - HMD.requestHideHandControllers(); setAwayEnabled(true); } @@ -293,6 +309,7 @@ var stepEnableControllers = function(name) { stepEnableControllers.prototype = { start: function(onFinish) { reenableEverything(); + HMD.requestHideHandControllers(); onFinish(); }, cleanup: function() { @@ -340,13 +357,11 @@ stepOrient.prototype = { var tag = this.tag; // Spawn content set - debug("raise hands...", this.tag); editEntitiesWithTag(this.tag, { visible: true }); - this.checkIntervalID = null; function checkForHandsAboveHead() { - debug("Orient: Checking for hands above head..."); + debug("Orient | Checking for hands above head"); if (MyAvatar.getLeftPalmPosition().y > (MyAvatar.getHeadPosition().y + 0.1)) { Script.clearInterval(this.checkIntervalID); this.checkIntervalID = null; @@ -359,6 +374,7 @@ stepOrient.prototype = { this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); }, cleanup: function() { + debug("Orient | Cleanup"); if (this.active) { this.active = false; } @@ -375,61 +391,6 @@ stepOrient.prototype = { } }; -/////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -// // -// STEP: Raise hands above head // -// // -/////////////////////////////////////////////////////////////////////////////// -var stepRaiseAboveHead = function(name) { - this.tag = name; - this.tempTag = name + "-temporary"; -} -stepRaiseAboveHead.prototype = { - start: function(onFinish) { - var tag = this.tag; - - var STATE_START = 0; - var STATE_HANDS_DOWN = 1; - var STATE_HANDS_UP = 2; - this.state = STATE_START; - - debug("raise hands...", this.tag); - editEntitiesWithTag(this.tag, { visible: true }); - - // Wait 2 seconds before starting to check for hands - this.checkIntervalID = null; - function checkForHandsAboveHead() { - debug("Raise above head: Checking hands..."); - if (this.state == STATE_START) { - if (MyAvatar.getLeftPalmPosition().y < (MyAvatar.getHeadPosition().y - 0.1)) { - this.state = STATE_HANDS_DOWN; - } - } else if (this.state == STATE_HANDS_DOWN) { - if (MyAvatar.getLeftPalmPosition().y > (MyAvatar.getHeadPosition().y + 0.1)) { - this.state = STATE_HANDS_UP; - Script.clearInterval(this.checkIntervalID); - this.checkIntervalID = null; - playSuccessSound(); - onFinish(); - } - } - } - this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); - }, - cleanup: function() { - if (this.checkIntervalID) { - Script.clearInterval(this.checkIntervalID); - this.checkIntervalID = null - } - if (this.waitTimeoutID) { - Script.clearTimeout(this.waitTimeoutID); - this.waitTimeoutID = null; - } - editEntitiesWithTag(this.tag, { visible: false, collisionless: 1 }); - deleteEntitiesWithTag(this.tempTag); - } -}; /////////////////////////////////////////////////////////////////////////////// @@ -451,30 +412,26 @@ stepNearGrab.prototype = { this.finished = false; this.onFinish = onFinish; - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); var tag = this.tag; // Spawn content set showEntitiesWithTag(this.tag, { visible: true }); showEntitiesWithTag('bothGrab', { visible: true }); - var boxSpawnID = findEntity({ name: NEAR_BOX_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - info("Error creating block, cannot find spawn"); - return null; - } - var boxSpawnPosition = Entities.getEntityProperties(boxSpawnID, 'position').position; - function createBlock() { - //Step1BlockData.position = boxSpawnPosition; - birdFirework1.position = boxSpawnPosition; - return spawnWithTag([birdFirework1], null, this.tempTag)[0]; + var boxSpawnPosition = getEntityWithName(NEAR_BOX_SPAWN_NAME).position; + function createBlock(fireworkNumber) { + fireworkBaseProps.position = boxSpawnPosition; + fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length]; + debug("Creating firework with url: ", fireworkBaseProps.modelURL); + return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0]; } this.birdIDs = []; - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); + this.birdIDs.push(createBlock.bind(this)(0)); + this.birdIDs.push(createBlock.bind(this)(1)); + this.birdIDs.push(createBlock.bind(this)(2)); this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4); }, onMessage: function(channel, message, seneder) { @@ -482,10 +439,12 @@ stepNearGrab.prototype = { return; } if (channel == "Entity-Exploded") { - debug("TUTORIAL: Got entity-exploded message"); + debug("NearGrab | Got entity-exploded message: ", message); var data = parseJSON(message); if (this.birdIDs.indexOf(data.entityID) >= 0) { + debug("NearGrab | It's one of the firecrackers"); + playFirecrackerSound(data.position); playSuccessSound(); this.finished = true; this.onFinish(); @@ -493,10 +452,10 @@ stepNearGrab.prototype = { } }, cleanup: function() { - debug("cleaning up near grab"); + debug("NearGrab | Cleanup"); this.finished = true; - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); hideEntitiesWithTag(this.tag, { visible: false}); deleteEntitiesWithTag(this.tempTag); if (this.positionWatcher) { @@ -530,8 +489,8 @@ stepFarGrab.prototype = { showEntitiesWithTag('bothGrab', { visible: true }); - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({ farGrabEnabled: true, })); @@ -540,21 +499,18 @@ stepFarGrab.prototype = { // Spawn content set showEntitiesWithTag(this.tag); - var boxSpawnID = findEntity({ name: FAR_BOX_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - debug("Error creating block, cannot find spawn"); - return null; - } - var boxSpawnPosition = Entities.getEntityProperties(boxSpawnID, 'position').position; - function createBlock() { - birdFirework1.position = boxSpawnPosition; - return spawnWithTag([birdFirework1], null, this.tempTag)[0]; + var boxSpawnPosition = getEntityWithName(FAR_BOX_SPAWN_NAME).position; + function createBlock(fireworkNumber) { + fireworkBaseProps.position = boxSpawnPosition; + fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length]; + debug("Creating firework with url: ", fireworkBaseProps.modelURL); + return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0]; } this.birdIDs = []; - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); + this.birdIDs.push(createBlock.bind(this)(3)); + this.birdIDs.push(createBlock.bind(this)(4)); + this.birdIDs.push(createBlock.bind(this)(5)); this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4); }, onMessage: function(channel, message, seneder) { @@ -562,9 +518,11 @@ stepFarGrab.prototype = { return; } if (channel == "Entity-Exploded") { - debug("TUTORIAL: Got entity-exploded message"); + debug("FarGrab | Got entity-exploded message: ", message); var data = parseJSON(message); if (this.birdIDs.indexOf(data.entityID) >= 0) { + debug("FarGrab | It's one of the firecrackers"); + playFirecrackerSound(data.position); playSuccessSound(); this.finished = true; this.onFinish(); @@ -572,9 +530,10 @@ stepFarGrab.prototype = { } }, cleanup: function() { + debug("FarGrab | Cleanup"); this.finished = true; - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); hideEntitiesWithTag(this.tag, { visible: false}); hideEntitiesWithTag('bothGrab', { visible: false}); deleteEntitiesWithTag(this.tempTag); @@ -586,12 +545,13 @@ stepFarGrab.prototype = { }; function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) { + debug("Creating position watcher"); this.watcherIntervalID = Script.setInterval(function() { for (var i = 0; i < entityIDs.length; ++i) { var entityID = entityIDs[i]; var props = Entities.getEntityProperties(entityID, ['position']); if (props.position.y < minY || Vec3.distance(originalPosition, props.position) > maxDistance) { - Entities.editEntity(entityID, { + Entities.editEntity(entityID, { position: originalPosition, velocity: { x: 0, y: -0.01, z: 0 }, angularVelocity: { x: 0, y: 0, z: 0 } @@ -603,6 +563,7 @@ function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) { PositionWatcher.prototype = { destroy: function() { + debug("Destroying position watcher"); Script.clearInterval(this.watcherIntervalID); } }; @@ -630,8 +591,8 @@ var stepEquip = function(name) { } stepEquip.prototype = { start: function(onFinish) { - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({ holdEnabled: true, })); @@ -644,40 +605,41 @@ stepEquip.prototype = { this.currentPart = this.PART1; - function createGun() { - var boxSpawnID = findEntity({ name: GUN_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - info("Error creating block, cannot find spawn"); - return null; - } - + function createLighter() { var transform = {}; - transform.position = Entities.getEntityProperties(boxSpawnID, 'position').position; - transform.rotation = Entities.getEntityProperties(boxSpawnID, 'rotation').rotation; + var boxSpawnProps = getEntityWithName(GUN_SPAWN_NAME); + transform.position = boxSpawnProps.position; + transform.rotation = boxSpawnProps.rotation; + transform.velocity = { x: 0, y: -0.01, z: 0 }; + transform.angularVelocity = { x: 0, y: 0, z: 0 }; this.spawnTransform = transform; return doCreateButaneLighter(transform).id; } - this.gunID = createGun.bind(this)(); - this.startWatchingGun(); - debug("Created", this.gunID); + this.lighterID = createLighter.bind(this)(); + this.startWatchingLighter(); + debug("Created lighter", this.lighterID); this.onFinish = onFinish; }, - startWatchingGun: function() { + startWatchingLighter: function() { if (!this.watcherIntervalID) { + debug("Starting to watch lighter position"); this.watcherIntervalID = Script.setInterval(function() { - var props = Entities.getEntityProperties(this.gunID, ['position']); - if (props.position.y < -0.4 + debug("Checking lighter position"); + var props = Entities.getEntityProperties(this.lighterID, ['position']); + if (props.position.y < -0.4 || Vec3.distance(this.spawnTransform.position, props.position) > 4) { - Entities.editEntity(this.gunID, this.spawnTransform); + debug("Moving lighter back to table"); + Entities.editEntity(this.lighterID, this.spawnTransform); } }.bind(this), 1000); } }, stopWatchingGun: function() { if (this.watcherIntervalID) { + debug("Stopping watch of lighter position"); Script.clearInterval(this.watcherIntervalID); this.watcherIntervalID = null; } @@ -687,24 +649,28 @@ stepEquip.prototype = { return; } - debug("Got message", channel, message, sender, MyAvatar.sessionUUID); + debug("Equip | Got message", channel, message, sender, MyAvatar.sessionUUID); if (channel == "Tutorial-Spinner") { if (this.currentPart == this.PART1 && message == "wasLit") { this.currentPart = this.PART2; + debug("Equip | Starting part 2"); Script.setTimeout(function() { + debug("Equip | Starting part 3"); this.currentPart = this.PART3; hideEntitiesWithTag(this.tagPart1); showEntitiesWithTag(this.tagPart2); + setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('tips', 'grip'); Messages.subscribe('Hifi-Object-Manipulation'); + debug("Equip | Finished starting part 3"); }.bind(this), 9000); } } else if (channel == "Hifi-Object-Manipulation") { if (this.currentPart == this.PART3) { var data = parseJSON(message); - if (data.action == 'release' && data.grabbedEntity == this.gunID) { - info("got release"); + if (data.action == 'release' && data.grabbedEntity == this.lighterID) { + debug("Equip | Got release, finishing step"); this.stopWatchingGun(); this.currentPart = this.COMPLETE; playSuccessSound(); @@ -714,13 +680,14 @@ stepEquip.prototype = { } }, cleanup: function() { + debug("Equip | Got yaw action"); if (this.watcherIntervalID) { Script.clearInterval(this.watcherIntervalID); this.watcherIntervalID = null; } - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); this.stopWatchingGun(); this.currentPart = this.COMPLETE; @@ -748,30 +715,36 @@ var stepTurnAround = function(name) { this.tempTag = name + "-temporary"; this.onActionBound = this.onAction.bind(this); - this.numTimesTurnPressed = 0; + this.numTimesSnapTurnPressed = 0; + this.numTimesSmoothTurnPressed = 0; } stepTurnAround.prototype = { start: function(onFinish) { - setControllerVisible("left", true); - setControllerVisible("right", true); - setControllerPartLayer('touchpad', 'arrows'); setControllerPartLayer('tips', 'arrows'); showEntitiesWithTag(this.tag); - this.numTimesTurnPressed = 0; + this.numTimesSnapTurnPressed = 0; + this.numTimesSmoothTurnPressed = 0; + this.smoothTurnDown = false; Controller.actionEvent.connect(this.onActionBound); this.interval = Script.setInterval(function() { - var FORWARD_THRESHOLD = 30; - var REQ_NUM_TIMES_PRESSED = 6; + debug("TurnAround | Checking if finished", + this.numTimesSnapTurnPressed, this.numTimesSmoothTurnPressed); + var FORWARD_THRESHOLD = 90; + var REQ_NUM_TIMES_SNAP_TURN_PRESSED = 3; + var REQ_NUM_TIMES_SMOOTH_TURN_PRESSED = 2; var dir = Quat.getFront(MyAvatar.orientation); var angle = Math.atan2(dir.z, dir.x); var angleDegrees = ((angle / Math.PI) * 180); - if (this.numTimesTurnPressed >= REQ_NUM_TIMES_PRESSED && Math.abs(angleDegrees) < FORWARD_THRESHOLD) { + var hasTurnedEnough = this.numTimesSnapTurnPressed >= REQ_NUM_TIMES_SNAP_TURN_PRESSED + || this.numTimesSmoothTurnPressed >= REQ_NUM_TIMES_SMOOTH_TURN_PRESSED; + var facingForward = Math.abs(angleDegrees) < FORWARD_THRESHOLD + if (hasTurnedEnough && facingForward) { Script.clearInterval(this.interval); this.interval = null; playSuccessSound(); @@ -781,19 +754,28 @@ stepTurnAround.prototype = { }, onAction: function(action, value) { var STEP_YAW_ACTION = 6; + var SMOOTH_YAW_ACTION = 4; + if (action == STEP_YAW_ACTION && value != 0) { - this.numTimesTurnPressed += 1; + debug("TurnAround | Got step yaw action"); + ++this.numTimesSnapTurnPressed; + } else if (action == SMOOTH_YAW_ACTION) { + debug("TurnAround | Got smooth yaw action"); + if (this.smoothTurnDown && value === 0) { + this.smoothTurnDown = false; + ++this.numTimesSmoothTurnPressed; + } else if (!this.smoothTurnDown && value !== 0) { + this.smoothTurnDown = true; + } } }, cleanup: function() { + debug("TurnAround | Cleanup"); try { Controller.actionEvent.disconnect(this.onActionBound); } catch (e) { } - setControllerVisible("left", false); - setControllerVisible("right", false); - setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); @@ -826,22 +808,21 @@ stepTeleport.prototype = { Messages.sendLocalMessage('Hifi-Teleport-Disabler', 'none'); // Wait until touching teleport pad... - var padID = findEntity({ name: TELEPORT_PAD_NAME }, 100); - var padProps = Entities.getEntityProperties(padID, ["position", "dimensions"]); + var padProps = getEntityWithName(TELEPORT_PAD_NAME); var xMin = padProps.position.x - padProps.dimensions.x / 2; var xMax = padProps.position.x + padProps.dimensions.x / 2; var zMin = padProps.position.z - padProps.dimensions.z / 2; var zMax = padProps.position.z + padProps.dimensions.z / 2; function checkCollides() { - debug("Checking if on pad..."); + debug("Teleport | Checking if on pad..."); var pos = MyAvatar.position; - debug('x', pos.x, xMin, xMax); - debug('z', pos.z, zMin, zMax); + debug('Teleport | x', pos.x, xMin, xMax); + debug('Teleport | z', pos.z, zMin, zMax); if (pos.x > xMin && pos.x < xMax && pos.z > zMin && pos.z < zMax) { - debug("On teleport pad"); + debug("Teleport | On teleport pad"); Script.clearInterval(this.checkCollidesTimer); this.checkCollidesTimer = null; playSuccessSound(); @@ -853,6 +834,7 @@ stepTeleport.prototype = { showEntitiesWithTag(this.tag); }, cleanup: function() { + debug("Teleport | Cleanup"); setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); @@ -903,6 +885,10 @@ stepCleanupFinish.prototype = { +function isEntityInLocalTree(entityID) { + return Entities.getEntityProperties(entityID, 'visible').visible !== undefined; +} + function showEntitiesWithTag(tag) { var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; if (entities) { @@ -921,6 +907,7 @@ function showEntitiesWithTag(tag) { collisionless: collisionless, userData: JSON.stringify(data), }; + debug("Showing: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -945,6 +932,7 @@ function showEntitiesWithTag(tag) { Entities.editEntity(entityID, newProperties); }); } + function hideEntitiesWithTag(tag) { var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; if (entities) { @@ -960,6 +948,8 @@ function hideEntitiesWithTag(tag) { ignoreForCollisions: 1, userData: JSON.stringify(data), }; + + debug("Hiding: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -982,6 +972,15 @@ function hideEntitiesWithTag(tag) { }); } +// Return the entity properties for an entity with a given name if it is in our +// cached list of entities. Otherwise, return undefined. +function getEntityWithName(name) { + debug("Getting entity with name:", name); + var entityID = TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP[name]; + debug("Entity id: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); + return entityID; +} + TutorialManager = function() { var STEPS; @@ -990,6 +989,11 @@ TutorialManager = function() { var currentStep = null; var startedTutorialAt = 0; var startedLastStepAt = 0; + var didFinishTutorial = false; + + var wentToEntryStepNum; + var VERSION = 1; + var tutorialID; var self = this; @@ -997,10 +1001,13 @@ TutorialManager = function() { currentStepNum = -1; currentStep = null; startedTutorialAt = Date.now(); + + // Old versions of interface do not have the Script.generateUUID function. + // If Script.generateUUID is not available, default to an empty string. + tutorialID = Script.generateUUID ? Script.generateUUID() : ""; STEPS = [ - new stepDisableControllers("step0"), + new stepStart("start"), new stepOrient("orient"), - new stepRaiseAboveHead("raiseHands"), new stepNearGrab("nearGrab"), new stepFarGrab("farGrab"), new stepEquip("equip"), @@ -1009,6 +1016,7 @@ TutorialManager = function() { new stepFinish("finish"), new stepEnableControllers("enableControllers"), ]; + wentToEntryStepNum = STEPS.length; for (var i = 0; i < STEPS.length; ++i) { STEPS[i].cleanup(); } @@ -1017,11 +1025,9 @@ TutorialManager = function() { } this.onFinish = function() { + debug("onFinish", currentStepNum); if (currentStep && currentStep.shouldLog !== false) { - var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000; - var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; - UserActivityLogger.tutorialProgress( - currentStep.tag, currentStepNum, timeToFinishStep, tutorialTimeElapsed); + self.trackStep(currentStep.tag, currentStepNum); } self.startNextStep(); @@ -1034,16 +1040,22 @@ TutorialManager = function() { ++currentStepNum; + // This always needs to be set because we use this value when + // tracking that the user has gone through the entry portal. When the + // tutorial finishes, there is a last "pseudo" step that the user + // finishes when stepping into the portal. + startedLastStepAt = Date.now(); + if (currentStepNum >= STEPS.length) { // Done info("DONE WITH TUTORIAL"); currentStepNum = -1; currentStep = null; + didFinishTutorial = true; return false; } else { info("Starting step", currentStepNum); currentStep = STEPS[currentStepNum]; - startedLastStepAt = Date.now(); currentStep.start(this.onFinish); return true; } @@ -1059,11 +1071,30 @@ TutorialManager = function() { this.stopTutorial = function() { if (currentStep) { currentStep.cleanup(); + HMD.requestHideHandControllers(); } reenableEverything(); currentStepNum = -1; currentStep = null; } + + this.trackStep = function(name, stepNum) { + var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000; + var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; + UserActivityLogger.tutorialProgress( + name, stepNum, timeToFinishStep, tutorialTimeElapsed, + tutorialID, VERSION); + } + + // This is a message sent from the "entry" portal in the courtyard, + // after the tutorial has finished. + this.enteredEntryPortal = function() { + info("Got enteredEntryPortal"); + if (didFinishTutorial) { + info("Tracking wentToEntry"); + this.trackStep("wentToEntry", wentToEntryStepNum); + } + } } // To run the tutorial: diff --git a/tutorial/tutorialEntityIDs.js b/tutorial/tutorialEntityIDs.js index 38bd06e5ff..14b2a69892 100644 --- a/tutorial/tutorialEntityIDs.js +++ b/tutorial/tutorialEntityIDs.js @@ -83,7 +83,7 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = { } }, "equip-part2": { - "{8b92eec5-aeed-4368-bce0-432cc9ad4c51}": { + "{b5d17eda-90ab-40cf-b973-efcecb2e992e}": { "tag": "equip-part2" }, "{6307cd16-dd1d-4988-a339-578178436b45}": { @@ -148,4 +148,168 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = { "tag": "orient" } } -} +}; + +TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP = { + "tutorial/gun_spawn": { + "userData": "{\"tag\":\"equip\",\"visible\":false}", + "dimensions": { + "y": 0.0649842768907547, + "x": 0.0649842768907547, + "z": 0.0649842768907547 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.6283726096153259, + "x": 0.6865367293357849, + "scale": 0.11255607008934021, + "z": 0.3359576463699341 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + "y": 0.6846506595611572, + "x": 0.7428147792816162, + "z": 0.3922356963157654 + }, + "rotation": { + "y": 0.7066605091094971, + "x": 0.7066605091094971, + "z": -0.025131583213806152, + "w": -0.025101065635681152 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{9df518da-9e65-4b76-8a79-eeefdb0b7310}", + "name": "tutorial/gun_spawn" + }, + "tutorial/nearGrab/box_spawn": { + "userData": "{\"tag\":\"nearGrab\",\"visible\":false}", + "dimensions": { + "y": 0.08225371688604355, + "x": 0.08225371688604355, + "z": 0.08225371688604355 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 255, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.738319456577301, + "x": 0.8985498547554016, + "scale": 0.14246761798858643, + "z": 0.29067665338516235 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + "y": 0.8095532655715942, + "x": 0.9697836637496948, + "z": 0.36191046237945557 + }, + "rotation": { + "y": -1.52587890625e-05, + "x": -1.52587890625e-05, + "z": -1.52587890625e-05, + "w": 1 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{5cf22b9c-fb22-4854-8821-554422980b24}", + "name": "tutorial/nearGrab/box_spawn" + }, + "tutorial/farGrab/box_spawn": { + "userData": "{\"tag\":\"farGrab\",\"visible\":false}", + "dimensions": { + "y": 0.37358683347702026, + "x": 0.37358683347702026, + "z": 0.37358683347702026 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 255, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.3304251432418823, + "x": 3.0951309204101562, + "scale": 0.647071361541748, + "z": 0.18027013540267944 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + x: 3.4866, + y: 0.6716, + z: 0.4789 + }, + "rotation": { + "y": -1.52587890625e-05, + "x": -1.52587890625e-05, + "z": -1.52587890625e-05, + "w": 1 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{70fcd96c-cd59-4f23-9ca5-a167f2f85680}", + "name": "tutorial/farGrab/box_spawn" + }, + "tutorial/teleport/pad": { + "userData": "{\"tag\":\"teleport\"}", + "rotation": { + "y": -0.9702650308609009, + "x": -2.1246911273919977e-05, + "z": -4.222852112434339e-06, + "w": 0.2420452982187271 + }, + "dimensions": { + "y": 0.4365682601928711, + "x": 2.1751723289489746, + "z": 2.175173044204712 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "queryAACube": { + "y": -1.7979401350021362, + "x": 7.5136213302612305, + "scale": 3.106983184814453, + "z": -1.4602710008621216 + }, + "visible": 0, + "angularVelocity": { + "y": -0.5235987901687622, + "x": 0, + "z": 0 + }, + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "angularDamping": 0, + "position": { + "y": -0.2444484978914261, + "x": 9.067112922668457, + "z": 0.09322060644626617 + }, + "modelURL": "atp:/alan/dev/Teleport-Pad.fbx", + "ignoreForCollisions": 1, + "type": "Model", + "id": "{4478f7b5-d3ac-4213-9a7b-ad8cd69575b8}", + "name": "tutorial/teleport/pad" + } +}; diff --git a/tutorial/tutorialStartZone.js b/tutorial/tutorialStartZone.js index 5cff1a4e99..cb0d223200 100644 --- a/tutorial/tutorialStartZone.js +++ b/tutorial/tutorialStartZone.js @@ -20,7 +20,7 @@ print("TutorialStartZone | Parent ID is: ", parentID); if (parentID) { print("TutorialStartZone | Sending start"); - Entities.callEntityMethod(parentID, 'start'); + Entities.callEntityMethod(parentID, 'onEnteredStartZone'); } else { print("TutorialStartZone | ERROR: No parent id found on tutorial start zone"); } @@ -29,7 +29,7 @@ sendStart(); } else { print("TutorialStartZone | User tried to go to tutorial with HMD and hand controllers, sending back to /"); - Window.alert("To proceed with this tutorial, please connect your VR headset and hand controllers."); + Window.alert("To proceed with this tutorial, please connect your Vive headset and hand controllers."); location = "/"; } }, @@ -38,6 +38,12 @@ if (this.sendStartIntervalID) { Script.clearInterval(this.sendStartIntervalID); } + var parentID = Entities.getEntityProperties(this.entityID, 'parentID').parentID; + print("TutorialStartZone | Parent ID is: ", parentID); + if (parentID) { + print("TutorialStartZone | Sending onLeftStartZone"); + Entities.callEntityMethod(parentID, 'on'); + } } }; diff --git a/tutorial/tutorialZone.js b/tutorial/tutorialZone.js index db7306a529..01e2aa4c52 100644 --- a/tutorial/tutorialZone.js +++ b/tutorial/tutorialZone.js @@ -27,10 +27,14 @@ if (!Function.prototype.bind) { } (function() { - var ownershipTokenPath = Script.resolvePath("ownershipToken.js"); - var tutorialPath = Script.resolvePath("tutorial.js"); - Script.include(ownershipTokenPath); - Script.include(tutorialPath); + Script.include("ownershipToken.js"); + Script.include("tutorial.js"); + + var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; + function setAwayEnabled(value) { + var message = value ? 'enable' : 'disable'; + Messages.sendLocalMessage(CHANNEL_AWAY_ENABLE, message); + } var TutorialZone = function() { print("TutorialZone | Creating"); @@ -59,11 +63,16 @@ if (!Function.prototype.bind) { print("TutorialZone | Preload"); this.entityID = entityID; }, - start: function() { - print("TutorialZone | Got start"); + onEnteredStartZone: function() { + print("TutorialZone | Got onEnteredStartZone"); var self = this; if (!this.token) { print("TutorialZone | Creating token"); + // The start zone has been entered, hide the overlays immediately + setAwayEnabled(false); + Menu.setIsOptionChecked("Overlays", false); + MyAvatar.shouldRenderLocally = false; + Toolbars.getToolbar("com.highfidelity.interface.toolbar.system").writeProperty("visible", false); this.token = new OwnershipToken(Math.random() * 100000, this.entityID, { onGainedOwnership: function(token) { print("TutorialZone | GOT OWNERSHIP"); @@ -91,6 +100,26 @@ if (!Function.prototype.bind) { }); } }, + onLeftStartZone: function() { + print("TutorialZone | Got onLeftStartZone"); + + // If the start zone was exited, and the tutorial hasn't started, go ahead and + // re-enable the HUD/Overlays + if (!this.tutorialManager) { + Menu.setIsOptionChecked("Overlays", true); + MyAvatar.shouldRenderLocally = true; + setAwayEnabled(true); + Toolbars.getToolbar("com.highfidelity.interface.toolbar.system").writeProperty("visible", true); + } + }, + + onEnteredEntryPortal: function() { + print("TutorialZone | Got onEnteredEntryPortal"); + if (this.tutorialManager) { + print("TutorialZone | Calling enteredEntryPortal"); + this.tutorialManager.enteredEntryPortal(); + } + }, enterEntity: function() { print("TutorialZone | ENTERED THE TUTORIAL AREA"); @@ -102,6 +131,10 @@ if (!Function.prototype.bind) { this.token.destroy(); this.token = null; } + if (this.tutorialManager) { + this.tutorialManager.stopTutorial(); + //this.tutorialManager = null; + } } }; diff --git a/unpublishedScripts/DomainContent/Home/portal.js b/unpublishedScripts/DomainContent/Home/portal.js index 0ea090a6f7..ea6241265c 100644 --- a/unpublishedScripts/DomainContent/Home/portal.js +++ b/unpublishedScripts/DomainContent/Home/portal.js @@ -1,40 +1,47 @@ (function(){ var teleport; var portalDestination; + var thisEntityID; function playSound() { - Audio.playSound(teleport, { volume: 0.40, localOnly: true }); + var properties = Entities.getEntityProperties(thisEntityID, 'position'); + if (properties) { + Audio.playSound(teleport, { position: properties.position, volume: 0.40, localOnly: true }); + } }; this.preload = function(entityID) { + thisEntityID = entityID; teleport = SoundCache.getSound("atp:/sounds/teleport.raw"); - var properties = Entities.getEntityProperties(entityID); - portalDestination = properties.userData; - - print("portal.js | The portal destination is " + portalDestination); + var properties = Entities.getEntityProperties(entityID, 'userData'); + if (properties) { + portalDestination = properties.userData; + print("portal.js | The portal destination is " + portalDestination); + } } this.enterEntity = function(entityID) { print("portal.js | enterEntity"); - var properties = Entities.getEntityProperties(entityID); // in case the userData/portalURL has changed - portalDestination = properties.userData; + var properties = Entities.getEntityProperties(entityID, 'userData'); // in case the userData/portalURL has changed + if (properties) { + portalDestination = properties.userData; - print("portal.js | enterEntity() .... The portal destination is " + portalDestination); + print("portal.js | enterEntity() .... The portal destination is " + portalDestination); - if (portalDestination.length > 0) { - if (portalDestination[0] == '/') { - print("Teleporting to " + portalDestination); - Window.location = portalDestination; + if (portalDestination.length > 0) { + if (portalDestination[0] == '/') { + print("Teleporting to " + portalDestination); + Window.location = portalDestination; + } else { + print("Teleporting to hifi://" + portalDestination); + Window.location = "hifi://" + portalDestination; + } } else { - print("Teleporting to hifi://" + portalDestination); - Window.location = "hifi://" + portalDestination; + location.goToEntry(); // going forward: no data means go to appropriate entry point } - } else { - location.goToEntry(); // going forward: no data means go to appropriate entry point } - }; this.leaveEntity = function(entityID) {