diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 8ba909d32b..23bee2c91a 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -27,12 +28,16 @@ #include #include #include +#include #include #include #include #include +#include +#include + #include #include // TODO: consider moving to scriptengine.h @@ -42,6 +47,7 @@ #include "AbstractAudioInterface.h" #include "Agent.h" +#include "AvatarAudioTimer.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; @@ -62,6 +68,7 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -72,6 +79,17 @@ Agent::Agent(ReceivedMessage& message) : { PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); + packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); +} + +void Agent::playAvatarSound(SharedSoundPointer sound) { + // this must happen on Agent's main thread + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound)); + return; + } else { + setAvatarSound(sound); + } } void Agent::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -118,7 +136,6 @@ void Agent::handleAudioPacket(QSharedPointer message) { _receivedAudioStream.parseData(*message); _lastReceivedAudioLoudness = _receivedAudioStream.getNextOutputFrameLoudness(); - _receivedAudioStream.clearBuffer(); } @@ -214,6 +231,59 @@ void Agent::nodeActivated(SharedNodePointer activatedNode) { _pendingScriptRequest = nullptr; } + if (activatedNode->getType() == NodeType::AudioMixer) { + negotiateAudioFormat(); + } +} + +void Agent::negotiateAudioFormat() { + auto nodeList = DependencyManager::get(); + auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + quint8 numberOfCodecs = (quint8)codecPlugins.size(); + negotiateFormatPacket->writePrimitive(numberOfCodecs); + for (auto& plugin : codecPlugins) { + auto codecName = plugin->getName(); + negotiateFormatPacket->writeString(codecName); + } + + // grab our audio mixer from the NodeList, if it exists + SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (audioMixer) { + // send off this mute packet + nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); + } +} + +void Agent::handleSelectedAudioFormat(QSharedPointer message) { + QString selectedCodecName = message->readString(); + selectAudioFormat(selectedCodecName); +} + +void Agent::selectAudioFormat(const QString& selectedCodecName) { + _selectedCodecName = selectedCodecName; + + qDebug() << "Selected Codec:" << _selectedCodecName; + + // release any old codec encoder/decoder first... + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + _codec = nullptr; + } + _receivedAudioStream.cleanupCodec(); + + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& plugin : codecPlugins) { + if (_selectedCodecName == plugin->getName()) { + _codec = plugin; + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); + _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + qDebug() << "Selected Codec Plugin:" << _codec.get(); + break; + } + } } void Agent::scriptRequestFinished() { @@ -291,10 +361,6 @@ void Agent::executeScript() { // register ourselves to the script engine _scriptEngine->registerGlobalObject("Agent", this); - // FIXME -we shouldn't be calling this directly, it's normally called by run(), not sure why - // viewers would need this called. - //_scriptEngine->init(); // must be done before we set up the viewers - _scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor); @@ -314,10 +380,18 @@ void Agent::executeScript() { entityScriptingInterface->setEntityTree(_entityViewer.getTree()); DependencyManager::set(_entityViewer.getTree()); - - // wire up our additional agent related processing to the update signal - QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); - + + // 100Hz timer for audio + AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); + audioTimerWorker->moveToThread(&_avatarAudioTimerThread); + connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio); + connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); + connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); + connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); + _avatarAudioTimerThread.start(); + + // 60Hz timer for avatar + QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatar); _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); @@ -343,6 +417,10 @@ void Agent::setIsAvatar(bool isAvatar) { // start the timers _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + + // tell the avatarAudioTimer to start ticking + emit startAvatarAudioTimer(); + } if (!_isAvatar) { @@ -367,6 +445,7 @@ void Agent::setIsAvatar(bool isAvatar) { nodeList->sendPacketList(std::move(packetList), *node); }); } + emit stopAvatarAudioTimer(); } } @@ -377,11 +456,9 @@ void Agent::sendAvatarIdentityPacket() { } } -void Agent::processAgentAvatarAndAudio(float deltaTime) { +void Agent::processAgentAvatar() { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); - const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / SCRIPT_FPS + 0.5; - const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); scriptedAvatar->doneEncoding(true); @@ -395,95 +472,120 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { auto nodeList = DependencyManager::get(); nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + } +} +void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { + _flushEncoder = false; + static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); + if (_encoder) { + _encoder->encode(zeros, encodedZeros); + } else { + encodedZeros = zeros; + } +} - if (_isListeningToAudioStream || _avatarSound) { - // if we have an avatar audio stream then send it out to our audio-mixer - bool silentFrame = true; +void Agent::processAgentAvatarAudio() { + if (_isAvatar && (_isListeningToAudioStream || _avatarSound)) { + // if we have an avatar audio stream then send it out to our audio-mixer + auto scriptedAvatar = DependencyManager::get(); + bool silentFrame = true; - int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES; - const int16_t* nextSoundOutput = NULL; + int16_t numAvailableSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + const int16_t* nextSoundOutput = NULL; - if (_avatarSound) { - const QByteArray& soundByteArray = _avatarSound->getByteArray(); - nextSoundOutput = reinterpret_cast(soundByteArray.data() + if (_avatarSound) { + const QByteArray& soundByteArray = _avatarSound->getByteArray(); + nextSoundOutput = reinterpret_cast(soundByteArray.data() + _numAvatarSoundSentBytes); - int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES - ? SCRIPT_AUDIO_BUFFER_BYTES - : soundByteArray.size() - _numAvatarSoundSentBytes; - numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); + int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + ? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + : soundByteArray.size() - _numAvatarSoundSentBytes; + numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); - // check if the all of the _numAvatarAudioBufferSamples to be sent are silence - for (int i = 0; i < numAvailableSamples; ++i) { - if (nextSoundOutput[i] != 0) { - silentFrame = false; - break; - } - } - - _numAvatarSoundSentBytes += numAvailableBytes; - if (_numAvatarSoundSentBytes == soundByteArray.size()) { - // we're done with this sound object - so set our pointer back to NULL - // and our sent bytes back to zero - _avatarSound.clear(); - _numAvatarSoundSentBytes = 0; + // check if the all of the _numAvatarAudioBufferSamples to be sent are silence + for (int i = 0; i < numAvailableSamples; ++i) { + if (nextSoundOutput[i] != 0) { + silentFrame = false; + break; } } - auto audioPacket = NLPacket::create(silentFrame + _numAvatarSoundSentBytes += numAvailableBytes; + if (_numAvatarSoundSentBytes == soundByteArray.size()) { + // we're done with this sound object - so set our pointer back to NULL + // and our sent bytes back to zero + _avatarSound.clear(); + _numAvatarSoundSentBytes = 0; + _flushEncoder = true; + } + } + + auto audioPacket = NLPacket::create(silentFrame && !_flushEncoder ? PacketType::SilentAudioFrame : PacketType::MicrophoneAudioNoEcho); - // seek past the sequence number, will be packed when destination node is known - audioPacket->seek(sizeof(quint16)); + // seek past the sequence number, will be packed when destination node is known + audioPacket->seek(sizeof(quint16)); - if (silentFrame) { - if (!_isListeningToAudioStream) { - // if we have a silent frame and we're not listening then just send nothing and break out of here - return; - } - - // write the number of silent samples so the audio-mixer can uphold timing - audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(scriptedAvatar->getPosition()); - glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - } else if (nextSoundOutput) { - // write the codec - QString codecName; - audioPacket->writeString(codecName); - - // assume scripted avatar audio is mono and set channel flag to zero - audioPacket->writePrimitive((quint8)0); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(scriptedAvatar->getPosition()); - glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - // write the raw audio data - audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples * sizeof(int16_t)); + if (silentFrame) { + if (!_isListeningToAudioStream) { + // if we have a silent frame and we're not listening then just send nothing and break out of here + return; } - // write audio packet to AudioMixer nodes - auto nodeList = DependencyManager::get(); - nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){ - // only send to nodes of type AudioMixer - if (node->getType() == NodeType::AudioMixer) { - // pack sequence number - quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; - audioPacket->seek(0); - audioPacket->writePrimitive(sequence); + // write the codec + audioPacket->writeString(_selectedCodecName); + + // write the number of silent samples so the audio-mixer can uphold timing + audioPacket->writePrimitive(numAvailableSamples); - // send audio packet - nodeList->sendUnreliablePacket(*audioPacket, *node); + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(scriptedAvatar->getPosition()); + glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + } else if (nextSoundOutput) { + + // write the codec + audioPacket->writeString(_selectedCodecName); + + // assume scripted avatar audio is mono and set channel flag to zero + audioPacket->writePrimitive((quint8)0); + + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(scriptedAvatar->getPosition()); + glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + + QByteArray encodedBuffer; + if (_flushEncoder) { + encodeFrameOfZeros(encodedBuffer); + } else { + 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()); } + + // write audio packet to AudioMixer nodes + auto nodeList = DependencyManager::get(); + nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) { + // only send to nodes of type AudioMixer + if (node->getType() == NodeType::AudioMixer) { + // pack sequence number + quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; + audioPacket->seek(0); + audioPacket->writePrimitive(sequence); + // send audio packet + nodeList->sendUnreliablePacket(*audioPacket, *node); + } + }); } } @@ -498,7 +600,17 @@ void Agent::aboutToFinish() { DependencyManager::get()->setEntityTree(nullptr); ResourceManager::cleanup(); - + // cleanup the AudioInjectorManager (and any still running injectors) DependencyManager::destroy(); + DependencyManager::destroy(); + + emit stopAvatarAudioTimer(); + _avatarAudioTimerThread.quit(); + + // cleanup codec & encoder + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + } } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 3f4e36374c..939b51625a 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -26,8 +27,9 @@ #include #include -#include "MixedAudioStream.h" +#include +#include "MixedAudioStream.h" class Agent : public ThreadedAssignment { Q_OBJECT @@ -56,7 +58,7 @@ public: public slots: void run() override; - void playAvatarSound(SharedSoundPointer avatarSound) { setAvatarSound(avatarSound); } + void playAvatarSound(SharedSoundPointer avatarSound); private slots: void requestScript(); @@ -66,12 +68,21 @@ private slots: void handleAudioPacket(QSharedPointer message); void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); - - void processAgentAvatarAndAudio(float deltaTime); + void handleSelectedAudioFormat(QSharedPointer message); void nodeActivated(SharedNodePointer activatedNode); + + void processAgentAvatar(); + void processAgentAvatarAudio(); +signals: + void startAvatarAudioTimer(); + void stopAvatarAudioTimer(); private: + void negotiateAudioFormat(); + void selectAudioFormat(const QString& selectedCodecName); + void encodeFrameOfZeros(QByteArray& encodedZeros); + std::unique_ptr _scriptEngine; EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; @@ -92,7 +103,12 @@ private: bool _isAvatar = false; QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; - + + CodecPluginPointer _codec; + QString _selectedCodecName; + Encoder* _encoder { nullptr }; + QThread _avatarAudioTimerThread; + bool _flushEncoder { false }; }; #endif // hifi_Agent_h diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 800f00b352..3b4300b6fd 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include "AssignmentFactory.h" #include "AssignmentActionFactory.h" @@ -53,10 +52,9 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri QSettings::setDefaultFormat(QSettings::IniFormat); DependencyManager::set(); - + auto scriptableAvatar = DependencyManager::set(); auto addressManager = DependencyManager::set(); - auto scriptEngines = DependencyManager::set(); // create a NodeList as an unassigned client, must be after addressManager auto nodeList = DependencyManager::set(NodeType::Unassigned, listenPort); @@ -178,8 +176,6 @@ AssignmentClient::~AssignmentClient() { void AssignmentClient::aboutToQuit() { stopAssignmentClient(); - DependencyManager::destroy(); - // clear the log handler so that Qt doesn't call the destructor on LogHandler qInstallMessageHandler(0); } diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp new file mode 100644 index 0000000000..77dd61043e --- /dev/null +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -0,0 +1,37 @@ +// +// AvatarAudioTimer.cpp +// assignment-client/src +// +// Created by David Kelly on 10/12/13. +// 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 +// +#include +#include +#include "AvatarAudioTimer.h" + +// 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() << __FUNCTION__; + auto startTime = usecTimestampNow(); + quint64 frameCounter = 0; + const int TARGET_INTERVAL_USEC = 10000; // 10ms + while (!_quit) { + ++frameCounter; + + // tick every 10ms from startTime + quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; + quint64 now = usecTimestampNow(); + + // avoid quint64 underflow + if (now < targetTime) { + usleep(targetTime - now); + } + + emit avatarTick(); + } + qDebug() << "AvatarAudioTimer is finished"; +} diff --git a/assignment-client/src/AvatarAudioTimer.h b/assignment-client/src/AvatarAudioTimer.h new file mode 100644 index 0000000000..1f6381b030 --- /dev/null +++ b/assignment-client/src/AvatarAudioTimer.h @@ -0,0 +1,31 @@ +// +// AvatarAudioTimer.h +// assignment-client/src +// +// Created by David Kelly on 10/12/13. +// 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 +// + +#ifndef hifi_AvatarAudioTimer_h +#define hifi_AvatarAudioTimer_h + +#include + +class AvatarAudioTimer : public QObject { + Q_OBJECT + +signals: + void avatarTick(); + +public slots: + void start(); + void stop() { _quit = true; } + +private: + bool _quit { false }; +}; + +#endif //hifi_AvatarAudioTimer_h diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 905cc6fd30..2fbe2f6dfe 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -453,7 +453,7 @@ bool AssetServer::loadMappingsFromFile() { while (it != _fileMappings.end()) { bool shouldDrop = false; - if (!isValidPath(it.key())) { + if (!isValidFilePath(it.key())) { qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path."; shouldDrop = true; } @@ -508,7 +508,7 @@ bool AssetServer::writeMappingsToFile() { bool AssetServer::setMapping(AssetPath path, AssetHash hash) { path = path.trimmed(); - if (!isValidPath(path)) { + if (!isValidFilePath(path)) { qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash; return false; } @@ -637,8 +637,8 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { oldPath = oldPath.trimmed(); newPath = newPath.trimmed(); - if (!isValidPath(oldPath) || !isValidPath(newPath)) { - qWarning() << "Cannot perform rename with invalid paths - both should have leading forward slashes:" + if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) { + qWarning() << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:" << oldPath << "=>" << newPath; return false; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index ccd80a4a11..bc356b8ce1 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -90,8 +90,8 @@ 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"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); @@ -481,6 +481,7 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) { } void AudioMixer::handleNodeAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { + getOrCreateClientData(sendingNode.data()); DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); } @@ -579,18 +580,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); } @@ -636,13 +627,18 @@ QString AudioMixer::percentageForMixStats(int counter) { } void AudioMixer::sendStatsPacket() { - static QJsonObject statsObject; + QJsonObject statsObject; + + if (_numStatFrames == 0) { + return; + } statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == -1; 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); @@ -656,6 +652,7 @@ void AudioMixer::sendStatsPacket() { statsObject["mix_stats"] = mixStats; + _sumStreams = 0; _sumListeners = 0; _hrtfRenders = 0; _hrtfSilentRenders = 0; @@ -703,17 +700,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(); @@ -726,79 +730,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() @@ -814,7 +810,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); @@ -827,12 +824,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); @@ -872,24 +874,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..764c54c6cb 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -52,6 +52,7 @@ private slots: void removeHRTFsForFinishedInjector(const QUuid& streamID); private: + AudioMixerClientData* getOrCreateClientData(Node* node); void domainSettingsRequestComplete(); /// adds one stream to the mix for a listening node @@ -85,6 +86,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 290a361118..58d89697af 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -49,7 +49,7 @@ AudioMixerClientData::~AudioMixerClientData() { AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { QReadLocker readLocker { &_streamsLock }; - + auto it = _audioStreams.find(QUuid()); if (it != _audioStreams.end()) { return dynamic_cast(it->second.get()); @@ -75,7 +75,7 @@ void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid& int AudioMixerClientData::parseData(ReceivedMessage& message) { PacketType packetType = message.getType(); - + if (packetType == PacketType::AudioStreamStats) { // skip over header, appendFlag, and num stats packed @@ -180,7 +180,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { return 0; } -void AudioMixerClientData::checkBuffersBeforeFrameSend() { +int AudioMixerClientData::checkBuffersBeforeFrameSend() { QWriteLocker writeLocker { &_streamsLock }; auto it = _audioStreams.begin(); @@ -208,6 +208,8 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend() { ++it; } } + + return (int)_audioStreams.size(); } bool AudioMixerClientData::shouldSendStats(int frameNumber) { @@ -218,11 +220,10 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer& auto nodeList = DependencyManager::get(); - // The append flag is a boolean value that will be packed right after the header. The first packet sent - // inside this method will have 0 for this flag, while every subsequent packet will have 1 for this flag. - // The sole purpose of this flag is so the client can clear its map of injected audio stream stats when - // it receives a packet with an appendFlag of 0. This prevents the buildup of dead audio stream stats in the client. - quint8 appendFlag = 0; + // The append flag is a boolean value that will be packed right after the header. + // This flag allows the client to know when it has received all stats packets, so it can group any downstream effects, + // and clear its cache of injector stream stats; it helps to prevent buildup of dead audio stream stats in the client. + quint8 appendFlag = AudioStreamStats::START; auto streamsCopy = getAudioStreams(); @@ -233,14 +234,21 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer& while (numStreamStatsRemaining > 0) { auto statsPacket = NLPacket::create(PacketType::AudioStreamStats); - // pack the append flag in this packet - statsPacket->writePrimitive(appendFlag); - appendFlag = 1; - int numStreamStatsRoomFor = (int)(statsPacket->size() - sizeof(quint8) - sizeof(quint16)) / sizeof(AudioStreamStats); - // calculate and pack the number of stream stats to follow + // calculate the number of stream stats to follow quint16 numStreamStatsToPack = std::min(numStreamStatsRemaining, numStreamStatsRoomFor); + + // is this the terminal packet? + if (numStreamStatsRemaining <= numStreamStatsToPack) { + appendFlag |= AudioStreamStats::END; + } + + // pack the append flag in this packet + statsPacket->writePrimitive(appendFlag); + appendFlag = 0; + + // pack the number of stream stats to follow statsPacket->writePrimitive(numStreamStatsToPack); // pack the calculated number of stream stats @@ -349,7 +357,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); } @@ -360,6 +371,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..34263f9cbe 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -52,7 +52,8 @@ public: 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 +77,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 +110,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/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 82e88f67ef..15877eed25 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -46,14 +46,21 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() { return _animationDetails; } +void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { + _bind.reset(); + _animSkeleton.reset(); + AvatarData::setSkeletonModelURL(skeletonModelURL); +} void ScriptableAvatar::update(float deltatime) { if (_bind.isNull() && !_skeletonFBXURL.isEmpty()) { // AvatarData will parse the .fst, but not get the .fbx skeleton. _bind = DependencyManager::get()->getAnimation(_skeletonFBXURL); } // Run animation - if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && _bind->isLoaded()) { - + if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && !_bind.isNull() && _bind->isLoaded()) { + if (!_animSkeleton) { + _animSkeleton = std::make_shared(_bind->getGeometry()); + } float currentFrame = _animationDetails.currentFrame + deltatime * _animationDetails.fps; if (_animationDetails.loop || currentFrame < _animationDetails.lastFrame) { while (currentFrame >= _animationDetails.lastFrame) { @@ -64,14 +71,16 @@ void ScriptableAvatar::update(float deltatime) { const QVector& modelJoints = _bind->getGeometry().joints; QStringList animationJointNames = _animation->getJointNames(); - if (_jointData.size() != modelJoints.size()) { - _jointData.resize(modelJoints.size()); + const int nJoints = modelJoints.size(); + if (_jointData.size() != nJoints) { + _jointData.resize(nJoints); } const int frameCount = _animation->getFrames().size(); const FBXAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); const FBXAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); const float frameFraction = glm::fract(currentFrame); + std::vector poses = _animSkeleton->getRelativeDefaultPoses(); for (int i = 0; i < animationJointNames.size(); i++) { const QString& name = animationJointNames[i]; @@ -79,18 +88,21 @@ void ScriptableAvatar::update(float deltatime) { // trusting the .fst (which is sometimes not updated to match changes to .fbx). int mapping = _bind->getGeometry().getJointIndex(name); if (mapping != -1 && !_maskedJoints.contains(name)) { - JointData& data = _jointData[mapping]; - - auto newRotation = modelJoints[mapping].preRotation * - safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction); - // We could probably do translations as in interpolation in model space (rather than the parent space that each frame is in), - // but we don't do so for MyAvatar yet, so let's not be different here. - if (data.rotation != newRotation) { - data.rotation = newRotation; - data.rotationSet = true; - } + // Eventually, this should probably deal with post rotations and translations, too. + poses[mapping].rot = modelJoints[mapping].preRotation * + safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);; } } + _animSkeleton->convertRelativePosesToAbsolute(poses); + for (int i = 0; i < nJoints; i++) { + JointData& data = _jointData[i]; + AnimPose& pose = poses[i]; + if (data.rotation != pose.rot) { + data.rotation = pose.rot; + data.rotationSet = true; + } + } + } else { _animation.clear(); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 30c48d02bf..56707de471 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -13,6 +13,7 @@ #define hifi_ScriptableAvatar_h #include +#include #include #include @@ -25,6 +26,7 @@ public: bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); Q_INVOKABLE void stopAnimation(); Q_INVOKABLE AnimationDetails getAnimationDetails(); + virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; private slots: void update(float deltatime); @@ -34,6 +36,7 @@ private: AnimationDetails _animationDetails; QStringList _maskedJoints; AnimationPointer _bind; // a sleazy way to get the skeleton, given the various library/cmake dependencies + std::shared_ptr _animSkeleton; }; #endif // hifi_ScriptableAvatar_h \ No newline at end of file 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/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 531f9a1da2..6ce3c95cdd 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -20,7 +20,7 @@ macro(SET_PACKAGING_PARAMETERS) set(RELEASE_NUMBER $ENV{RELEASE_NUMBER}) string(TOLOWER "$ENV{BRANCH}" BUILD_BRANCH) set(BUILD_GLOBAL_SERVICES "DEVELOPMENT") - set(USE_STABLE_GLOBAL_SERVICES FALSE) + set(USE_STABLE_GLOBAL_SERVICES 0) message(STATUS "The BUILD_BRANCH variable is: ${BUILD_BRANCH}") message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}") @@ -43,7 +43,7 @@ macro(SET_PACKAGING_PARAMETERS) if (BUILD_BRANCH STREQUAL "stable") message(STATUS "The RELEASE_TYPE is PRODUCTION and the BUILD_BRANCH is stable...") set(BUILD_GLOBAL_SERVICES "STABLE") - set(USE_STABLE_GLOBAL_SERVICES TRUE) + set(USE_STABLE_GLOBAL_SERVICES 1) endif() elseif (RELEASE_TYPE STREQUAL "PR") 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/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 8a7d03ba75..4006673cad 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -863,15 +863,6 @@ "help": "The path to the directory assets are stored in.
If this path is relative, it will be relative to the application data directory.
If you change this path you will need to manually copy any existing assets from the previous directory.", "default": "", "advanced": true - }, - { - "name": "max_bandwidth", - "type": "double", - "label": "Max Bandwidth Per User", - "help": "The maximum upstream bandwidth each user can use (in Mb/s).", - "placeholder": "10.0", - "default": "", - "advanced": true } ] }, diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 26c37f1918..8b73b851b2 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -519,7 +519,7 @@ void DomainServer::setupNodeListAndAssignments() { // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); - // set a custum packetVersionMatch as the verify packet operator for the udt::Socket + // set a custom packetVersionMatch as the verify packet operator for the udt::Socket nodeList->setPacketFilterOperator(&DomainServer::packetVersionMatch); } @@ -2267,7 +2267,7 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag QByteArray viewpointUTF8 = responseViewpoint.toUtf8(); // prepare a packet for the response - auto pathResponsePacket = NLPacket::create(PacketType::DomainServerPathResponse); + auto pathResponsePacket = NLPacket::create(PacketType::DomainServerPathResponse, -1, true); // check the number of bytes the viewpoint is quint16 numViewpointBytes = viewpointUTF8.size(); diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index 3f229d2f87..f8bf1f62ae 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -30,7 +30,7 @@ const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000; IceServer::IceServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _id(QUuid::createUuid()), - _serverSocket(), + _serverSocket(0, false), _activePeers() { // start the ice-server socket diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 9c564f6518..b43376c374 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -41,8 +41,10 @@ endif () if (ANDROID) set(PLATFORM_QT_COMPONENTS AndroidExtras) + set(PLATFORM_QT_LIBRARIES Qt5::AndroidExtras) else () set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets) + set(PLATFORM_QT_LIBRARIES Qt5::WebEngine Qt5::WebEngineWidgets) endif () find_package( @@ -244,7 +246,8 @@ target_link_libraries( ${TARGET_NAME} Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg - Qt5::WebChannel Qt5::WebEngine + Qt5::WebChannel Qt5::WebEngine + ${PLATFORM_QT_LIBRARIES} ) if (UNIX) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index c50f7681d9..236a200841 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -11,43 +11,35 @@ { "from": "OculusTouch.LY", "to": "Standard.LY", "filters": [ - { "type": "deadZone", "min": 0.05 }, + { "type": "deadZone", "min": 0.3 }, "invert" ] }, - { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" }, - { "from": "OculusTouch.LT", "to": "Standard.LTClick", + { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.LX" }, + { "from": "OculusTouch.LT", "to": "Standard.LTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LTClick", - "peek": true, - "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] - }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, - { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, + { "from": "OculusTouch.LeftGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.LeftGrip" }, + { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, { "from": "OculusTouch.RY", "to": "Standard.RY", "filters": [ - { "type": "deadZone", "min": 0.05 }, + { "type": "deadZone", "min": 0.3 }, "invert" ] }, - { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" }, - { "from": "OculusTouch.RT", "to": "Standard.RTClick", + { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.RX" }, + { "from": "OculusTouch.RT", "to": "Standard.RTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.LTClick", - "peek": true, - "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] - }, - { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, - { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, + { "from": "OculusTouch.RightGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.RightGrip" }, + { "from": "OculusTouch.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, { "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" }, @@ -66,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/controllers/xbox.json b/interface/resources/controllers/xbox.json index c4b9da9ae2..b0e97b849f 100644 --- a/interface/resources/controllers/xbox.json +++ b/interface/resources/controllers/xbox.json @@ -3,6 +3,11 @@ "channels": [ { "from": "GamePad.LY", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateZ" }, { "from": "GamePad.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" }, + + { "from": "GamePad.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "GamePad.LT", "to": "Standard.LT" }, { "from": "GamePad.LB", "to": "Standard.LB" }, { "from": "GamePad.LS", "to": "Standard.LS" }, @@ -31,6 +36,10 @@ ] }, + { "from": "GamePad.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "GamePad.RT", "to": "Standard.RT" }, { "from": "GamePad.RB", "to": "Standard.RB" }, { "from": "GamePad.RS", "to": "Standard.RS" }, diff --git a/interface/resources/html/img/controls-help-gamepad.png b/interface/resources/html/img/controls-help-gamepad.png index 6887c88adb..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/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index aeca4dc112..3897c9ff3f 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -11,6 +11,7 @@ var POLL_FREQUENCY = 500; // ms var MAX_WARNINGS = 3; var numWarnings = 0; + var isWindowFocused = true; var isKeyboardRaised = false; var isNumericKeyboard = false; var KEYBOARD_HEIGHT = 200; @@ -38,15 +39,15 @@ var keyboardRaised = shouldRaiseKeyboard(); var numericKeyboard = shouldSetNumeric(); - if (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard) { + if (isWindowFocused && (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard)) { - if (typeof EventBridge !== "undefined") { + if (typeof EventBridge !== "undefined" && EventBridge !== null) { EventBridge.emitWebEvent( keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "")) : "_LOWER_KEYBOARD" ); } else { if (numWarnings < MAX_WARNINGS) { - console.log("WARNING: no global EventBridge object found"); + console.log("WARNING: No global EventBridge object found"); numWarnings++; } } @@ -65,4 +66,14 @@ isNumericKeyboard = numericKeyboard; } }, POLL_FREQUENCY); + + window.addEventListener("focus", function () { + isWindowFocused = true; + }); + + window.addEventListener("blur", function () { + isWindowFocused = false; + isKeyboardRaised = false; + isNumericKeyboard = false; + }); })(); 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/meshes/controller/vive_body.fbm/onepointfive_spec.png b/interface/resources/meshes/controller/vive_body.fbm/onepointfive_spec.png new file mode 100644 index 0000000000..eedf0470a5 Binary files /dev/null and b/interface/resources/meshes/controller/vive_body.fbm/onepointfive_spec.png differ diff --git a/interface/resources/meshes/controller/vive_body.fbm/onepointfive_texture.png b/interface/resources/meshes/controller/vive_body.fbm/onepointfive_texture.png new file mode 100644 index 0000000000..5deca5c7bc Binary files /dev/null and b/interface/resources/meshes/controller/vive_body.fbm/onepointfive_texture.png differ diff --git a/interface/resources/meshes/controller/vive_body.fbx b/interface/resources/meshes/controller/vive_body.fbx new file mode 100644 index 0000000000..91fc1a05f4 Binary files /dev/null and b/interface/resources/meshes/controller/vive_body.fbx differ diff --git a/interface/resources/meshes/controller/vive_button.fbx b/interface/resources/meshes/controller/vive_button.fbx new file mode 100644 index 0000000000..fb2f108ed1 Binary files /dev/null and b/interface/resources/meshes/controller/vive_button.fbx differ diff --git a/interface/resources/meshes/controller/vive_l_grip.fbx b/interface/resources/meshes/controller/vive_l_grip.fbx new file mode 100644 index 0000000000..833fdae663 Binary files /dev/null and b/interface/resources/meshes/controller/vive_l_grip.fbx differ diff --git a/interface/resources/meshes/controller/vive_r_grip.fbx b/interface/resources/meshes/controller/vive_r_grip.fbx new file mode 100644 index 0000000000..ae3957b830 Binary files /dev/null and b/interface/resources/meshes/controller/vive_r_grip.fbx differ diff --git a/interface/resources/meshes/controller/vive_sys_button.fbx b/interface/resources/meshes/controller/vive_sys_button.fbx new file mode 100644 index 0000000000..c72bb4346c Binary files /dev/null and b/interface/resources/meshes/controller/vive_sys_button.fbx differ diff --git a/interface/resources/meshes/controller/vive_tips.fbm/Blank.png b/interface/resources/meshes/controller/vive_tips.fbm/Blank.png new file mode 100644 index 0000000000..ff20ac638e Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbm/Blank.png differ diff --git a/interface/resources/meshes/controller/vive_tips.fbm/Grip.png b/interface/resources/meshes/controller/vive_tips.fbm/Grip.png new file mode 100644 index 0000000000..f23c74c686 Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbm/Grip.png differ diff --git a/interface/resources/meshes/controller/vive_tips.fbm/Rotate.png b/interface/resources/meshes/controller/vive_tips.fbm/Rotate.png new file mode 100644 index 0000000000..7fe7dcc3f7 Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbm/Rotate.png differ diff --git a/interface/resources/meshes/controller/vive_tips.fbm/Teleport.png b/interface/resources/meshes/controller/vive_tips.fbm/Teleport.png new file mode 100644 index 0000000000..fbffd7eea4 Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbm/Teleport.png differ diff --git a/interface/resources/meshes/controller/vive_tips.fbm/Trigger.png b/interface/resources/meshes/controller/vive_tips.fbm/Trigger.png new file mode 100644 index 0000000000..d66c6da3bf Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbm/Trigger.png differ diff --git a/interface/resources/meshes/controller/vive_tips.fbx b/interface/resources/meshes/controller/vive_tips.fbx new file mode 100644 index 0000000000..f363f442f2 Binary files /dev/null and b/interface/resources/meshes/controller/vive_tips.fbx differ diff --git a/interface/resources/meshes/controller/vive_trackpad.fbx b/interface/resources/meshes/controller/vive_trackpad.fbx new file mode 100644 index 0000000000..8202fc2a19 Binary files /dev/null and b/interface/resources/meshes/controller/vive_trackpad.fbx differ diff --git a/interface/resources/meshes/controller/vive_trigger.fbx b/interface/resources/meshes/controller/vive_trigger.fbx new file mode 100644 index 0000000000..f6178bcaa2 Binary files /dev/null and b/interface/resources/meshes/controller/vive_trigger.fbx differ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7ed91a2940..941099b7cc 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -21,20 +21,23 @@ import "controls-uit" as HifiControls Window { id: root HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifiStyleConstants } objectName: "AddressBarDialog" - frame: HiddenFrame {} - hideBackground: true + title: "Go To" shown: false destroyOnHidden: false resizable: false - scale: 1.25 // Make this dialog a little larger than normal + pinnable: false; width: addressBarDialog.implicitWidth height: addressBarDialog.implicitHeight - onShownChanged: addressBarDialog.observeShownChanged(shown); + onShownChanged: { + addressBarDialog.keyboardEnabled = HMD.active; + addressBarDialog.observeShownChanged(shown); + } Component.onCompleted: { root.parentChanged.connect(center); center(); @@ -70,11 +73,12 @@ Window { AddressBarDialog { id: addressBarDialog + property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false implicitWidth: backgroundImage.width - implicitHeight: backgroundImage.height + (keyboardRaised ? 200 : 0) + implicitHeight: backgroundImage.height + (keyboardEnabled ? keyboard.height : 0) + cardHeight; // The buttons have their button state changed on hover, so we have to manually fix them up here onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; @@ -93,8 +97,7 @@ Window { spacing: hifi.layout.spacing; clip: true; anchors { - bottom: backgroundImage.top; - bottomMargin: 2 * hifi.layout.spacing; + bottom: backgroundImage.top horizontalCenter: backgroundImage.horizontalCenter } model: suggestions; @@ -129,12 +132,16 @@ Window { verticalCenter: scroll.verticalCenter; } } + Image { id: backgroundImage source: "../images/address-bar.svg" - width: 576 * root.scale - height: 80 * root.scale - property int inputAreaHeight: 56.0 * root.scale // Height of the background's input area + width: 720 + height: 100 + anchors { + bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom; + } + property int inputAreaHeight: 70 property int inputAreaStep: (height - inputAreaHeight) / 2 ToolbarButton { @@ -181,7 +188,7 @@ Window { HifiStyles.RalewayLight { id: notice; - font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.50; + font.pixelSize: hifi.fonts.pixelSize * 0.50; anchors { top: parent.top topMargin: parent.inputAreaStep + 12 @@ -210,7 +217,7 @@ Window { topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) bottomMargin: parent.inputAreaStep } - font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 + font.pixelSize: hifi.fonts.pixelSize * 0.75 cursorVisible: false onTextChanged: { filterChoicesByText(); @@ -259,7 +266,6 @@ Window { Window { width: 938 height: 625 - scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25) HifiControls.WebView { anchors.fill: parent; id: storyCardHTML; @@ -274,35 +280,18 @@ Window { verticalCenter: backgroundImage.verticalCenter; horizontalCenter: scroll.horizontalCenter; } + z: 100 } - // virtual keyboard, letters HifiControls.Keyboard { - id: keyboard1 - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && !parent.punctuationMode - enabled: parent.keyboardRaised && !parent.punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - } - - HifiControls.KeyboardPunctuation { - id: keyboard2 - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && parent.punctuationMode - enabled: parent.keyboardRaised && parent.punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 + id: keyboard + raised: parent.keyboardEnabled // Ignore keyboardRaised; keep keyboard raised if enabled (i.e., in HMD). + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } } } @@ -442,10 +431,10 @@ Window { function updateLocationText(enteringAddress) { if (enteringAddress) { notice.text = "Go to a place, @user, path or network address"; - notice.color = "gray"; + notice.color = hifiStyleConstants.colors.baseGrayHighlight; } else { notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected"; - notice.color = AddressManager.isConnected ? "gray" : "crimson"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight; // Display hostname, which includes ip address, localhost, and other non-placenames. location.text = (AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); } diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 050bc8e99e..cf61a2ae4a 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -184,7 +184,7 @@ ScrollingWindow { prompt.selected.connect(function (jsonResult) { if (jsonResult) { var result = JSON.parse(jsonResult); - var url = result.textInput; + var url = result.textInput.trim(); var shapeType; switch (result.comboBox) { case SHAPE_TYPE_SIMPLE_HULL: @@ -349,7 +349,7 @@ ScrollingWindow { }, function(err, path) { print(err, path); - if (!err) { + if (err === "") { uploadProgressLabel.text = "Upload Complete"; timer.interval = 1000; timer.repeat = false; @@ -362,14 +362,15 @@ ScrollingWindow { console.log("Asset Browser - finished uploading: ", fileUrl); reload(); } else { - if (err > 0) { - console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); - var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + Assets.getErrorString(err)); - box.selected.connect(reload); - } uploadSpinner.visible = false; uploadButton.enabled = true; uploadOpen = false; + + if (err !== -1) { + console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); + var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); + box.selected.connect(reload); + } } }, dropping); } 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/Browser.qml b/interface/resources/qml/Browser.qml index b258dadae4..869c0556fb 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -31,13 +31,6 @@ ScrollingWindow { addressBar.text = webview.url } - onParentChanged: { - if (visible) { - addressBar.forceActiveFocus(); - addressBar.selectAll() - } - } - function showPermissionsBar(){ permissionsContainer.visible=true; } diff --git a/interface/resources/qml/ConnectionFailureDialog.qml b/interface/resources/qml/ConnectionFailureDialog.qml new file mode 100644 index 0000000000..0d5bdfd38d --- /dev/null +++ b/interface/resources/qml/ConnectionFailureDialog.qml @@ -0,0 +1,14 @@ +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "dialogs" + +MessageDialog { + id: root + objectName: "ConnectionFailureDialog" + + title: "No Connection" + text: "Unable to connect to this domain. Click the 'GO TO' button on the toolbar to visit another domain." + buttons: OriginalDialogs.StandardButton.Ok + icon: OriginalDialogs.StandardIcon.Warning + defaultButton: OriginalDialogs.StandardButton.NoButton; +} diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 1f84024e15..08895ecaa1 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -33,6 +33,8 @@ ModalWindow { property string title: "" property int titleWidth: 0 + keyboardOverride: true // Disable ModalWindow's keyboard. + LoginDialog { id: loginDialog diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index fc7eac3d00..f1b36ff6a7 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -29,8 +29,9 @@ Item { readonly property int maxHeight: 720 function resize() { - var targetWidth = Math.max(titleWidth, additionalTextContainer.contentWidth) - var targetHeight = 4 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + var targetWidth = Math.max(titleWidth, Math.max(additionalTextContainer.contentWidth, + termsContainer.contentWidth)) + var targetHeight = 5 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + termsContainer.height root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) @@ -43,7 +44,7 @@ Item { top: parent.top horizontalCenter: parent.horizontalCenter margins: 0 - topMargin: 3 * hifi.dimensions.contentSpacing.y + topMargin: 2 * hifi.dimensions.contentSpacing.y } spacing: hifi.dimensions.contentSpacing.x onHeightChanged: d.resize(); onWidthChanged: d.resize(); @@ -91,6 +92,25 @@ Item { } } + InfoItem { + id: termsContainer + anchors { + top: additionalTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + + text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + Component.onCompleted: { root.title = qsTr("Complete Your Profile") root.iconText = "<" diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index debd40a67b..fbcb3d5e37 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -27,6 +27,7 @@ Item { loginDialog.login(usernameField.text, passwordField.text) } + property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false @@ -41,12 +42,18 @@ Item { function resize() { var targetWidth = Math.max(titleWidth, form.contentWidth); - var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height - + 4 * hifi.dimensions.contentSpacing.y + form.height + hifi.dimensions.contentSpacing.y + buttons.height; + var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + + 4 * hifi.dimensions.contentSpacing.y + form.height + + hifi.dimensions.contentSpacing.y + buttons.height; + + if (additionalInformation.visible) { + targetWidth = Math.max(targetWidth, additionalInformation.width); + targetHeight += hifi.dimensions.contentSpacing.y + additionalInformation.height + } root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) - + (linkAccountBody.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); + + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); } } @@ -135,30 +142,34 @@ Item { } - // Override ScrollingWindow's keyboard that would be at very bottom of dialog. - Keyboard { - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && !parent.punctuationMode - enabled: parent.keyboardRaised && !parent.punctuationMode + InfoItem { + id: additionalInformation anchors { + top: form.bottom left: parent.left - right: parent.right - bottom: buttons.top - bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y } + + visible: loginDialog.isSteamRunning() + + text: qsTr("Your steam account informations will not be exposed to other users.") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter } - KeyboardPunctuation { - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && parent.punctuationMode - enabled: parent.keyboardRaised && parent.punctuationMode + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode anchors { left: parent.left right: parent.right bottom: buttons.top - bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 } } @@ -195,9 +206,10 @@ Item { Component.onCompleted: { root.title = qsTr("Sign Into High Fidelity") root.iconText = "<" + keyboardEnabled = HMD.active; d.resize(); - usernameField.forceActiveFocus() + usernameField.forceActiveFocus(); } Connections { diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index b949c660d6..eafe9e4b47 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -27,6 +27,13 @@ Item { loginDialog.createAccountFromStream(textField.text) } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + QtObject { id: d readonly property int minWidth: 480 @@ -35,15 +42,16 @@ Item { readonly property int maxHeight: 720 function resize() { - var targetWidth = Math.max(titleWidth, Math.max(mainTextContainer.contentWidth, - termsContainer.contentWidth)) + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) var targetHeight = mainTextContainer.height + - 2 * hifi.dimensions.contentSpacing.y + textField.height + - 5 * hifi.dimensions.contentSpacing.y + termsContainer.height + - 1 * hifi.dimensions.contentSpacing.y + buttons.height + hifi.dimensions.contentSpacing.y + textField.height + + hifi.dimensions.contentSpacing.y + buttons.height root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y) + + height = root.height } } @@ -71,39 +79,32 @@ Item { top: mainTextContainer.bottom left: parent.left margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y + topMargin: hifi.dimensions.contentSpacing.y } width: 250 placeholderText: "Choose your own" } - InfoItem { - id: termsContainer + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode anchors { - top: textField.bottom left: parent.left - margins: 0 - topMargin: 3 * hifi.dimensions.contentSpacing.y + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 } - - text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - - onLinkActivated: loginDialog.openUrl(link) } Row { id: buttons anchors { - top: termsContainer.bottom + bottom: parent.bottom right: parent.right margins: 0 - topMargin: 1 * hifi.dimensions.contentSpacing.y + topMargin: hifi.dimensions.contentSpacing.y } spacing: hifi.dimensions.contentSpacing.x onHeightChanged: d.resize(); onWidthChanged: d.resize(); @@ -130,8 +131,10 @@ Item { Component.onCompleted: { root.title = qsTr("Complete Your Profile") root.iconText = "<" + keyboardEnabled = HMD.active; d.resize(); } + Connections { target: loginDialog onHandleCreateCompleted: { diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 7ea45bff6b..7bb26a885a 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -56,6 +56,10 @@ Windows.ScrollingWindow { onWidthChanged: notifyResized(); onHeightChanged: notifyResized(); + onShownChanged: { + keyboardEnabled = HMD.active; + } + Item { width: pane.contentWidth implicitHeight: pane.scrollHeight diff --git a/interface/resources/qml/StatText.qml b/interface/resources/qml/StatText.qml new file mode 100644 index 0000000000..69963c1373 --- /dev/null +++ b/interface/resources/qml/StatText.qml @@ -0,0 +1,9 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 + +Text { + color: "white"; + style: Text.Outline; + styleColor: "black"; + font.pixelSize: 12; +} diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index a0750c1f7f..1c5df5c71d 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -1,6 +1,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 +import '.' Item { id: stats @@ -28,9 +29,7 @@ Item { implicitWidth: row.width anchors.horizontalCenter: parent.horizontalCenter - readonly property int fontSize: 12 - readonly property string fontColor: "white" - readonly property string bgColor: "#99333333" + readonly property string bgColor: "#AA111111" Row { id: row @@ -49,64 +48,40 @@ Item { Column { id: generalCol spacing: 4; x: 4; y: 4; - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Servers: " + root.serverCount } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Avatars: " + root.avatarCount } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Frame Rate: " + root.framerate.toFixed(2); } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Render Rate: " + root.renderrate.toFixed(2); } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Present Rate: " + root.presentrate.toFixed(2); } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Present New Rate: " + root.presentnewrate.toFixed(2); } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Present Drop Rate: " + root.presentdroprate.toFixed(2); } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Simrate: " + root.simrate } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Avatar Simrate: " + root.avatarSimrate } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2) } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "Asset Mbps In/Out: " + root.assetMbpsIn.toFixed(2) + "/" + root.assetMbpsOut.toFixed(2) } @@ -126,29 +101,19 @@ Item { Column { id: pingCol spacing: 4; x: 4; y: 4; - Text { - color: root.fontColor - font.pixelSize: root.fontSize + StatText { text: "Audio ping: " + root.audioPing } - Text { - color: root.fontColor - font.pixelSize: root.fontSize + StatText { text: "Avatar ping: " + root.avatarPing } - Text { - color: root.fontColor - font.pixelSize: root.fontSize + StatText { text: "Entities avg ping: " + root.entitiesPing } - Text { - color: root.fontColor - font.pixelSize: root.fontSize + StatText { text: "Asset ping: " + root.assetPing } - Text { - color: root.fontColor - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: "Messages max ping: " + root.messagePing } @@ -167,46 +132,32 @@ Item { Column { id: geoCol spacing: 4; x: 4; y: 4; - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Position: " + root.position.x.toFixed(1) + ", " + root.position.y.toFixed(1) + ", " + root.position.z.toFixed(1) } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Speed: " + root.speed.toFixed(1) } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Yaw: " + root.yaw.toFixed(1) } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: "Avatar Mixer In: " + root.avatarMixerInKbps + " kbps, " + root.avatarMixerInPps + "pps"; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: "Avatar Mixer Out: " + root.avatarMixerOutKbps + " kbps, " + root.avatarMixerOutPps + "pps"; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: "Downloads: " + root.downloads + "/" + root.downloadLimit + ", Pending: " + root.downloadsPending; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded && root.downloadUrls.length > 0; text: "Download URLs:" } @@ -217,9 +168,7 @@ Item { visible: root.expanded && root.downloadUrls.length > 0; model: root.downloadUrls - delegate: Text { - color: root.fontColor; - font.pixelSize: root.fontSize + delegate: StatText { visible: root.expanded; text: modelData.length > 30 ? modelData.substring(0, 5) + "..." + modelData.substring(modelData.length - 22) @@ -240,84 +189,110 @@ Item { Column { id: octreeCol spacing: 4; x: 4; y: 4; - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { text: "Triangles: " + root.triangles + " / Material Switches: " + root.materialSwitches } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { + text: "GPU Free Memory: " + root.gpuFreeMemory + " MB"; + } + StatText { + text: "GPU Textures: "; + } + StatText { + text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); + } + StatText { + text: " Count: " + root.gpuTextures; + } + StatText { + text: " Rectified: " + root.rectifiedTextureCount; + } + StatText { + text: " Decimated: " + root.decimatedTextureCount; + } + StatText { + text: " Sparse Count: " + root.gpuTexturesSparse; + visible: 0 != root.gpuSparseTextureEnabled; + } + StatText { + text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; + } + StatText { + text: " Commited Memory: " + root.gpuTextureMemory + " MB"; + } + StatText { + text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; + } + StatText { + text: " Sparse Memory: " + root.gpuTextureSparseMemory + " MB"; + visible: 0 != root.gpuSparseTextureEnabled; + } + StatText { + text: "GPU Buffers: " + } + StatText { + text: " Count: " + root.gpuTextures; + } + StatText { + text: " Memory: " + root.gpuBufferMemory; + } + StatText { + text: "GL Swapchain Memory: " + root.glContextSwapchainMemory + " MB"; + } + StatText { + text: "QML Texture Memory: " + root.qmlTextureMemory + " MB"; + } + StatText { visible: root.expanded; text: "Items rendered / considered: " + root.itemRendered + " / " + root.itemConsidered; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: " out of view: " + root.itemOutOfView + " too small: " + root.itemTooSmall; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: "Shadows rendered / considered: " + root.shadowRendered + " / " + root.shadowConsidered; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded; text: " out of view: " + root.shadowOutOfView + " too small: " + root.shadowTooSmall; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: !root.expanded text: "Octree Elements Server: " + root.serverElements + " Local: " + root.localElements; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "Octree Sending Mode: " + root.sendingMode; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "Octree Packets to Process: " + root.packetStats; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "Octree Elements - "; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "\tServer: " + root.serverElements + " Internal: " + root.serverInternal + " Leaves: " + root.serverLeaves; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "\tLocal: " + root.localElements + " Internal: " + root.localInternal + " Leaves: " + root.localLeaves; } - Text { - color: root.fontColor; - font.pixelSize: root.fontSize + StatText { visible: root.expanded text: "LOD: " + root.lodStatus; } @@ -331,12 +306,10 @@ Item { width: perfText.width + 8 height: perfText.height + 8 color: root.bgColor; - Text { + StatText { x: 4; y: 4 id: perfText - color: root.fontColor font.family: root.monospaceFont - font.pixelSize: 12 text: "------------------------------------------ Function " + "--------------------------------------- --msecs- -calls--\n" + root.timingStats; diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index 68c8099970..9c0b0a8c1a 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -42,6 +42,10 @@ ScrollingWindow { } } + onShownChanged: { + keyboardEnabled = HMD.active; + } + Settings { category: "ToolWindow.Position" property alias x: toolWindow.x diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index ef4764b08f..763e6530fb 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -10,10 +10,10 @@ import QtQuick 2.5 import QtWebEngine 1.2 +import HFWebEngineProfile 1.0 WebEngineView { id: root - property var newUrl; profile: desktop.browserProfile @@ -25,30 +25,6 @@ WebEngineView { }); } - - - // FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6 - Timer { - id: urlReplacementTimer - running: false - repeat: false - interval: 50 - onTriggered: url = newUrl; - } - - onUrlChanged: { - var originalUrl = url.toString(); - newUrl = urlHandler.fixupUrl(originalUrl).toString(); - if (newUrl !== originalUrl) { - root.stop(); - if (urlReplacementTimer.running) { - console.warn("Replacement timer already running"); - return; - } - urlReplacementTimer.start(); - } - } - onLoadingChanged: { // Required to support clicking on "hifi://" links if (WebEngineView.LoadStartedStatus == loadRequest.status) { diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index fa100a965f..573fed4ef9 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -32,6 +32,8 @@ FocusScope { readonly property ComboBox control: comboBox + signal accepted(); + implicitHeight: comboBox.height; focus: true @@ -134,6 +136,7 @@ FocusScope { function hideList() { popup.visible = false; scrollView.hoverEnabled = false; + root.accepted(); } FocusScope { diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml index 1d957ce9cb..07488fe3e6 100644 --- a/interface/resources/qml/controls-uit/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -1,10 +1,28 @@ +// +// FileDialog.qml +// +// Created by Anthony Thibault on 31 Oct 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 +// + import QtQuick 2.0 Item { id: keyboardBase - height: 200 - property alias shiftKey: key27 + + property bool raised: false + property bool numeric: false + + readonly property int raisedHeight: 200 + + height: enabled && raised ? raisedHeight : 0 + visible: enabled && raised + property bool shiftMode: false + property bool numericShiftMode: false function resetShiftMode(mode) { shiftMode = mode; @@ -37,8 +55,8 @@ Item { function forEachKey(func) { var i, j; - for (i = 0; i < column1.children.length; i++) { - var row = column1.children[i]; + for (i = 0; i < columnAlpha.children.length; i++) { + var row = columnAlpha.children[i]; for (j = 0; j < row.children.length; j++) { var key = row.children[j]; func(key); @@ -48,10 +66,12 @@ Item { onShiftModeChanged: { forEachKey(function (key) { - if (shiftMode) { - key.glyph = keyboardBase.toUpper(key.glyph); - } else { - key.glyph = keyboardBase.toLower(key.glyph); + if (/[a-z]/i.test(key.glyph)) { + if (shiftMode) { + key.glyph = keyboardBase.toUpper(key.glyph); + } else { + key.glyph = keyboardBase.toLower(key.glyph); + } } }); } @@ -97,265 +117,177 @@ Item { anchors.bottomMargin: 0 Column { - id: column1 + id: columnAlpha width: 480 height: 200 + visible: !numeric Row { - id: row1 width: 480 height: 50 anchors.left: parent.left - anchors.leftMargin: 0 + anchors.leftMargin: 4 - Key { - id: key1 - width: 44 - glyph: "q" - } - - Key { - id: key2 - width: 44 - glyph: "w" - } - - Key { - id: key3 - width: 44 - glyph: "e" - } - - Key { - id: key4 - width: 43 - glyph: "r" - } - - Key { - id: key5 - width: 43 - glyph: "t" - } - - Key { - id: key6 - width: 44 - glyph: "y" - } - - Key { - id: key7 - width: 44 - glyph: "u" - } - - Key { - id: key8 - width: 43 - glyph: "i" - } - - Key { - id: key9 - width: 42 - glyph: "o" - } - - Key { - id: key10 - width: 44 - glyph: "p" - } - - Key { - id: key28 - width: 45 - glyph: "←" - } + Key { width: 43; glyph: "q"; } + Key { width: 43; glyph: "w"; } + Key { width: 43; glyph: "e"; } + Key { width: 43; glyph: "r"; } + Key { width: 43; glyph: "t"; } + Key { width: 43; glyph: "y"; } + Key { width: 43; glyph: "u"; } + Key { width: 43; glyph: "i"; } + Key { width: 43; glyph: "o"; } + Key { width: 43; glyph: "p"; } + Key { width: 43; glyph: "←"; } } Row { - id: row2 width: 480 height: 50 anchors.left: parent.left - anchors.leftMargin: 18 + anchors.leftMargin: 20 - Key { - id: key11 - width: 43 - } - - Key { - id: key12 - width: 43 - glyph: "s" - } - - Key { - id: key13 - width: 43 - glyph: "d" - } - - Key { - id: key14 - width: 43 - glyph: "f" - } - - Key { - id: key15 - width: 43 - glyph: "g" - } - - Key { - id: key16 - width: 43 - glyph: "h" - } - - Key { - id: key17 - width: 43 - glyph: "j" - } - - Key { - id: key18 - width: 43 - glyph: "k" - } - - Key { - id: key19 - width: 43 - glyph: "l" - } - - Key { - id: key32 - width: 75 - glyph: "⏎" - } + Key { width: 43; glyph: "a"; } + Key { width: 43; glyph: "s"; } + Key { width: 43; glyph: "d"; } + Key { width: 43; glyph: "f"; } + Key { width: 43; glyph: "g"; } + Key { width: 43; glyph: "h"; } + Key { width: 43; glyph: "j"; } + Key { width: 43; glyph: "k"; } + Key { width: 43; glyph: "l"; } + Key { width: 70; glyph: "⏎"; } } Row { - id: row3 width: 480 height: 50 anchors.left: parent.left - anchors.leftMargin: 0 + anchors.leftMargin: 4 Key { - id: key27 - width: 46 + id: shiftKey + width: 43 glyph: "⇪" toggle: true - onToggledChanged: { - shiftMode = toggled; - } + onToggledChanged: shiftMode = toggled } - - Key { - id: key20 - width: 43 - glyph: "z" - } - - Key { - id: key21 - width: 43 - glyph: "x" - } - - Key { - id: key22 - width: 43 - glyph: "c" - } - - Key { - id: key23 - width: 43 - glyph: "v" - } - - Key { - id: key24 - width: 43 - glyph: "b" - } - - Key { - id: key25 - width: 43 - glyph: "n" - } - - Key { - id: key26 - width: 44 - glyph: "m" - } - - Key { - id: key31 - width: 43 - glyph: "_" - } - - Key { - id: key33 - width: 43 - glyph: "?" - } - - Key { - id: key36 - width: 46 - glyph: "/" - } - + Key { width: 43; glyph: "z"; } + Key { width: 43; glyph: "x"; } + Key { width: 43; glyph: "c"; } + Key { width: 43; glyph: "v"; } + Key { width: 43; glyph: "b"; } + Key { width: 43; glyph: "n"; } + Key { width: 43; glyph: "m"; } + Key { width: 43; glyph: "_"; } + Key { width: 43; glyph: "/"; } + Key { width: 43; glyph: "?"; } } Row { - id: row4 width: 480 height: 50 anchors.left: parent.left - anchors.leftMargin: 19 + anchors.leftMargin: 4 Key { - id: key30 - width: 89 - glyph: "&123" - mouseArea.onClicked: { - keyboardBase.parent.punctuationMode = true; - } + width: 70 + glyph: "123" + mouseArea.onClicked: keyboardBase.parent.punctuationMode = true } + Key { width: 231; glyph: " "; } + Key { width: 43; glyph: ","; } + Key { width: 43; glyph: "."; } + Key { width: 43; glyph: "\u276C"; } + Key { width: 43; glyph: "\u276D"; } + } + } + + Column { + id: columnNumeric + width: 480 + height: 200 + visible: numeric + + Row { + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 4 + + Key { width: 43; glyph: "1"; } + Key { width: 43; glyph: "2"; } + Key { width: 43; glyph: "3"; } + Key { width: 43; glyph: "4"; } + Key { width: 43; glyph: "5"; } + Key { width: 43; glyph: "6"; } + Key { width: 43; glyph: "7"; } + Key { width: 43; glyph: "8"; } + Key { width: 43; glyph: "9"; } + Key { width: 43; glyph: "0"; } + Key { width: 43; glyph: "←"; } + } + + Row { + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 4 + + Key { width: 43; glyph: "!"; } + Key { width: 43; glyph: "@"; } + Key { width: 43; glyph: "#"; } + Key { width: 43; glyph: "$"; } + Key { width: 43; glyph: "%"; } + Key { width: 43; glyph: "^"; } + Key { width: 43; glyph: "&"; } + Key { width: 43; glyph: "*"; } + Key { width: 43; glyph: "("; } + Key { width: 43; glyph: ")"; } + Key { width: 43; glyph: "⏎"; } + } + + Row { + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 4 Key { - id: key29 - width: 285 - glyph: " " - } - - Key { - id: key34 + id: numericShiftKey width: 43 - glyph: "⇦" + glyph: "\u21E8" + toggle: true + onToggledChanged: numericShiftMode = toggled } + Key { width: 43; glyph: numericShiftMode ? "`" : "+"; } + Key { width: 43; glyph: numericShiftMode ? "~" : "-"; } + Key { width: 43; glyph: numericShiftMode ? "\u00A3" : "="; } + Key { width: 43; glyph: numericShiftMode ? "\u20AC" : ";"; } + Key { width: 43; glyph: numericShiftMode ? "\u00A5" : ":"; } + Key { width: 43; glyph: numericShiftMode ? "<" : "'"; } + Key { width: 43; glyph: numericShiftMode ? ">" : "\""; } + Key { width: 43; glyph: numericShiftMode ? "[" : "{"; } + Key { width: 43; glyph: numericShiftMode ? "]" : "}"; } + Key { width: 43; glyph: numericShiftMode ? "\\" : "|"; } + } + + Row { + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 4 Key { - id: key35 - x: 343 - width: 43 - glyph: "⇨" + width: 70 + glyph: "abc" + mouseArea.onClicked: keyboardBase.parent.punctuationMode = false } - + Key { width: 231; glyph: " "; } + Key { width: 43; glyph: ","; } + Key { width: 43; glyph: "."; } + Key { width: 43; glyph: "\u276C"; } + Key { width: 43; glyph: "\u276D"; } } } } @@ -386,5 +318,4 @@ Item { anchors.top: parent.top anchors.topMargin: 0 } - } diff --git a/interface/resources/qml/controls-uit/KeyboardPunctuation.qml b/interface/resources/qml/controls-uit/KeyboardPunctuation.qml deleted file mode 100644 index 485468b46a..0000000000 --- a/interface/resources/qml/controls-uit/KeyboardPunctuation.qml +++ /dev/null @@ -1,324 +0,0 @@ -import QtQuick 2.0 - -Item { - id: keyboardBase - height: 200 - Rectangle { - id: leftRect - y: 0 - height: 200 - color: "#252525" - anchors.right: keyboardRect.left - anchors.rightMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - } - - Rectangle { - id: keyboardRect - x: 206 - y: 0 - width: 480 - height: 200 - color: "#252525" - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - - Column { - id: column1 - width: 480 - height: 200 - - Row { - id: row1 - width: 480 - height: 50 - anchors.left: parent.left - anchors.leftMargin: 0 - - Key { - id: key1 - width: 43 - glyph: "1" - } - - Key { - id: key2 - width: 43 - glyph: "2" - } - - Key { - id: key3 - width: 43 - glyph: "3" - } - - Key { - id: key4 - width: 43 - glyph: "4" - } - - Key { - id: key5 - width: 43 - glyph: "5" - } - - Key { - id: key6 - width: 43 - glyph: "6" - } - - Key { - id: key7 - width: 43 - glyph: "7" - } - - Key { - id: key8 - width: 43 - glyph: "8" - } - - Key { - id: key9 - width: 43 - glyph: "9" - } - - Key { - id: key10 - width: 43 - glyph: "0" - } - - Key { - id: key28 - width: 50 - glyph: "←" - } - } - - Row { - id: row2 - width: 480 - height: 50 - anchors.left: parent.left - anchors.leftMargin: 0 - - Key { - id: key11 - width: 43 - glyph: "!" - } - - Key { - id: key12 - width: 43 - glyph: "@" - } - - Key { - id: key13 - width: 43 - glyph: "#" - } - - Key { - id: key14 - width: 43 - glyph: "$" - } - - Key { - id: key15 - width: 43 - glyph: "%" - } - - Key { - id: key16 - width: 43 - glyph: "^" - } - - Key { - id: key17 - width: 43 - glyph: "&" - } - - Key { - id: key18 - width: 43 - glyph: "*" - } - - Key { - id: key19 - width: 43 - glyph: "(" - } - - Key { - id: key32 - width: 43 - glyph: ")" - } - - Key { - id: key37 - width: 50 - glyph: "⏎" - } - } - - Row { - id: row3 - width: 480 - height: 50 - anchors.left: parent.left - anchors.leftMargin: 4 - - Key { - id: key27 - width: 43 - glyph: "=" - } - - Key { - id: key20 - width: 43 - glyph: "+" - } - - Key { - id: key21 - width: 43 - glyph: "-" - } - - Key { - id: key22 - width: 43 - glyph: "," - } - - Key { - id: key23 - width: 43 - glyph: "." - } - - Key { - id: key24 - width: 43 - glyph: ";" - } - - Key { - id: key25 - width: 43 - glyph: ":" - } - - Key { - id: key26 - width: 43 - glyph: "'" - } - - Key { - id: key31 - width: 43 - glyph: "\"" - } - - Key { - id: key33 - width: 43 - glyph: "<" - } - - Key { - id: key36 - width: 43 - glyph: ">" - } - - } - - Row { - id: row4 - width: 480 - height: 50 - anchors.left: parent.left - anchors.leftMargin: 19 - - Key { - id: key30 - width: 65 - glyph: "abc" - mouseArea.onClicked: { - keyboardBase.parent.punctuationMode = false - } - } - - Key { - id: key29 - width: 285 - glyph: " " - } - - Key { - id: key34 - width: 43 - glyph: "⇦" - } - - Key { - id: key35 - x: 343 - width: 43 - glyph: "⇨" - } - - } - } - } - - Rectangle { - id: rightRect - y: 280 - height: 200 - color: "#252525" - border.width: 0 - anchors.left: keyboardRect.right - anchors.leftMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - } - - Rectangle { - id: rectangle1 - color: "#ffffff" - anchors.bottom: keyboardRect.top - anchors.bottomMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.top: parent.top - anchors.topMargin: 0 - } - -} diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index aa1d10f030..8bce092947 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -199,6 +199,11 @@ TreeView { unfocusHelper.forceActiveFocus(); } } + + onReadOnlyChanged: { + // Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time. + keyboardRaised = activeFocus; + } } } } diff --git a/interface/resources/qml/controls-uit/WebView.qml b/interface/resources/qml/controls-uit/WebView.qml index 2ce007c42a..2895f36944 100644 --- a/interface/resources/qml/controls-uit/WebView.qml +++ b/interface/resources/qml/controls-uit/WebView.qml @@ -13,8 +13,9 @@ import "." BaseWebView { onNewViewRequested: { - var component = Qt.createComponent("../Browser.qml"); - var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView) + // Load dialog via OffscreenUi so that JavaScript EventBridge is available. + var browser = OffscreenUi.load("Browser.qml"); + request.openIn(browser.webView); + browser.webView.forceActiveFocus(); } } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index c3381ab824..f388e5c120 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -2,13 +2,23 @@ import QtQuick 2.5 import QtWebEngine 1.1 import QtWebChannel 1.0 import "../controls-uit" as HiFiControls +import HFWebEngineProfile 1.0 Item { property alias url: root.url property alias eventBridge: eventBridgeWrapper.eventBridge + property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false property bool keyboardRaised: false property bool punctuationMode: false + // FIXME - Keyboard HMD only: Make Interface either set keyboardRaised property directly in OffscreenQmlSurface + // or provide HMDinfo object to QML in RenderableWebEntityItem and do the following. + /* + onKeyboardRaisedChanged: { + keyboardEnabled = HMDinfo.active; + } + */ + QtObject { id: eventBridgeWrapper WebChannel.id: "eventBridgeWrapper" @@ -17,10 +27,16 @@ Item { WebEngineView { id: root + objectName: "webEngineView" x: 0 y: 0 width: parent.width - height: keyboardRaised ? parent.height - keyboard1.height : parent.height + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFWebEngineProfile { + id: webviewProfile + storageName: "qmlWebEngine" + } // creates a global EventBridge object. WebEngineScript { @@ -53,28 +69,6 @@ Item { root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; } - // FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6 - Timer { - id: urlReplacementTimer - running: false - repeat: false - interval: 50 - onTriggered: url = root.newUrl; - } - - onUrlChanged: { - var originalUrl = url.toString(); - root.newUrl = urlHandler.fixupUrl(originalUrl).toString(); - if (root.newUrl !== originalUrl) { - root.stop(); - if (urlReplacementTimer.running) { - console.warn("Replacement timer already running"); - return; - } - urlReplacementTimer.start(); - } - } - onFeaturePermissionRequested: { grantFeaturePermission(securityOrigin, feature, true); } @@ -82,7 +76,7 @@ Item { onLoadingChanged: { keyboardRaised = false; punctuationMode = false; - keyboard1.resetShiftMode(false); + keyboard.resetShiftMode(false); // Required to support clicking on "hifi://" links if (WebEngineView.LoadStartedStatus == loadRequest.status) { @@ -105,32 +99,15 @@ Item { } } - // virtual keyboard, letters HiFiControls.Keyboard { - id: keyboard1 - y: keyboardRaised ? parent.height : 0 - height: keyboardRaised ? 200 : 0 - visible: keyboardRaised && !punctuationMode - enabled: keyboardRaised && !punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } } - HiFiControls.KeyboardPunctuation { - id: keyboard2 - y: keyboardRaised ? parent.height : 0 - height: keyboardRaised ? 200 : 0 - visible: keyboardRaised && punctuationMode - enabled: keyboardRaised && punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - } } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index ea2f048c1f..b207087be0 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -33,7 +33,7 @@ FocusScope { } } - + onHeightChanged: d.handleSizeChanged(); onWidthChanged: d.handleSizeChanged(); diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 563dfc3099..97f55d087b 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -22,7 +22,7 @@ ModalWindow { implicitWidth: 640; implicitHeight: 320; visible: true; - keyboardEnabled: false // Disable ModalWindow's keyboard. + keyboardOverride: true // Disable ModalWindow's keyboard. signal selected(var result); signal canceled(); @@ -51,6 +51,7 @@ ModalWindow { } } + property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false onKeyboardRaisedChanged: d.resize(); @@ -116,7 +117,7 @@ ModalWindow { var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + - (root.keyboardRaised ? (200 + hifi.dimensions.contentSpacing.y) : 0); + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? @@ -153,38 +154,15 @@ ModalWindow { } } - Item { + Keyboard { id: keyboard - - height: keyboardRaised ? 200 : 0 - + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode anchors { left: parent.left right: parent.right bottom: parent.bottom - bottomMargin: keyboardRaised ? hifi.dimensions.contentSpacing.y : 0 - } - - Keyboard { - id: keyboard1 - visible: keyboardRaised && !punctuationMode - enabled: keyboardRaised && !punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - } - - KeyboardPunctuation { - id: keyboard2 - visible: keyboardRaised && punctuationMode - enabled: keyboardRaised && punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } + bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0 } } } @@ -211,6 +189,7 @@ ModalWindow { left: parent.left; bottom: parent.bottom; leftMargin: 6; // Magic number to align with warning icon + bottomMargin: 6; } } @@ -224,7 +203,10 @@ ModalWindow { bottom: parent.bottom; } model: root.comboBox ? root.comboBox.items : []; - onCurrentTextChanged: updateCheckbox(); + onAccepted: { + updateCheckbox(); + focus = true; + } } } @@ -335,7 +317,9 @@ ModalWindow { } Component.onCompleted: { + keyboardEnabled = HMD.active; updateIcon(); + updateCheckbox(); d.resize(); textField.forceActiveFocus(); } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 0942e728f9..0886a25949 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -27,7 +27,7 @@ ModalWindow { id: root resizable: true implicitWidth: 480 - implicitHeight: 360 + (fileDialogItem.keyboardRaised ? 200 + hifi.dimensions.contentSpacing.y : 0) + implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) minSize: Qt.vector2d(360, 240) draggable: true @@ -70,7 +70,9 @@ ModalWindow { signal canceled(); Component.onCompleted: { - console.log("Helper " + helper + " drives " + drives) + console.log("Helper " + helper + " drives " + drives); + + fileDialogItem.keyboardEnabled = HMD.active; // HACK: The following lines force the model to initialize properly such that the go-up button // works properly from the initial screen. @@ -85,6 +87,8 @@ ModalWindow { if (selectDirectory) { currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; } helper.contentsChanged.connect(function() { @@ -106,6 +110,7 @@ ModalWindow { height: pane.height anchors.margins: 0 + property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false @@ -626,7 +631,7 @@ ModalWindow { left: parent.left right: selectionType.visible ? selectionType.left: parent.right rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 - bottom: keyboard1.top + bottom: keyboard.top bottomMargin: hifi.dimensions.contentSpacing.y } readOnly: !root.saveDialog @@ -648,25 +653,15 @@ ModalWindow { } Keyboard { - id: keyboard1 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && !parent.punctuationMode - enabled: visible - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: buttonRow.top - anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 - } - - KeyboardPunctuation { - id: keyboard2 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && parent.punctuationMode - enabled: visible - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: buttonRow.top - anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } } Row { diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index cf1b1e370a..a922e6efc7 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -22,6 +22,7 @@ ModalWindow { implicitWidth: 640 implicitHeight: 320 visible: true + keyboardOverride: true // Disable ModalWindow's keyboard. signal selected(var result); signal canceled(); @@ -45,6 +46,12 @@ ModalWindow { property int titleWidth: 0 onTitleWidthChanged: d.resize(); + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + function updateIcon() { if (!root) { return; @@ -59,11 +66,6 @@ ModalWindow { height: pane.height anchors.margins: 0 - property bool keyboardRaised: false - property bool punctuationMode: false - - onKeyboardRaisedChanged: d.resize(); - QtObject { id: d readonly property int minWidth: 480 @@ -74,15 +76,15 @@ ModalWindow { function resize() { var targetWidth = Math.max(titleWidth, pane.width) var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height - root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth) - root.height = ((targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + (modalWindowItem.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0) + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + root.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) } } Item { anchors { top: parent.top - bottom: keyboard1.top; + bottom: keyboard.top; left: parent.left; right: parent.right; margins: 0 @@ -116,33 +118,16 @@ ModalWindow { } } - // virtual keyboard, letters Keyboard { - id: keyboard1 - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && !parent.punctuationMode - enabled: parent.keyboardRaised && !parent.punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: buttons.top - anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 - } - - KeyboardPunctuation { - id: keyboard2 - y: parent.keyboardRaised ? parent.height : 0 - height: parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardRaised && parent.punctuationMode - enabled: parent.keyboardRaised && parent.punctuationMode - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: buttons.top - anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } } Flow { @@ -203,6 +188,7 @@ ModalWindow { } Component.onCompleted: { + keyboardEnabled = HMD.active; updateIcon(); d.resize(); textResult.forceActiveFocus(); diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index 16d25b3c4c..652e02b6b9 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -10,23 +10,84 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtWebEngine 1.2 -import "../../windows" as Windows -import "../../controls-uit" as Controls +import "../../windows" +import "../../controls-uit" import "../../styles-uit" -Windows.Window { +Window { id: root HifiConstants { id: hifi } width: 900; height: 700 resizable: true modality: Qt.ApplicationModal - Controls.WebView { - id: webview + property alias eventBridge: eventBridgeWrapper.eventBridge + + Item { anchors.fill: parent - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" - focus: true + + property bool keyboardEnabled: false + property bool keyboardRaised: true + property bool punctuationMode: false + + BaseWebView { + id: webview + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: keyboard.top + } + + property alias eventBridgeWrapper: eventBridgeWrapper + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + webChannel.registeredObjects: [eventBridgeWrapper] + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } } } diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 8f05ca4ffe..0c5c5bf630 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -74,11 +74,6 @@ Preference { colorScheme: hifi.colorSchemes.dark } - Component { - id: avatarBrowserBuilder; - AvatarBrowser { } - } - Button { id: button text: "Browse" @@ -87,12 +82,12 @@ Preference { verticalCenter: dataTextField.verticalCenter } onClicked: { - root.browser = avatarBrowserBuilder.createObject(desktop); + // Load dialog via OffscreenUi so that JavaScript EventBridge is available. + root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml"); root.browser.windowDestroyed.connect(function(){ root.browser = null; - }) + }); } } - } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 7e523bbb70..7f1fbcb174 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -2,6 +2,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1; import Qt.labs.settings 1.0 +import HFWebEngineProfile 1.0 import "../desktop" as OriginalDesktop import ".." @@ -20,17 +21,14 @@ OriginalDesktop.Desktop { onEntered: ApplicationCompositor.reticleOverDesktop = true onExited: ApplicationCompositor.reticleOverDesktop = false acceptedButtons: Qt.NoButton - - } // The tool window, one instance property alias toolWindow: toolWindow ToolWindow { id: toolWindow } - property var browserProfile: WebEngineProfile { + property var browserProfile: HFWebEngineProfile { id: webviewProfile - httpUserAgent: "Chrome/48.0 (HighFidelityInterface)" storageName: "qmlWebEngine" } @@ -53,9 +51,10 @@ OriginalDesktop.Desktop { Toolbar { id: sysToolbar; objectName: "com.highfidelity.interface.toolbar.system"; - // Magic: sysToolbar.x and y come from settings, and are bound before the properties specified here are applied. - x: sysToolbar.x; - y: sysToolbar.y; + // 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 + y: 50 } property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar map[sysToolbar.objectName] = sysToolbar; @@ -127,5 +126,3 @@ OriginalDesktop.Desktop { return result; } } - - diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index cc9a570d47..27d225b58e 100644 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -45,7 +45,7 @@ ScrollingWindow { Rectangle { width: parent.width - height: root.height - (keyboardRaised ? 200 : 0) + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) radius: 4 color: hifi.colors.baseGray @@ -210,7 +210,7 @@ ScrollingWindow { } onKeyboardRaisedChanged: { - if (keyboardRaised) { + if (keyboardEnabled && keyboardRaised) { // Scroll to item with focus if necessary. var footerHeight = newAttachmentButton.height + buttonRow.height + 3 * hifi.dimensions.contentSpacing.y; var delta = activator.mouseY diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index a5a254f605..7a63c0604c 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -30,7 +30,7 @@ ScrollingWindow { Rectangle { width: parent.width - height: root.height - (keyboardRaised ? 200 : 0) + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) radius: 4 color: hifi.colors.baseGray diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index ce4bd45cff..1f9b59d2b4 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -34,7 +34,8 @@ Window { property var footer: Item { } // Optional static footer at the bottom of the dialog. readonly property var footerContentHeight: footer.height > 0 ? (footer.height + 2 * hifi.dimensions.contentSpacing.y + 3) : 0 - property bool keyboardEnabled: true // Set false if derived control implements its own keyboard. + property bool keyboardOverride: false // Set true in derived control if it implements its own keyboard. + property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false @@ -132,7 +133,7 @@ Window { // Optional non-scrolling footer. id: footerPane - property alias keyboardEnabled: window.keyboardEnabled + property alias keyboardOverride: window.keyboardOverride property alias keyboardRaised: window.keyboardRaised property alias punctuationMode: window.punctuationMode @@ -141,9 +142,9 @@ Window { bottom: parent.bottom } width: parent.contentWidth - height: footerContentHeight + (keyboardEnabled && keyboardRaised ? 200 : 0) + height: footerContentHeight + (keyboard.enabled && keyboard.raised ? keyboard.height : 0) color: hifi.colors.baseGray - visible: footer.height > 0 || keyboardEnabled && keyboardRaised + visible: footer.height > 0 || keyboard.enabled && keyboard.raised Item { // Horizontal rule. @@ -181,22 +182,10 @@ Window { } HiFiControls.Keyboard { - id: keyboard1 - height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode - enabled: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - } - - HiFiControls.KeyboardPunctuation { - id: keyboard2 - height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0 - visible: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode - enabled: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode + id: keyboard + enabled: !keyboardOverride + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode anchors { left: parent.left right: parent.right @@ -207,9 +196,9 @@ Window { } onKeyboardRaisedChanged: { - if (keyboardEnabled && keyboardRaised) { + if (!keyboardOverride && keyboardEnabled && keyboardRaised) { var delta = activator.mouseY - - (activator.height + activator.y - 200 - footerContentHeight - hifi.dimensions.controlLineHeight); + - (activator.height + activator.y - keyboard.raisedHeight - footerContentHeight - hifi.dimensions.controlLineHeight); if (delta > 0) { pane.scrollBy(delta); @@ -220,4 +209,10 @@ Window { } } } + + Component.onCompleted: { + if (typeof HMD !== "undefined") { + keyboardEnabled = HMD.active; + } + } } diff --git a/interface/resources/shaders/hmd_ui_glow.frag b/interface/resources/shaders/hmd_ui_glow.frag index 9270842092..5dda76e89d 100644 --- a/interface/resources/shaders/hmd_ui_glow.frag +++ b/interface/resources/shaders/hmd_ui_glow.frag @@ -13,6 +13,9 @@ struct OverlayData { vec4 glowPoints; vec4 glowColors[2]; vec4 resolutionRadiusAlpha; + + vec4 extraGlowColor; + vec2 extraGlowPoint; }; layout(std140) uniform overlayBuffer { @@ -25,6 +28,9 @@ float alpha = overlay.resolutionRadiusAlpha.w; vec4 glowPoints = overlay.glowPoints; vec4 glowColors[2] = overlay.glowColors; +vec2 extraGlowPoint = overlay.extraGlowPoint; +vec4 extraGlowColor = overlay.extraGlowColor; + in vec3 vPosition; in vec2 vTexCoord; @@ -48,11 +54,16 @@ void main() { float glowIntensity = 0.0; float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); - float dist = min(dist1, dist2); + float dist3 = distance(vTexCoord * aspect, extraGlowPoint * aspect); + float distX = min(dist1, dist2); + float dist = min(distX, dist3); vec3 glowColor = glowColors[0].rgb; if (dist2 < dist1) { glowColor = glowColors[1].rgb; } + if (dist3 < dist2) { + glowColor = extraGlowColor.rgb; + } if (dist <= radius) { glowIntensity = 1.0 - (dist / radius); diff --git a/interface/resources/shaders/hmd_ui_glow.vert b/interface/resources/shaders/hmd_ui_glow.vert index 54eb062590..71089d8608 100644 --- a/interface/resources/shaders/hmd_ui_glow.vert +++ b/interface/resources/shaders/hmd_ui_glow.vert @@ -11,6 +11,9 @@ struct OverlayData { vec4 glowPoints; vec4 glowColors[2]; vec4 resolutionRadiusAlpha; + + vec4 extraGlowColor; + vec2 extraGlowPoint; }; layout(std140) uniform overlayBuffer { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 80e444462f..5fe15fa8e5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -34,6 +34,8 @@ #include #include +#include + #include #include @@ -94,6 +96,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +128,7 @@ #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" +#include "networking/HFWebEngineProfile.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" @@ -415,8 +419,6 @@ bool setupEssentials(int& argc, char** argv) { static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); bool previousSessionCrashed = CrashHandler::checkForResetSettings(suppressPrompt); - CrashHandler::writeRunningMarkerFiler(); - qAddPostRoutine(CrashHandler::deleteRunningMarkerFile); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); @@ -504,8 +506,11 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; -Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : +Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : QApplication(argc, argv), + _shouldRunServer(runServer), + _runServerPath(runServerPathOption), + _runningMarker(this, RUNNING_MARKER_FILENAME), _window(new MainWindow(desktop())), _sessionRunTimer(startupTimer), _previousSessionCrashed(setupEssentials(argc, argv)), @@ -529,7 +534,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _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 PluginManager::getInstance()->setContainer(pluginContainer); @@ -564,6 +571,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _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; @@ -575,6 +583,37 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; #endif + + bool wantsSandboxRunning = shouldRunServer(); + static bool determinedSandboxState = false; + static bool sandboxIsRunning = false; + SandboxUtils sandboxUtils; + // updateHeartbeat() because we are going to poll shortly... + updateHeartbeat(); + sandboxUtils.ifLocalSandboxRunningElse([&]() { + qCDebug(interfaceapp) << "Home sandbox appears to be running....."; + determinedSandboxState = true; + sandboxIsRunning = true; + }, [&, wantsSandboxRunning]() { + qCDebug(interfaceapp) << "Home sandbox does not appear to be running...."; + if (wantsSandboxRunning) { + QString contentPath = getRunServerPath(); + SandboxUtils::runLocalSandbox(contentPath, true, RUNNING_MARKER_FILENAME); + sandboxIsRunning = true; + } + determinedSandboxState = true; + }); + + // SandboxUtils::runLocalSandbox currently has 2 sec delay after spawning sandbox, so 4 + // sec here is ok I guess. TODO: ping sandbox so we know it is up, perhaps? + quint64 MAX_WAIT_TIME = USECS_PER_SECOND * 4; + auto startWaiting = usecTimestampNow(); + while (!determinedSandboxState && (usecTimestampNow() - startWaiting <= MAX_WAIT_TIME)) { + QCoreApplication::processEvents(); + // updateHeartbeat() while polling so we don't scare the deadlock watchdog + updateHeartbeat(); + usleep(USECS_PER_MSEC * 50); // 20hz + } _bookmarks = new Bookmarks(); // Before setting up the menu @@ -668,6 +707,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); + // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected + // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); + // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; @@ -818,7 +861,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, { "gl_version", glContextData["version"] }, { "gl_vender", glContextData["vendor"] }, - { "gl_sl_version", glContextData["slVersion"] }, + { "gl_sl_version", glContextData["sl_version"] }, { "gl_renderer", glContextData["renderer"] }, { "ideal_thread_count", QThread::idealThreadCount() } }; @@ -842,8 +885,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : UserActivityLogger::getInstance().logAction("launch", properties); - _connectionMonitor.init(); - // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); _entityEditSender.setMyAvatar(myAvatar.get()); @@ -1104,7 +1145,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : }); // 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); }); @@ -1135,15 +1176,25 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); } + // content location and build info - useful for filtering stats + auto addressManager = DependencyManager::get(); + auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only + auto currentPath = addressManager->currentPath(true); // with orientation + properties["current_domain"] = currentDomain; + properties["current_path"] = currentPath; + properties["build_version"] = BuildInfo::VERSION; + auto displayPlugin = qApp->getActiveDisplayPlugin(); properties["fps"] = _frameCounter.rate(); properties["target_frame_rate"] = getTargetFrameRate(); + properties["render_rate"] = displayPlugin->renderRate(); properties["present_rate"] = displayPlugin->presentRate(); properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); 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(); @@ -1184,6 +1235,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); properties["using_hmd"] = isHMDMode(); + 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); lastHMDHeadPose = hmdHeadPose; @@ -1261,7 +1317,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Get sandbox content set version, if available auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; - qDebug() << "Checking " << contentVersionPath << " for content version"; + qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; QFile contentVersionFile(contentVersionPath); if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -1269,7 +1325,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // toInt() returns 0 if the conversion fails, so we don't need to specifically check for failure contentVersion = line.toInt(); } - qDebug() << "Server content version: " << contentVersion; + qCDebug(interfaceapp) << "Server content version: " << contentVersion; bool hasTutorialContent = contentVersion >= 1; @@ -1279,10 +1335,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : bool shouldGoToTutorial = hasHMDAndHandControllers && hasTutorialContent && !tutorialComplete.get(); - qDebug() << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName(); - qDebug() << "Has tutorial content: " << hasTutorialContent; - qDebug() << "Tutorial complete: " << tutorialComplete.get(); - qDebug() << "Should go to tutorial: " << shouldGoToTutorial; + qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName(); + qCDebug(interfaceapp) << "Has tutorial content: " << hasTutorialContent; + qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get(); + qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial; // when --url in command line, teleport to location const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; @@ -1295,11 +1351,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : const QString TUTORIAL_PATH = "/tutorial_begin"; if (shouldGoToTutorial) { - DependencyManager::get()->ifLocalSandboxRunningElse([=]() { - qDebug() << "Home sandbox appears to be running, going to Home."; + if(sandboxIsRunning) { + qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(TUTORIAL_PATH); - }, [=]() { - qDebug() << "Home sandbox does not appear to be running, going to Entry."; + } else { + qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; if (firstRun.get()) { showHelp(); } @@ -1308,7 +1364,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } else { DependencyManager::get()->loadSettings(addressLookupString); } - }); + } } else { bool isFirstRun = firstRun.get(); @@ -1320,22 +1376,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // If this is a first run we short-circuit the address passed in if (isFirstRun) { if (hasHMDAndHandControllers) { - DependencyManager::get()->ifLocalSandboxRunningElse([=]() { - qDebug() << "Home sandbox appears to be running, going to Home."; + if(sandboxIsRunning) { + qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); - }, [=]() { - qDebug() << "Home sandbox does not appear to be running, going to Entry."; + } else { + qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; DependencyManager::get()->goToEntry(); - }); + } } else { DependencyManager::get()->goToEntry(); } } else { - qDebug() << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); + qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); } } + _connectionMonitor.init(); + // After all of the constructor is completed, then set firstRun to false. firstRun.set(false); } @@ -1438,6 +1496,7 @@ void Application::updateHeartbeat() const { void Application::aboutToQuit() { emit beforeAboutToQuit(); + DependencyManager::get()->beforeAboutToQuit(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->isActive()) { @@ -1498,6 +1557,9 @@ void Application::cleanupBeforeQuit() { DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts DependencyManager::destroy(); + _displayPlugin.reset(); + PluginManager::getInstance()->shutdown(); + // Cleanup all overlays after the scripts, as scripts might add more _overlays.cleanupAllOverlays(); @@ -1513,17 +1575,6 @@ void Application::cleanupBeforeQuit() { saveSettings(); _window->saveGeometry(); - // stop the AudioClient - QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); - - // destroy the AudioClient so it and its thread have a chance to go down safely - DependencyManager::destroy(); - - // destroy the AudioInjectorManager so it and its thread have a chance to go down safely - // this will also stop any ongoing network injectors - DependencyManager::destroy(); - // Destroy third party processes after scripts have finished using them. #ifdef HAVE_DDE DependencyManager::destroy(); @@ -1532,10 +1583,29 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); #endif + // stop QML DependencyManager::destroy(); + + // stop audio after QML, as there are unexplained audio crashes originating in qtwebengine + + // stop the AudioClient, synchronously + QMetaObject::invokeMethod(DependencyManager::get().data(), + "stop", Qt::BlockingQueuedConnection); + + // destroy Audio so it and its threads have a chance to go down safely + DependencyManager::destroy(); + DependencyManager::destroy(); + + // shutdown render engine + _main3DScene = nullptr; + _renderEngine = nullptr; + + qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; } Application::~Application() { + DependencyManager::destroy(); + _entityClipboard->eraseAllOctreeElements(); _entityClipboard.reset(); @@ -1553,7 +1623,6 @@ Application::~Application() { DependencyManager::get()->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); - DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); @@ -1667,6 +1736,7 @@ void Application::initializeUi() { UpdateDialog::registerType(); qmlRegisterType("Hifi", 1, 0, "Preference"); + qmlRegisterType("HFWebEngineProfile", 1, 0, "HFWebEngineProfile"); auto offscreenUi = DependencyManager::get(); offscreenUi->create(_glWidget->qglContext()); @@ -1694,6 +1764,7 @@ void Application::initializeUi() { // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); rootContext->setContextProperty("Entities", DependencyManager::get().data()); FileScriptingInterface* fileDownload = new FileScriptingInterface(engine); @@ -2120,13 +2191,10 @@ void Application::resizeGL() { auto offscreenUi = DependencyManager::get(); auto uiSize = displayPlugin->getRecommendedUiSize(); // Bit of a hack since there's no device pixel ratio change event I can find. - static qreal lastDevicePixelRatio = 0; - qreal devicePixelRatio = _window->devicePixelRatio(); - if (offscreenUi->size() != fromGlm(uiSize) || devicePixelRatio != lastDevicePixelRatio) { + if (offscreenUi->size() != fromGlm(uiSize)) { qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; offscreenUi->resize(fromGlm(uiSize), true); _offscreenContext->makeCurrent(); - lastDevicePixelRatio = devicePixelRatio; } } @@ -3404,7 +3472,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 @@ -3670,7 +3738,18 @@ void Application::setKeyboardFocusEntity(QUuid id) { void Application::setKeyboardFocusEntity(EntityItemID entityItemID) { auto entityScriptingInterface = DependencyManager::get(); if (_keyboardFocusedItem.get() != entityItemID) { + // reset focused entity _keyboardFocusedItem.set(UNKNOWN_ENTITY_ID); + if (_keyboardFocusHighlight) { + _keyboardFocusHighlight->setVisible(false); + } + + // if invalid, return without expensive (locking) operations + if (entityItemID == UNKNOWN_ENTITY_ID) { + return; + } + + // if valid, query properties auto properties = entityScriptingInterface->getEntityProperties(entityItemID); if (!properties.getLocked() && properties.getVisible()) { auto entity = getEntities()->getTree()->findEntityByID(entityItemID); @@ -3681,6 +3760,8 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) { } _keyboardFocusedItem.set(entityItemID); _lastAcceptedKeyPress = usecTimestampNow(); + + // create a focus if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) { _keyboardFocusHighlight = std::make_shared(); _keyboardFocusHighlight->setAlpha(1.0f); @@ -3692,17 +3773,16 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) { _keyboardFocusHighlight->setColorPulse(1.0); _keyboardFocusHighlight->setIgnoreRayIntersection(true); _keyboardFocusHighlight->setDrawInFront(false); + _keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight); } + + // position the focus _keyboardFocusHighlight->setRotation(entity->getRotation()); _keyboardFocusHighlight->setPosition(entity->getPosition()); _keyboardFocusHighlight->setDimensions(entity->getDimensions() * 1.05f); _keyboardFocusHighlight->setVisible(true); - _keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight); } } - if (_keyboardFocusedItem.get() == UNKNOWN_ENTITY_ID && _keyboardFocusHighlight) { - _keyboardFocusHighlight->setVisible(false); - } } } @@ -3712,12 +3792,6 @@ void Application::updateDialogs(float deltaTime) const { PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); auto dialogsManager = DependencyManager::get(); - // Update audio stats dialog, if any - AudioStatsDialog* audioStatsDialog = dialogsManager->getAudioStatsDialog(); - if(audioStatsDialog) { - audioStatsDialog->update(); - } - // Update bandwidth dialog, if any BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog(); if (bandwidthDialog) { @@ -3921,8 +3995,6 @@ void Application::update(float deltaTime) { auto collisionEvents = _physicsEngine->getCollisionEvents(); avatarManager->handleCollisionEvents(collisionEvents); - _physicsEngine->dumpStatsIfNecessary(); - if (!_aboutToQuit) { PerformanceTimer perfTimer("entities"); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk @@ -3935,6 +4007,13 @@ void Application::update(float deltaTime) { } myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + + if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); } } } @@ -4966,6 +5045,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); // Caches scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); @@ -5634,6 +5714,9 @@ void Application::updateDisplayMode() { // Make the switch atomic from the perspective of other threads { std::unique_lock lock(_displayPluginLock); + // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. + bool wasRepositionLocked = offscreenUi->getDesktop()->property("repositionLocked").toBool(); + offscreenUi->getDesktop()->setProperty("repositionLocked", true); auto oldDisplayPlugin = _displayPlugin; if (_displayPlugin) { @@ -5670,6 +5753,7 @@ void Application::updateDisplayMode() { _offscreenContext->makeCurrent(); getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; + offscreenUi->getDesktop()->setProperty("repositionLocked", wasRepositionLocked); } emit activeDisplayPluginChanged(); diff --git a/interface/src/Application.h b/interface/src/Application.h index e6429f1ebe..79c9ae735c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -46,6 +46,8 @@ #include #include +#include + #include "avatar/MyAvatar.h" #include "Bookmarks.h" #include "Camera.h" @@ -57,7 +59,6 @@ #include "scripting/ControllerScriptingInterface.h" #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" -#include "ui/AudioStatsDialog.h" #include "ui/BandwidthDialog.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" @@ -87,6 +88,8 @@ static const UINT UWM_SHOW_APPLICATION = RegisterWindowMessage("UWM_SHOW_APPLICATION_{71123FD6-3DA8-4DC1-9C27-8A12A6250CBA}_" + qgetenv("USERNAME")); #endif +static const QString RUNNING_MARKER_FILENAME = "Interface.running"; + class Application; #if defined(qApp) #undef qApp @@ -103,7 +106,16 @@ class Application : public QApplication, // TODO? Get rid of those friend class OctreePacketProcessor; +private: + bool _shouldRunServer { false }; + QString _runServerPath; + RunningMarker _runningMarker; + public: + // startup related getter/setters + bool shouldRunServer() const { return _shouldRunServer; } + bool hasRunServerPath() const { return !_runServerPath.isEmpty(); } + QString getRunServerPath() const { return _runServerPath; } // virtual functions required for PluginContainer virtual ui::Menu* getPrimaryMenu() override; @@ -127,7 +139,7 @@ public: static void initPlugins(const QStringList& arguments); static void shutdownPlugins(); - Application(int& argc, char** argv, QElapsedTimer& startup_time); + Application(int& argc, char** argv, QElapsedTimer& startup_time, bool runServer, QString runServerPathOption); ~Application(); void postLambdaEvent(std::function f) override; @@ -168,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; } @@ -217,7 +229,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 diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index bab626c0b9..fcb1908994 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -13,34 +13,42 @@ #include "ui/DialogsManager.h" -#include #include #include -#include +#include +// Because the connection monitor is created at startup, the time we wait on initial load +// should be longer to allow the application to initialize. +static const int ON_INITIAL_LOAD_DISPLAY_AFTER_DISCONNECTED_FOR_X_MS = 10000; static const int DISPLAY_AFTER_DISCONNECTED_FOR_X_MS = 5000; void ConnectionMonitor::init() { // Connect to domain disconnected message auto nodeList = DependencyManager::get(); const DomainHandler& domainHandler = nodeList->getDomainHandler(); - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &ConnectionMonitor::disconnectedFromDomain); - connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::connectedToDomain); + connect(&domainHandler, &DomainHandler::resetting, this, &ConnectionMonitor::startTimer); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &ConnectionMonitor::startTimer); + connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::stopTimer); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer); _timer.setSingleShot(true); - _timer.setInterval(DISPLAY_AFTER_DISCONNECTED_FOR_X_MS); if (!domainHandler.isConnected()) { - _timer.start(); + _timer.start(ON_INITIAL_LOAD_DISPLAY_AFTER_DISCONNECTED_FOR_X_MS); } - auto dialogsManager = DependencyManager::get(); - connect(&_timer, &QTimer::timeout, dialogsManager.data(), &DialogsManager::showAddressBar); + connect(&_timer, &QTimer::timeout, this, []() { + qDebug() << "ConnectionMonitor: Showing connection failure window"; + DependencyManager::get()->setDomainConnectionFailureVisibility(true); + }); } -void ConnectionMonitor::disconnectedFromDomain() { - _timer.start(); +void ConnectionMonitor::startTimer() { + qDebug() << "ConnectionMonitor: Starting timer"; + _timer.start(DISPLAY_AFTER_DISCONNECTED_FOR_X_MS); } -void ConnectionMonitor::connectedToDomain(const QString& name) { +void ConnectionMonitor::stopTimer() { + qDebug() << "ConnectionMonitor: Stopping timer"; _timer.stop(); + DependencyManager::get()->setDomainConnectionFailureVisibility(false); } diff --git a/interface/src/ConnectionMonitor.h b/interface/src/ConnectionMonitor.h index ddd80c4af5..e3d393163b 100644 --- a/interface/src/ConnectionMonitor.h +++ b/interface/src/ConnectionMonitor.h @@ -23,8 +23,8 @@ public: void init(); private slots: - void disconnectedFromDomain(); - void connectedToDomain(const QString& name); + void startTimer(); + void stopTimer(); private: QTimer _timer; diff --git a/interface/src/CrashHandler.cpp b/interface/src/CrashHandler.cpp index 3c5f03bef3..0c9698ffc3 100644 --- a/interface/src/CrashHandler.cpp +++ b/interface/src/CrashHandler.cpp @@ -23,9 +23,10 @@ #include #include +#include "Application.h" #include "Menu.h" -static const QString RUNNING_MARKER_FILENAME = "Interface.running"; +#include bool CrashHandler::checkForResetSettings(bool suppressPrompt) { QSettings::setDefaultFormat(QSettings::IniFormat); @@ -39,7 +40,7 @@ bool CrashHandler::checkForResetSettings(bool suppressPrompt) { // If option does not exist in Interface.ini so assume default behavior. bool displaySettingsResetOnCrash = !displayCrashOptions.isValid() || displayCrashOptions.toBool(); - QFile runningMarkerFile(runningMarkerFilePath()); + QFile runningMarkerFile(RunningMarker::getMarkerFilePath(RUNNING_MARKER_FILENAME)); bool wasLikelyCrash = runningMarkerFile.exists(); if (suppressPrompt) { @@ -161,20 +162,3 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { } } -void CrashHandler::writeRunningMarkerFiler() { - QFile runningMarkerFile(runningMarkerFilePath()); - if (!runningMarkerFile.exists()) { - runningMarkerFile.open(QIODevice::WriteOnly); - runningMarkerFile.close(); - } -} -void CrashHandler::deleteRunningMarkerFile() { - QFile runningMarkerFile(runningMarkerFilePath()); - if (runningMarkerFile.exists()) { - runningMarkerFile.remove(); - } -} - -const QString CrashHandler::runningMarkerFilePath() { - return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + RUNNING_MARKER_FILENAME; -} diff --git a/interface/src/CrashHandler.h b/interface/src/CrashHandler.h index a65fed677d..308cac3411 100644 --- a/interface/src/CrashHandler.h +++ b/interface/src/CrashHandler.h @@ -19,9 +19,6 @@ class CrashHandler { public: static bool checkForResetSettings(bool suppressPrompt = false); - static void writeRunningMarkerFiler(); - static void deleteRunningMarkerFile(); - private: enum Action { DELETE_INTERFACE_INI, @@ -31,8 +28,6 @@ private: static Action promptUserForAction(bool showCrashMessage); static void handleCrash(Action action); - - static const QString runningMarkerFilePath(); }; #endif // hifi_CrashHandler_h 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 a475b3e6d6..d357713bff 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); @@ -605,6 +608,7 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandMyAvatarSimulateTiming, 0, false); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandOtherAvatarTiming, 0, false); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandPaintGLTiming, 0, false); + addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandPhysicsSimulationTiming, 0, false); addCheckableActionToQMenuAndActionHash(timingMenu, MenuOption::FrameTimer); addActionToQMenuAndActionHash(timingMenu, MenuOption::RunTimingTests, 0, qApp, SLOT(runTests())); @@ -616,6 +620,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"); @@ -654,10 +666,6 @@ Menu::Menu() { audioScopeFramesGroup->addAction(fiftyFrames); } - // Developer > Audio > Audio Network Stats... - addActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNetworkStats, 0, - dialogsManager.data(), SLOT(audioStatsDetails())); - // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 95cd4c5aa6..640a3e05d2 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -37,7 +37,6 @@ namespace MenuOption { const QString AssetMigration = "ATP Asset Migration"; const QString AssetServer = "Asset Browser"; const QString Attachments = "Attachments..."; - const QString AudioNetworkStats = "Audio Network Stats"; const QString AudioNoiseReduction = "Audio Noise Reduction"; const QString AudioScope = "Show Scope"; const QString AudioScopeFiftyFrames = "Fifty"; @@ -105,6 +104,7 @@ namespace MenuOption { const QString ExpandMyAvatarTiming = "Expand /myAvatar"; const QString ExpandOtherAvatarTiming = "Expand /otherAvatar"; const QString ExpandPaintGLTiming = "Expand /paintGL"; + const QString ExpandPhysicsSimulationTiming = "Expand /physics"; const QString ExpandUpdateTiming = "Expand /update"; const QString Faceshift = "Faceshift"; const QString FirstPerson = "First Person"; @@ -136,6 +136,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/Util.cpp b/interface/src/Util.cpp index 00715bcd69..7bf48d98d2 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -50,50 +50,69 @@ void renderWorldBox(gpu::Batch& batch) { static const float DASH_LENGTH = 1.0f; static const float GAP_LENGTH = 1.0f; auto transform = Transform{}; + static std::array geometryIds; + static std::once_flag initGeometryIds; + std::call_once(initGeometryIds, [&] { + for (size_t i = 0; i < geometryIds.size(); ++i) { + geometryIds[i] = geometryCache->allocateID(); + } + }); batch.setModelTransform(transform); - geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(HALF_TREE_SCALE, 0.0f, 0.0f), RED); + geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(HALF_TREE_SCALE, 0.0f, 0.0f), RED, geometryIds[0]); geometryCache->renderDashedLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-HALF_TREE_SCALE, 0.0f, 0.0f), DASHED_RED, - DASH_LENGTH, GAP_LENGTH); + DASH_LENGTH, GAP_LENGTH, geometryIds[1]); - geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, HALF_TREE_SCALE, 0.0f), GREEN); + geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, HALF_TREE_SCALE, 0.0f), GREEN, geometryIds[2]); geometryCache->renderDashedLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -HALF_TREE_SCALE, 0.0f), DASHED_GREEN, - DASH_LENGTH, GAP_LENGTH); + DASH_LENGTH, GAP_LENGTH, geometryIds[3]); - geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, HALF_TREE_SCALE), BLUE); + geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, HALF_TREE_SCALE), BLUE, geometryIds[4]); geometryCache->renderDashedLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -HALF_TREE_SCALE), DASHED_BLUE, - DASH_LENGTH, GAP_LENGTH); + DASH_LENGTH, GAP_LENGTH, geometryIds[5]); // X center boundaries geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, -HALF_TREE_SCALE, 0.0f), - glm::vec3(HALF_TREE_SCALE, -HALF_TREE_SCALE, 0.0f), GREY); + glm::vec3(HALF_TREE_SCALE, -HALF_TREE_SCALE, 0.0f), GREY, + geometryIds[6]); geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, -HALF_TREE_SCALE, 0.0f), - glm::vec3(-HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY); + glm::vec3(-HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY, + geometryIds[7]); geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), - glm::vec3(HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY); + glm::vec3(HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY, + geometryIds[8]); geometryCache->renderLine(batch, glm::vec3(HALF_TREE_SCALE, -HALF_TREE_SCALE, 0.0f), - glm::vec3(HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY); + glm::vec3(HALF_TREE_SCALE, HALF_TREE_SCALE, 0.0f), GREY, + geometryIds[9]); // Z center boundaries geometryCache->renderLine(batch, glm::vec3(0.0f, -HALF_TREE_SCALE, -HALF_TREE_SCALE), - glm::vec3(0.0f, -HALF_TREE_SCALE, HALF_TREE_SCALE), GREY); + glm::vec3(0.0f, -HALF_TREE_SCALE, HALF_TREE_SCALE), GREY, + geometryIds[10]); geometryCache->renderLine(batch, glm::vec3(0.0f, -HALF_TREE_SCALE, -HALF_TREE_SCALE), - glm::vec3(0.0f, HALF_TREE_SCALE, -HALF_TREE_SCALE), GREY); + glm::vec3(0.0f, HALF_TREE_SCALE, -HALF_TREE_SCALE), GREY, + geometryIds[11]); geometryCache->renderLine(batch, glm::vec3(0.0f, HALF_TREE_SCALE, -HALF_TREE_SCALE), - glm::vec3(0.0f, HALF_TREE_SCALE, HALF_TREE_SCALE), GREY); + glm::vec3(0.0f, HALF_TREE_SCALE, HALF_TREE_SCALE), GREY, + geometryIds[12]); geometryCache->renderLine(batch, glm::vec3(0.0f, -HALF_TREE_SCALE, HALF_TREE_SCALE), - glm::vec3(0.0f, HALF_TREE_SCALE, HALF_TREE_SCALE), GREY); + glm::vec3(0.0f, HALF_TREE_SCALE, HALF_TREE_SCALE), GREY, + geometryIds[13]); // Center boundaries geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, 0.0f, -HALF_TREE_SCALE), - glm::vec3(-HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY); + glm::vec3(-HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY, + geometryIds[14]); geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, 0.0f, -HALF_TREE_SCALE), - glm::vec3(HALF_TREE_SCALE, 0.0f, -HALF_TREE_SCALE), GREY); + glm::vec3(HALF_TREE_SCALE, 0.0f, -HALF_TREE_SCALE), GREY, + geometryIds[15]); geometryCache->renderLine(batch, glm::vec3(HALF_TREE_SCALE, 0.0f, -HALF_TREE_SCALE), - glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY); + glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY, + geometryIds[16]); geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), - glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY); + glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY, + geometryIds[17]); geometryCache->renderWireCubeInstance(batch, GREY4); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 717a0eeac7..0366d566d2 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -99,12 +99,17 @@ Avatar::Avatar(RigPointer rig) : _skeletonModel = std::make_shared(this, nullptr, rig); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + + auto geometryCache = DependencyManager::get(); + _nameRectGeometryID = geometryCache->allocateID(); + _leftPointerGeometryID = geometryCache->allocateID(); + _rightPointerGeometryID = geometryCache->allocateID(); } 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([&] { @@ -119,6 +124,13 @@ Avatar::~Avatar() { delete _motionState; _motionState = nullptr; } + + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_nameRectGeometryID); + geometryCache->releaseID(_leftPointerGeometryID); + geometryCache->releaseID(_rightPointerGeometryID); + } } void Avatar::init() { @@ -187,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; @@ -492,7 +504,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { pointerTransform.setRotation(rotation); batch.setModelTransform(pointerTransform); geometryCache->bindSimpleProgram(batch); - geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor); + geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor, _leftPointerGeometryID); } } @@ -516,7 +528,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { pointerTransform.setRotation(rotation); batch.setModelTransform(pointerTransform); geometryCache->bindSimpleProgram(batch); - geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor); + geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor, _rightPointerGeometryID); } } } @@ -603,14 +615,14 @@ void Avatar::fixupModelsInScene() { _skeletonModel->removeFromScene(scene, pendingChanges); _skeletonModel->addToScene(scene, pendingChanges); } - for (auto& attachmentModel : _attachmentModels) { + for (auto attachmentModel : _attachmentModels) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { attachmentModel->removeFromScene(scene, pendingChanges); attachmentModel->addToScene(scene, pendingChanges); } } - for (auto& attachmentModelToRemove : _attachmentsToRemove) { + for (auto attachmentModelToRemove : _attachmentsToRemove) { attachmentModelToRemove->removeFromScene(scene, pendingChanges); } _attachmentsToDelete.insert(_attachmentsToDelete.end(), _attachmentsToRemove.begin(), _attachmentsToRemove.end()); @@ -782,7 +794,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true); DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, - bevelDistance, backgroundColor); + bevelDistance, backgroundColor, _nameRectGeometryID); } // Render actual name diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 910f2cc1e6..9f5ac392bc 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -248,6 +248,9 @@ protected: ThreadSafeValueCache _rightPalmRotationCache { glm::quat() }; private: + int _leftPointerGeometryID { 0 }; + int _rightPointerGeometryID { 0 }; + int _nameRectGeometryID { 0 }; bool _initialized; bool _shouldAnimate { true }; bool _shouldSkipRender { false }; diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index e85cb8913c..85ff485d7a 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -37,9 +37,13 @@ AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntit } AvatarActionHold::~AvatarActionHold() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - if (myAvatar) { - myAvatar->removeHoldAction(this); + // Sometimes actions are destroyed after the AvatarManager is destroyed by the Application. + auto avatarManager = DependencyManager::get(); + if (avatarManager) { + auto myAvatar = avatarManager->getMyAvatar(); + if (myAvatar) { + myAvatar->removeHoldAction(this); + } } #if WANT_DEBUG diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 18856ff914..e7f3deeaea 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -36,6 +36,7 @@ #include "Application.h" #include "Avatar.h" #include "AvatarManager.h" +#include "InterfaceLogging.h" #include "Menu.h" #include "MyAvatar.h" #include "SceneScriptingInterface.h" @@ -208,11 +209,15 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe auto rawRenderableAvatar = std::static_pointer_cast(newAvatar); render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - if (DependencyManager::get()->shouldRenderAvatars()) { - rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges); + if (scene) { + render::PendingChanges pendingChanges; + if (DependencyManager::get()->shouldRenderAvatars()) { + rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges); + } + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown"; } - scene->enqueuePendingChanges(pendingChanges); return newAvatar; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 19346e51db..735ba05810 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -247,6 +247,15 @@ void MyAvatar::centerBody() { auto worldBodyPos = extractTranslation(worldBodyMatrix); auto worldBodyRot = glm::normalize(glm::quat_cast(worldBodyMatrix)); + if (_characterController.getState() == CharacterController::State::Ground) { + // the avatar's physical aspect thinks it is standing on something + // therefore need to be careful to not "center" the body below the floor + float downStep = glm::dot(worldBodyPos - getPosition(), _worldUpDirection); + if (downStep < -0.5f * _characterController.getCapsuleHalfHeight() + _characterController.getCapsuleRadius()) { + worldBodyPos -= downStep * _worldUpDirection; + } + } + // this will become our new position. setPosition(worldBodyPos); setOrientation(worldBodyRot); @@ -463,7 +472,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; @@ -1929,7 +1938,7 @@ void MyAvatar::setCharacterControllerEnabled(bool enabled) { } bool ghostingAllowed = true; - EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + auto entityTreeRenderer = qApp->getEntities(); if (entityTreeRenderer) { std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); if (zone) { @@ -2280,15 +2289,16 @@ 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) { - // to prevent actions from adding or removing themselves from the _holdActions vector - // while we are iterating, we need to enter a critical section. - std::lock_guard guard(_holdActionsMutex); - // lateAvatarUpdate will modify entity position & orientation, so we need an entity write lock entityTree->withWriteLock([&] { + + // to prevent actions from adding or removing themselves from the _holdActions vector + // while we are iterating, we need to enter a critical section. + std::lock_guard guard(_holdActionsMutex); + for (auto& holdAction : _holdActions) { holdAction->lateAvatarUpdate(prePhysicsPose, postUpdatePose); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3c4a3fd77a..a88388050b 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -37,6 +37,12 @@ #include #endif +#ifdef Q_OS_WIN +extern "C" { + typedef int(__stdcall * CHECKMINSPECPROC) (); +} +#endif + int main(int argc, const char* argv[]) { #if HAS_BUGSPLAT static QString BUG_SPLAT_DATABASE = "interface_alpha"; @@ -128,22 +134,9 @@ int main(int argc, const char* argv[]) { parser.addOption(runServerOption); parser.addOption(serverContentPathOption); parser.parse(arguments); - if (parser.isSet(runServerOption)) { - QString applicationDirPath = QFileInfo(arguments[0]).path(); - QString serverPath = applicationDirPath + "/server-console/server-console.exe"; - qDebug() << "Application dir path is: " << applicationDirPath; - qDebug() << "Server path is: " << serverPath; - QStringList args; - if (parser.isSet(serverContentPathOption)) { - QString serverContentPath = QFileInfo(arguments[0]).path() + "/" + parser.value(serverContentPathOption); - args << "--" << "--contentPath" << serverContentPath; - } - qDebug() << QFileInfo(arguments[0]).path(); - qDebug() << QProcess::startDetached(serverPath, args); - - // Sleep a short amount of time to give the server a chance to start - usleep(2000000); - } + bool runServer = parser.isSet(runServerOption); + bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); + QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); QElapsedTimer startupTime; startupTime.start(); @@ -166,10 +159,32 @@ 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); + Application app(argc, const_cast(argv), startupTime, runServer, serverContentPathOptionValue); // If we failed the OpenGLVersion check, log it. if (override) { @@ -223,7 +238,6 @@ int main(int argc, const char* argv[]) { QTranslator translator; translator.load("i18n/interface_en"); app.installTranslator(&translator); - qCDebug(interfaceapp, "Created QT Application."); exitCode = app.exec(); server.close(); diff --git a/interface/src/networking/HFWebEngineProfile.cpp b/interface/src/networking/HFWebEngineProfile.cpp new file mode 100644 index 0000000000..6b377fa900 --- /dev/null +++ b/interface/src/networking/HFWebEngineProfile.cpp @@ -0,0 +1,27 @@ +// +// HFWebEngineProfile.cpp +// interface/src/networking +// +// Created by Stephen Birarda on 2016-10-17. +// 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 +// + +#include "HFWebEngineProfile.h" + +#include "HFWebEngineRequestInterceptor.h" + +static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; + +HFWebEngineProfile::HFWebEngineProfile(QObject* parent) : + QQuickWebEngineProfile(parent) +{ + static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)"; + setHttpUserAgent(WEB_ENGINE_USER_AGENT); + + // we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user + auto requestInterceptor = new HFWebEngineRequestInterceptor(this); + setRequestInterceptor(requestInterceptor); +} diff --git a/interface/src/networking/HFWebEngineProfile.h b/interface/src/networking/HFWebEngineProfile.h new file mode 100644 index 0000000000..5c7655479e --- /dev/null +++ b/interface/src/networking/HFWebEngineProfile.h @@ -0,0 +1,25 @@ +// +// HFWebEngineProfile.h +// interface/src/networking +// +// Created by Stephen Birarda on 2016-10-17. +// 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 +// + +#pragma once + +#ifndef hifi_HFWebEngineProfile_h +#define hifi_HFWebEngineProfile_h + +#include + +class HFWebEngineProfile : public QQuickWebEngineProfile { +public: + HFWebEngineProfile(QObject* parent = Q_NULLPTR); +}; + + +#endif // hifi_HFWebEngineProfile_h diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.cpp b/interface/src/networking/HFWebEngineRequestInterceptor.cpp new file mode 100644 index 0000000000..9c3f0b232e --- /dev/null +++ b/interface/src/networking/HFWebEngineRequestInterceptor.cpp @@ -0,0 +1,40 @@ +// +// HFWebEngineRequestInterceptor.cpp +// interface/src/networking +// +// Created by Stephen Birarda on 2016-10-14. +// 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 +// + +#include "HFWebEngineRequestInterceptor.h" + +#include + +#include + +bool isAuthableHighFidelityURL(const QUrl& url) { + static const QStringList HF_HOSTS = { + "highfidelity.com", "highfidelity.io", + "metaverse.highfidelity.com", "metaverse.highfidelity.io" + }; + + return url.scheme() == "https" && HF_HOSTS.contains(url.host()); +} + +void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { + // check if this is a request to a highfidelity URL + if (isAuthableHighFidelityURL(info.requestUrl())) { + // if we have an access token, add it to the right HTTP header for authorization + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; + + QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; + info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit()); + } + } +} diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.h b/interface/src/networking/HFWebEngineRequestInterceptor.h new file mode 100644 index 0000000000..a4c308426c --- /dev/null +++ b/interface/src/networking/HFWebEngineRequestInterceptor.h @@ -0,0 +1,26 @@ +// +// HFWebEngineRequestInterceptor.h +// interface/src/networking +// +// Created by Stephen Birarda on 2016-10-14. +// 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 +// + +#pragma once + +#ifndef hifi_HFWebEngineRequestInterceptor_h +#define hifi_HFWebEngineRequestInterceptor_h + +#include + +class HFWebEngineRequestInterceptor : public QWebEngineUrlRequestInterceptor { +public: + HFWebEngineRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {}; + + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; +}; + +#endif // hifi_HFWebEngineRequestInterceptor_h 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/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 2c769c37d4..fb1440ebdf 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -57,6 +57,20 @@ bool HMDScriptingInterface::isHandControllerAvailable() { return PluginUtils::isHandControllerAvailable(); } +void HMDScriptingInterface::requestShowHandControllers() { + _showHandControllersCount++; + emit shouldShowHandControllersChanged(); +} + +void HMDScriptingInterface::requestHideHandControllers() { + _showHandControllersCount--; + emit shouldShowHandControllersChanged(); +} + +bool HMDScriptingInterface::shouldShowHandControllers() const { + return _showHandControllersCount > 0; +} + QScriptValue HMDScriptingInterface::getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine) { glm::vec3 hudIntersection; auto instance = DependencyManager::get(); @@ -130,6 +144,27 @@ bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::ve color, direction); } +bool HMDScriptingInterface::setExtraLaser(const glm::vec3& worldStart, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([offscreenUi, enabled] { + offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); + }); + + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto sensorToWorld = myAvatar->getSensorToWorldMatrix(); + auto worldToSensor = glm::inverse(sensorToWorld); + auto sensorStart = ::transformPoint(worldToSensor, worldStart); + auto sensorDirection = ::transformVectorFast(worldToSensor, direction); + + return qApp->getActiveDisplayPlugin()->setExtraLaser(enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, + color, sensorStart, sensorDirection); +} + +void HMDScriptingInterface::disableExtraLaser() const { + setExtraLaser(vec3(0), false, vec4(0), vec3(0)); +} + void HMDScriptingInterface::disableHandLasers(int hands) const { setHandLasers(hands, false, vec4(0), vec3(0)); } diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 4148b1cb4a..c9ed7f0097 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -41,9 +41,17 @@ public: Q_INVOKABLE bool isHMDAvailable(); Q_INVOKABLE bool isHandControllerAvailable(); - Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + Q_INVOKABLE void requestShowHandControllers(); + Q_INVOKABLE void requestHideHandControllers(); + Q_INVOKABLE bool shouldShowHandControllers() const; + Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; Q_INVOKABLE void disableHandLasers(int hands) const; + + Q_INVOKABLE bool setExtraLaser(const glm::vec3& worldStart, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + Q_INVOKABLE void disableExtraLaser() const; + + /// Suppress the activation of any on-screen keyboard so that a script operation will /// not be interrupted by a keyboard popup /// Returns false if there is already an active keyboard displayed. @@ -61,6 +69,9 @@ public: // rotate the overlay UI sphere so that it is centered about the the current HMD position and orientation Q_INVOKABLE void centerUI(); +signals: + bool shouldShowHandControllersChanged(); + public: HMDScriptingInterface(); static QScriptValue getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine); @@ -77,6 +88,7 @@ private: bool getHUDLookAtPosition3D(glm::vec3& result) const; glm::mat4 getWorldHMDMatrix() const; + std::atomic _showHandControllersCount { 0 }; }; #endif // hifi_HMDScriptingInterface_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index f4937c4459..32336fe3be 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -40,9 +40,18 @@ ApplicationOverlay::ApplicationOverlay() auto geometryCache = DependencyManager::get(); _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); + _qmlGeometryId = geometryCache->allocateID(); + _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_domainStatusBorder); + geometryCache->releaseID(_magnifierBorder); + geometryCache->releaseID(_qmlGeometryId); + geometryCache->releaseID(_rearViewGeometryId); + } } // Renders the overlays either to a texture or to the screen @@ -89,9 +98,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(__FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D([](uint32_t recycleTexture, void* recycleFence){ - DependencyManager::get()->releaseTexture({ recycleTexture, recycleFence }); - })); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -112,7 +119,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { batch.setModelTransform(Transform()); batch.resetViewTransform(); batch.setResourceTexture(0, _uiTexture); - geometryCache->renderUnitQuad(batch, glm::vec4(1)); + geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId); } void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) { @@ -188,7 +195,7 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { batch.setResourceTexture(0, selfieTexture); float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha)); + geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); batch.setResourceTexture(0, renderArgs->_whiteTexture); } @@ -258,7 +265,7 @@ void ApplicationOverlay::buildFramebufferObject() { auto uiSize = qApp->getUiSize(); if (!_overlayFramebuffer || uiSize != _overlayFramebuffer->getSize()) { - _overlayFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _overlayFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("ApplicationOverlay")); } auto width = uiSize.x; diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index d20b569457..7ace5ee885 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -50,6 +50,8 @@ private: gpu::TexturePointer _overlayDepthTexture; gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; + int _qmlGeometryId { 0 }; + int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AudioStatsDialog.cpp b/interface/src/ui/AudioStatsDialog.cpp deleted file mode 100644 index e3cca9f0fe..0000000000 --- a/interface/src/ui/AudioStatsDialog.cpp +++ /dev/null @@ -1,296 +0,0 @@ -// -// AudioStatsDialog.cpp -// interface/src/ui -// -// Created by Bridget Went on 7/9/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "AudioStatsDialog.h" - -#include - -#include -#include -#include -#include -#include -#include -#include - - - -const unsigned COLOR0 = 0x33cc99ff; -const unsigned COLOR1 = 0xffef40c0; -const unsigned COLOR2 = 0xd0d0d0a0; -const unsigned COLOR3 = 0x01DD7880; - - -AudioStatsDisplay::AudioStatsDisplay(QFormLayout* form, - QString text, unsigned colorRGBA) : -_text(text), -_colorRGBA(colorRGBA) -{ - _label = new QLabel(); - _label->setAlignment(Qt::AlignCenter); - - QPalette palette = _label->palette(); - unsigned rgb = colorRGBA >> 8; - rgb = ((rgb & 0xfefefeu) >> 1) + ((rgb & 0xf8f8f8) >> 3); - palette.setColor(QPalette::WindowText, QColor::fromRgb(rgb)); - _label->setPalette(palette); - - form->addRow(_label); -} - -void AudioStatsDisplay::paint() { - _label->setText(_strBuf); -} - -void AudioStatsDisplay::updatedDisplay(QString str) { - _strBuf = str; -} - - -AudioStatsDialog::AudioStatsDialog(QWidget* parent) : - QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint) { - - setWindowTitle("Audio Network Statistics"); - - // Get statistics from the Audio Client - _stats = &DependencyManager::get()->getStats(); - - // Create layout - _form = new QFormLayout(); - _form->setSizeConstraint(QLayout::SetFixedSize); - - // Initialize channels' content (needed to correctly size channels) - updateStats(); - - // Create channels - _audioDisplayChannels = QVector>(1); - - _audioMixerID = addChannel(_form, _audioMixerStats, COLOR0); - _upstreamClientID = addChannel(_form, _upstreamClientStats, COLOR1); - _upstreamMixerID = addChannel(_form, _upstreamMixerStats, COLOR2); - _downstreamID = addChannel(_form, _downstreamStats, COLOR3); - _upstreamInjectedID = addChannel(_form, _upstreamInjectedStats, COLOR0); - - // Initialize channels - updateChannels(); - - // Future renders - connect(averageUpdateTimer, SIGNAL(timeout()), this, SLOT(renderStats())); - averageUpdateTimer->start(200); - - // Initial render - QDialog::setLayout(_form); -} - -int AudioStatsDialog::addChannel(QFormLayout* form, QVector& stats, const unsigned color) { - - int channelID = _audioDisplayChannels.size() - 1; - - for (int i = 0; i < stats.size(); i++) - // Create new display label - _audioDisplayChannels[channelID].push_back(new AudioStatsDisplay(form, stats.at(i), color)); - - // Expand vector to fit next channel - _audioDisplayChannels.resize(_audioDisplayChannels.size() + 1); - - return channelID; -} - -void AudioStatsDialog::renderStats() { - updateStats(); - updateChannels(); -} - -void AudioStatsDialog::updateChannels() { - updateChannel(_audioMixerStats, _audioMixerID); - updateChannel(_upstreamClientStats, _upstreamClientID); - updateChannel(_upstreamMixerStats, _upstreamMixerID); - updateChannel(_downstreamStats, _downstreamID); - updateChannel(_upstreamInjectedStats, _upstreamInjectedID); -} - -void AudioStatsDialog::updateChannel(QVector& stats, int channelID) { - // Update all stat displays at specified channel - for (int i = 0; i < stats.size(); i++) - _audioDisplayChannels[channelID].at(i)->updatedDisplay(stats.at(i)); -} - -void AudioStatsDialog::updateStats() { - - // Clear current stats from all vectors - clearAllChannels(); - - double audioInputBufferLatency{ 0.0 }; - double inputRingBufferLatency{ 0.0 }; - double networkRoundtripLatency{ 0.0 }; - double mixerRingBufferLatency{ 0.0 }; - double outputRingBufferLatency{ 0.0 }; - double audioOutputBufferLatency{ 0.0 }; - - if (SharedNodePointer audioMixerNodePointer = DependencyManager::get()->soloNodeOfType(NodeType::AudioMixer)) { - audioInputBufferLatency = (double)_stats->getInputMsRead().getWindowMax(); - inputRingBufferLatency = (double)_stats->getInputMsUnplayed().getWindowMax(); - networkRoundtripLatency = (double)audioMixerNodePointer->getPingMs(); - mixerRingBufferLatency = (double)_stats->getMixerAvatarStreamStats()._unplayedMs; - outputRingBufferLatency = (double)_stats->getMixerDownstreamStats()._unplayedMs; - audioOutputBufferLatency = (double)_stats->getOutputMsUnplayed().getWindowMax(); - } - - double totalLatency = audioInputBufferLatency + inputRingBufferLatency + mixerRingBufferLatency - + outputRingBufferLatency + audioOutputBufferLatency + networkRoundtripLatency; - - QString stats; - _audioMixerStats.push_back("PIPELINE (averaged over the past 10s)"); - stats = "Input Read:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(audioInputBufferLatency, 'f', 0))); - stats = "Input Ring:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(inputRingBufferLatency, 'f', 0))); - stats = "Network (client->mixer):\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(networkRoundtripLatency / 2, 'f', 0))); - stats = "Mixer Ring:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(mixerRingBufferLatency, 'f', 0))); - stats = "Network (mixer->client):\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(networkRoundtripLatency / 2, 'f', 0))); - stats = "Output Ring:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(outputRingBufferLatency, 'f', 0))); - stats = "Output Read:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(audioOutputBufferLatency, 'f', 0))); - stats = "TOTAL:\t%1 ms"; - _audioMixerStats.push_back(stats.arg(QString::number(totalLatency, 'f', 0))); - - const MovingMinMaxAvg& packetSentTimeGaps = _stats->getPacketTimegaps(); - - _upstreamClientStats.push_back("\nUpstream Mic Audio Packets Sent Gaps (by client):"); - - stats = "Inter-packet timegaps"; - _upstreamClientStats.push_back(stats); - stats = "overall min:\t%1, max:\t%2, avg:\t%3"; - stats = stats.arg(formatUsecTime(packetSentTimeGaps.getMin()), - formatUsecTime(packetSentTimeGaps.getMax()), - formatUsecTime(packetSentTimeGaps.getAverage())); - _upstreamClientStats.push_back(stats); - - stats = "last window min:\t%1, max:\t%2, avg:\t%3"; - stats = stats.arg(formatUsecTime(packetSentTimeGaps.getWindowMin()), - formatUsecTime(packetSentTimeGaps.getWindowMax()), - formatUsecTime(packetSentTimeGaps.getWindowAverage())); - _upstreamClientStats.push_back(stats); - - _upstreamMixerStats.push_back("\nMIXER STREAM"); - _upstreamMixerStats.push_back("(this client's remote mixer stream performance)"); - - renderAudioStreamStats(&_stats->getMixerAvatarStreamStats(), &_upstreamMixerStats); - - _downstreamStats.push_back("\nCLIENT STREAM"); - - AudioStreamStats downstreamStats = _stats->getMixerDownstreamStats(); - - renderAudioStreamStats(&downstreamStats, &_downstreamStats); - - - if (_shouldShowInjectedStreams) { - - foreach(const AudioStreamStats& injectedStreamAudioStats, _stats->getMixerInjectedStreamStatsMap()) { - stats = "\nINJECTED STREAM (ID: %1)"; - stats = stats.arg(injectedStreamAudioStats._streamIdentifier.toString()); - _upstreamInjectedStats.push_back(stats); - - renderAudioStreamStats(&injectedStreamAudioStats, &_upstreamInjectedStats); - } - - } -} - - -void AudioStatsDialog::renderAudioStreamStats(const AudioStreamStats* streamStats, QVector* audioStreamStats) { - - QString stats = "Packet Loss"; - audioStreamStats->push_back(stats); - stats = "overall:\t%1%\t(%2 lost), window:\t%3%\t(%4 lost)"; - stats = stats.arg(QString::number((int)(streamStats->_packetStreamStats.getLostRate() * 100.0f)), - QString::number((int)(streamStats->_packetStreamStats._lost)), - QString::number((int)(streamStats->_packetStreamWindowStats.getLostRate() * 100.0f)), - QString::number((int)(streamStats->_packetStreamWindowStats._lost))); - audioStreamStats->push_back(stats); - - stats = "Ringbuffer"; - audioStreamStats->push_back(stats); - stats = "available frames (avg):\t%1\t(%2), desired:\t%3"; - stats = stats.arg(QString::number(streamStats->_framesAvailable), - QString::number(streamStats->_framesAvailableAverage), - QString::number(streamStats->_desiredJitterBufferFrames)); - audioStreamStats->push_back(stats); - stats = "starves:\t%1, last starve duration:\t%2, drops:\t%3, overflows:\t%4"; - stats = stats.arg(QString::number(streamStats->_starveCount), - QString::number(streamStats->_consecutiveNotMixedCount), - QString::number(streamStats->_framesDropped), - QString::number(streamStats->_overflowCount)); - audioStreamStats->push_back(stats); - - stats = "Inter-packet timegaps"; - audioStreamStats->push_back(stats); - - stats = "overall min:\t%1, max:\t%2, avg:\t%3"; - stats = stats.arg(formatUsecTime(streamStats->_timeGapMin), - formatUsecTime(streamStats->_timeGapMax), - formatUsecTime(streamStats->_timeGapAverage)); - audioStreamStats->push_back(stats); - - - stats = "last window min:\t%1, max:\t%2, avg:\t%3"; - stats = stats.arg(formatUsecTime(streamStats->_timeGapWindowMin), - formatUsecTime(streamStats->_timeGapWindowMax), - formatUsecTime(streamStats->_timeGapWindowAverage)); - audioStreamStats->push_back(stats); -} - -void AudioStatsDialog::clearAllChannels() { - _audioMixerStats.clear(); - _upstreamClientStats.clear(); - _upstreamMixerStats.clear(); - _downstreamStats.clear(); - _upstreamInjectedStats.clear(); -} - -void AudioStatsDialog::paintEvent(QPaintEvent* event) { - - // Repaint each stat in each channel - for (int i = 0; i < _audioDisplayChannels.size(); i++) { - for(int j = 0; j < _audioDisplayChannels[i].size(); j++) { - _audioDisplayChannels[i].at(j)->paint(); - } - } - - QDialog::paintEvent(event); -} - -void AudioStatsDialog::reject() { - // Just regularly close upon ESC - QDialog::close(); -} - -void AudioStatsDialog::closeEvent(QCloseEvent* event) { - QDialog::closeEvent(event); - emit closed(); -} - -AudioStatsDialog::~AudioStatsDialog() { - clearAllChannels(); - for (int i = 0; i < _audioDisplayChannels.size(); i++) { - _audioDisplayChannels[i].clear(); - for(int j = 0; j < _audioDisplayChannels[i].size(); j++) { - delete _audioDisplayChannels[i].at(j); - } - } - -} - - diff --git a/interface/src/ui/AudioStatsDialog.h b/interface/src/ui/AudioStatsDialog.h deleted file mode 100644 index 59da056de4..0000000000 --- a/interface/src/ui/AudioStatsDialog.h +++ /dev/null @@ -1,112 +0,0 @@ -// -// AudioStatsDialog.h -// hifi -// -// Created by Bridget Went on 7/9/15. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef __hifi__AudioStatsDialog__ -#define __hifi__AudioStatsDialog__ - -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include - -class AudioIOStats; -class AudioStreamStats; - -//display -class AudioStatsDisplay : public QObject, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY -public: - AudioStatsDisplay(QFormLayout* form, QString text, unsigned colorRGBA); - void updatedDisplay(QString str); - void paint(); - -private: - QString _strBuf; - QLabel* _label; - QString _text; - unsigned _colorRGBA; - -}; - -//dialog -class AudioStatsDialog : public QDialog { - Q_OBJECT -public: - AudioStatsDialog(QWidget* parent); - ~AudioStatsDialog(); - - void paintEvent(QPaintEvent*) override; - -private: - // audio stats methods for rendering - QVector _audioMixerStats; - QVector _upstreamClientStats; - QVector _upstreamMixerStats; - QVector _downstreamStats; - QVector _upstreamInjectedStats; - - int _audioMixerID; - int _upstreamClientID; - int _upstreamMixerID; - int _downstreamID; - int _upstreamInjectedID; - - QVector> _audioDisplayChannels; - - void updateStats(); - int addChannel(QFormLayout* form, QVector& stats, const unsigned color); - void updateChannel(QVector& stats, const int channelID); - void updateChannels(); - void clearAllChannels(); - void renderAudioStreamStats(const AudioStreamStats* streamStats, QVector* audioStreamstats); - - - const AudioIOStats* _stats; - QFormLayout* _form; - - bool _shouldShowInjectedStreams{ false }; - - -signals: - - - void closed(); - - public slots: - - - void reject() override; - void renderStats(); - -protected: - - // Emits a 'closed' signal when this dialog is closed. - void closeEvent(QCloseEvent*) override; - -private: - QTimer* averageUpdateTimer = new QTimer(this); -}; - - - - - -#endif /* defined(__hifi__AudioStatsDialog__) */ - diff --git a/interface/src/ui/ConnectionFailureDialog.cpp b/interface/src/ui/ConnectionFailureDialog.cpp new file mode 100644 index 0000000000..560c76629b --- /dev/null +++ b/interface/src/ui/ConnectionFailureDialog.cpp @@ -0,0 +1,3 @@ +#include "ConnectionFailureDialog.h" + +HIFI_QML_DEF(ConnectionFailureDialog) diff --git a/interface/src/ui/ConnectionFailureDialog.h b/interface/src/ui/ConnectionFailureDialog.h new file mode 100644 index 0000000000..94cab7d11d --- /dev/null +++ b/interface/src/ui/ConnectionFailureDialog.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +class ConnectionFailureDialog : public OffscreenQmlDialog { + Q_OBJECT + HIFI_QML_DECL +}; \ No newline at end of file diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 1b868f4154..679fb7f59d 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -21,6 +21,7 @@ #include "AddressBarDialog.h" #include "BandwidthDialog.h" #include "CachesSizeDialog.h" +#include "ConnectionFailureDialog.h" #include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" @@ -59,6 +60,14 @@ void DialogsManager::showFeed() { emit setUseFeed(true); } +void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { + if (visible) { + ConnectionFailureDialog::show(); + } else { + ConnectionFailureDialog::hide(); + } +} + void DialogsManager::toggleDiskCacheEditor() { maybeCreateDialog(_diskCacheEditor); _diskCacheEditor->toggle(); @@ -99,20 +108,6 @@ void DialogsManager::cachesSizeDialog() { _cachesSizeDialog->raise(); } -void DialogsManager::audioStatsDetails() { - if (! _audioStatsDialog) { - _audioStatsDialog = new AudioStatsDialog(qApp->getWindow()); - connect(_audioStatsDialog, SIGNAL(closed()), _audioStatsDialog, SLOT(deleteLater())); - - if (_hmdToolsDialog) { - _hmdToolsDialog->watchWindow(_audioStatsDialog->windowHandle()); - } - - _audioStatsDialog->show(); - } - _audioStatsDialog->raise(); -} - void DialogsManager::bandwidthDetails() { if (! _bandwidthDialog) { _bandwidthDialog = new BandwidthDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 5e25afd130..e89bc43020 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -20,7 +20,6 @@ class AnimationsDialog; class AttachmentsDialog; -class AudioStatsDialog; class BandwidthDialog; class CachesSizeDialog; class DiskCacheEditor; @@ -35,7 +34,6 @@ class DialogsManager : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - QPointer getAudioStatsDialog() const { return _audioStatsDialog; } QPointer getBandwidthDialog() const { return _bandwidthDialog; } QPointer getHMDToolsDialog() const { return _hmdToolsDialog; } QPointer getLodToolsDialog() const { return _lodToolsDialog; } @@ -46,12 +44,12 @@ public slots: void toggleAddressBar(); void showAddressBar(); void showFeed(); + void setDomainConnectionFailureVisibility(bool visible); void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); void cachesSizeDialog(); - void audioStatsDetails(); void bandwidthDetails(); void lodTools(); void hmdTools(bool showTools); @@ -77,7 +75,6 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; - QPointer _audioStatsDialog; QPointer _bandwidthDialog; QPointer _cachesSizeDialog; QPointer _diskCacheEditor; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index a0ee260bcc..3334b0301b 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -43,9 +43,7 @@ const QString SNAPSHOTS_DIRECTORY = "Snapshots"; const QString URL = "highfidelity_url"; -Setting::Handle Snapshot::snapshotsLocation("snapshotsLocation", - QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); -Setting::Handle Snapshot::hasSetSnapshotsLocation("hasSetSnapshotsLocation", false); +Setting::Handle Snapshot::snapshotsLocation("snapshotsLocation"); SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { @@ -105,42 +103,44 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { const int IMAGE_QUALITY = 100; if (!isTemporary) { - QString snapshotFullPath; - if (!hasSetSnapshotsLocation.get()) { - snapshotFullPath = QFileDialog::getExistingDirectory(nullptr, "Choose Snapshots Directory", snapshotsLocation.get()); - hasSetSnapshotsLocation.set(true); + QString snapshotFullPath = snapshotsLocation.get(); + + if (snapshotFullPath.isEmpty()) { + snapshotFullPath = OffscreenUi::getExistingDirectory(nullptr, "Choose Snapshots Directory", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); snapshotsLocation.set(snapshotFullPath); - } else { - snapshotFullPath = snapshotsLocation.get(); } - if (!snapshotFullPath.endsWith(QDir::separator())) { - snapshotFullPath.append(QDir::separator()); + if (!snapshotFullPath.isEmpty()) { // not cancelled + + if (!snapshotFullPath.endsWith(QDir::separator())) { + snapshotFullPath.append(QDir::separator()); + } + + snapshotFullPath.append(filename); + + QFile* imageFile = new QFile(snapshotFullPath); + imageFile->open(QIODevice::WriteOnly); + + shot.save(imageFile, 0, IMAGE_QUALITY); + imageFile->close(); + + return imageFile; } - snapshotFullPath.append(filename); - - QFile* imageFile = new QFile(snapshotFullPath); - imageFile->open(QIODevice::WriteOnly); - - shot.save(imageFile, 0, IMAGE_QUALITY); - imageFile->close(); - - return imageFile; - - } else { - QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename); - - if (!imageTempFile->open()) { - qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save."; - return NULL; - } - - shot.save(imageTempFile, 0, IMAGE_QUALITY); - imageTempFile->close(); - - return imageTempFile; } + // Either we were asked for a tempororary, or the user didn't set a directory. + QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename); + + if (!imageTempFile->open()) { + qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save."; + return NULL; + } + imageTempFile->setAutoRemove(isTemporary); + + shot.save(imageTempFile, 0, IMAGE_QUALITY); + imageTempFile->close(); + + return imageTempFile; } void Snapshot::uploadSnapshot(const QString& filename) { diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 90138d450d..2daed0e860 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -39,7 +39,6 @@ public: static SnapshotMetaData* parseSnapshotData(QString snapshotPath); static Setting::Handle snapshotsLocation; - static Setting::Handle hasSetSnapshotsLocation; static void uploadSnapshot(const QString& filename); private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 8141d1a0db..11660a332d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -25,6 +25,8 @@ #include #include +#include + #include "BandwidthRecorder.h" #include "Menu.h" #include "Util.h" @@ -55,7 +57,9 @@ Stats::Stats(QQuickItem* parent) : QQuickItem(parent) { bool Stats::includeTimingRecord(const QString& name) { if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails)) { if (name.startsWith("/idle/update/")) { - if (name.startsWith("/idle/update/myAvatar/")) { + if (name.startsWith("/idle/update/physics/")) { + return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming); + } else if (name.startsWith("/idle/update/myAvatar/")) { if (name.startsWith("/idle/update/myAvatar/simulate/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandMyAvatarSimulateTiming); } @@ -92,6 +96,8 @@ bool Stats::includeTimingRecord(const QString& name) { } \ } +extern std::atomic DECIMATED_TEXTURE_COUNT; +extern std::atomic RECTIFIED_TEXTURE_COUNT; void Stats::updateStats(bool force) { if (!force) { @@ -111,7 +117,6 @@ void Stats::updateStats(bool force) { PerformanceTimer::setActive(shouldDisplayTimingDetail); } - auto nodeList = DependencyManager::get(); auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves @@ -285,6 +290,23 @@ void Stats::updateStats(bool force) { STAT_UPDATE(sendingMode, sendingModeResult); } + STAT_UPDATE(gpuBuffers, (int)gpu::Context::getBufferGPUCount()); + STAT_UPDATE(gpuBufferMemory, (int)BYTES_TO_MB(gpu::Context::getBufferGPUMemoryUsage())); + STAT_UPDATE(gpuTextures, (int)gpu::Context::getTextureGPUCount()); + STAT_UPDATE(gpuTexturesSparse, (int)gpu::Context::getTextureGPUSparseCount()); + + STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); + + STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); + STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); + 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(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); + STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); + STAT_UPDATE(decimatedTextureCount, (int)DECIMATED_TEXTURE_COUNT.load()); + // Incoming packets QLocale locale(QLocale::English); auto voxelPacketsToProcess = qApp->getOctreePacketProcessor().packetsToProcessCount(); @@ -360,7 +382,7 @@ void Stats::updateStats(bool force) { QString functionName = j.value(); const PerformanceTimerRecord& record = allRecords.value(functionName); perfLines += QString("%1: %2 [%3]\n"). - arg(QString(qPrintable(functionName)), 90, noBreakingSpace). + arg(QString(qPrintable(functionName)), -80, noBreakingSpace). arg((float)record.getMovingAverage() / (float)USECS_PER_MSEC, 8, 'f', 3, noBreakingSpace). arg((int)record.getCount(), 6, 10, noBreakingSpace); linesDisplayed++; diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 138f24cf19..76c6effed7 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -28,8 +28,6 @@ class Stats : public QQuickItem { Q_PROPERTY(bool expanded READ isExpanded WRITE setExpanded NOTIFY expandedChanged) Q_PROPERTY(bool timingExpanded READ isTimingExpanded NOTIFY timingExpandedChanged) Q_PROPERTY(QString monospaceFont READ monospaceFont CONSTANT) - Q_PROPERTY(float audioPacketlossUpstream READ getAudioPacketLossUpstream) - Q_PROPERTY(float audioPacketlossDownstream READ getAudioPacketLossDownstream) STATS_PROPERTY(int, serverCount, 0) // How often the app is creating new gpu::Frames @@ -89,6 +87,20 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, localElements, 0) STATS_PROPERTY(int, localInternal, 0) STATS_PROPERTY(int, localLeaves, 0) + STATS_PROPERTY(int, rectifiedTextureCount, 0) + STATS_PROPERTY(int, decimatedTextureCount, 0) + STATS_PROPERTY(int, gpuBuffers, 0) + STATS_PROPERTY(int, gpuBufferMemory, 0) + STATS_PROPERTY(int, gpuTextures, 0) + STATS_PROPERTY(int, gpuTexturesSparse, 0) + STATS_PROPERTY(int, glContextSwapchainMemory, 0) + STATS_PROPERTY(int, qmlTextureMemory, 0) + STATS_PROPERTY(int, gpuTextureMemory, 0) + STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) + STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) + STATS_PROPERTY(int, gpuTextureSparseMemory, 0) + STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) + STATS_PROPERTY(int, gpuFreeMemory, 0) public: static Stats* getInstance(); @@ -100,9 +112,6 @@ public: return _monospaceFont; } - float getAudioPacketLossUpstream() { return _audioStats->getMixerAvatarStreamStats()._packetStreamStats.getLostRate(); } - float getAudioPacketLossDownstream() { return _audioStats->getMixerDownstreamStats()._packetStreamStats.getLostRate(); } - void updateStats(bool force = false); bool isExpanded() { return _expanded; } @@ -177,6 +186,20 @@ signals: void localInternalChanged(); void localLeavesChanged(); void timingStatsChanged(); + void glContextSwapchainMemoryChanged(); + void qmlTextureMemoryChanged(); + void gpuBuffersChanged(); + void gpuBufferMemoryChanged(); + void gpuTexturesChanged(); + void gpuTexturesSparseChanged(); + void gpuTextureMemoryChanged(); + void gpuTextureVirtualMemoryChanged(); + void gpuTextureFramebufferMemoryChanged(); + void gpuTextureSparseMemoryChanged(); + void gpuSparseTextureEnabledChanged(); + void gpuFreeMemoryChanged(); + void rectifiedTextureCountChanged(); + void decimatedTextureCountChanged(); private: int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index e9ee997aac..bea737baa9 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -19,6 +19,7 @@ QString const Circle3DOverlay::TYPE = "circle3d"; Circle3DOverlay::Circle3DOverlay() { memset(&_minorTickMarksColor, 0, sizeof(_minorTickMarksColor)); memset(&_majorTickMarksColor, 0, sizeof(_majorTickMarksColor)); + qDebug() << "Building circle3d overlay"; } Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : @@ -39,8 +40,27 @@ Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), _minorTicksVerticesID(GeometryCache::UNKNOWN_ID) { + qDebug() << "Building circle3d overlay"; } +Circle3DOverlay::~Circle3DOverlay() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + if (_quadVerticesID) { + geometryCache->releaseID(_quadVerticesID); + } + if (_lineVerticesID) { + geometryCache->releaseID(_lineVerticesID); + } + if (_majorTicksVerticesID) { + geometryCache->releaseID(_majorTicksVerticesID); + } + if (_minorTicksVerticesID) { + geometryCache->releaseID(_minorTicksVerticesID); + } + } + qDebug() << "Destroying circle3d overlay"; +} void Circle3DOverlay::render(RenderArgs* args) { if (!_visible) { return; // do nothing if we're not visible diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index 82c7c47dc7..11c9c9710f 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -23,6 +23,7 @@ public: Circle3DOverlay(); Circle3DOverlay(const Circle3DOverlay* circle3DOverlay); + ~Circle3DOverlay(); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index a61e442436..8af4c1d908 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -18,9 +18,29 @@ QString const Cube3DOverlay::TYPE = "cube"; +Cube3DOverlay::Cube3DOverlay() { + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _geometryIds.size(); ++i) { + _geometryIds[i] = geometryCache->allocateID(); + } +} + Cube3DOverlay::Cube3DOverlay(const Cube3DOverlay* cube3DOverlay) : Volume3DOverlay(cube3DOverlay) { + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _geometryIds.size(); ++i) { + _geometryIds[i] = geometryCache->allocateID(); + } +} + +Cube3DOverlay::~Cube3DOverlay() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + for (size_t i = 0; i < _geometryIds.size(); ++i) { + geometryCache->releaseID(_geometryIds[i]); + } + } } void Cube3DOverlay::render(RenderArgs* args) { @@ -71,20 +91,20 @@ void Cube3DOverlay::render(RenderArgs* args) { glm::vec3 topLeftFar(-halfDimensions.x, halfDimensions.y, halfDimensions.z); glm::vec3 topRightFar(halfDimensions.x, halfDimensions.y, halfDimensions.z); - geometryCache->renderDashedLine(*batch, bottomLeftNear, bottomRightNear, cubeColor); - geometryCache->renderDashedLine(*batch, bottomRightNear, bottomRightFar, cubeColor); - geometryCache->renderDashedLine(*batch, bottomRightFar, bottomLeftFar, cubeColor); - geometryCache->renderDashedLine(*batch, bottomLeftFar, bottomLeftNear, cubeColor); + geometryCache->renderDashedLine(*batch, bottomLeftNear, bottomRightNear, cubeColor, _geometryIds[0]); + geometryCache->renderDashedLine(*batch, bottomRightNear, bottomRightFar, cubeColor, _geometryIds[1]); + geometryCache->renderDashedLine(*batch, bottomRightFar, bottomLeftFar, cubeColor, _geometryIds[2]); + geometryCache->renderDashedLine(*batch, bottomLeftFar, bottomLeftNear, cubeColor, _geometryIds[3]); - geometryCache->renderDashedLine(*batch, topLeftNear, topRightNear, cubeColor); - geometryCache->renderDashedLine(*batch, topRightNear, topRightFar, cubeColor); - geometryCache->renderDashedLine(*batch, topRightFar, topLeftFar, cubeColor); - geometryCache->renderDashedLine(*batch, topLeftFar, topLeftNear, cubeColor); + geometryCache->renderDashedLine(*batch, topLeftNear, topRightNear, cubeColor, _geometryIds[4]); + geometryCache->renderDashedLine(*batch, topRightNear, topRightFar, cubeColor, _geometryIds[5]); + geometryCache->renderDashedLine(*batch, topRightFar, topLeftFar, cubeColor, _geometryIds[6]); + geometryCache->renderDashedLine(*batch, topLeftFar, topLeftNear, cubeColor, _geometryIds[7]); - geometryCache->renderDashedLine(*batch, bottomLeftNear, topLeftNear, cubeColor); - geometryCache->renderDashedLine(*batch, bottomRightNear, topRightNear, cubeColor); - geometryCache->renderDashedLine(*batch, bottomLeftFar, topLeftFar, cubeColor); - geometryCache->renderDashedLine(*batch, bottomRightFar, topRightFar, cubeColor); + geometryCache->renderDashedLine(*batch, bottomLeftNear, topLeftNear, cubeColor, _geometryIds[8]); + geometryCache->renderDashedLine(*batch, bottomRightNear, topRightNear, cubeColor, _geometryIds[9]); + geometryCache->renderDashedLine(*batch, bottomLeftFar, topLeftFar, cubeColor, _geometryIds[10]); + geometryCache->renderDashedLine(*batch, bottomRightFar, topRightFar, cubeColor, _geometryIds[11]); } else { transform.setScale(dimensions); diff --git a/interface/src/ui/overlays/Cube3DOverlay.h b/interface/src/ui/overlays/Cube3DOverlay.h index 78b8b06582..9289af4de5 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.h +++ b/interface/src/ui/overlays/Cube3DOverlay.h @@ -20,9 +20,10 @@ public: static QString const TYPE; virtual QString getType() const override { return TYPE; } - Cube3DOverlay() {} + Cube3DOverlay(); Cube3DOverlay(const Cube3DOverlay* cube3DOverlay); - + ~Cube3DOverlay(); + virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -37,6 +38,8 @@ public: private: float _borderSize; + // edges on a cube + std::array _geometryIds; }; diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index e9bbcddf59..1c2b4c162d 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -24,6 +24,7 @@ const float DEFAULT_SCALE = 100.0f; Grid3DOverlay::Grid3DOverlay() { setDimensions(DEFAULT_SCALE); updateGrid(); + _geometryId = DependencyManager::get()->allocateID(); } Grid3DOverlay::Grid3DOverlay(const Grid3DOverlay* grid3DOverlay) : @@ -32,6 +33,14 @@ Grid3DOverlay::Grid3DOverlay(const Grid3DOverlay* grid3DOverlay) : _minorGridEvery(grid3DOverlay->_minorGridEvery) { updateGrid(); + _geometryId = DependencyManager::get()->allocateID(); +} + +Grid3DOverlay::~Grid3DOverlay() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } AABox Grid3DOverlay::getBounds() const { @@ -80,7 +89,7 @@ void Grid3DOverlay::render(RenderArgs* args) { DependencyManager::get()->renderGrid(*batch, minCorner, maxCorner, _minorGridRowDivisions, _minorGridColDivisions, MINOR_GRID_EDGE, _majorGridRowDivisions, _majorGridColDivisions, MAJOR_GRID_EDGE, - gridColor, _drawInFront); + gridColor, _drawInFront, _geometryId); } } diff --git a/interface/src/ui/overlays/Grid3DOverlay.h b/interface/src/ui/overlays/Grid3DOverlay.h index 90e3083dba..0d042af6ca 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.h +++ b/interface/src/ui/overlays/Grid3DOverlay.h @@ -23,6 +23,7 @@ public: Grid3DOverlay(); Grid3DOverlay(const Grid3DOverlay* grid3DOverlay); + ~Grid3DOverlay(); virtual AABox getBounds() const override; @@ -48,6 +49,7 @@ private: float _minorGridEvery { 1.0f }; float _minorGridRowDivisions; float _minorGridColDivisions; + int _geometryId { 0 }; }; #endif // hifi_Grid3DOverlay_h diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index a384c992ad..37f36d5e34 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -24,7 +24,7 @@ QString const Image3DOverlay::TYPE = "image3d"; Image3DOverlay::Image3DOverlay() { _isLoaded = false; - _emissive = false; + _geometryId = DependencyManager::get()->allocateID(); } Image3DOverlay::Image3DOverlay(const Image3DOverlay* image3DOverlay) : @@ -34,6 +34,14 @@ Image3DOverlay::Image3DOverlay(const Image3DOverlay* image3DOverlay) : _emissive(image3DOverlay->_emissive), _fromImage(image3DOverlay->_fromImage) { + _geometryId = DependencyManager::get()->allocateID(); +} + +Image3DOverlay::~Image3DOverlay() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } void Image3DOverlay::update(float deltatime) { @@ -100,7 +108,8 @@ void Image3DOverlay::render(RenderArgs* args) { DependencyManager::get()->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, - glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) + glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha), + _geometryId ); batch->setResourceTexture(0, args->_whiteTexture); // restore default white color after me diff --git a/interface/src/ui/overlays/Image3DOverlay.h b/interface/src/ui/overlays/Image3DOverlay.h index 159c6b4ccb..2e5f74749c 100644 --- a/interface/src/ui/overlays/Image3DOverlay.h +++ b/interface/src/ui/overlays/Image3DOverlay.h @@ -26,7 +26,7 @@ public: Image3DOverlay(); Image3DOverlay(const Image3DOverlay* image3DOverlay); - + ~Image3DOverlay(); virtual void render(RenderArgs* args) override; virtual void update(float deltatime) override; @@ -48,9 +48,10 @@ public: private: QString _url; NetworkTexturePointer _texture; - bool _emissive; + bool _emissive { false }; QRect _fromImage; // where from in the image to sample + int _geometryId { 0 }; }; #endif // hifi_Image3DOverlay_h diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 1616d4c2e2..795faa2748 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -19,6 +19,7 @@ QString const Line3DOverlay::TYPE = "line3d"; Line3DOverlay::Line3DOverlay() : _geometryCacheID(DependencyManager::get()->allocateID()) { + qDebug() << "Building line3D overlay"; } Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : @@ -27,9 +28,15 @@ Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : _end(line3DOverlay->_end), _geometryCacheID(DependencyManager::get()->allocateID()) { + qDebug() << "Building line3D overlay"; } Line3DOverlay::~Line3DOverlay() { + qDebug() << "Destryoing line3D overlay"; + auto geometryCache = DependencyManager::get(); + if (_geometryCacheID && geometryCache) { + geometryCache->releaseID(_geometryCacheID); + } } glm::vec3 Line3DOverlay::getStart() const { @@ -84,7 +91,6 @@ void Line3DOverlay::render(RenderArgs* args) { xColor color = getColor(); const float MAX_COLOR = 255.0f; glm::vec4 colorv4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); - auto batch = args->_batch; if (batch) { batch->setModelTransform(getTransform()); diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index 35c479dce6..f981fe1462 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -19,15 +19,33 @@ QString const Rectangle3DOverlay::TYPE = "rectangle3d"; Rectangle3DOverlay::Rectangle3DOverlay() : _geometryCacheID(DependencyManager::get()->allocateID()) { + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _rectGeometryIds.size(); ++i) { + _rectGeometryIds[i] = geometryCache->allocateID(); + } + qDebug() << "Building rect3d overlay"; } Rectangle3DOverlay::Rectangle3DOverlay(const Rectangle3DOverlay* rectangle3DOverlay) : Planar3DOverlay(rectangle3DOverlay), _geometryCacheID(DependencyManager::get()->allocateID()) { + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _rectGeometryIds.size(); ++i) { + _rectGeometryIds[i] = geometryCache->allocateID(); + } + qDebug() << "Building rect3d overlay"; } Rectangle3DOverlay::~Rectangle3DOverlay() { + qDebug() << "Destryoing rect3d overlay"; + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryCacheID); + for (size_t i = 0; i < _rectGeometryIds.size(); ++i) { + geometryCache->releaseID(_rectGeometryIds[i]); + } + } } void Rectangle3DOverlay::render(RenderArgs* args) { @@ -59,7 +77,7 @@ void Rectangle3DOverlay::render(RenderArgs* args) { glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, 0.0f); glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, 0.0f); geometryCache->bindSimpleProgram(*batch); - geometryCache->renderQuad(*batch, topLeft, bottomRight, rectangleColor); + geometryCache->renderQuad(*batch, topLeft, bottomRight, rectangleColor, _geometryCacheID); } else { geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); if (getIsDashedLine()) { @@ -68,10 +86,10 @@ void Rectangle3DOverlay::render(RenderArgs* args) { glm::vec3 point3(halfDimensions.x, halfDimensions.y, 0.0f); glm::vec3 point4(-halfDimensions.x, halfDimensions.y, 0.0f); - geometryCache->renderDashedLine(*batch, point1, point2, rectangleColor); - geometryCache->renderDashedLine(*batch, point2, point3, rectangleColor); - geometryCache->renderDashedLine(*batch, point3, point4, rectangleColor); - geometryCache->renderDashedLine(*batch, point4, point1, rectangleColor); + geometryCache->renderDashedLine(*batch, point1, point2, rectangleColor, _rectGeometryIds[0]); + geometryCache->renderDashedLine(*batch, point2, point3, rectangleColor, _rectGeometryIds[1]); + geometryCache->renderDashedLine(*batch, point3, point4, rectangleColor, _rectGeometryIds[2]); + geometryCache->renderDashedLine(*batch, point4, point1, rectangleColor, _rectGeometryIds[3]); } else { if (halfDimensions != _previousHalfDimensions) { QVector border; diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.h b/interface/src/ui/overlays/Rectangle3DOverlay.h index 525224ef29..a0c342a25b 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.h +++ b/interface/src/ui/overlays/Rectangle3DOverlay.h @@ -30,6 +30,7 @@ public: virtual Rectangle3DOverlay* createClone() const override; private: int _geometryCacheID; + std::array _rectGeometryIds; glm::vec2 _previousHalfDimensions; }; diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 153f787fc7..2e2d586abc 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -25,6 +25,7 @@ QString const Text3DOverlay::TYPE = "text3d"; Text3DOverlay::Text3DOverlay() { _textRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE); + _geometryId = DependencyManager::get()->allocateID(); } Text3DOverlay::Text3DOverlay(const Text3DOverlay* text3DOverlay) : @@ -39,10 +40,15 @@ Text3DOverlay::Text3DOverlay(const Text3DOverlay* text3DOverlay) : _bottomMargin(text3DOverlay->_bottomMargin) { _textRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE); + _geometryId = DependencyManager::get()->allocateID(); } Text3DOverlay::~Text3DOverlay() { delete _textRenderer; + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } xColor Text3DOverlay::getBackgroundColor() { @@ -97,7 +103,7 @@ void Text3DOverlay::render(RenderArgs* args) { glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, SLIGHTLY_BEHIND); glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, SLIGHTLY_BEHIND); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, quadColor); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, quadColor, _geometryId); // Same font properties as textSize() float maxHeight = (float)_textRenderer->computeExtent("Xy").y * LINE_SCALE_RATIO; diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index b7756d0400..5ba4fe5939 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -74,6 +74,7 @@ private: float _topMargin { 0.1f }; float _rightMargin { 0.1f }; float _bottomMargin { 0.1f }; + int _geometryId { 0 }; }; #endif // hifi_Text3DOverlay_h diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index be564a768e..773f850e2f 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -32,7 +32,9 @@ static float OPAQUE_ALPHA_THRESHOLD = 0.99f; QString const Web3DOverlay::TYPE = "web3d"; -Web3DOverlay::Web3DOverlay() : _dpi(DPI) { } +Web3DOverlay::Web3DOverlay() : _dpi(DPI) { + _geometryId = DependencyManager::get()->allocateID(); +} Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : Billboard3DOverlay(Web3DOverlay), @@ -40,12 +42,15 @@ Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : _dpi(Web3DOverlay->_dpi), _resolution(Web3DOverlay->_resolution) { + _geometryId = DependencyManager::get()->allocateID(); } Web3DOverlay::~Web3DOverlay() { if (_webSurface) { _webSurface->pause(); _webSurface->disconnect(_connection); + + // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than // member variables, since they would implicitly refer to a this that @@ -55,6 +60,10 @@ Web3DOverlay::~Web3DOverlay() { webSurface->deleteLater(); }); } + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } void Web3DOverlay::update(float deltatime) { @@ -79,6 +88,9 @@ void Web3DOverlay::render(RenderArgs* args) { }); }; _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); + // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces + // and the current rendering load) + _webSurface->setMaxFps(10); _webSurface->create(currentContext); _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/")); _webSurface->load("WebView.qml"); @@ -101,9 +113,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([webSurface](uint32_t recycleTexture, void* recycleFence) { - webSurface->releaseTexture({ recycleTexture, recycleFence }); - })); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; @@ -122,7 +132,7 @@ void Web3DOverlay::render(RenderArgs* args) { } else { geometryCache->bindOpaqueWebBrowserProgram(batch); } - geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color); + geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color, _geometryId); batch.setResourceTexture(0, args->_whiteTexture); // restore default white color after me } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 1e75bbbb06..d9498bded5 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -47,6 +47,7 @@ private: QString _url; float _dpi; vec2 _resolution{ 640, 480 }; + int _geometryId { 0 }; }; #endif // hifi_Web3DOverlay_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 5fced85d2d..5208b893ac 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -82,10 +82,10 @@ public: CheckDevicesThread(AudioClient* audioClient) : _audioClient(audioClient) { + } - connect(qApp, &QCoreApplication::aboutToQuit, [this] { - _quit = true; - }); + void beforeAboutToQuit() { + _quit = true; } void run() override { @@ -159,10 +159,10 @@ AudioClient::AudioClient() : _outputDevices = getDeviceNames(QAudio::AudioOutput); // start a thread to detect any device changes - QThread* checkDevicesThread = new CheckDevicesThread(this); - checkDevicesThread->setObjectName("CheckDevices Thread"); - checkDevicesThread->setPriority(QThread::LowPriority); - checkDevicesThread->start(); + _checkDevicesThread = new CheckDevicesThread(this); + _checkDevicesThread->setObjectName("CheckDevices Thread"); + _checkDevicesThread->setPriority(QThread::LowPriority); + _checkDevicesThread->start(); configureReverb(); @@ -177,6 +177,7 @@ AudioClient::AudioClient() : } AudioClient::~AudioClient() { + delete _checkDevicesThread; stop(); if (_codec && _encoder) { _codec->releaseEncoder(_encoder); @@ -184,6 +185,11 @@ AudioClient::~AudioClient() { } } +void AudioClient::beforeAboutToQuit() { + static_cast(_checkDevicesThread)->beforeAboutToQuit(); +} + + void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; selectAudioFormat(recievedCodec); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 1b2a2da296..d6f111cafc 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -148,13 +148,15 @@ public slots: void handleSelectedAudioFormat(QSharedPointer message); void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); - void sendDownstreamAudioStatsPacket() { _stats.sendDownstreamAudioStatsPacket(); } + void sendDownstreamAudioStatsPacket() { _stats.publish(); } void handleAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); void toggleMute(); + void beforeAboutToQuit(); + virtual void setIsStereoInput(bool stereo) override; void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; } @@ -332,6 +334,8 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; // for outbound mic stream + + QThread* _checkDevicesThread { nullptr }; }; diff --git a/libraries/audio-client/src/AudioIOStats.cpp b/libraries/audio-client/src/AudioIOStats.cpp index 330854058f..3bd3f4a47d 100644 --- a/libraries/audio-client/src/AudioIOStats.cpp +++ b/libraries/audio-client/src/AudioIOStats.cpp @@ -18,22 +18,24 @@ #include "AudioIOStats.h" -// This is called 5x/sec (see AudioStatsDialog), and we want it to log the last 5s -static const int INPUT_READS_WINDOW = 25; -static const int INPUT_UNPLAYED_WINDOW = 25; -static const int OUTPUT_UNPLAYED_WINDOW = 25; +// This is called 1x/sec (see AudioClient) and we want it to log the last 5s +static const int INPUT_READS_WINDOW = 5; +static const int INPUT_UNPLAYED_WINDOW = 5; +static const int OUTPUT_UNPLAYED_WINDOW = 5; static const int APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS = (int)(30.0f * 1000.0f / AudioConstants::NETWORK_FRAME_MSECS); AudioIOStats::AudioIOStats(MixedProcessedAudioStream* receivedAudioStream) : - _receivedAudioStream(receivedAudioStream), - _inputMsRead(0, INPUT_READS_WINDOW), - _inputMsUnplayed(0, INPUT_UNPLAYED_WINDOW), - _outputMsUnplayed(0, OUTPUT_UNPLAYED_WINDOW), + _interface(new AudioStatsInterface(this)), + _inputMsRead(1, INPUT_READS_WINDOW), + _inputMsUnplayed(1, INPUT_UNPLAYED_WINDOW), + _outputMsUnplayed(1, OUTPUT_UNPLAYED_WINDOW), _lastSentPacketTime(0), - _packetTimegaps(0, APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS) + _packetTimegaps(1, APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS), + _receivedAudioStream(receivedAudioStream) { + } void AudioIOStats::reset() { @@ -44,11 +46,13 @@ void AudioIOStats::reset() { _outputMsUnplayed.reset(); _packetTimegaps.reset(); - _mixerAvatarStreamStats = AudioStreamStats(); - _mixerInjectedStreamStatsMap.clear(); + _interface->updateLocalBuffers(_inputMsRead, _inputMsUnplayed, _outputMsUnplayed, _packetTimegaps); + _interface->updateMixerStream(AudioStreamStats()); + _interface->updateClientStream(AudioStreamStats()); + _interface->updateInjectorStreams(QHash()); } -void AudioIOStats::sentPacket() { +void AudioIOStats::sentPacket() const { // first time this is 0 if (_lastSentPacketTime == 0) { _lastSentPacketTime = usecTimestampNow(); @@ -60,37 +64,13 @@ void AudioIOStats::sentPacket() { } } -const MovingMinMaxAvg& AudioIOStats::getInputMsRead() const { - _inputMsRead.currentIntervalComplete(); - return _inputMsRead; -} - -const MovingMinMaxAvg& AudioIOStats::getInputMsUnplayed() const { - _inputMsUnplayed.currentIntervalComplete(); - return _inputMsUnplayed; -} - -const MovingMinMaxAvg& AudioIOStats::getOutputMsUnplayed() const { - _outputMsUnplayed.currentIntervalComplete(); - return _outputMsUnplayed; -} - -const MovingMinMaxAvg& AudioIOStats::getPacketTimegaps() const { - _packetTimegaps.currentIntervalComplete(); - return _packetTimegaps; -} - -const AudioStreamStats AudioIOStats::getMixerDownstreamStats() const { - return _receivedAudioStream->getAudioStreamStats(); -} - void AudioIOStats::processStreamStatsPacket(QSharedPointer message, SharedNodePointer sendingNode) { // parse the appendFlag, clear injected audio stream stats if 0 quint8 appendFlag; message->readPrimitive(&appendFlag); - if (!appendFlag) { - _mixerInjectedStreamStatsMap.clear(); + if (appendFlag & AudioStreamStats::START) { + _injectorStreams.clear(); } // parse the number of stream stats structs to follow @@ -103,14 +83,18 @@ void AudioIOStats::processStreamStatsPacket(QSharedPointer mess message->readPrimitive(&streamStats); if (streamStats._streamType == PositionalAudioStream::Microphone) { - _mixerAvatarStreamStats = streamStats; + _interface->updateMixerStream(streamStats); } else { - _mixerInjectedStreamStatsMap[streamStats._streamIdentifier] = streamStats; + _injectorStreams[streamStats._streamIdentifier] = streamStats; } } + + if (appendFlag & AudioStreamStats::END) { + _interface->updateInjectorStreams(_injectorStreams); + } } -void AudioIOStats::sendDownstreamAudioStatsPacket() { +void AudioIOStats::publish() { auto audioIO = DependencyManager::get(); // call _receivedAudioStream's per-second callback @@ -122,10 +106,15 @@ void AudioIOStats::sendDownstreamAudioStatsPacket() { return; } - quint8 appendFlag = 0; + quint8 appendFlag = AudioStreamStats::START | AudioStreamStats::END; quint16 numStreamStatsToPack = 1; AudioStreamStats stats = _receivedAudioStream->getAudioStreamStats(); + // update the interface + _interface->updateLocalBuffers(_inputMsRead, _inputMsUnplayed, _outputMsUnplayed, _packetTimegaps); + _interface->updateClientStream(stats); + + // prepare a packet to the mixer int statsPacketSize = sizeof(appendFlag) + sizeof(numStreamStatsToPack) + sizeof(stats); auto statsPacket = NLPacket::create(PacketType::AudioStreamStats, statsPacketSize); @@ -137,7 +126,88 @@ void AudioIOStats::sendDownstreamAudioStatsPacket() { // pack downstream audio stream stats statsPacket->writePrimitive(stats); - + // send packet nodeList->sendPacket(std::move(statsPacket), *audioMixer); } + +AudioStreamStatsInterface::AudioStreamStatsInterface(QObject* parent) : + QObject(parent) {} + +void AudioStreamStatsInterface::updateStream(const AudioStreamStats& stats) { + lossRate(stats._packetStreamStats.getLostRate()); + lossCount(stats._packetStreamStats._lost); + lossRateWindow(stats._packetStreamWindowStats.getLostRate()); + lossCountWindow(stats._packetStreamWindowStats._lost); + + framesDesired(stats._desiredJitterBufferFrames); + framesAvailable(stats._framesAvailable); + framesAvailableAvg(stats._framesAvailableAverage); + + unplayedMsMax(stats._unplayedMs); + + starveCount(stats._starveCount); + lastStarveDurationCount(stats._consecutiveNotMixedCount); + dropCount(stats._framesDropped); + overflowCount(stats._overflowCount); + + timegapMsMax(stats._timeGapMax / USECS_PER_MSEC); + timegapMsAvg(stats._timeGapAverage / USECS_PER_MSEC); + timegapMsMaxWindow(stats._timeGapWindowMax / USECS_PER_MSEC); + timegapMsAvgWindow(stats._timeGapWindowAverage / USECS_PER_MSEC); +} + +AudioStatsInterface::AudioStatsInterface(QObject* parent) : + QObject(parent), + _client(new AudioStreamStatsInterface(this)), + _mixer(new AudioStreamStatsInterface(this)), + _injectors(new QObject(this)) {} + + +void AudioStatsInterface::updateLocalBuffers(const MovingMinMaxAvg& inputMsRead, + const MovingMinMaxAvg& inputMsUnplayed, + const MovingMinMaxAvg& outputMsUnplayed, + const MovingMinMaxAvg& timegaps) { + if (SharedNodePointer audioNode = DependencyManager::get()->soloNodeOfType(NodeType::AudioMixer)) { + pingMs(audioNode->getPingMs()); + } + + inputReadMsMax(inputMsRead.getWindowMax()); + inputUnplayedMsMax(inputMsUnplayed.getWindowMax()); + outputUnplayedMsMax(outputMsUnplayed.getWindowMax()); + + sentTimegapMsMax(timegaps.getMax() / USECS_PER_MSEC); + sentTimegapMsAvg(timegaps.getAverage() / USECS_PER_MSEC); + sentTimegapMsMaxWindow(timegaps.getWindowMax() / USECS_PER_MSEC); + sentTimegapMsAvgWindow(timegaps.getWindowAverage() / USECS_PER_MSEC); +} + +void AudioStatsInterface::updateInjectorStreams(const QHash& stats) { + // Get existing injectors + auto injectorIds = _injectors->dynamicPropertyNames(); + + // Go over reported injectors + QHash::const_iterator injector = stats.constBegin(); + while (injector != stats.constEnd()) { + const auto id = injector.key().toByteArray(); + // Mark existing injector (those left will be removed) + injectorIds.removeOne(id); + auto injectorProperty = _injectors->property(id); + // Add new injector + if (!injectorProperty.isValid()) { + injectorProperty = QVariant::fromValue(new AudioStreamStatsInterface(this)); + _injectors->setProperty(id, injectorProperty); + } + // Update property with reported injector + injectorProperty.value()->updateStream(injector.value()); + ++injector; + } + + // Remove unreported injectors + for (auto& id : injectorIds) { + _injectors->property(id).value()->deleteLater(); + _injectors->setProperty(id, QVariant()); + } + + emit injectorStreamsChanged(); +} diff --git a/libraries/audio-client/src/AudioIOStats.h b/libraries/audio-client/src/AudioIOStats.h index 45217c5af6..da668da1ac 100644 --- a/libraries/audio-client/src/AudioIOStats.h +++ b/libraries/audio-client/src/AudioIOStats.h @@ -22,44 +22,124 @@ class MixedProcessedAudioStream; +#define AUDIO_PROPERTY(TYPE, NAME) \ + Q_PROPERTY(TYPE NAME READ NAME NOTIFY NAME##Changed) \ + public: \ + TYPE NAME() const { return _##NAME; } \ + void NAME(TYPE value) { \ + if (_##NAME != value) { \ + _##NAME = value; \ + emit NAME##Changed(value); \ + } \ + } \ + Q_SIGNAL void NAME##Changed(TYPE value); \ + private: \ + TYPE _##NAME{ (TYPE)0 }; + +class AudioStreamStatsInterface : public QObject { + Q_OBJECT + AUDIO_PROPERTY(float, lossRate) + AUDIO_PROPERTY(float, lossCount) + AUDIO_PROPERTY(float, lossRateWindow) + AUDIO_PROPERTY(float, lossCountWindow) + + AUDIO_PROPERTY(int, framesDesired) + AUDIO_PROPERTY(int, framesAvailable) + AUDIO_PROPERTY(int, framesAvailableAvg) + AUDIO_PROPERTY(float, unplayedMsMax) + + AUDIO_PROPERTY(int, starveCount) + AUDIO_PROPERTY(int, lastStarveDurationCount) + AUDIO_PROPERTY(int, dropCount) + AUDIO_PROPERTY(int, overflowCount) + + AUDIO_PROPERTY(quint64, timegapMsMax) + AUDIO_PROPERTY(quint64, timegapMsAvg) + AUDIO_PROPERTY(quint64, timegapMsMaxWindow) + AUDIO_PROPERTY(quint64, timegapMsAvgWindow) + +public: + void updateStream(const AudioStreamStats& stats); + +private: + friend class AudioStatsInterface; + AudioStreamStatsInterface(QObject* parent); +}; + +class AudioStatsInterface : public QObject { + Q_OBJECT + AUDIO_PROPERTY(float, pingMs); + + AUDIO_PROPERTY(float, inputReadMsMax); + AUDIO_PROPERTY(float, inputUnplayedMsMax); + AUDIO_PROPERTY(float, outputUnplayedMsMax); + + AUDIO_PROPERTY(quint64, sentTimegapMsMax); + AUDIO_PROPERTY(quint64, sentTimegapMsAvg); + AUDIO_PROPERTY(quint64, sentTimegapMsMaxWindow); + AUDIO_PROPERTY(quint64, sentTimegapMsAvgWindow); + + Q_PROPERTY(AudioStreamStatsInterface* mixerStream READ getMixerStream NOTIFY mixerStreamChanged); + Q_PROPERTY(AudioStreamStatsInterface* clientStream READ getClientStream NOTIFY clientStreamChanged); + Q_PROPERTY(QObject* injectorStreams READ getInjectorStreams NOTIFY injectorStreamsChanged); + +public: + AudioStreamStatsInterface* getMixerStream() const { return _mixer; } + AudioStreamStatsInterface* getClientStream() const { return _client; } + QObject* getInjectorStreams() const { return _injectors; } + + void updateLocalBuffers(const MovingMinMaxAvg& inputMsRead, + const MovingMinMaxAvg& inputMsUnplayed, + const MovingMinMaxAvg& outputMsUnplayed, + const MovingMinMaxAvg& timegaps); + void updateMixerStream(const AudioStreamStats& stats) { _mixer->updateStream(stats); emit mixerStreamChanged(); } + void updateClientStream(const AudioStreamStats& stats) { _client->updateStream(stats); emit clientStreamChanged(); } + void updateInjectorStreams(const QHash& stats); + +signals: + void mixerStreamChanged(); + void clientStreamChanged(); + void injectorStreamsChanged(); + +private: + friend class AudioIOStats; + AudioStatsInterface(QObject* parent); + AudioStreamStatsInterface* _client; + AudioStreamStatsInterface* _mixer; + QObject* _injectors; +}; + class AudioIOStats : public QObject { Q_OBJECT public: AudioIOStats(MixedProcessedAudioStream* receivedAudioStream); - - void reset(); - - void updateInputMsRead(float ms) { _inputMsRead.update(ms); } - void updateInputMsUnplayed(float ms) { _inputMsUnplayed.update(ms); } - void updateOutputMsUnplayed(float ms) { _outputMsUnplayed.update(ms); } - void sentPacket(); - - const MovingMinMaxAvg& getInputMsRead() const; - const MovingMinMaxAvg& getInputMsUnplayed() const; - const MovingMinMaxAvg& getOutputMsUnplayed() const; - const MovingMinMaxAvg& getPacketTimegaps() const; - const AudioStreamStats getMixerDownstreamStats() const; - const AudioStreamStats& getMixerAvatarStreamStats() const { return _mixerAvatarStreamStats; } - const QHash& getMixerInjectedStreamStatsMap() const { return _mixerInjectedStreamStatsMap; } - - void sendDownstreamAudioStatsPacket(); + void reset(); + + AudioStatsInterface* data() const { return _interface; } + + void updateInputMsRead(float ms) const { _inputMsRead.update(ms); } + void updateInputMsUnplayed(float ms) const { _inputMsUnplayed.update(ms); } + void updateOutputMsUnplayed(float ms) const { _outputMsUnplayed.update(ms); } + void sentPacket() const; + + void publish(); public slots: void processStreamStatsPacket(QSharedPointer message, SharedNodePointer sendingNode); private: - MixedProcessedAudioStream* _receivedAudioStream; + AudioStatsInterface* _interface; mutable MovingMinMaxAvg _inputMsRead; mutable MovingMinMaxAvg _inputMsUnplayed; mutable MovingMinMaxAvg _outputMsUnplayed; - quint64 _lastSentPacketTime; + mutable quint64 _lastSentPacketTime; mutable MovingMinMaxAvg _packetTimegaps; - - AudioStreamStats _mixerAvatarStreamStats; - QHash _mixerInjectedStreamStatsMap; + + MixedProcessedAudioStream* _receivedAudioStream; + QHash _injectorStreams; }; #endif // hifi_AudioIOStats_h diff --git a/libraries/audio/src/AudioStreamStats.h b/libraries/audio/src/AudioStreamStats.h index 046c4e5a47..1230c14706 100644 --- a/libraries/audio/src/AudioStreamStats.h +++ b/libraries/audio/src/AudioStreamStats.h @@ -16,6 +16,13 @@ class AudioStreamStats { public: + // Intermediate packets should have no flag set + // Unique packets should have both flags set + enum AppendFlag : quint8 { + START = 1, + END = 2 + }; + AudioStreamStats() : _streamType(-1), _streamIdentifier(), diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 177a5ddcef..ba60088117 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -471,8 +471,8 @@ float calculateRepeatedFrameFadeFactor(int indexOfRepeat) { const float INITIAL_MSECS_NO_FADE = 20.0f; const float MSECS_FADE_TO_ZERO = 320.0f; - const float INITIAL_FRAMES_NO_FADE = INITIAL_MSECS_NO_FADE * AudioConstants::NETWORK_FRAME_MSECS; - const float FRAMES_FADE_TO_ZERO = MSECS_FADE_TO_ZERO * AudioConstants::NETWORK_FRAME_MSECS; + const float INITIAL_FRAMES_NO_FADE = INITIAL_MSECS_NO_FADE / AudioConstants::NETWORK_FRAME_MSECS; + const float FRAMES_FADE_TO_ZERO = MSECS_FADE_TO_ZERO / AudioConstants::NETWORK_FRAME_MSECS; const float SAMPLE_RANGE = std::numeric_limits::max(); @@ -480,8 +480,6 @@ float calculateRepeatedFrameFadeFactor(int indexOfRepeat) { return 1.0f; } else if (indexOfRepeat <= INITIAL_FRAMES_NO_FADE + FRAMES_FADE_TO_ZERO) { return pow(SAMPLE_RANGE, -(indexOfRepeat - INITIAL_FRAMES_NO_FADE) / FRAMES_FADE_TO_ZERO); - - //return 1.0f - ((indexOfRepeat - INITIAL_FRAMES_NO_FADE) / FRAMES_FADE_TO_ZERO); } return 0.0f; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index d30b69c5f8..9df23dad2c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1778,7 +1778,7 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { AvatarEntityIDs result; if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(const_cast(this), "getRecentlyDetachedIDs", Qt::BlockingQueuedConnection, + QMetaObject::invokeMethod(const_cast(this), "getAndClearRecentlyDetachedIDs", Qt::BlockingQueuedConnection, Q_RETURN_ARG(AvatarEntityIDs, result)); return result; } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index b0b96d86be..5be2d68cf9 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -111,6 +111,8 @@ public: void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; } + float getHmdUiRadius() const { return _hmdUIRadius; } + signals: void allowMouseCaptureChanged(); void alphaChanged(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 4a5021155f..c84f6b9954 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -356,6 +356,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); @@ -413,8 +414,7 @@ void OpenGLDisplayPlugin::customizeContext() { _cursorPipeline = gpu::Pipeline::create(program, state); } } - auto renderSize = getRecommendedRenderSize(); - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); + updateCompositeFramebuffer(); } void OpenGLDisplayPlugin::uncustomizeContext() { @@ -424,6 +424,7 @@ void OpenGLDisplayPlugin::uncustomizeContext() { _compositeFramebuffer.reset(); withPresentThreadLock([&] { _currentFrame.reset(); + _lastFrame = nullptr; while (!_newFrameQueue.empty()) { _gpuContext->consumeFrameUpdates(_newFrameQueue.front()); _newFrameQueue.pop(); @@ -557,10 +558,7 @@ void OpenGLDisplayPlugin::compositeScene() { } void OpenGLDisplayPlugin::compositeLayers() { - auto renderSize = getRecommendedRenderSize(); - if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); - } + updateCompositeFramebuffer(); { PROFILE_RANGE_EX("compositeScene", 0xff0077ff, (uint64_t)presentCount()) @@ -611,10 +609,10 @@ void OpenGLDisplayPlugin::present() { { withPresentThreadLock([&] { _renderRate.increment(); - if (_currentFrame != _lastFrame) { + if (_currentFrame.get() != _lastFrame) { _newFrameRate.increment(); } - _lastFrame = _currentFrame; + _lastFrame = _currentFrame.get(); }); // Execute the frame rendering commands PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) @@ -632,6 +630,8 @@ void OpenGLDisplayPlugin::present() { PROFILE_RANGE_EX("internalPresent", 0xff00ffff, (uint64_t)presentCount()) internalPresent(); } + + gpu::Backend::setFreeGPUMemory(gpu::gl::getFreeDedicatedMemory()); } } @@ -755,3 +755,15 @@ void OpenGLDisplayPlugin::render(std::function f) { f(batch); _gpuContext->executeBatch(batch); } + + +OpenGLDisplayPlugin::~OpenGLDisplayPlugin() { + qDebug() << "Destroying OpenGLDisplayPlugin"; +} + +void OpenGLDisplayPlugin::updateCompositeFramebuffer() { + auto renderSize = getRecommendedRenderSize(); + if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); + } +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 74f8cdbc10..a6de3f7baa 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -36,6 +36,7 @@ protected: using Lock = std::unique_lock; using Condition = std::condition_variable; public: + ~OpenGLDisplayPlugin(); // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; @@ -77,6 +78,8 @@ protected: glm::uvec2 getSurfaceSize() const; glm::uvec2 getSurfacePixels() const; + void updateCompositeFramebuffer(); + virtual void compositeLayers(); virtual void compositeScene(); virtual void compositeOverlay(); @@ -115,7 +118,7 @@ protected: RateCounter<> _renderRate; gpu::FramePointer _currentFrame; - gpu::FramePointer _lastFrame; + gpu::Frame* _lastFrame { nullptr }; gpu::FramebufferPointer _compositeFramebuffer; gpu::PipelinePointer _overlayPipeline; gpu::PipelinePointer _simplePipeline; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ad165beba1..2e66659de7 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -112,6 +112,11 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); _overlayRenderer.build(); + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _geometryIds.size(); ++i) { + _geometryIds[i] = geometryCache->allocateID(); + } + _extraLaserID = geometryCache->allocateID(); } void HmdDisplayPlugin::uncustomizeContext() { @@ -125,6 +130,13 @@ void HmdDisplayPlugin::uncustomizeContext() { batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); }); _overlayRenderer = OverlayRenderer(); + _previewTexture.reset(); + + auto geometryCache = DependencyManager::get(); + for (size_t i = 0; i < _geometryIds.size(); ++i) { + geometryCache->releaseID(_geometryIds[i]); + } + geometryCache->releaseID(_extraLaserID); Parent::uncustomizeContext(); } @@ -265,6 +277,7 @@ void HmdDisplayPlugin::internalPresent() { gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); _previewTexture->autoGenerateMips(-1); @@ -348,11 +361,18 @@ void HmdDisplayPlugin::updateFrameData() { _presentHandLasers = _handLasers; _presentHandPoses = _handPoses; _presentUiModelTransform = _uiModelTransform; + + _presentExtraLaser = _extraLaser; + _presentExtraLaserStart = _extraLaserStart; }); auto compositorHelper = DependencyManager::get(); glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - std::array handGlowPoints{ { vec2(-1), vec2(-1) } }; + static const float OUT_OF_BOUNDS = -1; + std::array handGlowPoints { { vec2(OUT_OF_BOUNDS), vec2(OUT_OF_BOUNDS) } }; + vec2 extraGlowPoint(OUT_OF_BOUNDS); + + float uiRadius = compositorHelper->getHmdUiRadius(); // compute the glow point interesections for (size_t i = 0; i < NUMBER_OF_HANDS; ++i) { @@ -368,23 +388,17 @@ void HmdDisplayPlugin::updateFrameData() { mat4 model = _presentHandPoses[i]; vec3 castStart = vec3(model[3]); vec3 castDirection = glm::quat_cast(model) * laserDirection; - if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { - castDirection = glm::normalize(castDirection); - castDirection = glm::inverse(_presentUiModelTransform.getRotation()) * castDirection; - } - // this offset needs to match GRAB_POINT_SPHERE_OFFSET in scripts/system/libraries/controllers.js - //static const vec3 GRAB_POINT_SPHERE_OFFSET = vec3(0.1f, 0.04f, -0.32f); - static const vec3 GRAB_POINT_SPHERE_OFFSET = vec3(0.0f, 0.0f, -0.175f); - vec3 grabPointOffset = GRAB_POINT_SPHERE_OFFSET; + // this offset needs to match GRAB_POINT_SPHERE_OFFSET in scripts/system/libraries/controllers.js:19 + static const vec3 GRAB_POINT_SPHERE_OFFSET(0.04f, 0.13f, 0.039f); // x = upward, y = forward, z = lateral + + // swizzle grab point so that (x = upward, y = lateral, z = forward) + vec3 grabPointOffset = glm::vec3(GRAB_POINT_SPHERE_OFFSET.x, GRAB_POINT_SPHERE_OFFSET.z, -GRAB_POINT_SPHERE_OFFSET.y); if (i == 0) { grabPointOffset.x *= -1.0f; // this changes between left and right hands } castStart += glm::quat_cast(model) * grabPointOffset; - // FIXME fetch the actual UI radius from... somewhere? - float uiRadius = 1.0f; - // Find the intersection of the laser with he UI and use it to scale the model matrix float distance; if (!glm::intersectRaySphere(castStart, castDirection, @@ -417,6 +431,42 @@ void HmdDisplayPlugin::updateFrameData() { handGlowPoints[i] = yawPitch; } + // compute the glow point interesections + if (_presentExtraLaser.valid()) { + const vec3& laserDirection = _presentExtraLaser.direction; + vec3 castStart = _presentExtraLaserStart; + vec3 castDirection = laserDirection; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (glm::intersectRaySphere(castStart, castDirection, + _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + + + _presentExtraLaserPoints.first = castStart; + _presentExtraLaserPoints.second = _presentExtraLaserPoints.first + (castDirection * distance); + + vec3 intersectionPosition = castStart + (castDirection * distance) - _presentUiModelTransform.getTranslation(); + intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; + + // Take the interesection normal and convert it to a texture coordinate + vec2 yawPitch; + { + vec2 xdir = glm::normalize(vec2(intersectionPosition.x, -intersectionPosition.z)); + yawPitch.x = glm::atan(xdir.x, xdir.y); + yawPitch.y = (acosf(intersectionPosition.y) * -1.0f) + (float)M_PI_2; + } + vec2 halfFov = CompositorHelper::VIRTUAL_UI_TARGET_FOV / 2.0f; + + // Are we out of range + if (!glm::any(glm::greaterThan(glm::abs(yawPitch), halfFov))) { + yawPitch /= CompositorHelper::VIRTUAL_UI_TARGET_FOV; + yawPitch += 0.5f; + extraGlowPoint = yawPitch; + } + } + } + for_each_eye([&](Eye eye) { auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; @@ -430,6 +480,8 @@ void HmdDisplayPlugin::updateFrameData() { uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); uniforms.glowColors[0] = _presentHandLasers[0].color; uniforms.glowColors[1] = _presentHandLasers[1].color; + uniforms.extraGlowPoint = extraGlowPoint; + uniforms.extraGlowColor = _presentExtraLaser.color; } } @@ -605,13 +657,29 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve return true; } +bool HmdDisplayPlugin::setExtraLaser(HandLaserMode mode, const vec4& color, const glm::vec3& sensorSpaceStart, const vec3& sensorSpaceDirection) { + HandLaserInfo info; + info.mode = mode; + info.color = color; + info.direction = sensorSpaceDirection; + withNonPresentThreadLock([&] { + _extraLaser = info; + _extraLaserStart = sensorSpaceStart; + }); + + // FIXME defer to a child class plugin to determine if hand lasers are actually + // available based on the presence or absence of hand controllers + return true; +} + + void HmdDisplayPlugin::compositeExtra() { // If neither hand laser is activated, exit - if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid()) { + if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid() && !_presentExtraLaser.valid()) { return; } - if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX) { + if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } @@ -628,9 +696,17 @@ void HmdDisplayPlugin::compositeExtra() { const auto& laser = _presentHandLasers[index]; if (laser.valid()) { const auto& points = _presentHandLaserPoints[index]; - geometryCache->renderGlowLine(batch, points.first, points.second, laser.color); + geometryCache->renderGlowLine(batch, points.first, points.second, laser.color, _geometryIds[index]); } }); + + if (_presentExtraLaser.valid()) { + const auto& points = _presentExtraLaserPoints; + geometryCache->renderGlowLine(batch, points.first, points.second, _presentExtraLaser.color, _extraLaserID); + } }); } +HmdDisplayPlugin::~HmdDisplayPlugin() { + qDebug() << "Destroying HmdDisplayPlugin"; +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index d02fc5af05..e50183dd90 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -23,6 +23,7 @@ class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: + ~HmdDisplayPlugin(); bool isHmd() const override final { return true; } float getIPD() const override final { return _ipd; } glm::mat4 getEyeToHeadTransform(Eye eye) const override final { return _eyeOffsets[eye]; } @@ -37,6 +38,7 @@ public: virtual glm::mat4 getHeadPose() const override; bool setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) override; + bool setExtraLaser(HandLaserMode mode, const vec4& color, const glm::vec3& sensorSpaceStart, const vec3& sensorSpaceDirection) override; bool wantVsync() const override { return false; @@ -76,9 +78,17 @@ protected: Transform _presentUiModelTransform; std::array _presentHandLasers; + std::array _geometryIds; + int _extraLaserID; std::array _presentHandPoses; std::array, 2> _presentHandLaserPoints; + HandLaserInfo _extraLaser; + HandLaserInfo _presentExtraLaser; + vec3 _extraLaserStart; + vec3 _presentExtraLaserStart; + std::pair _presentExtraLaserPoints; + std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; @@ -129,6 +139,9 @@ private: vec2 resolution { CompositorHelper::VIRTUAL_SCREEN_SIZE }; float radius { 0.005f }; float alpha { 1.0f }; + + vec4 extraGlowColor; + vec2 extraGlowPoint { -1 }; } uniforms; struct Vertex { diff --git a/libraries/display-plugins/src/hmd_ui_glow.slf b/libraries/display-plugins/src/hmd_ui_glow.slf deleted file mode 100644 index 9270842092..0000000000 --- a/libraries/display-plugins/src/hmd_ui_glow.slf +++ /dev/null @@ -1,75 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-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 -// - -uniform sampler2D sampler; - -struct OverlayData { - mat4 mvp; - vec4 glowPoints; - vec4 glowColors[2]; - vec4 resolutionRadiusAlpha; -}; - -layout(std140) uniform overlayBuffer { - OverlayData overlay; -}; - -vec2 resolution = overlay.resolutionRadiusAlpha.xy; -float radius = overlay.resolutionRadiusAlpha.z; -float alpha = overlay.resolutionRadiusAlpha.w; -vec4 glowPoints = overlay.glowPoints; -vec4 glowColors[2] = overlay.glowColors; - -in vec3 vPosition; -in vec2 vTexCoord; - -out vec4 FragColor; - -float easeInOutCubic(float f) { - const float d = 1.0; - const float b = 0.0; - const float c = 1.0; - float t = f; - if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; - return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; -} - -void main() { - FragColor = texture(sampler, vTexCoord); - - vec2 aspect = resolution; - aspect /= resolution.x; - - float glowIntensity = 0.0; - float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); - float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); - float dist = min(dist1, dist2); - vec3 glowColor = glowColors[0].rgb; - if (dist2 < dist1) { - glowColor = glowColors[1].rgb; - } - - if (dist <= radius) { - glowIntensity = 1.0 - (dist / radius); - glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); - glowIntensity = easeInOutCubic(glowIntensity); - glowIntensity = pow(glowIntensity, 0.5); - } - - if (alpha <= 0.0) { - if (glowIntensity <= 0.0) { - discard; - } - - FragColor = vec4(glowColor, glowIntensity); - return; - } - - FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); - FragColor.a *= alpha; -} \ No newline at end of file diff --git a/libraries/display-plugins/src/hmd_ui_glow.slv b/libraries/display-plugins/src/hmd_ui_glow.slv deleted file mode 100644 index 54eb062590..0000000000 --- a/libraries/display-plugins/src/hmd_ui_glow.slv +++ /dev/null @@ -1,32 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-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 -// - -struct OverlayData { - mat4 mvp; - vec4 glowPoints; - vec4 glowColors[2]; - vec4 resolutionRadiusAlpha; -}; - -layout(std140) uniform overlayBuffer { - OverlayData overlay; -}; - -mat4 mvp = overlay.mvp; - -layout(location = 0) in vec3 Position; -layout(location = 3) in vec2 TexCoord; - -out vec3 vPosition; -out vec2 vTexCoord; - -void main() { - gl_Position = mvp * vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; -} diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e0fc03897a..2bd022d240 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -128,11 +128,15 @@ void EntityTreeRenderer::clear() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); - render::PendingChanges pendingChanges; - foreach(auto entity, _entitiesInScene) { - entity->removeFromScene(entity, scene, pendingChanges); + if (scene) { + render::PendingChanges pendingChanges; + foreach(auto entity, _entitiesInScene) { + entity->removeFromScene(entity, scene, pendingChanges); + } + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } - scene->enqueuePendingChanges(pendingChanges); _entitiesInScene.clear(); // reset the zone to the default (while we load the next scene) @@ -516,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(); } @@ -529,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; } @@ -901,8 +905,12 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { auto entity = _entitiesInScene.take(entityID); render::PendingChanges pendingChanges; auto scene = _viewState->getMain3DScene(); - entity->removeFromScene(entity, scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + if (scene) { + entity->removeFromScene(entity, scene, pendingChanges); + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(entitiesrenderer) << "EntityTreeRenderer::deletingEntity(), Unexpected null scene, possibly during application shutdown"; + } } } @@ -919,10 +927,14 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { // here's where we add the entity payload to the scene render::PendingChanges pendingChanges; auto scene = _viewState->getMain3DScene(); - if (entity->addToScene(entity, scene, pendingChanges)) { - _entitiesInScene.insert(entity->getEntityItemID(), entity); + if (scene) { + if (entity->addToScene(entity, scene, pendingChanges)) { + _entitiesInScene.insert(entity->getEntityItemID(), entity); + } + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(entitiesrenderer) << "EntityTreeRenderer::addEntityToScene(), Unexpected null scene, possibly during application shutdown"; } - scene->enqueuePendingChanges(pendingChanges); } 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/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 22b6264520..ec65bab1c8 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -15,6 +15,7 @@ #include #include #include "AbstractViewStateInterface.h" +#include "EntitiesRendererLogging.h" // These or the icon "name" used by the render item status value, they correspond to the atlas texture used by the DrawItemStatus @@ -79,10 +80,14 @@ public: render::PendingChanges pendingChanges; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - pendingChanges.updateItem(_myItem, [](RenderableEntityItemProxy& data) { - }); + if (scene) { + pendingChanges.updateItem(_myItem, [](RenderableEntityItemProxy& data) { + }); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(entitiesrenderer) << "SimpleRenderableEntityItem::notifyChanged(), Unexpected null scene, possibly during application shutdown"; + } } private: diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index f2e938ece3..95d28f74f3 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -65,7 +65,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 +368,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 +470,7 @@ void RenderableModelEntityItem::render(RenderArgs* args) { } } -ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { +ModelPointer RenderableModelEntityItem::getModel(QSharedPointer renderer) { if (!renderer) { return nullptr; } @@ -495,7 +495,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; 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/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 20adff83df..9c98e699f1 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -24,6 +24,14 @@ EntityItemPointer RenderableTextEntityItem::factory(const EntityItemID& entityID return entity; } +RenderableTextEntityItem::~RenderableTextEntityItem() { + auto geometryCache = DependencyManager::get(); + if (_geometryID && geometryCache) { + geometryCache->releaseID(_geometryID); + } + delete _textRenderer; +} + void RenderableTextEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableTextEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Text); @@ -62,9 +70,12 @@ void RenderableTextEntityItem::render(RenderArgs* args) { transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed batch.setModelTransform(transformToTopLeft); - - DependencyManager::get()->bindSimpleProgram(batch, false, transparent, false, false, true); - DependencyManager::get()->renderQuad(batch, minCorner, maxCorner, backgroundColor); + auto geometryCache = DependencyManager::get(); + if (!_geometryID) { + _geometryID = geometryCache->allocateID(); + } + geometryCache->bindSimpleProgram(batch, false, transparent, false, false, true); + geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); float scale = _lineHeight / _textRenderer->getFontSize(); transformToTopLeft.setScale(scale); // Scale to have the correct line height diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index cbe2b11c27..ee75931513 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -23,13 +23,14 @@ class RenderableTextEntityItem : public TextEntityItem { public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); RenderableTextEntityItem(const EntityItemID& entityItemID) : TextEntityItem(entityItemID) { } - ~RenderableTextEntityItem() { delete _textRenderer; } + ~RenderableTextEntityItem(); virtual void render(RenderArgs* args) override; SIMPLE_RENDERABLE(); private: + int _geometryID { 0 }; TextRenderer3D* _textRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE / 2.0f); }; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 021edde2c2..f426f4a816 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -52,14 +52,19 @@ RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemI _touchDevice.setType(QTouchDevice::TouchScreen); _touchDevice.setName("RenderableWebEntityItemTouchDevice"); _touchDevice.setMaximumTouchPoints(4); + _geometryId = DependencyManager::get()->allocateID(); } RenderableWebEntityItem::~RenderableWebEntityItem() { destroyWebSurface(); qDebug() << "Destroyed web entity " << getID(); + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } -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; @@ -90,16 +95,25 @@ 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); + // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces + // and the current rendering load) + _webSurface->setMaxFps(10); + // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than // member variables, since they would implicitly refer to a this that // is no longer valid - _webSurface->create(currentContext); _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/")); _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { @@ -108,6 +122,7 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _sourceUrl); _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); + // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. // forward web events to EntityScriptingInterface auto entities = DependencyManager::get(); @@ -124,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; @@ -175,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(); @@ -192,10 +209,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - auto recycler = [webSurface] (uint32_t recycleTexture, void* recycleFence) { - webSurface->releaseTexture({ recycleTexture, recycleFence }); - }; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(recycler)); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; @@ -227,7 +241,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { } else { DependencyManager::get()->bindOpaqueWebBrowserProgram(batch); } - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio)); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId); } void RenderableWebEntityItem::setSourceUrl(const QString& value) { @@ -320,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 5414f43dc8..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; @@ -69,6 +69,7 @@ private: QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; QMetaObject::Connection _hoverLeaveConnection; + int _geometryId { 0 }; }; #endif // hifi_RenderableWebEntityItem_h diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 9aa52f5ad3..0215ce4d07 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -248,9 +248,12 @@ void RenderableZoneEntityItem::notifyBoundChanged() { } render::PendingChanges pendingChanges; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (scene) { + pendingChanges.updateItem(_myMetaItem, [](RenderableZoneEntityItemMeta& data) { + }); - pendingChanges.updateItem(_myMetaItem, [](RenderableZoneEntityItemMeta& data) { - }); - - scene->enqueuePendingChanges(pendingChanges); + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(entitiesrenderer) << "RenderableZoneEntityItem::notifyBoundChanged(), Unexpected null scene, possibly during application shutdown"; + } } diff --git a/libraries/entities/src/AnimationPropertyGroup.cpp b/libraries/entities/src/AnimationPropertyGroup.cpp index 015d24ab74..258d86172f 100644 --- a/libraries/entities/src/AnimationPropertyGroup.cpp +++ b/libraries/entities/src/AnimationPropertyGroup.cpp @@ -23,7 +23,7 @@ void AnimationPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desire if (_animationLoop) { COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_FPS, Animation, animation, FPS, fps, _animationLoop->getFPS); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_FRAME_INDEX, Animation, animation, CurrentFrame, currentFrame, _animationLoop->getFPS); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_FRAME_INDEX, Animation, animation, CurrentFrame, currentFrame, _animationLoop->getCurrentFrame); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_PLAYING, Animation, animation, Running, running, _animationLoop->getRunning); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_LOOP, Animation, animation, Loop, loop, _animationLoop->getLoop); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_FIRST_FRAME, Animation, animation, FirstFrame, firstFrame, _animationLoop->getFirstFrame); @@ -79,6 +79,27 @@ void AnimationPropertyGroup::copyFromScriptValue(const QScriptValue& object, boo } +void AnimationPropertyGroup::merge(const AnimationPropertyGroup& other) { + COPY_PROPERTY_IF_CHANGED(url); + if (_animationLoop) { + _fps = _animationLoop->getFPS(); + _currentFrame = _animationLoop->getCurrentFrame(); + _running = _animationLoop->getRunning(); + _loop = _animationLoop->getLoop(); + _firstFrame = _animationLoop->getFirstFrame(); + _lastFrame = _animationLoop->getLastFrame(); + _hold = _animationLoop->getHold(); + } else { + COPY_PROPERTY_IF_CHANGED(fps); + COPY_PROPERTY_IF_CHANGED(currentFrame); + COPY_PROPERTY_IF_CHANGED(running); + COPY_PROPERTY_IF_CHANGED(loop); + COPY_PROPERTY_IF_CHANGED(firstFrame); + COPY_PROPERTY_IF_CHANGED(lastFrame); + COPY_PROPERTY_IF_CHANGED(hold); + } +} + void AnimationPropertyGroup::setFromOldAnimationSettings(const QString& value) { // the animations setting is a JSON string that may contain various animation settings. // if it includes fps, currentFrame, or running, those values will be parsed out and diff --git a/libraries/entities/src/AnimationPropertyGroup.h b/libraries/entities/src/AnimationPropertyGroup.h index 4284be1eda..c9d2a03e57 100644 --- a/libraries/entities/src/AnimationPropertyGroup.h +++ b/libraries/entities/src/AnimationPropertyGroup.h @@ -38,6 +38,9 @@ public: QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const override; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) override; + + void merge(const AnimationPropertyGroup& other); + virtual void debugDump() const override; virtual void listChangedProperties(QList& out) override; diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 3ea3c92b0c..4478e37003 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -63,8 +63,8 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, // the properties that get serialized into the avatar identity packet should be the entire set // rather than just the ones being edited. - entity->setProperties(properties); EntityItemProperties entityProperties = entity->getProperties(); + entityProperties.merge(properties); QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); QVariant variantProperties = scriptProperties.toVariant(); diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 588c6e2977..a0142dec8f 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -749,6 +749,133 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool _lastEdited = usecTimestampNow(); } +void EntityItemProperties::merge(const EntityItemProperties& other) { + COPY_PROPERTY_IF_CHANGED(position); + COPY_PROPERTY_IF_CHANGED(dimensions); + COPY_PROPERTY_IF_CHANGED(rotation); + COPY_PROPERTY_IF_CHANGED(density); + COPY_PROPERTY_IF_CHANGED(velocity); + COPY_PROPERTY_IF_CHANGED(gravity); + COPY_PROPERTY_IF_CHANGED(acceleration); + COPY_PROPERTY_IF_CHANGED(damping); + COPY_PROPERTY_IF_CHANGED(restitution); + COPY_PROPERTY_IF_CHANGED(friction); + COPY_PROPERTY_IF_CHANGED(lifetime); + COPY_PROPERTY_IF_CHANGED(script); + COPY_PROPERTY_IF_CHANGED(scriptTimestamp); + COPY_PROPERTY_IF_CHANGED(registrationPoint); + COPY_PROPERTY_IF_CHANGED(angularVelocity); + COPY_PROPERTY_IF_CHANGED(angularDamping); + COPY_PROPERTY_IF_CHANGED(visible); + COPY_PROPERTY_IF_CHANGED(color); + COPY_PROPERTY_IF_CHANGED(colorSpread); + COPY_PROPERTY_IF_CHANGED(colorStart); + COPY_PROPERTY_IF_CHANGED(colorFinish); + COPY_PROPERTY_IF_CHANGED(alpha); + COPY_PROPERTY_IF_CHANGED(alphaSpread); + COPY_PROPERTY_IF_CHANGED(alphaStart); + COPY_PROPERTY_IF_CHANGED(alphaFinish); + COPY_PROPERTY_IF_CHANGED(emitterShouldTrail); + COPY_PROPERTY_IF_CHANGED(modelURL); + COPY_PROPERTY_IF_CHANGED(compoundShapeURL); + COPY_PROPERTY_IF_CHANGED(localRenderAlpha); + COPY_PROPERTY_IF_CHANGED(collisionless); + COPY_PROPERTY_IF_CHANGED(collisionMask); + COPY_PROPERTY_IF_CHANGED(dynamic); + COPY_PROPERTY_IF_CHANGED(isSpotlight); + COPY_PROPERTY_IF_CHANGED(intensity); + COPY_PROPERTY_IF_CHANGED(falloffRadius); + COPY_PROPERTY_IF_CHANGED(exponent); + COPY_PROPERTY_IF_CHANGED(cutoff); + COPY_PROPERTY_IF_CHANGED(locked); + COPY_PROPERTY_IF_CHANGED(textures); + COPY_PROPERTY_IF_CHANGED(userData); + COPY_PROPERTY_IF_CHANGED(text); + COPY_PROPERTY_IF_CHANGED(lineHeight); + COPY_PROPERTY_IF_CHANGED(textColor); + COPY_PROPERTY_IF_CHANGED(backgroundColor); + COPY_PROPERTY_IF_CHANGED(shapeType); + COPY_PROPERTY_IF_CHANGED(maxParticles); + COPY_PROPERTY_IF_CHANGED(lifespan); + COPY_PROPERTY_IF_CHANGED(isEmitting); + COPY_PROPERTY_IF_CHANGED(emitRate); + COPY_PROPERTY_IF_CHANGED(emitSpeed); + COPY_PROPERTY_IF_CHANGED(speedSpread); + COPY_PROPERTY_IF_CHANGED(emitOrientation); + COPY_PROPERTY_IF_CHANGED(emitDimensions); + COPY_PROPERTY_IF_CHANGED(emitRadiusStart); + COPY_PROPERTY_IF_CHANGED(polarStart); + COPY_PROPERTY_IF_CHANGED(polarFinish); + COPY_PROPERTY_IF_CHANGED(azimuthStart); + COPY_PROPERTY_IF_CHANGED(azimuthFinish); + COPY_PROPERTY_IF_CHANGED(emitAcceleration); + COPY_PROPERTY_IF_CHANGED(accelerationSpread); + COPY_PROPERTY_IF_CHANGED(particleRadius); + COPY_PROPERTY_IF_CHANGED(radiusSpread); + COPY_PROPERTY_IF_CHANGED(radiusStart); + COPY_PROPERTY_IF_CHANGED(radiusFinish); + COPY_PROPERTY_IF_CHANGED(marketplaceID); + COPY_PROPERTY_IF_CHANGED(name); + COPY_PROPERTY_IF_CHANGED(collisionSoundURL); + + COPY_PROPERTY_IF_CHANGED(backgroundMode); + COPY_PROPERTY_IF_CHANGED(sourceUrl); + COPY_PROPERTY_IF_CHANGED(voxelVolumeSize); + COPY_PROPERTY_IF_CHANGED(voxelData); + COPY_PROPERTY_IF_CHANGED(voxelSurfaceStyle); + COPY_PROPERTY_IF_CHANGED(lineWidth); + COPY_PROPERTY_IF_CHANGED(linePoints); + COPY_PROPERTY_IF_CHANGED(href); + COPY_PROPERTY_IF_CHANGED(description); + COPY_PROPERTY_IF_CHANGED(faceCamera); + COPY_PROPERTY_IF_CHANGED(actionData); + COPY_PROPERTY_IF_CHANGED(normals); + COPY_PROPERTY_IF_CHANGED(strokeWidths); + COPY_PROPERTY_IF_CHANGED(created); + + _animation.merge(other._animation); + _keyLight.merge(other._keyLight); + _skybox.merge(other._skybox); + _stage.merge(other._stage); + + COPY_PROPERTY_IF_CHANGED(xTextureURL); + COPY_PROPERTY_IF_CHANGED(yTextureURL); + COPY_PROPERTY_IF_CHANGED(zTextureURL); + + COPY_PROPERTY_IF_CHANGED(xNNeighborID); + COPY_PROPERTY_IF_CHANGED(yNNeighborID); + COPY_PROPERTY_IF_CHANGED(zNNeighborID); + + COPY_PROPERTY_IF_CHANGED(xPNeighborID); + COPY_PROPERTY_IF_CHANGED(yPNeighborID); + COPY_PROPERTY_IF_CHANGED(zPNeighborID); + + COPY_PROPERTY_IF_CHANGED(parentID); + COPY_PROPERTY_IF_CHANGED(parentJointIndex); + COPY_PROPERTY_IF_CHANGED(queryAACube); + + COPY_PROPERTY_IF_CHANGED(localPosition); + COPY_PROPERTY_IF_CHANGED(localRotation); + COPY_PROPERTY_IF_CHANGED(localVelocity); + COPY_PROPERTY_IF_CHANGED(localAngularVelocity); + + COPY_PROPERTY_IF_CHANGED(jointRotationsSet); + COPY_PROPERTY_IF_CHANGED(jointRotations); + COPY_PROPERTY_IF_CHANGED(jointTranslationsSet); + COPY_PROPERTY_IF_CHANGED(jointTranslations); + COPY_PROPERTY_IF_CHANGED(shape); + + COPY_PROPERTY_IF_CHANGED(flyingAllowed); + COPY_PROPERTY_IF_CHANGED(ghostingAllowed); + + COPY_PROPERTY_IF_CHANGED(clientOnly); + COPY_PROPERTY_IF_CHANGED(owningAvatarID); + + COPY_PROPERTY_IF_CHANGED(dpi); + + _lastEdited = usecTimestampNow(); +} + QScriptValue EntityItemPropertiesToScriptValue(QScriptEngine* engine, const EntityItemProperties& properties) { return properties.copyToScriptValue(engine, false); } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 3468b7dcde..d5cce92f87 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -68,6 +68,8 @@ public: EntityItemProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()); virtual ~EntityItemProperties() = default; + void merge(const EntityItemProperties& other); + EntityTypes::EntityType getType() const { return _type; } void setType(EntityTypes::EntityType type) { _type = type; } @@ -365,6 +367,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, Damping, damping, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Restitution, restitution, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Friction, friction, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, Created, created, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Lifetime, lifetime, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Script, script, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, ScriptTimestamp, scriptTimestamp, ""); diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index f386a474d8..278d945ab0 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -305,7 +305,16 @@ inline xColor xColor_convertFromScriptValue(const QScriptValue& v, bool& isValid } return newValue; } - + + +#define COPY_PROPERTY_IF_CHANGED(P) \ +{ \ + if (other._##P##Changed) { \ + _##P = other._##P; \ + } \ +} + + #define COPY_PROPERTY_FROM_QSCRIPTVALUE(P, T, S) \ { \ diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index ffb6ec31e2..7de841a321 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -945,6 +945,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c properties.setLifetime(_maxTmpEntityLifetime); // also bump up the lastEdited time of the properties so that the interface that created this edit // will accept our adjustment to lifetime back into its own entity-tree. + if (properties.getLastEdited() == UNKNOWN_CREATED_TIME) { + properties.setLastEdited(usecTimestampNow()); + } properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP); } } diff --git a/libraries/entities/src/KeyLightPropertyGroup.cpp b/libraries/entities/src/KeyLightPropertyGroup.cpp index cac47c907a..f1dae18adc 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.cpp +++ b/libraries/entities/src/KeyLightPropertyGroup.cpp @@ -49,6 +49,15 @@ void KeyLightPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(keyLightDirection, glmVec3, setDirection, getDirection); } +void KeyLightPropertyGroup::merge(const KeyLightPropertyGroup& other) { + COPY_PROPERTY_IF_CHANGED(color); + COPY_PROPERTY_IF_CHANGED(intensity); + COPY_PROPERTY_IF_CHANGED(ambientIntensity); + COPY_PROPERTY_IF_CHANGED(direction); + COPY_PROPERTY_IF_CHANGED(ambientURL); +} + + void KeyLightPropertyGroup::debugDump() const { qDebug() << " KeyLightPropertyGroup: ---------------------------------------------"; diff --git a/libraries/entities/src/KeyLightPropertyGroup.h b/libraries/entities/src/KeyLightPropertyGroup.h index 029a257bf1..21ef074679 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.h +++ b/libraries/entities/src/KeyLightPropertyGroup.h @@ -34,6 +34,9 @@ public: QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const override; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) override; + + void merge(const KeyLightPropertyGroup& other); + virtual void debugDump() const override; virtual void listChangedProperties(QList& out) override; diff --git a/libraries/entities/src/SkyboxPropertyGroup.cpp b/libraries/entities/src/SkyboxPropertyGroup.cpp index 0fd00bfe6e..60800bd819 100644 --- a/libraries/entities/src/SkyboxPropertyGroup.cpp +++ b/libraries/entities/src/SkyboxPropertyGroup.cpp @@ -27,6 +27,12 @@ void SkyboxPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(skybox, url, QString, setURL); } +void SkyboxPropertyGroup::merge(const SkyboxPropertyGroup& other) { + COPY_PROPERTY_IF_CHANGED(color); + COPY_PROPERTY_IF_CHANGED(url); +} + + void SkyboxPropertyGroup::debugDump() const { qDebug() << " SkyboxPropertyGroup: ---------------------------------------------"; qDebug() << " Color:" << getColor() << " has changed:" << colorChanged(); diff --git a/libraries/entities/src/SkyboxPropertyGroup.h b/libraries/entities/src/SkyboxPropertyGroup.h index 8b485353b8..57cfe095ee 100644 --- a/libraries/entities/src/SkyboxPropertyGroup.h +++ b/libraries/entities/src/SkyboxPropertyGroup.h @@ -34,6 +34,9 @@ public: QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const override; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) override; + + void merge(const SkyboxPropertyGroup& other); + virtual void debugDump() const override; virtual void listChangedProperties(QList& out) override; diff --git a/libraries/entities/src/StagePropertyGroup.cpp b/libraries/entities/src/StagePropertyGroup.cpp index 0c10795d6f..d8328164bf 100644 --- a/libraries/entities/src/StagePropertyGroup.cpp +++ b/libraries/entities/src/StagePropertyGroup.cpp @@ -55,6 +55,17 @@ void StagePropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _ COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(stage, automaticHourDay, bool, setAutomaticHourDay); } +void StagePropertyGroup::merge(const StagePropertyGroup& other) { + COPY_PROPERTY_IF_CHANGED(sunModelEnabled); + COPY_PROPERTY_IF_CHANGED(latitude); + COPY_PROPERTY_IF_CHANGED(longitude); + COPY_PROPERTY_IF_CHANGED(altitude); + COPY_PROPERTY_IF_CHANGED(day); + COPY_PROPERTY_IF_CHANGED(hour); + COPY_PROPERTY_IF_CHANGED(automaticHourDay); +} + + void StagePropertyGroup::debugDump() const { qDebug() << " StagePropertyGroup: ---------------------------------------------"; qDebug() << " _sunModelEnabled:" << _sunModelEnabled; diff --git a/libraries/entities/src/StagePropertyGroup.h b/libraries/entities/src/StagePropertyGroup.h index 80f02f851a..1b6b234cf5 100644 --- a/libraries/entities/src/StagePropertyGroup.h +++ b/libraries/entities/src/StagePropertyGroup.h @@ -34,6 +34,9 @@ public: QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const override; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) override; + + void merge(const StagePropertyGroup& other); + virtual void debugDump() const override; virtual void listChangedProperties(QList& out) override; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 6afaf933f2..370a7bb89f 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -773,23 +773,29 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } else if (object.name == "Texture") { TextureParam tex; foreach (const FBXNode& subobject, object.children) { - if (subobject.name == "RelativeFilename") { + const int RELATIVE_FILENAME_MIN_SIZE = 1; + const int TEXTURE_NAME_MIN_SIZE = 1; + const int TEXTURE_ALPHA_SOURCE_MIN_SIZE = 1; + const int MODEL_UV_TRANSLATION_MIN_SIZE = 2; + const int MODEL_UV_SCALING_MIN_SIZE = 2; + const int CROPPING_MIN_SIZE = 4; + if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { QByteArray filename = subobject.properties.at(0).toByteArray(); QByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); - } else if (subobject.name == "TextureName") { + } else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); name = name.left(name.indexOf('[')); _textureNames.insert(getID(object.properties), name); - } else if (subobject.name == "Texture_Alpha_Source") { + } else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); - } else if (subobject.name == "ModelUVTranslation") { + } else if (subobject.name == "ModelUVTranslation" && subobject.properties.length() >= MODEL_UV_TRANSLATION_MIN_SIZE) { tex.assign(tex.UVTranslation, glm::vec2(subobject.properties.at(0).value(), - subobject.properties.at(1).value())); - } else if (subobject.name == "ModelUVScaling") { + subobject.properties.at(1).value())); + } else if (subobject.name == "ModelUVScaling" && subobject.properties.length() >= MODEL_UV_SCALING_MIN_SIZE) { tex.assign(tex.UVScaling, glm::vec2(subobject.properties.at(0).value(), subobject.properties.at(1).value())); if (tex.UVScaling.x == 0.0f) { @@ -798,7 +804,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (tex.UVScaling.y == 0.0f) { tex.UVScaling.y = 1.0f; } - } else if (subobject.name == "Cropping") { + } else if (subobject.name == "Cropping" && subobject.properties.length() >= CROPPING_MIN_SIZE) { tex.assign(tex.cropping, glm::vec4(subobject.properties.at(0).value(), subobject.properties.at(1).value(), subobject.properties.at(2).value(), 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/Context.cpp b/libraries/gl/src/gl/Context.cpp index 5b8a1a2fa1..ec1c284e99 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -40,6 +40,27 @@ static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contain using namespace gl; + +std::atomic Context::_totalSwapchainMemoryUsage { 0 }; + +size_t Context::getSwapchainMemoryUsage() { return _totalSwapchainMemoryUsage.load(); } + +size_t Context::evalSurfaceMemoryUsage(uint32_t width, uint32_t height, uint32_t pixelSize) { + return width * height * pixelSize; +} + +void Context::updateSwapchainMemoryUsage(size_t prevSize, size_t newSize) { + if (prevSize == newSize) { + return; + } + if (newSize > prevSize) { + _totalSwapchainMemoryUsage.fetch_add(newSize - prevSize); + } else { + _totalSwapchainMemoryUsage.fetch_sub(prevSize - newSize); + } +} + + Context* Context::PRIMARY = nullptr; Context::Context() {} @@ -78,18 +99,35 @@ void Context::release() { if (PRIMARY == this) { PRIMARY = nullptr; } - } + updateSwapchainMemoryCounter(); +} Context::~Context() { release(); } +void Context::updateSwapchainMemoryCounter() { + if (_window) { + auto newSize = _window->size(); + auto newMemSize = gl::Context::evalSurfaceMemoryUsage(newSize.width(), newSize.height(), (uint32_t) _swapchainPixelSize); + gl::Context::updateSwapchainMemoryUsage(_swapchainMemoryUsage, newMemSize); + _swapchainMemoryUsage = newMemSize; + } else { + // No window ? no more swapchain + gl::Context::updateSwapchainMemoryUsage(_swapchainMemoryUsage, 0); + _swapchainMemoryUsage = 0; + } +} + void Context::setWindow(QWindow* window) { release(); _window = window; + #ifdef Q_OS_WIN _hwnd = (HWND)window->winId(); #endif + + updateSwapchainMemoryCounter(); } #ifdef Q_OS_WIN @@ -98,6 +136,8 @@ static const char* PRIMARY_CONTEXT_PROPERTY_NAME = "com.highfidelity.gl.primaryC bool Context::makeCurrent() { BOOL result = wglMakeCurrent(_hdc, _hglrc); assert(result); + updateSwapchainMemoryCounter(); + return result; } @@ -217,6 +257,11 @@ void Context::create() { wglChoosePixelFormatARB(_hdc, &formatAttribs[0], NULL, 1, &pixelFormat, &numFormats); DescribePixelFormat(_hdc, pixelFormat, sizeof(pfd), &pfd); } + // The swap chain pixel size for swap chains is : rgba32 + depth24stencil8 + // We don't apply the length of the swap chain into this pixelSize since it is not vsible for the Process (on windows). + _swapchainPixelSize = 32 + 32; + updateSwapchainMemoryCounter(); + SetPixelFormat(_hdc, pixelFormat, &pfd); { std::vector contextAttribs; @@ -277,6 +322,8 @@ void OffscreenContext::create() { _window->setSurfaceType(QSurface::OpenGLSurface); _window->create(); setWindow(_window); + QSize windowSize = _window->size() * _window->devicePixelRatio(); + qCDebug(glLogging) << "New Offscreen GLContext, window size = " << windowSize.width() << " , " << windowSize.height(); QGuiApplication::processEvents(); } Parent::create(); diff --git a/libraries/gl/src/gl/Context.h b/libraries/gl/src/gl/Context.h index 6dc859f222..d1b0d53631 100644 --- a/libraries/gl/src/gl/Context.h +++ b/libraries/gl/src/gl/Context.h @@ -11,6 +11,7 @@ #include #include +#include #if defined(Q_OS_WIN) #include @@ -23,7 +24,7 @@ class QThread; namespace gl { -class Context { + class Context { protected: QWindow* _window { nullptr }; static Context* PRIMARY; @@ -57,6 +58,17 @@ class Context { virtual void create(); QOpenGLContext* qglContext(); void moveToThread(QThread* thread); + + static size_t evalSurfaceMemoryUsage(uint32_t width, uint32_t height, uint32_t pixelSize); + static size_t getSwapchainMemoryUsage(); + static void updateSwapchainMemoryUsage(size_t prevSize, size_t newSize); + + private: + static std::atomic _totalSwapchainMemoryUsage; + + size_t _swapchainMemoryUsage { 0 }; + size_t _swapchainPixelSize { 0 }; + void updateSwapchainMemoryCounter(); }; class OffscreenContext : public Context { @@ -67,6 +79,7 @@ class Context { virtual ~OffscreenContext(); void create() override; }; + } #endif // hifi_gpu_GPUConfig_h diff --git a/libraries/gl/src/gl/ContextQt.cpp b/libraries/gl/src/gl/ContextQt.cpp index ff59b8f1e1..4d5d2c4e9f 100644 --- a/libraries/gl/src/gl/ContextQt.cpp +++ b/libraries/gl/src/gl/ContextQt.cpp @@ -15,6 +15,8 @@ #include #endif +#include "GLHelpers.h" + using namespace gl; void Context::destroyContext(QOpenGLContext* context) { @@ -45,6 +47,7 @@ void Context::moveToThread(QThread* thread) { #ifndef Q_OS_WIN bool Context::makeCurrent() { + updateSwapchainMemoryCounter(); return _context->makeCurrent(_window); } @@ -70,6 +73,9 @@ void Context::create() { } _context->setFormat(getDefaultOpenGLSurfaceFormat()); _context->create(); + + _swapchainPixelSize = evalGLFormatSwapchainPixelSize(_context->format()); + updateSwapchainMemoryCounter(); } #endif diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index dde1e0ec97..dca1214b32 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -13,6 +13,17 @@ #include +size_t evalGLFormatSwapchainPixelSize(const QSurfaceFormat& format) { + size_t pixelSize = format.redBufferSize() + format.greenBufferSize() + format.blueBufferSize() + format.alphaBufferSize(); + // We don't apply the length of the swap chain into this pixelSize since it is not vsible for the Process (on windows). + // Let s keep this here remember that: + // if (format.swapBehavior() > 0) { + // pixelSize *= format.swapBehavior(); // multiply the color buffer pixel size by the actual swapchain depth + // } + pixelSize += format.stencilBufferSize() + format.depthBufferSize(); + return pixelSize; +} + const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; static std::once_flag once; @@ -35,17 +46,22 @@ int glVersionToInteger(QString glVersion) { } QJsonObject getGLContextData() { - QString glVersion = QString((const char*)glGetString(GL_VERSION)); - QString glslVersion = QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); - QString glVendor = QString((const char*) glGetString(GL_VENDOR)); - QString glRenderer = QString((const char*)glGetString(GL_RENDERER)); + static QJsonObject result; + static std::once_flag once; + std::call_once(once, [] { + QString glVersion = QString((const char*)glGetString(GL_VERSION)); + QString glslVersion = QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); + QString glVendor = QString((const char*) glGetString(GL_VENDOR)); + QString glRenderer = QString((const char*)glGetString(GL_RENDERER)); - return QJsonObject { - { "version", glVersion }, - { "slVersion", glslVersion }, - { "vendor", glVendor }, - { "renderer", glRenderer }, - }; + result = QJsonObject { + { "version", glVersion }, + { "sl_version", glslVersion }, + { "vendor", glVendor }, + { "renderer", glRenderer }, + }; + }); + return result; } QThread* RENDER_THREAD = nullptr; diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index 8834a718fd..daa181467d 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -26,6 +26,8 @@ class QGLFormat; template void setGLFormatVersion(F& format, int major = 4, int minor = 5) { format.setVersion(major, minor); } +size_t evalGLFormatSwapchainPixelSize(const QSurfaceFormat& format); + const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); QJsonObject getGLContextData(); int glVersionToInteger(QString glVersion); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index dbd338bc34..e54846196b 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -17,11 +17,15 @@ #include #include +#include "Context.h" #include "GLHelpers.h" #include "GLLogging.h" -OffscreenGLCanvas::OffscreenGLCanvas() : _context(new QOpenGLContext), _offscreenSurface(new QOffscreenSurface){ +OffscreenGLCanvas::OffscreenGLCanvas() : + _context(new QOpenGLContext), + _offscreenSurface(new QOffscreenSurface) +{ } OffscreenGLCanvas::~OffscreenGLCanvas() { @@ -56,7 +60,6 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); Q_ASSERT(result); - std::call_once(_reportOnce, [this]{ qCDebug(glLogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); qCDebug(glLogging) << "GL Shader Language Version: " << QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index f48e2e6092..06f755c1dd 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -8,9 +8,8 @@ #include "OffscreenQmlSurface.h" #include "Config.h" -#include -#include -#include +#include +#include #include #include @@ -37,23 +36,160 @@ #include "OffscreenGLCanvas.h" #include "GLHelpers.h" #include "GLLogging.h" -#include "TextureRecycler.h" #include "Context.h" -QString fixupHifiUrl(const QString& urlString) { - static const QString ACCESS_TOKEN_PARAMETER = "access_token"; - static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; - QUrl url(urlString); - QUrlQuery query(url); - if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - auto accountManager = DependencyManager::get(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token); - url.setQuery(query.query()); - return url.toString(); - } - return urlString; +struct TextureSet { + // The number of surfaces with this size + size_t count { 0 }; + std::list returnedTextures; +}; + +uint64_t uvec2ToUint64(const uvec2& v) { + uint64_t result = v.x; + result <<= 32; + result |= v.y; + return result; } +class OffscreenTextures { +public: + GLuint getNextTexture(const uvec2& size) { + assert(QThread::currentThread() == qApp->thread()); + + recycle(); + + ++_activeTextureCount; + auto sizeKey = uvec2ToUint64(size); + assert(_textures.count(sizeKey)); + auto& textureSet = _textures[sizeKey]; + if (!textureSet.returnedTextures.empty()) { + auto textureAndFence = textureSet.returnedTextures.front(); + textureSet.returnedTextures.pop_front(); + waitOnFence(static_cast(textureAndFence.second)); + return textureAndFence.first; + } + + return createTexture(size); + } + + void releaseSize(const uvec2& size) { + assert(QThread::currentThread() == qApp->thread()); + auto sizeKey = uvec2ToUint64(size); + assert(_textures.count(sizeKey)); + auto& textureSet = _textures[sizeKey]; + if (0 == --textureSet.count) { + for (const auto& textureAndFence : textureSet.returnedTextures) { + destroy(textureAndFence); + } + _textures.erase(sizeKey); + } + } + + void acquireSize(const uvec2& size) { + assert(QThread::currentThread() == qApp->thread()); + auto sizeKey = uvec2ToUint64(size); + auto& textureSet = _textures[sizeKey]; + ++textureSet.count; + } + + // May be called on any thread + void releaseTexture(const OffscreenQmlSurface::TextureAndFence & textureAndFence) { + --_activeTextureCount; + Lock lock(_mutex); + _returnedTextures.push_back(textureAndFence); + } + + void report() { + uint64_t now = usecTimestampNow(); + if ((now - _lastReport) > USECS_PER_SECOND * 5) { + _lastReport = now; + qCDebug(glLogging) << "Current offscreen texture count " << _allTextureCount; + qCDebug(glLogging) << "Current offscreen active texture count " << _activeTextureCount; + } + } + + size_t getUsedTextureMemory() { return _totalTextureUsage; } +private: + static void waitOnFence(GLsync fence) { + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + static size_t getMemoryForSize(const uvec2& size) { + // Base size + mips + return static_cast(((size.x * size.y) << 2) * 1.33f); + } + + void destroyTexture(GLuint texture) { + --_allTextureCount; + auto size = _textureSizes[texture]; + assert(getMemoryForSize(size) <= _totalTextureUsage); + _totalTextureUsage -= getMemoryForSize(size); + _textureSizes.erase(texture); + glDeleteTextures(1, &texture); + } + + void destroy(const OffscreenQmlSurface::TextureAndFence& textureAndFence) { + waitOnFence(static_cast(textureAndFence.second)); + destroyTexture(textureAndFence.first); + } + + GLuint createTexture(const uvec2& size) { + // Need a new texture + uint32_t newTexture; + glGenTextures(1, &newTexture); + ++_allTextureCount; + _textureSizes[newTexture] = size; + _totalTextureUsage += getMemoryForSize(size); + glBindTexture(GL_TEXTURE_2D, newTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + return newTexture; + } + + void recycle() { + assert(QThread::currentThread() == qApp->thread()); + // First handle any global returns + std::list returnedTextures; + { + Lock lock(_mutex); + returnedTextures.swap(_returnedTextures); + } + + for (auto textureAndFence : returnedTextures) { + GLuint texture = textureAndFence.first; + uvec2 size = _textureSizes[texture]; + auto sizeKey = uvec2ToUint64(size); + // Textures can be returned after all surfaces of the given size have been destroyed, + // in which case we just destroy the texture + if (!_textures.count(sizeKey)) { + destroy(textureAndFence); + continue; + } + _textures[sizeKey].returnedTextures.push_back(textureAndFence); + } + } + + using Mutex = std::mutex; + using Lock = std::unique_lock; + std::atomic _allTextureCount; + std::atomic _activeTextureCount; + std::unordered_map _textures; + std::unordered_map _textureSizes; + Mutex _mutex; + std::list _returnedTextures; + uint64_t _lastReport { 0 }; + size_t _totalTextureUsage { 0 }; +} offscreenTextures; + class UrlHandler : public QObject { Q_OBJECT public: @@ -66,11 +202,6 @@ public: static auto handler = dynamic_cast(qApp); return handler->acceptURL(url); } - - // FIXME hack for authentication, remove when we migrate to Qt 5.6 - Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { - return fixupHifiUrl(originalUrl); - } }; // Time between receiving a request to render the offscreen UI actually triggering @@ -97,6 +228,10 @@ private: friend class OffscreenQmlSurface; }; +size_t OffscreenQmlSurface::getUsedTextureMemory() { + return offscreenTextures.getUsedTextureMemory(); +} + class QmlNetworkAccessManager : public NetworkAccessManager { public: friend class QmlNetworkAccessManagerFactory; @@ -116,31 +251,12 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") -void OffscreenQmlSurface::setupFbo() { - _canvas->makeCurrent(); - _textures.setSize(_size); - if (_depthStencil) { - glDeleteRenderbuffers(1, &_depthStencil); - _depthStencil = 0; - } - glGenRenderbuffers(1, &_depthStencil); - glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y); - - if (_fbo) { - glDeleteFramebuffers(1, &_fbo); - _fbo = 0; - } - glGenFramebuffers(1, &_fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - _canvas->doneCurrent(); -} - void OffscreenQmlSurface::cleanup() { _canvas->makeCurrent(); + _renderControl->invalidate(); + delete _renderControl; // and invalidate + if (_depthStencil) { glDeleteRenderbuffers(1, &_depthStencil); _depthStencil = 0; @@ -150,7 +266,8 @@ void OffscreenQmlSurface::cleanup() { _fbo = 0; } - _textures.clear(); + offscreenTextures.releaseSize(_size); + _canvas->doneCurrent(); } @@ -164,26 +281,7 @@ void OffscreenQmlSurface::render() { _renderControl->sync(); _quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y)); - // Clear out any pending textures to be returned - { - std::list returnedTextures; - { - std::unique_lock lock(_textureMutex); - returnedTextures.swap(_returnedTextures); - } - if (!returnedTextures.empty()) { - for (const auto& textureAndFence : returnedTextures) { - GLsync fence = static_cast(textureAndFence.second); - if (fence) { - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - _textures.recycleTexture(textureAndFence.first); - } - } - } - - GLuint texture = _textures.getNextTexture(); + GLuint texture = offscreenTextures.getNextTexture(_size); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); PROFILE_RANGE("qml_render->rendercontrol") @@ -193,12 +291,11 @@ void OffscreenQmlSurface::render() { glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); + { - std::unique_lock lock(_textureMutex); // If the most recent texture was unused, we can directly recycle it if (_latestTextureAndFence.first) { - _textures.recycleTexture(_latestTextureAndFence.first); - glDeleteSync(static_cast(_latestTextureAndFence.second)); + offscreenTextures.releaseTexture(_latestTextureAndFence); _latestTextureAndFence = { 0, 0 }; } @@ -215,7 +312,6 @@ void OffscreenQmlSurface::render() { bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { textureAndFence = { 0, 0 }; - std::unique_lock lock(_textureMutex); if (0 == _latestTextureAndFence.first) { return false; } @@ -226,20 +322,18 @@ bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { return true; } -void OffscreenQmlSurface::releaseTexture(const TextureAndFence& textureAndFence) { - std::unique_lock lock(_textureMutex); - _returnedTextures.push_back(textureAndFence); +std::function OffscreenQmlSurface::getDiscardLambda() { + return [](uint32_t texture, void* fence) { + offscreenTextures.releaseTexture({ texture, static_cast(fence) }); + }; } bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) { // If we already have a pending texture, don't render another one // i.e. don't render faster than the consumer context, since it wastes // GPU cycles on producing output that will never be seen - { - std::unique_lock lock(_textureMutex); - if (0 != _latestTextureAndFence.first) { - return false; - } + if (0 != _latestTextureAndFence.first) { + return false; } auto minRenderInterval = USECS_PER_SECOND / fps; @@ -250,12 +344,10 @@ 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); - cleanup(); _canvas->deleteLater(); @@ -309,13 +401,14 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { } // FIXME + _glData = ::getGLContextData(); // Initialize JSON structure so that it can be filled in later and then used in QML. _qmlEngine->rootContext()->setContextProperty("GL", _glData); _qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); _qmlComponent = new QQmlComponent(_qmlEngine); - connect(_renderControl, &QQuickRenderControl::renderRequested, [this] { _render = true; }); - connect(_renderControl, &QQuickRenderControl::sceneChanged, [this] { _render = _polish = true; }); + connect(_renderControl, &QQuickRenderControl::renderRequested, this, [this] { _render = true; }); + connect(_renderControl, &QQuickRenderControl::sceneChanged, this, [this] { _render = _polish = true; }); if (!_canvas->makeCurrent()) { qWarning("Failed to make context current for QML Renderer"); @@ -323,7 +416,6 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { } _glData = ::getGLContextData(); _renderControl->initialize(_canvas->getContext()); - setupFbo(); // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. @@ -337,26 +429,23 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); } +static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { + return glm::clamp(size, glm::uvec2(1), glm::uvec2(maxDimension)); +} + +static QSize clampSize(const QSize& qsize, uint32_t maxDimension) { + return fromGlm(clampSize(toGlm(qsize), maxDimension)); +} + void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { if (!_quickWindow) { return; } - const float MAX_OFFSCREEN_DIMENSION = 4096; - QSize newSize = newSize_; - - if (newSize.width() > MAX_OFFSCREEN_DIMENSION || newSize.height() > MAX_OFFSCREEN_DIMENSION) { - float scale = std::min( - ((float)newSize.width() / MAX_OFFSCREEN_DIMENSION), - ((float)newSize.height() / MAX_OFFSCREEN_DIMENSION)); - newSize = QSize( - std::max(static_cast(scale * newSize.width()), 10), - std::max(static_cast(scale * newSize.height()), 10)); - } - - QSize currentSize = _quickWindow->geometry().size(); - if (newSize == currentSize && !forceResize) { + const uint32_t MAX_OFFSCREEN_DIMENSION = 4096; + const QSize newSize = clampSize(newSize_, MAX_OFFSCREEN_DIMENSION); + if (!forceResize && newSize == _quickWindow->geometry().size()) { return; } @@ -372,20 +461,46 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { // Qt bug in 5.4 forces this check of pixel ratio, // even though we're rendering offscreen. - qreal pixelRatio = 1.0; - if (_renderControl && _renderControl->_renderWindow) { - pixelRatio = _renderControl->_renderWindow->devicePixelRatio(); - } - - uvec2 newOffscreenSize = toGlm(newSize * pixelRatio); + uvec2 newOffscreenSize = toGlm(newSize); if (newOffscreenSize == _size) { return; } - qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; + qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height(); + + _canvas->makeCurrent(); + + // Release hold on the textures of the old size + if (uvec2() != _size) { + // If the most recent texture was unused, we can directly recycle it + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = { 0, 0 }; + } + offscreenTextures.releaseSize(_size); + } + _size = newOffscreenSize; - _textures.setSize(_size); - setupFbo(); + + // Acquire the new texture size + if (uvec2() != _size) { + offscreenTextures.acquireSize(_size); + if (_depthStencil) { + glDeleteRenderbuffers(1, &_depthStencil); + _depthStencil = 0; + } + glGenRenderbuffers(1, &_depthStencil); + glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y); + if (!_fbo) { + glGenFramebuffers(1, &_fbo); + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + _canvas->doneCurrent(); } QQuickItem* OffscreenQmlSurface::getRootItem() { @@ -437,7 +552,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionisError()) { QList errorList = _qmlComponent->errors(); foreach(const QQmlError& error, errorList) - qWarning() << error.url() << error.line() << error; + qCWarning(glLogging) << error.url() << error.line() << error; if (!_rootItem) { qFatal("Unable to finish loading QML root"); } @@ -490,6 +605,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionmetaObject()->className()).left(7) == "SpinBox"; if (item->property("keyboardRaised").isValid()) { + // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. if (item->property("punctuationMode").isValid()) { item->setProperty("punctuationMode", QVariant(numeric)); } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 639213868c..f6168e7b6d 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -10,16 +10,16 @@ #define hifi_OffscreenQmlSurface_h #include +#include +#include #include #include -#include -#include - +#include +#include #include #include -#include "TextureRecycler.h" class QWindow; class QMyQuickRenderControl; @@ -30,6 +30,11 @@ class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; + +// GPU resources are typically buffered for one copy being used by the renderer, +// one copy in flight, and one copy being used by the receiver +#define GPU_RESOURCE_BUFFER_SIZE 3 + class OffscreenQmlSurface : public QObject { Q_OBJECT Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) @@ -82,9 +87,9 @@ public: // when the texture is safe to read. // Returns false if no new texture is available bool fetchTexture(TextureAndFence& textureAndFence); - // Release a previously acquired texture, along with a fence which indicates when reads from the - // texture have completed. - void releaseTexture(const TextureAndFence& textureAndFence); + + static std::function getDiscardLambda(); + static size_t getUsedTextureMemory(); signals: void focusObjectChanged(QObject* newFocus); @@ -133,14 +138,10 @@ private: uint32_t _fbo { 0 }; uint32_t _depthStencil { 0 }; uint64_t _lastRenderTime { 0 }; - uvec2 _size { 1920, 1080 }; - TextureRecycler _textures { true }; + uvec2 _size; // Texture management - std::mutex _textureMutex; TextureAndFence _latestTextureAndFence { 0, 0 }; - std::list _returnedTextures; - bool _render { false }; bool _polish { true }; diff --git a/libraries/gl/src/gl/TextureRecycler.cpp b/libraries/gl/src/gl/TextureRecycler.cpp deleted file mode 100644 index 4438e158c5..0000000000 --- a/libraries/gl/src/gl/TextureRecycler.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016-10-05 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "TextureRecycler.h" -#include "Config.h" - -#include - - -void TextureRecycler::setSize(const uvec2& size) { - if (size == _size) { - return; - } - _size = size; - while (!_readyTextures.empty()) { - _readyTextures.pop(); - } - std::set toDelete; - std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) { - if (!item.second._active && item.second._size != _size) { - toDelete.insert(item.first); - } - }); - std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) { - _allTextures.erase(key); - }); -} - -void TextureRecycler::clear() { - while (!_readyTextures.empty()) { - _readyTextures.pop(); - } - _allTextures.clear(); -} - -uint32_t TextureRecycler::getNextTexture() { - if (_readyTextures.empty()) { - uint32_t newTexture; - glGenTextures(1, &newTexture); - glBindTexture(GL_TEXTURE_2D, newTexture); - if (_useMipmaps) { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - } else { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - } - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); - _allTextures.emplace(std::piecewise_construct, std::forward_as_tuple(newTexture), std::forward_as_tuple(newTexture, _size)); - _readyTextures.push(newTexture); - } - - uint32_t result = _readyTextures.front(); - _readyTextures.pop(); - auto& item = _allTextures[result]; - item._active = true; - return result; -} - -void TextureRecycler::recycleTexture(GLuint texture) { - Q_ASSERT(_allTextures.count(texture)); - auto& item = _allTextures[texture]; - Q_ASSERT(item._active); - item._active = false; - if (item._size != _size) { - // Buh-bye - _allTextures.erase(texture); - return; - } - - _readyTextures.push(item._tex); -} - diff --git a/libraries/gl/src/gl/TextureRecycler.h b/libraries/gl/src/gl/TextureRecycler.h deleted file mode 100644 index 46cbcad219..0000000000 --- a/libraries/gl/src/gl/TextureRecycler.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015-04-04 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#pragma once -#ifndef hifi_TextureRecycler_h -#define hifi_TextureRecycler_h - -#include -#include -#include - -#include - -class TextureRecycler { -public: - TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {} - void setSize(const uvec2& size); - void clear(); - uint32_t getNextTexture(); - void recycleTexture(uint32_t texture); - -private: - - struct TexInfo { - const uint32_t _tex{ 0 }; - const uvec2 _size; - bool _active { false }; - - TexInfo() {} - TexInfo(uint32_t tex, const uvec2& size) : _tex(tex), _size(size) {} - TexInfo(const TexInfo& other) : _tex(other._tex), _size(other._size) {} - }; - - using Map = std::map; - using Queue = std::queue; - - Map _allTextures; - Queue _readyTextures; - uvec2 _size{ 1920, 1080 }; - bool _useMipmaps; -}; - -#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 3c34765011..2e1084e581 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -663,9 +663,19 @@ void GLBackend::recycle() const { Lock lock(_trashMutex); std::swap(_externalTexturesTrash, externalTexturesTrash); } - for (auto pair : externalTexturesTrash) { - auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - pair.second(pair.first, fence); + if (!externalTexturesTrash.empty()) { + std::vector fences; + fences.resize(externalTexturesTrash.size()); + for (size_t i = 0; i < externalTexturesTrash.size(); ++i) { + fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + // External texture fences will be read in another thread/context, so we need a flush + glFlush(); + size_t index = 0; + for (auto pair : externalTexturesTrash) { + auto fence = fences[index++]; + pair.second(pair.first, fence); + } } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index eaca31ebdc..0ff00697ca 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -13,7 +13,7 @@ using namespace gpu; using namespace gpu::gl; GLFramebuffer::~GLFramebuffer() { - if (_id) { + if (_id) { auto backend = _backend.lock(); if (backend) { backend->releaseFramebuffer(_id); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1caecb0b72..61a76c2d0b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -18,22 +18,6 @@ using namespace gpu::gl; std::shared_ptr GLTexture::_textureTransferHelper; -// FIXME placeholder for texture memory over-use -#define DEFAULT_MAX_MEMORY_MB 256 -#define OVER_MEMORY_PRESSURE 2.0f - -// FIXME other apps show things like Oculus home consuming large amounts of GPU memory -// which causes us to blur textures needlessly (since other app GPU memory usage will likely -// be swapped out and not cause any actual impact -//#define CHECK_MIN_FREE_GPU_MEMORY -#ifdef CHECK_MIN_FREE_GPU_MEMORY -#define MIN_FREE_GPU_MEMORY_PERCENTAGE 0.25f -#endif - -// Allow 65% of all available GPU memory to be consumed by textures -// FIXME overly conservative? -#define MAX_CONSUMED_TEXTURE_MEMORY_PERCENTAGE 0.65f - const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, @@ -105,41 +89,34 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } +// Default texture memory = GPU total memory - 2GB +#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) +// Minimum texture memory = 1GB +#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) + float GLTexture::getMemoryPressure() { // Check for an explicit memory limit auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); + // If no memory limit has been set, use a percentage of the total dedicated memory if (!availableTextureMemory) { - auto totalGpuMemory = getDedicatedMemory(); - - if (!totalGpuMemory) { - // If we can't query the dedicated memory just use a fallback fixed value of 256 MB - totalGpuMemory = MB_TO_BYTES(DEFAULT_MAX_MEMORY_MB); +#if 0 + auto totalMemory = getDedicatedMemory(); + if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; } else { -#ifdef CHECK_MIN_FREE_GPU_MEMORY - // Check the global free GPU memory - auto freeGpuMemory = getFreeDedicatedMemory(); - if (freeGpuMemory) { - static gpu::Size lastFreeGpuMemory = 0; - auto freePercentage = (float)freeGpuMemory / (float)totalGpuMemory; - if (freeGpuMemory != lastFreeGpuMemory) { - lastFreeGpuMemory = freeGpuMemory; - if (freePercentage < MIN_FREE_GPU_MEMORY_PERCENTAGE) { - qCDebug(gpugllogging) << "Exceeded min free GPU memory " << freePercentage; - return OVER_MEMORY_PRESSURE; - } - } - } -#endif + availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; } - - availableTextureMemory = static_cast(totalGpuMemory * MAX_CONSUMED_TEXTURE_MEMORY_PERCENTAGE); +#else + // Hardcode texture limit for sparse textures at 1 GB for now + availableTextureMemory = GPU_MEMORY_RESERVE_BYTES; +#endif } // Return the consumed texture memory divided by the available texture memory. - auto consumedGpuMemory = Context::getTextureGPUMemoryUsage(); + auto consumedGpuMemory = Context::getTextureGPUSparseMemoryUsage(); float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; static Context::Size lastConsumedGpuMemory = 0; if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { @@ -205,8 +182,14 @@ GLTexture::~GLTexture() { qWarning() << "No recycler available for texture " << _id << " possible leak"; } } else if (_id) { + // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for + // the GL45Texture destructor for doing any required work tracking GPU stats backend->releaseTexture(_id, _size); } + + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); + } } Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); } @@ -243,6 +226,9 @@ void GLTexture::withPreservedTexture(std::function f) const { } void GLTexture::setSize(GLuint size) const { + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); + } Backend::updateTextureGPUMemoryUsage(_size, size); const_cast(_size) = size; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 9091a63086..0f75a6fe51 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -30,7 +30,7 @@ public: static std::shared_ptr _textureTransferHelper; template - static GLTextureType* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { + static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { const Texture& texture = *texturePointer; // Special case external textures @@ -74,7 +74,7 @@ public: } // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTextureType* object = Backend::getGPUObject(texture); + GLTexture* object = Backend::getGPUObject(texture); // Create the texture if need be (force re-creation if the storage stamp changes // for easier use of immutable storage) @@ -84,6 +84,7 @@ public: if (!object->_transferrable) { object->createTexture(); object->_contentStamp = texture.getDataStamp(); + object->updateSize(); object->postTransfer(); } } @@ -119,7 +120,7 @@ public: if (!texture) { return 0; } - GLTextureType* object { nullptr }; + GLTexture* object { nullptr }; if (shouldSync) { object = sync(backend, texture, shouldSync); } else { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 6c4aacc993..033f6298ee 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -252,6 +252,9 @@ GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& if (_transferrable && Texture::getEnableSparseTextures()) { _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } } } @@ -261,6 +264,7 @@ GL45Texture::~GL45Texture() { qCDebug(gpugl45logging) << "Destroying texture " << _id << " from source " << _source.c_str(); } if (_sparseInfo.sparse) { + Backend::decrementTextureGPUSparseCount(); // Remove this texture from the candidate list of derezzable textures { auto mipLevels = usedMipLevels(); @@ -300,6 +304,7 @@ GL45Texture::~GL45Texture() { } auto size = _size; + const_cast(_size) = 0; _textureTransferHelper->queueExecution([id, size, destructionFunctions] { for (auto function : destructionFunctions) { function(); @@ -307,6 +312,7 @@ GL45Texture::~GL45Texture() { glDeleteTextures(1, &id); Backend::decrementTextureGPUCount(); Backend::updateTextureGPUMemoryUsage(size, 0); + Backend::updateTextureGPUSparseMemoryUsage(size, 0); }); } } @@ -316,7 +322,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(); } @@ -338,7 +346,9 @@ void GL45Texture::updateSize() const { qFatal("Compressed textures not yet supported"); } - if (_transferrable) { + if (_transferrable && _sparseInfo.sparse) { + auto size = _allocatedPages * _sparseInfo.pageBytes; + Backend::updateTextureGPUSparseMemoryUsage(_size, size); setSize(_allocatedPages * _sparseInfo.pageBytes); } else { setSize(_virtualSize); diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index af3552911c..ab6bba178a 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -162,14 +162,26 @@ Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const S } // Counters for Buffer and Texture usage in GPU/Context +std::atomic Context::_freeGPUMemory { 0 }; std::atomic Context::_fenceCount { 0 }; std::atomic Context::_bufferGPUCount { 0 }; std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; -std::atomic Context::_textureGPUMemoryUsage{ 0 }; -std::atomic Context::_textureGPUVirtualMemoryUsage{ 0 }; -std::atomic Context::_textureGPUTransferCount{ 0 }; +std::atomic Context::_textureGPUSparseCount { 0 }; +std::atomic Context::_textureGPUMemoryUsage { 0 }; +std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; +std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; +std::atomic Context::_textureGPUSparseMemoryUsage { 0 }; +std::atomic Context::_textureGPUTransferCount { 0 }; + +void Context::setFreeGPUMemory(Size size) { + _freeGPUMemory.store(size); +} + +Size Context::getFreeGPUMemory() { + return _freeGPUMemory.load(); +} void Context::incrementBufferGPUCount() { static std::atomic max { 0 }; @@ -217,6 +229,18 @@ void Context::decrementTextureGPUCount() { --_textureGPUCount; } +void Context::incrementTextureGPUSparseCount() { + static std::atomic max { 0 }; + auto total = ++_textureGPUSparseCount; + if (total > max.load()) { + max = total; + qCDebug(gpulogging) << "New max GPU textures " << total; + } +} +void Context::decrementTextureGPUSparseCount() { + --_textureGPUSparseCount; +} + void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -239,6 +263,28 @@ void Context::updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newOb } } +void Context::updateTextureGPUFramebufferMemoryUsage(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (newObjectSize > prevObjectSize) { + _textureGPUFramebufferMemoryUsage.fetch_add(newObjectSize - prevObjectSize); + } else { + _textureGPUFramebufferMemoryUsage.fetch_sub(prevObjectSize - newObjectSize); + } +} + +void Context::updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (newObjectSize > prevObjectSize) { + _textureGPUSparseMemoryUsage.fetch_add(newObjectSize - prevObjectSize); + } else { + _textureGPUSparseMemoryUsage.fetch_sub(prevObjectSize - newObjectSize); + } +} + void Context::incrementTextureGPUTransferCount() { static std::atomic max { 0 }; auto total = ++_textureGPUTransferCount; @@ -247,6 +293,7 @@ void Context::incrementTextureGPUTransferCount() { qCDebug(gpulogging) << "New max GPU textures transfers" << total; } } + void Context::decrementTextureGPUTransferCount() { --_textureGPUTransferCount; } @@ -263,6 +310,10 @@ uint32_t Context::getTextureGPUCount() { return _textureGPUCount.load(); } +uint32_t Context::getTextureGPUSparseCount() { + return _textureGPUSparseCount.load(); +} + Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -271,16 +322,30 @@ Context::Size Context::getTextureGPUVirtualMemoryUsage() { return _textureGPUVirtualMemoryUsage.load(); } +Context::Size Context::getTextureGPUFramebufferMemoryUsage() { + return _textureGPUFramebufferMemoryUsage.load(); +} + +Context::Size Context::getTextureGPUSparseMemoryUsage() { + return _textureGPUSparseMemoryUsage.load(); +} + uint32_t Context::getTextureGPUTransferCount() { return _textureGPUTransferCount.load(); } +void Backend::setFreeGPUMemory(Size size) { Context::setFreeGPUMemory(size); } +Resource::Size Backend::getFreeGPUMemory() { return Context::getFreeGPUMemory(); } void Backend::incrementBufferGPUCount() { Context::incrementBufferGPUCount(); } void Backend::decrementBufferGPUCount() { Context::decrementBufferGPUCount(); } void Backend::updateBufferGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateBufferGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); } void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } +void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } +void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } +void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } +void Backend::updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUSparseMemoryUsage(prevObjectSize, newObjectSize); } void Backend::incrementTextureGPUTransferCount() { Context::incrementTextureGPUTransferCount(); } void Backend::decrementTextureGPUTransferCount() { Context::decrementTextureGPUTransferCount(); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 4c701e170d..763e91b3e4 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -89,13 +89,19 @@ public: // These should only be accessed by Backend implementation to repport the buffer and texture allocations, // they are NOT public calls + static Resource::Size getFreeGPUMemory(); + static void setFreeGPUMemory(Resource::Size prevObjectSize); static void incrementBufferGPUCount(); static void decrementBufferGPUCount(); static void updateBufferGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void incrementTextureGPUCount(); static void decrementTextureGPUCount(); + static void incrementTextureGPUSparseCount(); + static void decrementTextureGPUSparseCount(); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); + static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); + static void updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void incrementTextureGPUTransferCount(); static void decrementTextureGPUTransferCount(); @@ -201,8 +207,12 @@ public: static Size getBufferGPUMemoryUsage(); static uint32_t getTextureGPUCount(); + static uint32_t getTextureGPUSparseCount(); + static Size getFreeGPUMemory(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); + static Size getTextureGPUFramebufferMemoryUsage(); + static Size getTextureGPUSparseMemoryUsage(); static uint32_t getTextureGPUTransferCount(); protected: @@ -233,22 +243,31 @@ protected: static void incrementFenceCount(); static void decrementFenceCount(); + static void setFreeGPUMemory(Size size); static void incrementTextureGPUCount(); static void decrementTextureGPUCount(); + static void incrementTextureGPUSparseCount(); + static void decrementTextureGPUSparseCount(); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); + static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); + static void updateTextureGPUFramebufferMemoryUsage(Size prevObjectSize, Size newObjectSize); static void incrementTextureGPUTransferCount(); static void decrementTextureGPUTransferCount(); // Buffer, Texture and Fence Counters + static std::atomic _freeGPUMemory; static std::atomic _fenceCount; static std::atomic _bufferGPUCount; static std::atomic _bufferGPUMemoryUsage; static std::atomic _textureGPUCount; + static std::atomic _textureGPUSparseCount; static std::atomic _textureGPUMemoryUsage; + static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; + static std::atomic _textureGPUFramebufferMemoryUsage; static std::atomic _textureGPUTransferCount; diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index b3e2d6f8a8..f28c18eb11 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -115,6 +115,7 @@ namespace gpu { GPUObject* getGPUObject() const { return _gpuObject.get(); } friend class Backend; + friend class Texture; }; namespace gl { diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 2cd39d49ae..e8ccfce3b2 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -20,16 +20,17 @@ using namespace gpu; Framebuffer::~Framebuffer() { } -Framebuffer* Framebuffer::create() { +Framebuffer* Framebuffer::create(const std::string& name) { auto framebuffer = new Framebuffer(); + framebuffer->setName(name); framebuffer->_renderBuffers.resize(MAX_NUM_RENDER_BUFFERS); framebuffer->_colorStamps.resize(MAX_NUM_RENDER_BUFFERS, 0); return framebuffer; } -Framebuffer* Framebuffer::create( const Format& colorBufferFormat, uint16 width, uint16 height) { - auto framebuffer = Framebuffer::create(); +Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { + auto framebuffer = Framebuffer::create(name); auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); @@ -39,13 +40,11 @@ Framebuffer* Framebuffer::create( const Format& colorBufferFormat, uint16 width, return framebuffer; } -Framebuffer* Framebuffer::create( const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { - auto framebuffer = Framebuffer::create(); +Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { + auto framebuffer = Framebuffer::create(name); auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - colorTexture->setSource("Framebuffer::colorTexture"); auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - depthTexture->setSource("Framebuffer::depthTexture"); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -53,11 +52,10 @@ Framebuffer* Framebuffer::create( const Format& colorBufferFormat, const Format& } Framebuffer* Framebuffer::createShadowmap(uint16 width) { - auto framebuffer = Framebuffer::create(); + auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); - depthTexture->setSource("Framebuffer::shadowMap"); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -155,6 +153,10 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin if (!validateTargetCompatibility(*texture, subresource)) { return -1; } + + if (texture->source().empty()) { + texture->setSource(_name + "::color::" + std::to_string(slot)); + } } ++_colorStamps[slot]; @@ -216,7 +218,6 @@ uint32 Framebuffer::getRenderBufferSubresource(uint32 slot) const { } bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const Format& format, uint32 subresource) { - ++_depthStamp; if (isSwapchain()) { return false; } @@ -226,8 +227,13 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For if (!validateTargetCompatibility(*texture)) { return false; } + + if (texture->source().empty()) { + texture->setSource(_name + "::depthStencil"); + } } + ++_depthStamp; updateSize(texture); // assign the new one diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index 8c26037ada..a65aaf765b 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -88,9 +88,9 @@ public: ~Framebuffer(); static Framebuffer* create(const SwapchainPointer& swapchain); - static Framebuffer* create(); - static Framebuffer* create(const Format& colorBufferFormat, uint16 width, uint16 height); - static Framebuffer* create(const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height); + static Framebuffer* create(const std::string& name); + static Framebuffer* create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height); + static Framebuffer* create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height); static Framebuffer* createShadowmap(uint16 width); bool isSwapchain() const; @@ -127,6 +127,8 @@ public: uint16 getWidth() const; uint16 getHeight() const; uint16 getNumSamples() const; + const std::string& getName() const { return _name; } + void setName(const std::string& name) { _name = name; } float getAspectRatio() const { return getWidth() / (float) getHeight() ; } @@ -145,6 +147,7 @@ public: static Transform evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport); protected: + std::string _name; SwapchainPointer _swapchain; Stamp _depthStamp { 0 }; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 924f5145b9..3283f5a4d9 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -70,6 +70,14 @@ void Texture::updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSiz } } +bool Texture::getEnableSparseTextures() { + return _enableSparseTextures.load(); +} + +bool Texture::getEnableIncrementalTextureTransfers() { + return _enableIncrementalTextureTransfers.load(); +} + uint32_t Texture::getTextureCPUCount() { return _textureCPUCount.load(); } @@ -82,6 +90,10 @@ uint32_t Texture::getTextureGPUCount() { return Context::getTextureGPUCount(); } +uint32_t Texture::getTextureGPUSparseCount() { + return Context::getTextureGPUSparseCount(); +} + Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -90,6 +102,15 @@ Texture::Size Texture::getTextureGPUVirtualMemoryUsage() { return Context::getTextureGPUVirtualMemoryUsage(); } + +Texture::Size Texture::getTextureGPUFramebufferMemoryUsage() { + return Context::getTextureGPUFramebufferMemoryUsage(); +} + +Texture::Size Texture::getTextureGPUSparseMemoryUsage() { + return Context::getTextureGPUSparseMemoryUsage(); +} + uint32_t Texture::getTextureGPUTransferCount() { return Context::getTextureGPUTransferCount(); } @@ -287,6 +308,22 @@ Texture::Texture(): Texture::~Texture() { _textureCPUCount--; + if (getUsage().isExternal()) { + Texture::ExternalUpdates externalUpdates; + { + Lock lock(_externalMutex); + _externalUpdates.swap(externalUpdates); + } + for (const auto& update : externalUpdates) { + assert(_externalRecycler); + _externalRecycler(update.first, update.second); + } + // Force the GL object to be destroyed here + // If we let the normal destructor do it, then it will be + // cleared after the _externalRecycler has been destroyed, + // resulting in leaked texture memory + gpuObject.setGPUObject(nullptr); + } } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { @@ -935,8 +972,20 @@ Vec3u Texture::evalMipDimensions(uint16 level) const { return glm::max(dimensions, Vec3u(1)); } +void Texture::setExternalRecycler(const ExternalRecycler& recycler) { + Lock lock(_externalMutex); + _externalRecycler = recycler; +} + +Texture::ExternalRecycler Texture::getExternalRecycler() const { + Lock lock(_externalMutex); + Texture::ExternalRecycler result = _externalRecycler; + return result; +} + void Texture::setExternalTexture(uint32 externalId, void* externalFence) { Lock lock(_externalMutex); + assert(_externalRecycler); _externalUpdates.push_back({ externalId, externalFence }); } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 9c3e88c67a..0f1022a0c9 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -151,14 +151,17 @@ public: static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); + static uint32_t getTextureGPUSparseCount(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); + static Size getTextureGPUFramebufferMemoryUsage(); + static Size getTextureGPUSparseMemoryUsage(); static uint32_t getTextureGPUTransferCount(); static Size getAllowedGPUMemoryUsage(); static void setAllowedGPUMemoryUsage(Size size); - static bool getEnableSparseTextures() { return _enableSparseTextures.load(); } - static bool getEnableIncrementalTextureTransfers() { return _enableIncrementalTextureTransfers.load(); } + static bool getEnableSparseTextures(); + static bool getEnableIncrementalTextureTransfers(); static void setEnableSparseTextures(bool enabled); static void setEnableIncrementalTextureTransfers(bool enabled); @@ -466,8 +469,8 @@ public: void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } void setExternalTexture(uint32 externalId, void* externalFence); - void setExternalRecycler(const ExternalRecycler& recycler) { _externalRecycler = recycler; } - ExternalRecycler getExternalRecycler() const { return _externalRecycler; } + void setExternalRecycler(const ExternalRecycler& recycler); + ExternalRecycler getExternalRecycler() const; const GPUObjectPointer gpuObject {}; diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 30f176b5a6..3e346a564e 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -21,12 +21,59 @@ using namespace gpu; // FIXME: Declare this to enable compression //#define COMPRESS_TEXTURES - +static const uvec2 SPARSE_PAGE_SIZE(128); +static const uvec2 MAX_TEXTURE_SIZE(4096); bool DEV_DECIMATE_TEXTURES = false; -QImage processSourceImage(const QImage& srcImage) { - if (DEV_DECIMATE_TEXTURES) { - return srcImage.scaled(srcImage.size() * 0.5f); + +bool needsSparseRectification(const uvec2& size) { + // Don't attempt to rectify small textures (textures less than the sparse page size in any dimension) + if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) { + return false; } + + // Don't rectify textures that are already an exact multiple of sparse page size + if (uvec2(0) == (size % SPARSE_PAGE_SIZE)) { + return false; + } + + // Texture is not sparse compatible, but is bigger than the sparse page size in both dimensions, rectify! + return true; +} + +uvec2 rectifyToSparseSize(const uvec2& size) { + uvec2 pages = ((size / SPARSE_PAGE_SIZE) + glm::clamp(size % SPARSE_PAGE_SIZE, uvec2(0), uvec2(1))); + uvec2 result = pages * SPARSE_PAGE_SIZE; + return result; +} + +std::atomic DECIMATED_TEXTURE_COUNT { 0 }; +std::atomic RECTIFIED_TEXTURE_COUNT { 0 }; + +QImage processSourceImage(const QImage& srcImage, bool cubemap) { + const uvec2 srcImageSize = toGlm(srcImage.size()); + uvec2 targetSize = srcImageSize; + + while (glm::any(glm::greaterThan(targetSize, MAX_TEXTURE_SIZE))) { + targetSize /= 2; + } + if (targetSize != srcImageSize) { + ++DECIMATED_TEXTURE_COUNT; + } + + if (!cubemap && needsSparseRectification(targetSize)) { + ++RECTIFIED_TEXTURE_COUNT; + targetSize = rectifyToSparseSize(targetSize); + } + + if (DEV_DECIMATE_TEXTURES && glm::all(glm::greaterThanEqual(targetSize / SPARSE_PAGE_SIZE, uvec2(2)))) { + targetSize /= 2; + } + + if (targetSize != srcImageSize) { + qDebug() << "Resizing texture from " << srcImageSize.x << "x" << srcImageSize.y << " to " << targetSize.x << "x" << targetSize.y; + return srcImage.scaled(fromGlm(targetSize)); + } + return srcImage; } @@ -60,7 +107,7 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { } const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); validAlpha = false; alphaAsMask = true; const uint8 OPAQUE_ALPHA = 255; @@ -228,7 +275,7 @@ gpu::Texture* TextureUsage::createLightmapTextureFromImage(const QImage& srcImag gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); @@ -262,7 +309,7 @@ double mapComponent(double sobelValue) { } gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); @@ -334,7 +381,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm } gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); if (!image.hasAlphaChannel()) { if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); @@ -368,7 +415,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma } gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); if (!image.hasAlphaChannel()) { if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); @@ -406,7 +453,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s } gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, false); if (!image.hasAlphaChannel()) { if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); @@ -699,7 +746,7 @@ const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance) { gpu::Texture* theTexture = nullptr; if ((srcImage.width() > 0) && (srcImage.height() > 0)) { - QImage image = processSourceImage(srcImage); + QImage image = processSourceImage(srcImage, true); if (image.format() != QImage::Format_RGB888) { image = image.convertToFormat(QImage::Format_RGB888); } @@ -709,7 +756,8 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - int foundLayout = CubeLayout::findLayout(image.width(), image.height()); + // Use the original image size since processSourceImage may have altered the size / aspect ratio + int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; // If found, go extract the faces as separate images diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index f7f305dcc8..4089fd4966 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,11 @@ #include "UserActivityLogger.h" #include "udt/PacketHeaders.h" +#if USE_STABLE_GLOBAL_SERVICES +const QString DEFAULT_HIFI_ADDRESS = "hifi://welcome"; +#else +const QString DEFAULT_HIFI_ADDRESS = "hifi://dev-welcome"; +#endif const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; const QString SETTINGS_CURRENT_ADDRESS_KEY = "address"; @@ -47,7 +53,7 @@ bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } -QUrl AddressManager::currentAddress() const { +QUrl AddressManager::currentAddress(bool domainOnly) const { QUrl hifiURL; hifiURL.setScheme(HIFI_URL_SCHEME); @@ -57,7 +63,9 @@ QUrl AddressManager::currentAddress() const { hifiURL.setPort(_port); } - hifiURL.setPath(currentPath()); + if (!domainOnly) { + hifiURL.setPath(currentPath()); + } return hifiURL; } @@ -69,8 +77,7 @@ QUrl AddressManager::currentFacingAddress() const { return hifiURL; } - -QUrl AddressManager::currentShareableAddress() const { +QUrl AddressManager::currentShareableAddress(bool domainOnly) const { if (!_shareablePlaceName.isEmpty()) { // if we have a shareable place name use that instead of whatever the current host is QUrl hifiURL; @@ -78,11 +85,13 @@ QUrl AddressManager::currentShareableAddress() const { hifiURL.setScheme(HIFI_URL_SCHEME); hifiURL.setHost(_shareablePlaceName); - hifiURL.setPath(currentPath()); + if (!domainOnly) { + hifiURL.setPath(currentPath()); + } return hifiURL; } else { - return currentAddress(); + return currentAddress(domainOnly); } } @@ -415,7 +424,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const // check if we had a path to override the path returned QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); - if (!overridePath.isEmpty()) { + if (!overridePath.isEmpty() && overridePath != "/") { // make sure we don't re-handle an overriden path if this was a refresh of info from API if (trigger != LookupTrigger::AttemptedRefresh) { handlePath(overridePath, trigger); @@ -831,32 +840,3 @@ void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { } } -void AddressManager::ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, - std::function localSandboxNotRunningDoThat) { - - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL); - sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(sandboxStatus); - - connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() { - auto statusData = reply->readAll(); - auto statusJson = QJsonDocument::fromJson(statusData); - if (!statusJson.isEmpty()) { - auto statusObject = statusJson.object(); - auto serversValue = statusObject.value("servers"); - if (!serversValue.isUndefined() && serversValue.isObject()) { - auto serversObject = serversValue.toObject(); - auto serversCount = serversObject.size(); - const int MINIMUM_EXPECTED_SERVER_COUNT = 5; - if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) { - localSandboxRunningDoThis(); - return; - } - } - } - localSandboxNotRunningDoThat(); - }); -} - diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index f753bb0548..f979002f7a 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -23,9 +23,10 @@ #include "AccountManager.h" const QString HIFI_URL_SCHEME = "hifi"; -const QString DEFAULT_HIFI_ADDRESS = "hifi://dev-welcome"; + +extern const QString DEFAULT_HIFI_ADDRESS; + const QString SANDBOX_HIFI_ADDRESS = "hifi://localhost"; -const QString SANDBOX_STATUS_URL = "http://localhost:60332/status"; const QString INDEX_PATH = "/"; const QString GET_PLACE = "/api/v1/places/%1"; @@ -58,9 +59,9 @@ public: bool isConnected(); const QString& getProtocol() { return HIFI_URL_SCHEME; }; - QUrl currentAddress() const; + QUrl currentAddress(bool domainOnly = false) const; QUrl currentFacingAddress() const; - QUrl currentShareableAddress() const; + QUrl currentShareableAddress(bool domainOnly = false) const; QUrl currentFacingShareableAddress() const; QString currentPath(bool withOrientation = true) const; QString currentFacingPath() const; @@ -78,11 +79,6 @@ public: const QStack& getBackStack() const { return _backStack; } const QStack& getForwardStack() const { return _forwardStack; } - /// determines if the local sandbox is likely running. It does not account for custom setups, and is only - /// intended to detect the standard local sandbox install. - void ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, - std::function localSandboxNotRunningDoThat); - public slots: void handleLookupString(const QString& lookupString, bool fromSuggestions = false); diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 6a2cbf20cf..0563141511 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -45,6 +45,8 @@ AssetClient::AssetClient() { packetReceiver.registerListener(PacketType::AssetUploadReply, this, "handleAssetUploadReply"); connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &AssetClient::handleNodeKilled); + connect(nodeList.data(), &LimitedNodeList::clientConnectionToNodeReset, + this, &AssetClient::handleNodeClientConnectionReset); } void AssetClient::init() { @@ -233,15 +235,15 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse packet->writePrimitive(start); packet->writePrimitive(end); - nodeList->sendPacket(std::move(packet), *assetServer); + if (nodeList->sendPacket(std::move(packet), *assetServer) != -1) { + _pendingRequests[assetServer][messageID] = { QSharedPointer(), callback, progressCallback }; - _pendingRequests[assetServer][messageID] = { QSharedPointer(), callback, progressCallback }; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QByteArray()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QByteArray()); + return INVALID_MESSAGE_ID; } MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callback) { @@ -259,15 +261,15 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac packet->writePrimitive(messageID); packet->write(QByteArray::fromHex(hash.toLatin1())); - nodeList->sendPacket(std::move(packet), *assetServer); + if (nodeList->sendPacket(std::move(packet), *assetServer) != -1) { + _pendingInfoRequests[assetServer][messageID] = callback; - _pendingInfoRequests[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, { "", 0 }); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, { "", 0 }); + return INVALID_MESSAGE_ID; } void AssetClient::handleAssetGetInfoReply(QSharedPointer message, SharedNodePointer senderNode) { @@ -453,15 +455,15 @@ MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCa packetList->writeString(path); - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; - _pendingMappingRequests[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QSharedPointer()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; } MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { @@ -478,15 +480,15 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { packetList->writePrimitive(AssetMappingOperationType::GetAll); - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; - _pendingMappingRequests[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QSharedPointer()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; } MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) { @@ -507,15 +509,15 @@ MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOp packetList->writeString(path); } - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; - _pendingMappingRequests[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QSharedPointer()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; } MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback) { @@ -535,15 +537,15 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has packetList->writeString(path); packetList->write(QByteArray::fromHex(hash.toUtf8())); - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; - _pendingMappingRequests[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QSharedPointer()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; } MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback) { @@ -561,16 +563,16 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP packetList->writeString(oldPath); packetList->writeString(newPath); - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; - _pendingMappingRequests[assetServer][messageID] = callback; + return messageID; - return messageID; - - } else { - callback(false, AssetServerError::NoError, QSharedPointer()); - return INVALID_MESSAGE_ID; + } } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; } bool AssetClient::cancelMappingRequest(MessageID id) { @@ -646,15 +648,15 @@ MessageID AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback packetList->writePrimitive(size); packetList->write(data.constData(), size); - nodeList->sendPacketList(std::move(packetList), *assetServer); + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingUploads[assetServer][messageID] = callback; - _pendingUploads[assetServer][messageID] = callback; - - return messageID; - } else { - callback(false, AssetServerError::NoError, QString()); - return INVALID_MESSAGE_ID; + return messageID; + } } + + callback(false, AssetServerError::NoError, QString()); + return INVALID_MESSAGE_ID; } void AssetClient::handleAssetUploadReply(QSharedPointer message, SharedNodePointer senderNode) { @@ -704,6 +706,34 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { return; } + forceFailureOfPendingRequests(node); + + { + auto messageMapIt = _pendingUploads.find(node); + if (messageMapIt != _pendingUploads.end()) { + for (const auto& value : messageMapIt->second) { + value.second(false, AssetServerError::NoError, ""); + } + messageMapIt->second.clear(); + } + } +} + +void AssetClient::handleNodeClientConnectionReset(SharedNodePointer node) { + // a client connection to a Node was reset + // if it was an AssetServer we need to cause anything pending to fail so it is re-attempted + + if (node->getType() != NodeType::AssetServer) { + return; + } + + qCDebug(asset_client) << "AssetClient detected client connection reset handshake with Asset Server - failing any pending requests"; + + forceFailureOfPendingRequests(node); +} + +void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { + { auto messageMapIt = _pendingRequests.find(node); if (messageMapIt != _pendingRequests.end()) { @@ -731,16 +761,6 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { } } - { - auto messageMapIt = _pendingUploads.find(node); - if (messageMapIt != _pendingUploads.end()) { - for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, ""); - } - messageMapIt->second.clear(); - } - } - { auto messageMapIt = _pendingMappingRequests.find(node); if (messageMapIt != _pendingMappingRequests.end()) { diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 536a2e3603..10a88c0676 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -75,6 +75,7 @@ private slots: void handleAssetUploadReply(QSharedPointer message, SharedNodePointer senderNode); void handleNodeKilled(SharedNodePointer node); + void handleNodeClientConnectionReset(SharedNodePointer node); private: MessageID getAssetMapping(const AssetHash& hash, MappingOperationCallback callback); @@ -96,6 +97,8 @@ private: void handleProgressCallback(const QWeakPointer& node, MessageID messageID, qint64 size, DataOffset length); void handleCompleteCallback(const QWeakPointer& node, MessageID messageID); + void forceFailureOfPendingRequests(SharedNodePointer node); + struct GetAssetRequestData { QSharedPointer message; ReceivedAssetCallback completeCallback; diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index c505d108e5..7818c8e5ce 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -70,6 +70,11 @@ bool saveToCache(const QUrl& url, const QByteArray& file) { return false; } +bool isValidFilePath(const AssetPath& filePath) { + QRegExp filePathRegex { ASSET_FILE_PATH_REGEX_STRING }; + return filePathRegex.exactMatch(filePath); +} + bool isValidPath(const AssetPath& path) { QRegExp pathRegex { ASSET_PATH_REGEX_STRING }; return pathRegex.exactMatch(path); diff --git a/libraries/networking/src/AssetUtils.h b/libraries/networking/src/AssetUtils.h index 3a663af420..9e508418ab 100644 --- a/libraries/networking/src/AssetUtils.h +++ b/libraries/networking/src/AssetUtils.h @@ -31,7 +31,8 @@ const size_t SHA256_HASH_LENGTH = 32; const size_t SHA256_HASH_HEX_LENGTH = 64; const uint64_t MAX_UPLOAD_SIZE = 1000 * 1000 * 1000; // 1GB -const QString ASSET_PATH_REGEX_STRING = "^\\/(?!\\/)(?:[^\\/]|\\/(?!\\/))*$"; +const QString ASSET_FILE_PATH_REGEX_STRING = "^(\\/[^\\/\\0]+)+$"; +const QString ASSET_PATH_REGEX_STRING = "^\\/([^\\/\\0]+(\\/)?)+$"; const QString ASSET_HASH_REGEX_STRING = QString("^[a-fA-F0-9]{%1}$").arg(SHA256_HASH_HEX_LENGTH); enum AssetServerError : uint8_t { @@ -59,6 +60,7 @@ QByteArray hashData(const QByteArray& data); QByteArray loadFromCache(const QUrl& url); bool saveToCache(const QUrl& url, const QByteArray& file); +bool isValidFilePath(const AssetPath& path); bool isValidPath(const AssetPath& path); bool isValidHash(const QString& hashString); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index ce555315e8..fa404b39d8 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -32,6 +32,7 @@ #include #include "AccountManager.h" +#include "AssetClient.h" #include "Assignment.h" #include "HifiSockAddr.h" #include "NetworkLogging.h" @@ -41,7 +42,8 @@ static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.Loc const std::set SOLO_NODE_TYPES = { NodeType::AvatarMixer, - NodeType::AudioMixer + NodeType::AudioMixer, + NodeType::AssetServer }; LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) : @@ -113,6 +115,12 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) : using std::placeholders::_1; _nodeSocket.setPacketFilterOperator(std::bind(&LimitedNodeList::isPacketVerified, this, _1)); + // set our socketBelongsToNode method as the connection creation filter operator for the udt::Socket + _nodeSocket.setConnectionCreationFilterOperator(std::bind(&LimitedNodeList::sockAddrBelongsToNode, this, _1)); + + // handle when a socket connection has its receiver side reset - might need to emit clientConnectionToNodeReset + connect(&_nodeSocket, &udt::Socket::clientHandshakeRequestComplete, this, &LimitedNodeList::clientConnectionToSockAddrReset); + _packetStatTimer.start(); if (_stunSockAddr.getAddress().isNull()) { @@ -292,7 +300,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe = LogHandler::getInstance().addRepeatedMessageRegex(UNKNOWN_REGEX); qCDebug(networking) << "Packet of type" << headerType - << "received from unknown node with UUID" << qPrintable(uuidStringWithoutCurlyBraces(sourceID)); + << "received from unknown node with UUID" << uuidStringWithoutCurlyBraces(sourceID); } } @@ -317,6 +325,8 @@ void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& conn } } +static const qint64 ERROR_SENDING_PACKET_BYTES = -1; + qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode) { Q_ASSERT(!packet.isPartOfMessage()); @@ -353,7 +363,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& return sendPacket(std::move(packet), *activeSocket, destinationNode.getConnectionSecret()); } else { qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node" << destinationNode << "- not sending"; - return 0; + return ERROR_SENDING_PACKET_BYTES; } } @@ -392,7 +402,7 @@ qint64 LimitedNodeList::sendPacketList(NLPacketList& packetList, const Node& des } else { qCDebug(networking) << "LimitedNodeList::sendPacketList called without active socket for node" << destinationNode << " - not sending."; - return 0; + return ERROR_SENDING_PACKET_BYTES; } } @@ -438,7 +448,7 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, return _nodeSocket.writePacketList(std::move(packetList), *activeSocket); } else { qCDebug(networking) << "LimitedNodeList::sendPacketList called without active socket for node. Not sending."; - return 0; + return ERROR_SENDING_PACKET_BYTES; } } @@ -446,7 +456,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& const HifiSockAddr& overridenSockAddr) { if (overridenSockAddr.isNull() && !destinationNode.getActiveSocket()) { qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node. Not sending."; - return 0; + return ERROR_SENDING_PACKET_BYTES; } // use the node's active socket as the destination socket if there is no overriden socket address @@ -1136,3 +1146,23 @@ void LimitedNodeList::flagTimeForConnectionStep(ConnectionStep connectionStep, q } } } + +void LimitedNodeList::clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr) { + // for certain reliable channels higher level classes may need to know if the udt::Connection has been reset + auto matchingNode = findNodeWithAddr(sockAddr); + + if (matchingNode) { + emit clientConnectionToNodeReset(matchingNode); + } +} + +#if (PR_BUILD || DEV_BUILD) + +void LimitedNodeList::sendFakedHandshakeRequestToNode(SharedNodePointer node) { + + if (node && node->getActiveSocket()) { + _nodeSocket.sendFakedHandshakeRequest(*node->getActiveSocket()); + } +} + +#endif diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index e74a6c49f8..598964c2b7 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -41,6 +41,7 @@ #include "NLPacketList.h" #include "PacketReceiver.h" #include "ReceivedMessage.h" +#include "udt/ControlPacket.h" #include "udt/PacketHeaders.h" #include "udt/Socket.h" #include "UUIDHasher.h" @@ -235,6 +236,9 @@ public: static void makeSTUNRequestPacket(char* stunRequestPacket); +#if (PR_BUILD || DEV_BUILD) + void sendFakedHandshakeRequestToNode(SharedNodePointer node); +#endif public slots: void reset(); @@ -262,6 +266,8 @@ signals: void nodeKilled(SharedNodePointer); void nodeActivated(SharedNodePointer); + void clientConnectionToNodeReset(SharedNodePointer); + void localSockAddrChanged(const HifiSockAddr& localSockAddr); void publicSockAddrChanged(const HifiSockAddr& publicSockAddr); @@ -274,6 +280,8 @@ signals: protected slots: void connectedForLocalSocketTest(); void errorTestingLocalSocket(); + + void clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr); protected: LimitedNodeList(int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT); @@ -299,6 +307,8 @@ protected: void sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerRequestID = QUuid()); + bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr) { return findNodeWithAddr(sockAddr) != SharedNodePointer(); } + QUuid _sessionUUID; NodeHash _nodeHash; mutable QReadWriteLock _nodeMutex; @@ -311,9 +321,8 @@ protected: PacketReceiver* _packetReceiver; - // XXX can BandwidthRecorder be used for this? - int _numCollectedPackets; - int _numCollectedBytes; + std::atomic _numCollectedPackets; + std::atomic _numCollectedBytes; QElapsedTimer _packetStatTimer; NodePermissions _permissions; diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index d3950f9f30..62a60c521b 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -57,7 +57,7 @@ GetMappingRequest::GetMappingRequest(const AssetPath& path) : _path(path.trimmed void GetMappingRequest::doStart() { // short circuit the request if the path is invalid - if (!isValidPath(_path)) { + if (!isValidFilePath(_path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -139,7 +139,7 @@ SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& has void SetMappingRequest::doStart() { // short circuit the request if the hash or path are invalid - auto validPath = isValidPath(_path); + auto validPath = isValidFilePath(_path); auto validHash = isValidHash(_hash); if (!validPath || !validHash) { _error = !validPath ? MappingRequest::InvalidPath : MappingRequest::InvalidHash; @@ -226,7 +226,7 @@ RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const Asset void RenameMappingRequest::doStart() { // short circuit the request if either of the paths are invalid - if (!isValidPath(_oldPath) || !isValidPath(_newPath)) { + if (!isValidFilePath(_oldPath) || !isValidFilePath(_newPath)) { _error = InvalidPath; emit finished(this); return; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 617ba85bad..82bac4cc3d 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -104,6 +104,10 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) connect(&_domainHandler, SIGNAL(connectedToDomain(QString)), &_keepAlivePingTimer, SLOT(start())); connect(&_domainHandler, &DomainHandler::disconnectedFromDomain, &_keepAlivePingTimer, &QTimer::stop); + // set our sockAddrBelongsToDomainOrNode method as the connection creation filter for the udt::Socket + using std::placeholders::_1; + _nodeSocket.setConnectionCreationFilterOperator(std::bind(&NodeList::sockAddrBelongsToDomainOrNode, this, _1)); + // we definitely want STUN to update our public socket, so call the LNL to kick that off startSTUNPublicSocketUpdate(); @@ -123,19 +127,30 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) packetReceiver.registerListener(PacketType::DomainServerRemovedNode, this, "processDomainServerRemovedNode"); } -qint64 NodeList::sendStats(const QJsonObject& statsObject, const HifiSockAddr& destination) { +qint64 NodeList::sendStats(QJsonObject statsObject, HifiSockAddr destination) { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "sendStats", Qt::QueuedConnection, + Q_ARG(QJsonObject, statsObject), + Q_ARG(HifiSockAddr, destination)); + return 0; + } + auto statsPacketList = NLPacketList::create(PacketType::NodeJsonStats, QByteArray(), true, true); QJsonDocument jsonDocument(statsObject); statsPacketList->write(jsonDocument.toBinaryData()); sendPacketList(std::move(statsPacketList), destination); - - // enumerate the resulting strings, breaking them into MTU sized packets return 0; } -qint64 NodeList::sendStatsToDomainServer(const QJsonObject& statsObject) { +qint64 NodeList::sendStatsToDomainServer(QJsonObject statsObject) { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "sendStatsToDomainServer", Qt::QueuedConnection, + Q_ARG(QJsonObject, statsObject)); + return 0; + } + return sendStats(statsObject, _domainHandler.getSockAddr()); } @@ -247,11 +262,16 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes) } void NodeList::sendDomainServerCheckIn() { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "sendDomainServerCheckIn", Qt::QueuedConnection); + return; + } + if (_isShuttingDown) { qCDebug(networking) << "Refusing to send a domain-server check in while shutting down."; return; } - + if (_publicSockAddr.isNull()) { // we don't know our public socket and we need to send it to the domain server qCDebug(networking) << "Waiting for inital public socket from STUN. Will not send domain-server check in."; @@ -330,7 +350,7 @@ void NodeList::sendDomainServerCheckIn() { // pack our data to send to the domain-server including // the hostname information (so the domain-server can see which place name we came in on) - packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); + packetStream << _ownerType.load() << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); packetStream << DependencyManager::get()->getPlaceName(); if (!_domainHandler.isConnected()) { @@ -389,7 +409,7 @@ void NodeList::sendDSPathQuery(const QString& newPath) { // only send a path query if we know who our DS is or is going to be if (_domainHandler.isSocketKnown()) { // construct the path query packet - auto pathQueryPacket = NLPacket::create(PacketType::DomainServerPathQuery); + auto pathQueryPacket = NLPacket::create(PacketType::DomainServerPathQuery, -1, true); // get the UTF8 representation of path query QByteArray pathQueryUTF8 = newPath.toUtf8(); @@ -703,6 +723,10 @@ void NodeList::sendKeepAlivePings() { }); } +bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr) { + return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr); +} + void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index f08c0dbe45..41a4a51515 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -51,11 +51,11 @@ class NodeList : public LimitedNodeList { SINGLETON_DEPENDENCY public: - NodeType_t getOwnerType() const { return _ownerType; } - void setOwnerType(NodeType_t ownerType) { _ownerType = ownerType; } + NodeType_t getOwnerType() const { return _ownerType.load(); } + void setOwnerType(NodeType_t ownerType) { _ownerType.store(ownerType); } - qint64 sendStats(const QJsonObject& statsObject, const HifiSockAddr& destination); - qint64 sendStatsToDomainServer(const QJsonObject& statsObject); + Q_INVOKABLE qint64 sendStats(QJsonObject statsObject, HifiSockAddr destination); + Q_INVOKABLE qint64 sendStatsToDomainServer(QJsonObject statsObject); int getNumNoReplyDomainCheckIns() const { return _numNoReplyDomainCheckIns; } DomainHandler& getDomainHandler() { return _domainHandler; } @@ -132,7 +132,9 @@ private: void pingPunchForInactiveNode(const SharedNodePointer& node); - NodeType_t _ownerType; + bool sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr); + + std::atomic _ownerType; NodeSet _nodeTypesOfInterest; DomainHandler _domainHandler; int _numNoReplyDomainCheckIns; diff --git a/libraries/networking/src/SandboxUtils.cpp b/libraries/networking/src/SandboxUtils.cpp new file mode 100644 index 0000000000..98c963b793 --- /dev/null +++ b/libraries/networking/src/SandboxUtils.cpp @@ -0,0 +1,96 @@ +// +// SandboxUtils.cpp +// libraries/networking/src +// +// Created by Brad Hefta-Gaub on 2016-10-15. +// 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 +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "SandboxUtils.h" +#include "NetworkAccessManager.h" + + +void SandboxUtils::ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, + std::function localSandboxNotRunningDoThat) { + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL); + sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(sandboxStatus); + + connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() { + if (reply->error() == QNetworkReply::NoError) { + auto statusData = reply->readAll(); + auto statusJson = QJsonDocument::fromJson(statusData); + if (!statusJson.isEmpty()) { + auto statusObject = statusJson.object(); + auto serversValue = statusObject.value("servers"); + if (!serversValue.isUndefined() && serversValue.isObject()) { + auto serversObject = serversValue.toObject(); + auto serversCount = serversObject.size(); + const int MINIMUM_EXPECTED_SERVER_COUNT = 5; + if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) { + localSandboxRunningDoThis(); + return; + } + } + } + } + localSandboxNotRunningDoThat(); + }); +} + + +void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName) { + QString applicationDirPath = QFileInfo(QCoreApplication::applicationFilePath()).path(); + QString serverPath = applicationDirPath + "/server-console/server-console.exe"; + qDebug() << "Application dir path is: " << applicationDirPath; + qDebug() << "Server path is: " << serverPath; + qDebug() << "autoShutdown: " << autoShutdown; + + bool hasContentPath = !contentPath.isEmpty(); + bool passArgs = autoShutdown || hasContentPath; + + QStringList args; + + if (passArgs) { + args << "--"; + } + + if (hasContentPath) { + QString serverContentPath = applicationDirPath + "/" + contentPath; + args << "--contentPath" << serverContentPath; + } + + if (autoShutdown) { + QString interfaceRunningStateFile = RunningMarker::getMarkerFilePath(runningMarkerName); + args << "--shutdownWatcher" << interfaceRunningStateFile; + } + + qDebug() << applicationDirPath; + qDebug() << "Launching sandbox with:" << args; + qDebug() << QProcess::startDetached(serverPath, args); + + // Sleep a short amount of time to give the server a chance to start + usleep(2000000); /// do we really need this?? +} diff --git a/libraries/networking/src/SandboxUtils.h b/libraries/networking/src/SandboxUtils.h new file mode 100644 index 0000000000..e40ff71a56 --- /dev/null +++ b/libraries/networking/src/SandboxUtils.h @@ -0,0 +1,32 @@ +// +// SandboxUtils.h +// libraries/networking/src +// +// Created by Brad Hefta-Gaub on 2016-10-15. +// 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 +// + +#ifndef hifi_SandboxUtils_h +#define hifi_SandboxUtils_h + +#include +#include + + +const QString SANDBOX_STATUS_URL = "http://localhost:60332/status"; + +class SandboxUtils : public QObject { + Q_OBJECT +public: + /// determines if the local sandbox is likely running. It does not account for custom setups, and is only + /// intended to detect the standard local sandbox install. + void ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, + std::function localSandboxNotRunningDoThat); + + static void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName); +}; + +#endif // hifi_SandboxUtils_h diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index 9277aa70cc..906bc240e6 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -87,11 +87,10 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy connect(&nodeList->getDomainHandler(), &DomainHandler::disconnectedFromDomain, &_statsTimer, &QTimer::stop); } -void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject &statsObject) { +void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) { auto nodeList = DependencyManager::get(); float packetsPerSecond, bytesPerSecond; - // XXX can BandwidthRecorder be used for this? nodeList->getPacketStats(packetsPerSecond, bytesPerSecond); nodeList->resetPacketStats(); diff --git a/libraries/networking/src/ThreadedAssignment.h b/libraries/networking/src/ThreadedAssignment.h index 13b9b5bf79..e9832a2a91 100644 --- a/libraries/networking/src/ThreadedAssignment.h +++ b/libraries/networking/src/ThreadedAssignment.h @@ -26,7 +26,7 @@ public: void setFinished(bool isFinished); virtual void aboutToFinish() { }; - void addPacketStatsAndSendStatsPacket(QJsonObject& statsObject); + void addPacketStatsAndSendStatsPacket(QJsonObject statsObject); public slots: /// threaded run of assignment 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/BasePacket.h b/libraries/networking/src/udt/BasePacket.h index 33b8020d3c..d9b624b595 100644 --- a/libraries/networking/src/udt/BasePacket.h +++ b/libraries/networking/src/udt/BasePacket.h @@ -18,6 +18,8 @@ #include +#include + #include "../HifiSockAddr.h" #include "Constants.h" @@ -80,6 +82,9 @@ public: qint64 writeString(const QString& string); QString readString(); + + void setReceiveTime(p_high_resolution_clock::time_point receiveTime) { _receiveTime = receiveTime; } + p_high_resolution_clock::time_point getReceiveTime() const { return _receiveTime; } template qint64 peekPrimitive(T* data); template qint64 readPrimitive(T* data); @@ -108,6 +113,8 @@ protected: qint64 _payloadSize = 0; // How much of the payload is actually used HifiSockAddr _senderSockAddr; // sender address for packet (only used on receiving end) + + p_high_resolution_clock::time_point _receiveTime; // captures the time the packet received (only used on receiving end) }; template qint64 BasePacket::peekPrimitive(T* data) { diff --git a/libraries/networking/src/udt/CongestionControl.cpp b/libraries/networking/src/udt/CongestionControl.cpp index 5826bfa11c..7ade4f004f 100644 --- a/libraries/networking/src/udt/CongestionControl.cpp +++ b/libraries/networking/src/udt/CongestionControl.cpp @@ -45,11 +45,11 @@ DefaultCC::DefaultCC() : { _mss = udt::MAX_PACKET_SIZE_WITH_UDP_HEADER; - _congestionWindowSize = 16.0; + _congestionWindowSize = 16; setPacketSendPeriod(1.0); } -void DefaultCC::onACK(SequenceNumber ackNum) { +bool DefaultCC::onACK(SequenceNumber ackNum, p_high_resolution_clock::time_point receiveTime) { double increase = 0; // Note from UDT original code: @@ -61,7 +61,7 @@ void DefaultCC::onACK(SequenceNumber ackNum) { // we will only adjust once per sync interval so check that it has been at least that long now auto now = p_high_resolution_clock::now(); if (duration_cast(now - _lastRCTime).count() < synInterval()) { - return; + return false; } // our last rate increase time is now @@ -93,13 +93,13 @@ void DefaultCC::onACK(SequenceNumber ackNum) { // during slow start we perform no rate increases if (_slowStart) { - return; + return false; } // if loss has happened since the last rate increase we do not perform another increase if (_loss) { _loss = false; - return; + return false; } double capacitySpeedDelta = (_bandwidth - USECS_PER_SECOND / _packetSendPeriod); @@ -132,6 +132,8 @@ void DefaultCC::onACK(SequenceNumber ackNum) { } setPacketSendPeriod((_packetSendPeriod * synInterval()) / (_packetSendPeriod * increase + synInterval())); + + return false; } void DefaultCC::onLoss(SequenceNumber rangeStart, SequenceNumber rangeEnd) { @@ -218,7 +220,7 @@ void DefaultCC::stopSlowStart() { // If no receiving rate is observed, we have to compute the sending // rate according to the current window size, and decrease it // using the method below. - setPacketSendPeriod(_congestionWindowSize / (_rtt + synInterval())); + setPacketSendPeriod(double(_congestionWindowSize) / (_rtt + synInterval())); } } diff --git a/libraries/networking/src/udt/CongestionControl.h b/libraries/networking/src/udt/CongestionControl.h index 3ab69efe52..3a93134a5e 100644 --- a/libraries/networking/src/udt/CongestionControl.h +++ b/libraries/networking/src/udt/CongestionControl.h @@ -40,9 +40,18 @@ public: void setMaxBandwidth(int maxBandwidth); virtual void init() {} - virtual void onACK(SequenceNumber ackNum) {} + + // return value specifies if connection should perform a fast re-transmit of ACK + 1 (used in TCP style congestion control) + virtual bool onACK(SequenceNumber ackNum, p_high_resolution_clock::time_point receiveTime) { return false; } + virtual void onLoss(SequenceNumber rangeStart, SequenceNumber rangeEnd) {} virtual void onTimeout() {} + + virtual bool shouldNAK() { return true; } + virtual bool shouldACK2() { return true; } + virtual bool shouldProbe() { return true; } + + virtual void onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) {} protected: void setAckInterval(int ackInterval) { _ackInterval = ackInterval; } void setRTO(int rto) { _userDefinedRTO = true; _rto = rto; } @@ -57,11 +66,11 @@ protected: void setPacketSendPeriod(double newSendPeriod); // call this internally to ensure send period doesn't go past max bandwidth double _packetSendPeriod { 1.0 }; // Packet sending period, in microseconds - double _congestionWindowSize { 16.0 }; // Congestion window size, in packets + int _congestionWindowSize { 16 }; // Congestion window size, in packets int _bandwidth { 0 }; // estimated bandwidth, packets per second std::atomic _maxBandwidth { -1 }; // Maximum desired bandwidth, bits per second - double _maxCongestionWindowSize { 0.0 }; // maximum cwnd size, in packets + int _maxCongestionWindowSize { 0 }; // maximum cwnd size, in packets int _mss { 0 }; // Maximum Packet Size, including all packet headers SequenceNumber _sendCurrSeqNum; // current maximum seq num sent out @@ -102,7 +111,7 @@ public: DefaultCC(); public: - virtual void onACK(SequenceNumber ackNum) override; + virtual bool onACK(SequenceNumber ackNum, p_high_resolution_clock::time_point receiveTime) override; virtual void onLoss(SequenceNumber rangeStart, SequenceNumber rangeEnd) override; virtual void onTimeout() override; diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index dcceef0fdc..a190fe3ddc 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -118,12 +118,14 @@ SendQueue& Connection::getSendQueue() { QObject::connect(_sendQueue.get(), &SendQueue::queueInactive, this, &Connection::queueInactive); QObject::connect(_sendQueue.get(), &SendQueue::timeout, this, &Connection::queueTimeout); QObject::connect(_sendQueue.get(), &SendQueue::shortCircuitLoss, this, &Connection::queueShortCircuitLoss); + // set defaults on the send queue from our congestion control object and estimatedTimeout() _sendQueue->setPacketSendPeriod(_congestionControl->_packetSendPeriod); _sendQueue->setSyncInterval(_synInterval); _sendQueue->setEstimatedTimeout(estimatedTimeout()); _sendQueue->setFlowWindowSize(std::min(_flowWindowSize, (int) _congestionControl->_congestionWindowSize)); + _sendQueue->setProbePacketEnabled(_congestionControl->shouldProbe()); // give the randomized sequence number to the congestion control object _congestionControl->setInitialSendSequenceNumber(_sendQueue->getCurrentSequenceNumber()); @@ -150,13 +152,13 @@ void Connection::queueInactive() { } void Connection::queueTimeout() { - updateCongestionControlAndSendQueue([this]{ + updateCongestionControlAndSendQueue([this] { _congestionControl->onTimeout(); }); } void Connection::queueShortCircuitLoss(quint32 sequenceNumber) { - updateCongestionControlAndSendQueue([this, sequenceNumber]{ + updateCongestionControlAndSendQueue([this, sequenceNumber] { _congestionControl->onLoss(SequenceNumber { sequenceNumber }, SequenceNumber { sequenceNumber }); }); } @@ -223,9 +225,11 @@ void Connection::sync() { // reset the number of light ACKs or non SYN ACKs during this sync interval _lightACKsDuringSYN = 1; _acksDuringSYN = 1; - - // we send out a periodic ACK every rate control interval - sendACK(); + + if (_congestionControl->_ackInterval > 1) { + // we send out a periodic ACK every rate control interval + sendACK(); + } if (_lossList.getLength() > 0) { // check if we need to re-transmit a loss list @@ -260,12 +264,17 @@ void Connection::sync() { } } -void Connection::recordSentPackets(int dataSize, int payloadSize) { - _stats.recordSentPackets(payloadSize, dataSize); +void Connection::recordSentPackets(int wireSize, int payloadSize, + SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { + _stats.recordSentPackets(payloadSize, wireSize); + + _congestionControl->onPacketSent(wireSize, seqNum, timePoint); } -void Connection::recordRetransmission() { +void Connection::recordRetransmission(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { _stats.record(ConnectionStats::Stats::Retransmission); + + _congestionControl->onPacketSent(wireSize, seqNum, timePoint); } void Connection::sendACK(bool wasCausedBySyncTimeout) { @@ -308,8 +317,8 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) { // pack the available buffer size, in packets // in our implementation we have no hard limit on receive buffer size, send the default value - _ackPacket->writePrimitive((int32_t) udt::CONNECTION_RECEIVE_BUFFER_SIZE_PACKETS); - + _ackPacket->writePrimitive((int32_t) udt::MAX_PACKETS_IN_FLIGHT); + if (wasCausedBySyncTimeout) { // grab the up to date packet receive speed and estimated bandwidth int32_t packetReceiveSpeed = _receiveWindow.getPacketReceiveSpeed(); @@ -426,18 +435,25 @@ SequenceNumber Connection::nextACK() const { } } +void Connection::sendHandshakeRequest() { + auto handshakeRequestPacket = ControlPacket::create(ControlPacket::HandshakeRequest, 0); + _parentSocket->writeBasePacket(*handshakeRequestPacket, _destination); + + _didRequestHandshake = true; +} + bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, int packetSize, int payloadSize) { if (!_hasReceivedHandshake) { // Refuse to process any packets until we've received the handshake // Send handshake request to re-request a handshake - auto handshakeRequestPacket = ControlPacket::create(ControlPacket::HandshakeRequest, 0); - _parentSocket->writeBasePacket(*handshakeRequestPacket, _destination); #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Received packet before receiving handshake, sending HandshakeRequest"; #endif + sendHandshakeRequest(); + return false; } @@ -468,23 +484,24 @@ bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, in } else { _lossList.append(_lastReceivedSequenceNumber + 1, sequenceNumber - 1); } - - // Send a NAK packet - sendNAK(sequenceNumber); - - // figure out when we should send the next loss report, if we haven't heard anything back - _nakInterval = estimatedTimeout(); - - int receivedPacketsPerSecond = _receiveWindow.getPacketReceiveSpeed(); - if (receivedPacketsPerSecond > 0) { - // the NAK interval is at least the _minNAKInterval - // but might be the time required for all lost packets to be retransmitted - _nakInterval += (int) (_lossList.getLength() * (USECS_PER_SECOND / receivedPacketsPerSecond)); - } - - // the NAK interval is at least the _minNAKInterval but might be the value calculated above, if that is larger - _nakInterval = std::max(_nakInterval, _minNAKInterval); + if (_congestionControl->shouldNAK()) { + // Send a NAK packet + sendNAK(sequenceNumber); + + // figure out when we should send the next loss report, if we haven't heard anything back + _nakInterval = estimatedTimeout(); + + int receivedPacketsPerSecond = _receiveWindow.getPacketReceiveSpeed(); + if (receivedPacketsPerSecond > 0) { + // the NAK interval is at least the _minNAKInterval + // but might be the time required for all lost packets to be retransmitted + _nakInterval += (int) (_lossList.getLength() * (USECS_PER_SECOND / receivedPacketsPerSecond)); + } + + // the NAK interval is at least the _minNAKInterval but might be the value calculated above, if that is larger + _nakInterval = std::max(_nakInterval, _minNAKInterval); + } } bool wasDuplicate = false; @@ -501,7 +518,10 @@ bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, in ++_packetsSinceACK; // check if we need to send an ACK, according to CC params - if (_congestionControl->_ackInterval > 0 && _packetsSinceACK >= _congestionControl->_ackInterval * _acksDuringSYN) { + if (_congestionControl->_ackInterval == 1) { + // using a congestion control that ACKs every packet (like TCP Vegas) + sendACK(true); + } else if (_congestionControl->_ackInterval > 0 && _packetsSinceACK >= _congestionControl->_ackInterval * _acksDuringSYN) { _acksDuringSYN++; sendACK(false); } else if (_congestionControl->_lightACKInterval > 0 @@ -591,7 +611,8 @@ void Connection::processACK(ControlPacketPointer controlPacket) { microseconds sinceLastACK2 = duration_cast(currentTime - lastACK2SendTime); - if (sinceLastACK2.count() >= _synInterval || currentACKSubSequenceNumber == _lastSentACK2) { + if (_congestionControl->shouldACK2() + && (sinceLastACK2.count() >= _synInterval || currentACKSubSequenceNumber == _lastSentACK2)) { // Send ACK2 packet sendACK2(currentACKSubSequenceNumber); @@ -671,8 +692,11 @@ void Connection::processACK(ControlPacketPointer controlPacket) { } // give this ACK to the congestion control and update the send queue parameters - updateCongestionControlAndSendQueue([this, ack](){ - _congestionControl->onACK(ack); + updateCongestionControlAndSendQueue([this, ack, &controlPacket] { + if (_congestionControl->onACK(ack, controlPacket->getReceiveTime())) { + // the congestion control has told us it needs a fast re-transmit of ack + 1, add that now + _sendQueue->fastRetransmit(ack + 1); + } }); _stats.record(ConnectionStats::Stats::ProcessedACK); @@ -757,7 +781,7 @@ void Connection::processNAK(ControlPacketPointer controlPacket) { getSendQueue().nak(start, end); // give the loss to the congestion control object and update the send queue parameters - updateCongestionControlAndSendQueue([this, start, end](){ + updateCongestionControlAndSendQueue([this, start, end] { _congestionControl->onLoss(start, end); }); @@ -789,6 +813,11 @@ void Connection::processHandshake(ControlPacketPointer controlPacket) { // indicate that handshake has been received _hasReceivedHandshake = true; + + if (_didRequestHandshake) { + emit receiverHandshakeRequestComplete(_destination); + _didRequestHandshake = false; + } } void Connection::processHandshakeACK(ControlPacketPointer controlPacket) { diff --git a/libraries/networking/src/udt/Connection.h b/libraries/networking/src/udt/Connection.h index d6121d47b2..f94550426e 100644 --- a/libraries/networking/src/udt/Connection.h +++ b/libraries/networking/src/udt/Connection.h @@ -79,13 +79,16 @@ public: void setMaxBandwidth(int maxBandwidth); + void sendHandshakeRequest(); + signals: void packetSent(); void connectionInactive(const HifiSockAddr& sockAddr); - + void receiverHandshakeRequestComplete(const HifiSockAddr& sockAddr); + private slots: - void recordSentPackets(int payload, int total); - void recordRetransmission(); + void recordSentPackets(int wireSize, int payloadSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint); + void recordRetransmission(int wireSize, SequenceNumber sequenceNumber, p_high_resolution_clock::time_point timePoint); void queueInactive(); void queueTimeout(); void queueShortCircuitLoss(quint32 sequenceNumber); @@ -129,6 +132,7 @@ private: bool _hasReceivedHandshake { false }; // flag for receipt of handshake from server bool _hasReceivedHandshakeACK { false }; // flag for receipt of handshake ACK from client + bool _didRequestHandshake { false }; // flag for request of handshake from server p_high_resolution_clock::time_point _connectionStart = p_high_resolution_clock::now(); // holds the time_point for creation of this connection p_high_resolution_clock::time_point _lastReceiveTime; // holds the last time we received anything from sender diff --git a/libraries/networking/src/udt/Constants.h b/libraries/networking/src/udt/Constants.h index 3186571f9b..243fa4edda 100644 --- a/libraries/networking/src/udt/Constants.h +++ b/libraries/networking/src/udt/Constants.h @@ -17,6 +17,7 @@ #include "SequenceNumber.h" namespace udt { + static const int UDP_IPV4_HEADER_SIZE = 28; static const int MAX_PACKET_SIZE_WITH_UDP_HEADER = 1492; static const int MAX_PACKET_SIZE = MAX_PACKET_SIZE_WITH_UDP_HEADER - UDP_IPV4_HEADER_SIZE; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index ec4e724c1b..6b86b7bc6e 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -58,8 +58,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: - // Removal of extension from Asset requests - return 18; + return static_cast(AssetServerPacketVersion::VegasCongestionControl); case PacketType::NodeIgnoreRequest: return 18; // Introduction of node ignore request (which replaced an unused packet tpye) @@ -77,7 +76,8 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::InjectAudio: case PacketType::MicrophoneAudioNoEcho: case PacketType::MicrophoneAudioWithEcho: - return static_cast(AudioVersion::Exactly10msAudioPackets); + case PacketType::AudioStreamStats: + return static_cast(AudioVersion::TerminatingStreamStats); default: return 17; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index aa775b9f53..0eca24176c 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -189,6 +189,10 @@ const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS = 62; const PacketVersion VERSION_WEB_ENTITIES_SUPPORT_DPI = 63; const PacketVersion VERSION_ENTITIES_ARROW_ACTION = 64; +enum class AssetServerPacketVersion: PacketVersion { + VegasCongestionControl = 19 +}; + enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, SoftAttachmentSupport, @@ -223,7 +227,8 @@ enum class DomainListVersion : PacketVersion { enum class AudioVersion : PacketVersion { HasCompressedAudio = 17, CodecNameInAudioPackets, - Exactly10msAudioPackets + Exactly10msAudioPackets, + TerminatingStreamStats, }; #endif // hifi_PacketHeaders_h diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index ba43076b31..f42e2b77fb 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -97,6 +97,9 @@ SendQueue::SendQueue(Socket* socket, HifiSockAddr dest) : _currentSequenceNumber = _initialSequenceNumber - 1; _atomicCurrentSequenceNumber = uint32_t(_currentSequenceNumber); _lastACKSequenceNumber = uint32_t(_currentSequenceNumber) - 1; + + // default the last receiver response to the current time + _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); } void SendQueue::queuePacket(std::unique_ptr packet) { @@ -166,7 +169,7 @@ void SendQueue::ack(SequenceNumber ack) { void SendQueue::nak(SequenceNumber start, SequenceNumber end) { // this is a response from the client, re-set our timeout expiry - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); + _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); { std::lock_guard nakLocker(_naksLock); @@ -177,6 +180,16 @@ void SendQueue::nak(SequenceNumber start, SequenceNumber end) { _emptyCondition.notify_one(); } +void SendQueue::fastRetransmit(udt::SequenceNumber ack) { + { + std::lock_guard nakLocker(_naksLock); + _naks.insert(ack, ack); + } + + // call notify_one on the condition_variable_any in case the send thread is sleeping waiting for losses to re-send + _emptyCondition.notify_one(); +} + void SendQueue::overrideNAKListFromPacket(ControlPacket& packet) { // this is a response from the client, re-set our timeout expiry _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); @@ -239,11 +252,13 @@ bool SendQueue::sendNewPacketAndAddToSentList(std::unique_ptr newPacket, newPacket->writeSequenceNumber(sequenceNumber); // Save packet/payload size before we move it - auto packetSize = newPacket->getDataSize(); + auto packetSize = newPacket->getWireSize(); auto payloadSize = newPacket->getPayloadSize(); auto bytesWritten = sendPacket(*newPacket); + emit packetSent(packetSize, payloadSize, sequenceNumber, p_high_resolution_clock::now()); + { // Insert the packet we have just sent in the sent list QWriteLocker locker(&_sentLock); @@ -253,8 +268,6 @@ bool SendQueue::sendNewPacketAndAddToSentList(std::unique_ptr newPacket, } Q_ASSERT_X(!newPacket, "SendQueue::sendNewPacketAndAddToSentList()", "Overriden packet in sent list"); - emit packetSent(packetSize, payloadSize); - if (bytesWritten < 0) { // this is a short-circuit loss - we failed to put this packet on the wire // so immediately add it to the loss list @@ -325,60 +338,66 @@ void SendQueue::run() { return; } - // push the next packet timestamp forwards by the current packet send period - auto nextPacketDelta = (newPacketCount == 2 ? 2 : 1) * _packetSendPeriod; - nextPacketTimestamp += std::chrono::microseconds(nextPacketDelta); + if (_packetSendPeriod > 0) { + // push the next packet timestamp forwards by the current packet send period + auto nextPacketDelta = (newPacketCount == 2 ? 2 : 1) * _packetSendPeriod; + nextPacketTimestamp += std::chrono::microseconds(nextPacketDelta); - // sleep as long as we need for next packet send, if we can - auto now = p_high_resolution_clock::now(); + // sleep as long as we need for next packet send, if we can + auto now = p_high_resolution_clock::now(); - auto timeToSleep = duration_cast(nextPacketTimestamp - now); + auto timeToSleep = duration_cast(nextPacketTimestamp - now); - // we use nextPacketTimestamp so that we don't fall behind, not to force long sleeps - // we'll never allow nextPacketTimestamp to force us to sleep for more than nextPacketDelta - // so cap it to that value - if (timeToSleep > std::chrono::microseconds(nextPacketDelta)) { - // reset the nextPacketTimestamp so that it is correct next time we come around - nextPacketTimestamp = now + std::chrono::microseconds(nextPacketDelta); + // we use nextPacketTimestamp so that we don't fall behind, not to force long sleeps + // we'll never allow nextPacketTimestamp to force us to sleep for more than nextPacketDelta + // so cap it to that value + if (timeToSleep > std::chrono::microseconds(nextPacketDelta)) { + // reset the nextPacketTimestamp so that it is correct next time we come around + nextPacketTimestamp = now + std::chrono::microseconds(nextPacketDelta); - timeToSleep = std::chrono::microseconds(nextPacketDelta); - } + timeToSleep = std::chrono::microseconds(nextPacketDelta); + } - // we're seeing SendQueues sleep for a long period of time here, - // which can lock the NodeList if it's attempting to clear connections - // for now we guard this by capping the time this thread and sleep for + // we're seeing SendQueues sleep for a long period of time here, + // which can lock the NodeList if it's attempting to clear connections + // for now we guard this by capping the time this thread and sleep for - const microseconds MAX_SEND_QUEUE_SLEEP_USECS { 2000000 }; - if (timeToSleep > MAX_SEND_QUEUE_SLEEP_USECS) { - qWarning() << "udt::SendQueue wanted to sleep for" << timeToSleep.count() << "microseconds"; - qWarning() << "Capping sleep to" << MAX_SEND_QUEUE_SLEEP_USECS.count(); - qWarning() << "PSP:" << _packetSendPeriod << "NPD:" << nextPacketDelta + const microseconds MAX_SEND_QUEUE_SLEEP_USECS { 2000000 }; + if (timeToSleep > MAX_SEND_QUEUE_SLEEP_USECS) { + qWarning() << "udt::SendQueue wanted to sleep for" << timeToSleep.count() << "microseconds"; + qWarning() << "Capping sleep to" << MAX_SEND_QUEUE_SLEEP_USECS.count(); + qWarning() << "PSP:" << _packetSendPeriod << "NPD:" << nextPacketDelta << "NPT:" << nextPacketTimestamp.time_since_epoch().count() << "NOW:" << now.time_since_epoch().count(); - // alright, we're in a weird state - // we want to know why this is happening so we can implement a better fix than this guard - // send some details up to the API (if the user allows us) that indicate how we could such a large timeToSleep - static const QString SEND_QUEUE_LONG_SLEEP_ACTION = "sendqueue-sleep"; + // alright, we're in a weird state + // we want to know why this is happening so we can implement a better fix than this guard + // send some details up to the API (if the user allows us) that indicate how we could such a large timeToSleep + static const QString SEND_QUEUE_LONG_SLEEP_ACTION = "sendqueue-sleep"; - // setup a json object with the details we want - QJsonObject longSleepObject; - longSleepObject["timeToSleep"] = qint64(timeToSleep.count()); - longSleepObject["packetSendPeriod"] = _packetSendPeriod.load(); - longSleepObject["nextPacketDelta"] = nextPacketDelta; - longSleepObject["nextPacketTimestamp"] = qint64(nextPacketTimestamp.time_since_epoch().count()); - longSleepObject["then"] = qint64(now.time_since_epoch().count()); + // setup a json object with the details we want + QJsonObject longSleepObject; + longSleepObject["timeToSleep"] = qint64(timeToSleep.count()); + longSleepObject["packetSendPeriod"] = _packetSendPeriod.load(); + longSleepObject["nextPacketDelta"] = nextPacketDelta; + longSleepObject["nextPacketTimestamp"] = qint64(nextPacketTimestamp.time_since_epoch().count()); + longSleepObject["then"] = qint64(now.time_since_epoch().count()); - // hopefully send this event using the user activity logger - UserActivityLogger::getInstance().logAction(SEND_QUEUE_LONG_SLEEP_ACTION, longSleepObject); - - timeToSleep = MAX_SEND_QUEUE_SLEEP_USECS; + // hopefully send this event using the user activity logger + UserActivityLogger::getInstance().logAction(SEND_QUEUE_LONG_SLEEP_ACTION, longSleepObject); + + timeToSleep = MAX_SEND_QUEUE_SLEEP_USECS; + } + + std::this_thread::sleep_for(timeToSleep); } - - std::this_thread::sleep_for(timeToSleep); } } +void SendQueue::setProbePacketEnabled(bool enabled) { + _shouldSendProbes = enabled; +} + int SendQueue::maybeSendNewPacket() { if (!isFlowWindowFull()) { // we didn't re-send a packet, so time to send a new one @@ -396,7 +415,7 @@ int SendQueue::maybeSendNewPacket() { std::unique_ptr secondPacket; bool shouldSendPairTail = false; - if (((uint32_t) nextNumber & 0xF) == 0) { + if (_shouldSendProbes && ((uint32_t) nextNumber & 0xF) == 0) { // the first packet is the first in a probe pair - every 16 (rightmost 16 bits = 0) packets // pull off a second packet if we can before we unlock shouldSendPairTail = true; @@ -489,7 +508,7 @@ bool SendQueue::maybeResendPacket() { sentLocker.unlock(); } - emit packetRetransmitted(); + emit packetRetransmitted(resendPacket.getWireSize(), it->first, p_high_resolution_clock::now()); // Signal that we did resend a packet return true; @@ -520,7 +539,6 @@ bool SendQueue::isInactive(bool attemptedToSendPacket) { if (sinceLastResponse > 0 && sinceLastResponse >= int64_t(NUM_TIMEOUTS_BEFORE_INACTIVE * (_estimatedTimeout / USECS_PER_MSEC)) && - _lastReceiverResponse > 0 && sinceLastResponse > MIN_MS_BEFORE_INACTIVE) { // If the flow window has been full for over CONSIDER_INACTIVE_AFTER, // then signal the queue is inactive and return so it can be cleaned up diff --git a/libraries/networking/src/udt/SendQueue.h b/libraries/networking/src/udt/SendQueue.h index 21f6141c3c..6e17c5b9c6 100644 --- a/libraries/networking/src/udt/SendQueue.h +++ b/libraries/networking/src/udt/SendQueue.h @@ -64,18 +64,21 @@ public: void setEstimatedTimeout(int estimatedTimeout) { _estimatedTimeout = estimatedTimeout; } void setSyncInterval(int syncInterval) { _syncInterval = syncInterval; } + + void setProbePacketEnabled(bool enabled); public slots: void stop(); void ack(SequenceNumber ack); void nak(SequenceNumber start, SequenceNumber end); + void fastRetransmit(SequenceNumber ack); void overrideNAKListFromPacket(ControlPacket& packet); void handshakeACK(SequenceNumber initialSequenceNumber); signals: - void packetSent(int dataSize, int payloadSize); - void packetRetransmitted(); + void packetSent(int wireSize, int payloadSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint); + void packetRetransmitted(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint); void queueInactive(); @@ -139,6 +142,9 @@ private: std::condition_variable _handshakeACKCondition; std::condition_variable_any _emptyCondition; + + + std::atomic _shouldSendProbes { true }; }; } diff --git a/libraries/networking/src/udt/SequenceNumber.cpp b/libraries/networking/src/udt/SequenceNumber.cpp index 3e94c35d44..0b567e333d 100644 --- a/libraries/networking/src/udt/SequenceNumber.cpp +++ b/libraries/networking/src/udt/SequenceNumber.cpp @@ -11,6 +11,14 @@ #include "SequenceNumber.h" +#include + +using namespace udt; + +Q_DECLARE_METATYPE(SequenceNumber); + +int sequenceNumberMetaTypeID = qRegisterMetaType(); + int udt::seqlen(const SequenceNumber& seq1, const SequenceNumber& seq2) { return (seq1._value <= seq2._value) ? (seq2._value - seq1._value + 1) : (seq2._value - seq1._value + SequenceNumber::MAX + 2); diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 37ededa55c..98cc62bdee 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -29,9 +29,11 @@ using namespace udt; -Socket::Socket(QObject* parent) : +Socket::Socket(QObject* parent, bool shouldChangeSocketOptions) : QObject(parent), - _synTimer(new QTimer(this)) + _synTimer(new QTimer(this)), + _readyReadBackupTimer(new QTimer(this)), + _shouldChangeSocketOptions(shouldChangeSocketOptions) { connect(&_udpSocket, &QUdpSocket::readyRead, this, &Socket::readPendingDatagrams); @@ -45,21 +47,29 @@ Socket::Socket(QObject* parent) : connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(handleSocketError(QAbstractSocket::SocketError))); 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 = 2 * 1000; + connect(_readyReadBackupTimer, &QTimer::timeout, this, &Socket::checkForReadyReadBackup); + _readyReadBackupTimer->start(READY_READ_BACKUP_CHECK_MSECS); } void Socket::bind(const QHostAddress& address, quint16 port) { _udpSocket.bind(address, port); - setSystemBufferSizes(); + + if (_shouldChangeSocketOptions) { + setSystemBufferSizes(); #if defined(Q_OS_LINUX) - auto sd = _udpSocket.socketDescriptor(); - int val = IP_PMTUDISC_DONT; - setsockopt(sd, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); + auto sd = _udpSocket.socketDescriptor(); + int val = IP_PMTUDISC_DONT; + setsockopt(sd, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); #elif defined(Q_OS_WINDOWS) - auto sd = _udpSocket.socketDescriptor(); - int val = 0; // false - setsockopt(sd, IPPROTO_IP, IP_DONTFRAGMENT, &val, sizeof(val)); + auto sd = _udpSocket.socketDescriptor(); + int val = 0; // false + setsockopt(sd, IPPROTO_IP, IP_DONTFRAGMENT, &val, sizeof(val)); #endif + } } void Socket::rebind() { @@ -118,8 +128,14 @@ qint64 Socket::writeBasePacket(const udt::BasePacket& packet, const HifiSockAddr qint64 Socket::writePacket(const Packet& packet, const HifiSockAddr& sockAddr) { Q_ASSERT_X(!packet.isReliable(), "Socket::writePacket", "Cannot send a reliable packet unreliably"); + SequenceNumber sequenceNumber; + { + Lock lock(_unreliableSequenceNumbersMutex); + sequenceNumber = ++_unreliableSequenceNumbers[sockAddr]; + } + // write the correct sequence number to the Packet here - packet.writeSequenceNumber(++_unreliableSequenceNumbers[sockAddr]); + packet.writeSequenceNumber(sequenceNumber); return writeDatagram(packet.getData(), packet.getDataSize(), sockAddr); } @@ -171,11 +187,28 @@ qint64 Socket::writePacketList(std::unique_ptr packetList, const Hif } void Socket::writeReliablePacket(Packet* packet, const HifiSockAddr& sockAddr) { - findOrCreateConnection(sockAddr).sendReliablePacket(std::unique_ptr(packet)); + auto connection = findOrCreateConnection(sockAddr); + if (connection) { + connection->sendReliablePacket(std::unique_ptr(packet)); + } +#ifdef UDT_CONNECTION_DEBUG + else { + qCDebug(networking) << "Socket::writeReliablePacket refusing to send packet - no connection was created"; + } +#endif + } void Socket::writeReliablePacketList(PacketList* packetList, const HifiSockAddr& sockAddr) { - findOrCreateConnection(sockAddr).sendReliablePacketList(std::unique_ptr(packetList)); + auto connection = findOrCreateConnection(sockAddr); + if (connection) { + connection->sendReliablePacketList(std::unique_ptr(packetList)); + } +#ifdef UDT_CONNECTION_DEBUG + else { + qCDebug(networking) << "Socket::writeReliablePacketList refusing to send packet list - no connection was created"; + } +#endif } qint64 Socket::writeDatagram(const char* data, qint64 size, const HifiSockAddr& sockAddr) { @@ -198,25 +231,40 @@ qint64 Socket::writeDatagram(const QByteArray& datagram, const HifiSockAddr& soc return bytesWritten; } -Connection& Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { +Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { auto it = _connectionsHash.find(sockAddr); if (it == _connectionsHash.end()) { - auto congestionControl = _ccFactory->create(); - congestionControl->setMaxBandwidth(_maxBandwidth); - auto connection = std::unique_ptr(new Connection(this, sockAddr, std::move(congestionControl))); + // we did not have a matching connection, time to see if we should make one - // we queue the connection to cleanup connection in case it asks for it during its own rate control sync - QObject::connect(connection.get(), &Connection::connectionInactive, this, &Socket::cleanupConnection); + if (_connectionCreationFilterOperator && !_connectionCreationFilterOperator(sockAddr)) { + // the connection creation filter did not allow us to create a new connection +#ifdef UDT_CONNECTION_DEBUG + qCDebug(networking) << "Socket::findOrCreateConnection refusing to create connection for" << sockAddr + << "due to connection creation filter"; +#endif + return nullptr; + } else { + auto congestionControl = _ccFactory->create(); + congestionControl->setMaxBandwidth(_maxBandwidth); + auto connection = std::unique_ptr(new Connection(this, sockAddr, std::move(congestionControl))); + + // we queue the connection to cleanup connection in case it asks for it during its own rate control sync + QObject::connect(connection.get(), &Connection::connectionInactive, this, &Socket::cleanupConnection); + + // allow higher-level classes to find out when connections have completed a handshake + QObject::connect(connection.get(), &Connection::receiverHandshakeRequestComplete, + this, &Socket::clientHandshakeRequestComplete); #ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Creating new connection to" << sockAddr; + qCDebug(networking) << "Creating new connection to" << sockAddr; #endif - it = _connectionsHash.insert(it, std::make_pair(sockAddr, std::move(connection))); + it = _connectionsHash.insert(it, std::make_pair(sockAddr, std::move(connection))); + } } - return *it->second; + return it->second.get(); } void Socket::clearConnections() { @@ -254,9 +302,35 @@ void Socket::messageFailed(Connection* connection, Packet::MessageNumber message } } +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); + } + } +} + void Socket::readPendingDatagrams() { int packetSizeWithHeader = -1; + while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) != -1) { + + // we're reading a packet so re-start the readyRead backup timer + _readyReadBackupTimer->start(); + + // grab a time point we can mark as the receive time of this packet + auto receiveTime = p_high_resolution_clock::now(); + // setup a HifiSockAddr to read into HifiSockAddr senderSockAddr; @@ -267,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) @@ -279,6 +357,7 @@ void Socket::readPendingDatagrams() { // we have a registered unfiltered handler for this HifiSockAddr - call that and return if (it->second) { auto basePacket = BasePacket::fromReceivedPacket(std::move(buffer), packetSizeWithHeader, senderSockAddr); + basePacket->setReceiveTime(receiveTime); it->second(std::move(basePacket)); } @@ -291,32 +370,42 @@ void Socket::readPendingDatagrams() { if (isControlPacket) { // setup a control packet from the data we just read auto controlPacket = ControlPacket::fromReceivedPacket(std::move(buffer), packetSizeWithHeader, senderSockAddr); + controlPacket->setReceiveTime(receiveTime); - // move this control packet to the matching connection - auto& connection = findOrCreateConnection(senderSockAddr); - connection.processControl(move(controlPacket)); + // move this control packet to the matching connection, if there is one + auto connection = findOrCreateConnection(senderSockAddr); + + if (connection) { + connection->processControl(move(controlPacket)); + } } else { // setup a Packet from the data we just read 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()) { // if this was a reliable packet then signal the matching connection with the sequence number - auto& connection = findOrCreateConnection(senderSockAddr); + auto connection = findOrCreateConnection(senderSockAddr); - if (!connection.processReceivedSequenceNumber(packet->getSequenceNumber(), - packet->getDataSize(), - packet->getPayloadSize())) { - // the connection indicated that we should not continue processing this packet + if (!connection || !connection->processReceivedSequenceNumber(packet->getSequenceNumber(), + packet->getDataSize(), + packet->getPayloadSize())) { + // the connection could not be created or indicated that we should not continue processing this packet continue; } } if (packet->isPartOfMessage()) { - auto& connection = findOrCreateConnection(senderSockAddr); - connection.queueReceivedMessagePacket(std::move(packet)); + auto connection = findOrCreateConnection(senderSockAddr); + if (connection) { + connection->queueReceivedMessagePacket(std::move(packet)); + } } else if (_packetHandler) { // call the verified packet callback to let it handle this packet _packetHandler(std::move(packet)); @@ -419,11 +508,26 @@ 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; } } + +#if (PR_BUILD || DEV_BUILD) + +void Socket::sendFakedHandshakeRequest(const HifiSockAddr& sockAddr) { + auto connection = findOrCreateConnection(sockAddr); + if (connection) { + connection->sendHandshakeRequest(); + } +} + +#endif diff --git a/libraries/networking/src/udt/Socket.h b/libraries/networking/src/udt/Socket.h index bc4393d4bd..1919e00b41 100644 --- a/libraries/networking/src/udt/Socket.h +++ b/libraries/networking/src/udt/Socket.h @@ -16,13 +16,14 @@ #include #include +#include #include #include #include #include "../HifiSockAddr.h" -#include "CongestionControl.h" +#include "TCPVegasCC.h" #include "Connection.h" //#define UDT_CONNECTION_DEBUG @@ -37,6 +38,7 @@ class PacketList; class SequenceNumber; using PacketFilterOperator = std::function; +using ConnectionCreationFilterOperator = std::function; using BasePacketHandler = std::function)>; using PacketHandler = std::function)>; @@ -45,10 +47,14 @@ using MessageFailureHandler = std::function; + public: using StatsVector = std::vector>; - Socket(QObject* object = 0); + Socket(QObject* object = 0, bool shouldChangeSocketOptions = true); quint16 localPort() const { return _udpSocket.localPort(); } @@ -68,6 +74,8 @@ public: void setPacketHandler(PacketHandler handler) { _packetHandler = handler; } void setMessageHandler(MessageHandler handler) { _messageHandler = handler; } void setMessageFailureHandler(MessageFailureHandler handler) { _messageFailureHandler = handler; } + void setConnectionCreationFilterOperator(ConnectionCreationFilterOperator filterOperator) + { _connectionCreationFilterOperator = filterOperator; } void addUnfilteredHandler(const HifiSockAddr& senderSockAddr, BasePacketHandler handler) { _unfilteredHandlers[senderSockAddr] = handler; } @@ -80,12 +88,20 @@ public: StatsVector sampleStatsForAllConnections(); +#if (PR_BUILD || DEV_BUILD) + void sendFakedHandshakeRequest(const HifiSockAddr& sockAddr); +#endif + +signals: + void clientHandshakeRequestComplete(const HifiSockAddr& sockAddr); + public slots: void cleanupConnection(HifiSockAddr sockAddr); void clearConnections(); private slots: void readPendingDatagrams(); + void checkForReadyReadBackup(); void rateControlSync(); void handleSocketError(QAbstractSocket::SocketError socketError); @@ -93,7 +109,8 @@ private slots: private: void setSystemBufferSizes(); - Connection& findOrCreateConnection(const HifiSockAddr& sockAddr); + Connection* findOrCreateConnection(const HifiSockAddr& sockAddr); + bool socketMatchesNodeOrDomain(const HifiSockAddr& sockAddr); // privatized methods used by UDTTest - they are private since they must be called on the Socket thread ConnectionStats::Stats sampleStatsForConnection(const HifiSockAddr& destination); @@ -109,7 +126,10 @@ private: PacketHandler _packetHandler; MessageHandler _messageHandler; MessageFailureHandler _messageFailureHandler; - + ConnectionCreationFilterOperator _connectionCreationFilterOperator; + + Mutex _unreliableSequenceNumbersMutex; + std::unordered_map _unfilteredHandlers; std::unordered_map _unreliableSequenceNumbers; std::unordered_map> _connectionsHash; @@ -117,9 +137,17 @@ private: int _synInterval { 10 }; // 10ms QTimer* _synTimer { nullptr }; + QTimer* _readyReadBackupTimer { nullptr }; + int _maxBandwidth { -1 }; - - std::unique_ptr _ccFactory { new CongestionControlFactory() }; + + std::unique_ptr _ccFactory { new CongestionControlFactory() }; + + bool _shouldChangeSocketOptions { true }; + + int _lastPacketSizeRead { 0 }; + SequenceNumber _lastReceivedSequenceNumber; + HifiSockAddr _lastPacketSockAddr; friend UDTTest; }; diff --git a/libraries/networking/src/udt/TCPVegasCC.cpp b/libraries/networking/src/udt/TCPVegasCC.cpp new file mode 100644 index 0000000000..5738ea8421 --- /dev/null +++ b/libraries/networking/src/udt/TCPVegasCC.cpp @@ -0,0 +1,282 @@ +// +// TCPVegasCC.cpp +// libraries/networking/src/udt +// +// Created by Stephen Birarda on 2016-09-20. +// 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 +// + +#include "TCPVegasCC.h" + +#include +#include + +using namespace udt; +using namespace std::chrono; + +TCPVegasCC::TCPVegasCC() { + _packetSendPeriod = 0.0; + _congestionWindowSize = 2; + + setAckInterval(1); // TCP sends an ACK for every packet received + + // set our minimum RTT variables to the maximum possible value + // we can't do this as a member initializer until our VS has support for constexpr + _currentMinRTT = std::numeric_limits::max(); + _baseRTT = std::numeric_limits::max(); +} + +bool TCPVegasCC::onACK(SequenceNumber ack, p_high_resolution_clock::time_point receiveTime) { + auto it = _sentPacketTimes.find(ack); + + auto previousAck = _lastACK; + _lastACK = ack; + + if (it != _sentPacketTimes.end()) { + + // calculate the RTT (receive time - time ACK sent) + int lastRTT = duration_cast(receiveTime - it->second).count(); + + const int MAX_RTT_SAMPLE_MICROSECONDS = 10000000; + + if (lastRTT < 0) { + Q_ASSERT_X(false, __FUNCTION__, "calculated an RTT that is not > 0"); + return false; + } else if (lastRTT == 0) { + // we do not allow a zero microsecond RTT (as per the UNIX kernel implementation of TCP Vegas) + lastRTT = 1; + } else if (lastRTT > MAX_RTT_SAMPLE_MICROSECONDS) { + // we cap the lastRTT to MAX_RTT_SAMPLE_MICROSECONDS to avoid overflows in window size calculations + lastRTT = MAX_RTT_SAMPLE_MICROSECONDS; + } + + if (_ewmaRTT == -1) { + // first RTT sample - set _ewmaRTT to the value and set the variance to half the value + _ewmaRTT = lastRTT; + _rttVariance = lastRTT / 2; + } else { + // This updates the RTT using exponential weighted moving average + // This is the Jacobson's forumla for RTT estimation + // http://www.mathcs.emory.edu/~cheung/Courses/455/Syllabus/7-transport/Jacobson-88.pdf + + // Estimated RTT = (1 - x)(estimatedRTT) + (x)(sampleRTT) + // (where x = 0.125 via Jacobson) + + // Deviation = (1 - x)(deviation) + x |sampleRTT - estimatedRTT| + // (where x = 0.25 via Jacobson) + + static const int RTT_ESTIMATION_ALPHA = 8; + static const int RTT_ESTIMATION_VARIANCE_ALPHA = 4; + + _ewmaRTT = (_ewmaRTT * (RTT_ESTIMATION_ALPHA - 1) + lastRTT) / RTT_ESTIMATION_ALPHA; + _rttVariance = (_rttVariance * (RTT_ESTIMATION_VARIANCE_ALPHA- 1) + + abs(lastRTT - _ewmaRTT)) / RTT_ESTIMATION_VARIANCE_ALPHA; + } + + // add 1 to the number of ACKs during this RTT + ++_numACKs; + + // keep track of the lowest RTT during connection + _baseRTT = std::min(_baseRTT, lastRTT); + + // find the min RTT during the last RTT + _currentMinRTT = std::min(_currentMinRTT, lastRTT); + + auto sinceLastAdjustment = duration_cast(p_high_resolution_clock::now() - _lastAdjustmentTime).count(); + if (sinceLastAdjustment >= _ewmaRTT) { + performCongestionAvoidance(ack); + } + + // remove this sent packet time from the hash + _sentPacketTimes.erase(it); + } + + ++_numACKSinceFastRetransmit; + + // perform the fast re-transmit check if this is a duplicate ACK or if this is the first or second ACK + // after a previous fast re-transmit + if (ack == previousAck || _numACKSinceFastRetransmit < 3) { + // we may need to re-send ackNum + 1 if it has been more than our estimated timeout since it was sent + + auto it = _sentPacketTimes.find(ack + 1); + if (it != _sentPacketTimes.end()) { + auto estimatedTimeout = _ewmaRTT + _rttVariance * 4; + + auto now = p_high_resolution_clock::now(); + auto sinceSend = duration_cast(now - it->second).count(); + + if (sinceSend >= estimatedTimeout) { + // break out of slow start, we've decided this is loss + _slowStart = false; + + // reset the fast re-transmit counter + _numACKSinceFastRetransmit = 0; + + // return true so the caller knows we needed a fast re-transmit + return true; + } + } + + // if this is the 3rd duplicate ACK, we fallback to Reno's fast re-transmit + static const int RENO_FAST_RETRANSMIT_DUPLICATE_COUNT = 3; + + ++_duplicateACKCount; + + if (ack == previousAck && _duplicateACKCount == RENO_FAST_RETRANSMIT_DUPLICATE_COUNT) { + // break out of slow start, we just hit loss + _slowStart = false; + + // reset our fast re-transmit counters + _numACKSinceFastRetransmit = 0; + _duplicateACKCount = 0; + + // return true so the caller knows we needed a fast re-transmit + return true; + } + } else { + _duplicateACKCount = 0; + } + + // ACK processed, no fast re-transmit required + return false; +} + +void TCPVegasCC::performCongestionAvoidance(udt::SequenceNumber ack) { + static int VEGAS_ALPHA_SEGMENTS = 4; + static int VEGAS_BETA_SEGMENTS = 6; + static int VEGAS_GAMMA_SEGMENTS = 1; + + // http://pages.cs.wisc.edu/~akella/CS740/S08/740-Papers/BOP94.pdf + // Use the Vegas algorithm to see if we should + // increase or decrease the congestion window size, and by how much + + // Grab the minimum RTT seen during the last RTT (since the last performed congestion avoidance) + + // Taking the min avoids the effects of delayed ACKs + // (though congestion may be noticed a bit later) + int rtt = _currentMinRTT; + + int64_t windowSizeDiff = (int64_t) _congestionWindowSize * (rtt - _baseRTT) / _baseRTT; + + if (_numACKs <= 2) { + performRenoCongestionAvoidance(ack); + } else { + if (_slowStart) { + if (windowSizeDiff > VEGAS_GAMMA_SEGMENTS) { + // we're going too fast - this breaks us out of slow start and we switch to linear increase/decrease + _slowStart = false; + + int expectedWindowSize = _congestionWindowSize * _baseRTT / rtt; + _baseRTT = std::numeric_limits::max(); + + // drop the congestion window size to the expected size, if smaller + _congestionWindowSize = std::min(_congestionWindowSize, expectedWindowSize + 1); + + } else if (++_slowStartOddAdjust & 1) { + // we're in slow start and not going too fast + // this means that once every second RTT we perform exponential congestion window growth + _congestionWindowSize *= 2; + } + } else { + // this is the normal linear increase/decrease of the Vegas algorithm + // to figure out where the congestion window should be + if (windowSizeDiff > VEGAS_BETA_SEGMENTS) { + // the old congestion window was too fast (difference > beta) + // so reduce it to slow down + --_congestionWindowSize; + + } else if (windowSizeDiff < VEGAS_ALPHA_SEGMENTS) { + // there aren't enough packets on the wire, add more to the congestion window + ++_congestionWindowSize; + } else { + // sending rate seems good, no congestion window adjustment + } + } + } + + // we never allow the congestion window to be smaller than two packets + static int VEGAS_CW_MIN_PACKETS = 2; + if (_congestionWindowSize < VEGAS_CW_MIN_PACKETS) { + _congestionWindowSize = VEGAS_CW_MIN_PACKETS; + } else if (_congestionWindowSize > udt::MAX_PACKETS_IN_FLIGHT) { + _congestionWindowSize = udt::MAX_PACKETS_IN_FLIGHT; + } + + // mark this as the last adjustment time + _lastAdjustmentTime = p_high_resolution_clock::now(); + + // reset our state for the next RTT + _currentMinRTT = std::numeric_limits::max(); + + // reset our count of collected RTT samples + _numACKs = 0; +} + +bool TCPVegasCC::isCongestionWindowLimited() { + if (_slowStart) { + return true; + } else { + return seqlen(_sendCurrSeqNum, _lastACK) < _congestionWindowSize; + } +} + +void TCPVegasCC::performRenoCongestionAvoidance(SequenceNumber ack) { + if (!isCongestionWindowLimited()) { + return; + } + + int numAcked = _numACKs; + + if (_slowStart) { + // while in slow start we grow the congestion window by the number of ACKed packets + // allowing it to grow as high as the slow start threshold + int congestionWindow = _congestionWindowSize + numAcked; + + if (congestionWindow > udt::MAX_PACKETS_IN_FLIGHT) { + // we're done with slow start, set the congestion window to the slow start threshold + _congestionWindowSize = udt::MAX_PACKETS_IN_FLIGHT; + + // figure out how many left over ACKs we should apply using the regular reno congestion avoidance + numAcked = congestionWindow - udt::MAX_PACKETS_IN_FLIGHT; + } else { + _congestionWindowSize = congestionWindow; + numAcked = 0; + } + } + + // grab the size of the window prior to reno additive increase + int preAIWindowSize = _congestionWindowSize; + + if (numAcked > 0) { + // Once we are out of slow start, we use additive increase to grow the window slowly. + // We grow the congestion window by a single packet everytime the entire congestion window is sent. + + // If credits accumulated at a higher preAIWindowSize, apply them gently now. + if (_ackAICount >= preAIWindowSize) { + _ackAICount = 0; + ++_congestionWindowSize; + } + + // increase the window size by (1 / window size) for every ACK received + _ackAICount += numAcked; + if (_ackAICount >= preAIWindowSize) { + // when _ackAICount % preAIWindowSize == 0 then _ackAICount is 0 + // when _ackAICount % preAIWindowSize != 0 then _ackAICount is _ackAICount - (_ackAICount % preAIWindowSize) + + int delta = _ackAICount / preAIWindowSize; + + _ackAICount -= delta * preAIWindowSize; + _congestionWindowSize += delta; + } + } +} + +void TCPVegasCC::onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { + if (_sentPacketTimes.find(seqNum) == _sentPacketTimes.end()) { + _sentPacketTimes[seqNum] = timePoint; + } +} + diff --git a/libraries/networking/src/udt/TCPVegasCC.h b/libraries/networking/src/udt/TCPVegasCC.h new file mode 100644 index 0000000000..862ea36d8f --- /dev/null +++ b/libraries/networking/src/udt/TCPVegasCC.h @@ -0,0 +1,77 @@ +// +// TCPVegasCC.h +// libraries/networking/src/udt +// +// Created by Stephen Birarda on 2016-09-20. +// 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 +// + +#pragma once + +#ifndef hifi_TCPVegasCC_h +#define hifi_TCPVegasCC_h + +#include + +#include "CongestionControl.h" +#include "Constants.h" + +namespace udt { + + +class TCPVegasCC : public CongestionControl { +public: + TCPVegasCC(); + + virtual bool onACK(SequenceNumber ackNum, p_high_resolution_clock::time_point receiveTime) override; + virtual void onLoss(SequenceNumber rangeStart, SequenceNumber rangeEnd) override {}; + virtual void onTimeout() override {}; + + virtual bool shouldNAK() override { return false; } + virtual bool shouldACK2() override { return false; } + virtual bool shouldProbe() override { return false; } + + virtual void onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) override; + +protected: + virtual void performCongestionAvoidance(SequenceNumber ack); + virtual void setInitialSendSequenceNumber(SequenceNumber seqNum) override { _lastACK = seqNum - 1; } +private: + bool isCongestionWindowLimited(); + void performRenoCongestionAvoidance(SequenceNumber ack); + + using PacketTimeList = std::map; + PacketTimeList _sentPacketTimes; // Map of sequence numbers to sent time + + p_high_resolution_clock::time_point _lastAdjustmentTime; // Time of last congestion control adjustment + + bool _slowStart { true }; // Marker for slow start phase + + SequenceNumber _lastACK; // Sequence number of last packet that was ACKed + + int _numACKSinceFastRetransmit { 3 }; // Number of ACKs received since fast re-transmit, default avoids immediate re-transmit + + int _currentMinRTT; // Current min RTT during last RTT (since last congestion avoidance check), in microseconds + int _baseRTT; // Lowest RTT during connection, in microseconds + int _ewmaRTT { -1 }; // Exponential weighted moving average RTT + int _rttVariance { 0 }; // Variance in collected RTT values + + int _numACKs { 0 }; // Number of ACKs received during the last RTT (since last performed congestion avoidance) + + int _ackAICount { 0 }; // Counter for number of ACKs received for Reno additive increase + int _duplicateACKCount { 0 }; // Counter for duplicate ACKs received + + int _slowStartOddAdjust { 0 }; // Marker for every window adjustment every other RTT in slow-start + +}; + +} + + + + + +#endif // hifi_TCPVegasCC_h 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/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 19207f13ed..5c85f8fc50 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -524,7 +524,7 @@ void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) { } void CharacterController::preSimulation() { - if (_enabled && _dynamicsWorld) { + if (_enabled && _dynamicsWorld && _rigidBody) { quint64 now = usecTimestampNow(); // slam body to where it is supposed to be @@ -632,9 +632,10 @@ void CharacterController::preSimulation() { void CharacterController::postSimulation() { // postSimulation() exists for symmetry and just in case we need to do something here later - - btVector3 velocity = _rigidBody->getLinearVelocity(); - _velocityChange = velocity - _preSimulationVelocity; + if (_enabled && _dynamicsWorld && _rigidBody) { + btVector3 velocity = _rigidBody->getLinearVelocity(); + _velocityChange = velocity - _preSimulationVelocity; + } } diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 1c6d5d535e..ba002d925c 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -11,6 +11,8 @@ #include +#include + #include "CharacterController.h" #include "ObjectMotionState.h" #include "PhysicsEngine.h" @@ -286,6 +288,47 @@ void PhysicsEngine::stepSimulation() { } } +void PhysicsEngine::harvestPerformanceStats() { + // unfortunately the full context names get too long for our stats presentation format + //QString contextName = PerformanceTimer::getContextName(); // TODO: how to show full context name? + QString contextName("..."); + + CProfileIterator* profileIterator = CProfileManager::Get_Iterator(); + if (profileIterator) { + // hunt for stepSimulation context + profileIterator->First(); + for (int32_t childIndex = 0; !profileIterator->Is_Done(); ++childIndex) { + if (QString(profileIterator->Get_Current_Name()) == "stepSimulation") { + profileIterator->Enter_Child(childIndex); + recursivelyHarvestPerformanceStats(profileIterator, contextName); + break; + } + profileIterator->Next(); + } + } +} + +void PhysicsEngine::recursivelyHarvestPerformanceStats(CProfileIterator* profileIterator, QString contextName) { + QString parentContextName = contextName + QString("/") + QString(profileIterator->Get_Current_Parent_Name()); + // get the stats for the children + int32_t numChildren = 0; + profileIterator->First(); + while (!profileIterator->Is_Done()) { + QString childContextName = parentContextName + QString("/") + QString(profileIterator->Get_Current_Name()); + uint64_t time = (uint64_t)((btScalar)MSECS_PER_SECOND * profileIterator->Get_Current_Total_Time()); + PerformanceTimer::addTimerRecord(childContextName, time); + profileIterator->Next(); + ++numChildren; + } + // recurse the children + for (int32_t i = 0; i < numChildren; ++i) { + profileIterator->Enter_Child(i); + recursivelyHarvestPerformanceStats(profileIterator, contextName); + } + // retreat back to parent + profileIterator->Enter_Parent(); +} + void PhysicsEngine::doOwnershipInfection(const btCollisionObject* objectA, const btCollisionObject* objectB) { BT_PROFILE("ownershipInfection"); diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 72a41b391a..bbafbb06b6 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -59,6 +59,7 @@ public: void reinsertObject(ObjectMotionState* object); void stepSimulation(); + void harvestPerformanceStats(); void updateContactMap(); bool hasOutgoingChanges() const { return _hasOutgoingChanges; } @@ -89,6 +90,7 @@ public: private: void addObjectToDynamicsWorld(ObjectMotionState* motionState); + void recursivelyHarvestPerformanceStats(CProfileIterator* profileIterator, QString contextName); /// \brief bump any objects that touch this one, then remove contact info void bumpAndPruneContacts(ObjectMotionState* motionState); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index eac08716a1..3a9107390a 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -115,6 +115,10 @@ public: return false; } + virtual bool setExtraLaser(HandLaserMode mode, const vec4& color, const glm::vec3& sensorSpaceStart, const vec3& sensorSpaceDirection) { + return false; + } + virtual bool suppressKeyboard() { return false; } virtual void unsuppressKeyboard() {}; virtual bool isKeyboardVisible() { return false; } @@ -135,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/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 4baf20c338..21b80e2370 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -276,4 +276,30 @@ void PluginManager::saveSettings() { saveInputPluginSettings(getInputPlugins()); } +void PluginManager::shutdown() { + for (auto inputPlugin : getInputPlugins()) { + if (inputPlugin->isActive()) { + inputPlugin->deactivate(); + } + } + + for (auto displayPlugins : getDisplayPlugins()) { + if (displayPlugins->isActive()) { + displayPlugins->deactivate(); + } + } + + auto loadedPlugins = getLoadedPlugins(); + // Now grab the dynamic plugins + for (auto loader : getLoadedPlugins()) { + InputProvider* inputProvider = qobject_cast(loader->instance()); + if (inputProvider) { + inputProvider->destroyInputPlugins(); + } + DisplayProvider* displayProvider = qobject_cast(loader->instance()); + if (displayProvider) { + displayProvider->destroyDisplayPlugins(); + } + } +} #endif diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 30d52298da..83aad7abcd 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -28,6 +28,8 @@ public: void disableInputs(const QStringList& inputs); void saveSettings(); void setContainer(PluginContainer* container) { _container = container; } + + void shutdown(); private: PluginContainer* _container { nullptr }; }; diff --git a/libraries/plugins/src/plugins/RuntimePlugin.h b/libraries/plugins/src/plugins/RuntimePlugin.h index 9bf15f344d..bb6f0251c2 100644 --- a/libraries/plugins/src/plugins/RuntimePlugin.h +++ b/libraries/plugins/src/plugins/RuntimePlugin.h @@ -19,6 +19,7 @@ public: virtual ~DisplayProvider() {} virtual DisplayPluginList getDisplayPlugins() = 0; + virtual void destroyDisplayPlugins() = 0; }; #define DisplayProvider_iid "com.highfidelity.plugins.display" @@ -29,6 +30,7 @@ class InputProvider { public: virtual ~InputProvider() {} virtual InputPluginList getInputPlugins() = 0; + virtual void destroyInputPlugins() = 0; }; #define InputProvider_iid "com.highfidelity.plugins.input" 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/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 4d8403fc4d..4a8f4d1dbc 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -75,11 +75,11 @@ void AmbientOcclusionFramebuffer::allocate() { auto height = _frameSize.y; _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 8147b06846..2941197e6d 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -29,6 +29,14 @@ Antialiasing::Antialiasing() { + _geometryId = DependencyManager::get()->allocateID(); +} + +Antialiasing::~Antialiasing() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { @@ -41,11 +49,10 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { if (!_antialiasingBuffer) { // Link the antialiasing FBO to texture - _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); - _antialiasingTexture->setSource("Antialiasing::_antialiasingTexture"); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } @@ -151,7 +158,7 @@ void Antialiasing::run(const render::SceneContextPointer& sceneContext, const re glm::vec2 topRight(1.0f, 1.0f); glm::vec2 texCoordTopLeft(0.0f, 0.0f); glm::vec2 texCoordBottomRight(1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color, _geometryId); // Blend step @@ -160,6 +167,6 @@ void Antialiasing::run(const render::SceneContextPointer& sceneContext, const re batch.setFramebuffer(sourceBuffer); batch.setPipeline(getBlendPipeline()); - DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color, _geometryId); }); } diff --git a/libraries/render-utils/src/AntialiasingEffect.h b/libraries/render-utils/src/AntialiasingEffect.h index f0992ed843..bc0ab28d95 100644 --- a/libraries/render-utils/src/AntialiasingEffect.h +++ b/libraries/render-utils/src/AntialiasingEffect.h @@ -29,6 +29,7 @@ public: using JobModel = render::Job::ModelI; Antialiasing(); + ~Antialiasing(); void configure(const Config& config) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceBuffer); @@ -46,7 +47,7 @@ private: gpu::PipelinePointer _antialiasingPipeline; gpu::PipelinePointer _blendPipeline; - + int _geometryId { 0 }; }; #endif // hifi_AntialiasingEffect_h diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 09dd4ff82a..d0e2859e90 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -237,6 +237,14 @@ DebugDeferredBuffer::DebugDeferredBuffer() { CustomPipeline pipeline; pipeline.info = QFileInfo(QString::fromStdString(CUSTOM_FILE)); _customPipelines.emplace(CUSTOM_FILE, pipeline); + _geometryId = DependencyManager::get()->allocateID(); +} + +DebugDeferredBuffer::~DebugDeferredBuffer() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_geometryId); + } } std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string customFile) { @@ -410,6 +418,14 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Lighting, deferredFramebuffer->getLightingTexture()); } + auto deferredLightingEffect = DependencyManager::get(); + assert(deferredLightingEffect->getLightStage()->getNumLights() > 0); + auto lightAndShadow = deferredLightingEffect->getLightStage()->getLightAndShadow(0); + const auto& globalShadow = lightAndShadow.second; + if (globalShadow) { + batch.setResourceTexture(Shadow, globalShadow->map); + } + if (linearDepthTarget) { batch.setResourceTexture(LinearDepth, linearDepthTarget->getLinearDepthTexture()); batch.setResourceTexture(HalfLinearDepth, linearDepthTarget->getHalfLinearDepthTexture()); @@ -426,7 +442,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren const glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); const glm::vec2 bottomLeft(_size.x, _size.y); const glm::vec2 topRight(_size.z, _size.w); - geometryBuffer->renderQuad(batch, bottomLeft, topRight, color); + geometryBuffer->renderQuad(batch, bottomLeft, topRight, color, _geometryId); batch.setResourceTexture(Albedo, nullptr); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index da78ac081a..eb1a541d2e 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -42,6 +42,7 @@ public: using JobModel = render::Job::ModelI; DebugDeferredBuffer(); + ~DebugDeferredBuffer(); void configure(const Config& config); void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); @@ -96,6 +97,7 @@ private: StandardPipelines _pipelines; CustomPipelines _customPipelines; + int _geometryId { 0 }; }; #endif // hifi_DebugDeferredBuffer_h \ No newline at end of file diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 067687f9ef..e8783e0e0d 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -43,8 +43,8 @@ void DeferredFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuf void DeferredFramebuffer::allocate() { - _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("deferred")); + _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create("deferredDepthColor")); auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto linearFormat = gpu::Element::COLOR_RGBA_32; @@ -54,11 +54,8 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredColorTexture->setSource("DeferredFramebuffer::_deferredColorTexture"); _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); - _deferredNormalTexture->setSource("DeferredFramebuffer::_deferredNormalTexture"); _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredSpecularTexture->setSource("DeferredFramebuffer::_deferredSpecularTexture"); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -69,7 +66,6 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); - _primaryDepthTexture->setSource("DeferredFramebuffer::_primaryDepthTexture"); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -80,8 +76,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); - _lightingTexture->setSource("DeferredFramebuffer::_lightingTexture"); - _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 1a3e4de9a0..c365ab6a9f 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -492,12 +492,11 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con } if (!_primaryFramebuffer) { - _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("deferredPrimary")); auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); - primaryColorTexture->setSource("PreparePrimaryFramebuffer::primaryColorTexture"); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); @@ -505,7 +504,6 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); - primaryDepthTexture->setSource("PreparePrimaryFramebuffer::primaryDepthTexture"); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 8930792d11..27429595b4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -36,7 +36,7 @@ void FramebufferCache::createPrimaryFramebuffer() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); _selfieFramebuffer->setRenderBuffer(0, tex); @@ -47,7 +47,7 @@ void FramebufferCache::createPrimaryFramebuffer() { gpu::FramebufferPointer FramebufferCache::getFramebuffer() { std::unique_lock lock(_mutex); if (_cachedFramebuffers.empty()) { - _cachedFramebuffers.push_back(gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _frameBufferSize.width(), _frameBufferSize.height()))); + _cachedFramebuffers.push_back(gpu::FramebufferPointer(gpu::Framebuffer::create("cached", gpu::Element::COLOR_SRGBA_32, _frameBufferSize.width(), _frameBufferSize.height()))); } gpu::FramebufferPointer result = _cachedFramebuffers.front(); _cachedFramebuffers.pop_front(); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index a3a9303156..dcd4961c32 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -441,6 +441,34 @@ GeometryCache::~GeometryCache() { #endif //def WANT_DEBUG } +void GeometryCache::releaseID(int id) { + _registeredQuad3DTextures.remove(id); + _lastRegisteredQuad2DTexture.remove(id); + _registeredQuad2DTextures.remove(id); + _lastRegisteredQuad3D.remove(id); + _registeredQuad3D.remove(id); + + _lastRegisteredQuad2D.remove(id); + _registeredQuad2D.remove(id); + + _lastRegisteredBevelRects.remove(id); + _registeredBevelRects.remove(id); + + _lastRegisteredLine3D.remove(id); + _registeredLine3DVBOs.remove(id); + + _lastRegisteredLine2D.remove(id); + _registeredLine2DVBOs.remove(id); + + _registeredVertices.remove(id); + + _lastRegisteredDashedLines.remove(id); + _registeredDashedLines.remove(id); + + _lastRegisteredGridBuffer.remove(id); + _registeredGridBuffers.remove(id); +} + void setupBatchInstance(gpu::Batch& batch, gpu::BufferPointer colorBuffer) { gpu::BufferView colorView(colorBuffer, COLOR_ELEMENT); batch.setInputBuffer(gpu::Stream::COLOR, colorView); @@ -497,8 +525,7 @@ void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, co Vec2FloatPairPair key(majorKey, minorKey); // Make the gridbuffer - if ((registered && (!_registeredGridBuffers.contains(id) || _lastRegisteredGridBuffer[id] != key)) || - (!registered && !_gridBuffers.contains(key))) { + if (registered && (!_registeredGridBuffers.contains(id) || _lastRegisteredGridBuffer[id] != key)) { GridSchema gridSchema; GridBuffer gridBuffer = std::make_shared(sizeof(GridSchema), (const gpu::Byte*) &gridSchema); @@ -506,12 +533,8 @@ void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, co gridBuffer = _registeredGridBuffers[id]; } - if (registered) { - _registeredGridBuffers[id] = gridBuffer; - _lastRegisteredGridBuffer[id] = key; - } else { - _gridBuffers[key] = gridBuffer; - } + _registeredGridBuffers[id] = gridBuffer; + _lastRegisteredGridBuffer[id] = key; gridBuffer.edit().period = glm::vec4(majorRows, majorCols, minorRows, minorCols); gridBuffer.edit().offset.x = -(majorEdge / majorRows) / 2; @@ -524,7 +547,7 @@ void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, co } // Set the grid pipeline - useGridPipeline(batch, registered ? _registeredGridBuffers[id] : _gridBuffers[key], isLayered); + useGridPipeline(batch, _registeredGridBuffers[id], isLayered); renderQuad(batch, minCorner, maxCorner, MIN_TEX_COORD, MAX_TEX_COORD, color, id); } @@ -777,7 +800,7 @@ void GeometryCache::renderVertices(gpu::Batch& batch, gpu::Primitive primitiveTy void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id) { bool registered = (id != UNKNOWN_ID); Vec3Pair key(glm::vec3(x, y, 0.0f), glm::vec3(width, height, bevelDistance)); - BatchItemDetails& details = registered ? _registeredBevelRects[id] : _bevelRects[key]; + BatchItemDetails& details = _registeredBevelRects[id]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { Vec3Pair& lastKey = _lastRegisteredBevelRects[id]; @@ -878,7 +901,7 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, const glm::vec4& color, int id) { bool registered = (id != UNKNOWN_ID); Vec4Pair key(glm::vec4(minCorner.x, minCorner.y, maxCorner.x, maxCorner.y), color); - BatchItemDetails& details = registered ? _registeredQuad2D[id] : _quad2D[key]; + BatchItemDetails& details = _registeredQuad2D[id]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { @@ -963,14 +986,13 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const glm::vec2& texCoordMinCorner, const glm::vec2& texCoordMaxCorner, const glm::vec4& color, int id) { - bool registered = (id != UNKNOWN_ID); Vec4PairVec4 key(Vec4Pair(glm::vec4(minCorner.x, minCorner.y, maxCorner.x, maxCorner.y), glm::vec4(texCoordMinCorner.x, texCoordMinCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y)), color); - BatchItemDetails& details = registered ? _registeredQuad2DTextures[id] : _quad2DTextures[key]; + BatchItemDetails& details = _registeredQuad2DTextures[id]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed - if (registered && details.isCreated) { + if (details.isCreated) { Vec4PairVec4& lastKey = _lastRegisteredQuad2DTexture[id]; if (lastKey != key) { details.clear(); @@ -1049,7 +1071,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color, int id) { bool registered = (id != UNKNOWN_ID); Vec3PairVec4 key(Vec3Pair(minCorner, maxCorner), color); - BatchItemDetails& details = registered ? _registeredQuad3D[id] : _quad3D[key]; + BatchItemDetails& details = _registeredQuad3D[id]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { @@ -1145,7 +1167,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons Vec4Pair(glm::vec4(texCoordTopLeft.x, texCoordTopLeft.y, texCoordBottomRight.x, texCoordBottomRight.y), color)); - BatchItemDetails& details = registered ? _registeredQuad3DTextures[id] : _quad3DTextures[key]; + BatchItemDetails& details = _registeredQuad3DTextures[id]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { @@ -1226,7 +1248,7 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, bool registered = (id != UNKNOWN_ID); Vec3PairVec2Pair key(Vec3Pair(start, end), Vec2Pair(glm::vec2(color.x, color.y), glm::vec2(color.z, color.w))); - BatchItemDetails& details = registered ? _registeredDashedLines[id] : _dashedLines[key]; + BatchItemDetails& details = _registeredDashedLines[id]; // if this is a registered , and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { @@ -1395,7 +1417,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm bool registered = (id != UNKNOWN_ID); Vec3Pair key(p1, p2); - BatchItemDetails& details = registered ? _registeredLine3DVBOs[id] : _line3DVBOs[key]; + BatchItemDetails& details = _registeredLine3DVBOs[id]; int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) | ((int(color1.y * 255.0f) & 0xFF) << 8) | @@ -1484,7 +1506,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm bool registered = (id != UNKNOWN_ID); Vec2Pair key(p1, p2); - BatchItemDetails& details = registered ? _registeredLine2DVBOs[id] : _line2DVBOs[key]; + BatchItemDetails& details = _registeredLine2DVBOs[id]; int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) | ((int(color1.y * 255.0f) & 0xFF) << 8) | @@ -1599,7 +1621,7 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const Vec3Pair key(p1, p2); bool registered = (id != UNKNOWN_ID); - BatchItemDetails& details = registered ? _registeredLine3DVBOs[id] : _line3DVBOs[key]; + BatchItemDetails& details = _registeredLine3DVBOs[id]; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | ((int(color.y * 255.0f) & 0xFF) << 8) | diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 244683759d..6e6ac89a8f 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -148,6 +148,7 @@ public: }; int allocateID() { return _nextID++; } + void releaseID(int id); static const int UNKNOWN_ID; // Bind the pipeline and get the state to render static geometry @@ -229,73 +230,79 @@ public: void renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, int majorRows, int majorCols, float majorEdge, int minorRows, int minorCols, float minorEdge, - const glm::vec4& color, bool isLayered, int id = UNKNOWN_ID); + const glm::vec4& color, bool isLayered, int id); void renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, - int rows, int cols, float edge, const glm::vec4& color, bool isLayered, int id = UNKNOWN_ID) { + int rows, int cols, float edge, const glm::vec4& color, bool isLayered, int id) { renderGrid(batch, minCorner, maxCorner, rows, cols, edge, 0, 0, 0.0f, color, isLayered, id); } - void renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id = UNKNOWN_ID); + void renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id); - void renderUnitQuad(gpu::Batch& batch, const glm::vec4& color = glm::vec4(1), int id = UNKNOWN_ID); + void renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id); - void renderQuad(gpu::Batch& batch, int x, int y, int width, int height, const glm::vec4& color, int id = UNKNOWN_ID) + void renderUnitQuad(gpu::Batch& batch, int id) { + renderUnitQuad(batch, glm::vec4(1), id); + } + + void renderQuad(gpu::Batch& batch, int x, int y, int width, int height, const glm::vec4& color, int id) { renderQuad(batch, glm::vec2(x,y), glm::vec2(x + width, y + height), color, id); } // TODO: I think there's a bug in this version of the renderQuad() that's not correctly rebuilding the vbos // if the color changes by the corners are the same, as evidenced by the audio meter which should turn white // when it's clipping - void renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, const glm::vec4& color, int id = UNKNOWN_ID); + void renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, const glm::vec4& color, int id); void renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, const glm::vec2& texCoordMinCorner, const glm::vec2& texCoordMaxCorner, - const glm::vec4& color, int id = UNKNOWN_ID); + const glm::vec4& color, int id); - void renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color, int id = UNKNOWN_ID); + void renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color, int id); void renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, const glm::vec3& bottomLeft, const glm::vec3& bottomRight, const glm::vec3& topRight, const glm::vec2& texCoordTopLeft, const glm::vec2& texCoordBottomLeft, const glm::vec2& texCoordBottomRight, const glm::vec2& texCoordTopRight, - const glm::vec4& color, int id = UNKNOWN_ID); + const glm::vec4& color, int id); - void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& color, int id = UNKNOWN_ID) + void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& color, int id) { renderLine(batch, p1, p2, color, color, id); } void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec3& color1, const glm::vec3& color2, int id = UNKNOWN_ID) + const glm::vec3& color1, const glm::vec3& color2, int id) { renderLine(batch, p1, p2, glm::vec4(color1, 1.0f), glm::vec4(color2, 1.0f), id); } void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, int id = UNKNOWN_ID) + const glm::vec4& color, int id) { renderLine(batch, p1, p2, color, color, id); } void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color1, const glm::vec4& color2, int id = UNKNOWN_ID); + const glm::vec4& color1, const glm::vec4& color2, int id); void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity = 1.0f, float glowWidth = 0.05f, int id = UNKNOWN_ID); + const glm::vec4& color, float glowIntensity, float glowWidth, int id); - void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, - int id = UNKNOWN_ID) + void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color, int id) + { renderGlowLine(batch, p1, p2, color, 1.0f, 0.05f, id); } + + void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { renderDashedLine(batch, start, end, color, 0.05f, 0.025f, id); } void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, - const float dash_length, const float gap_length, int id = UNKNOWN_ID); + const float dash_length, const float gap_length, int id); - void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec3& color, int id = UNKNOWN_ID) + void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec3& color, int id) { renderLine(batch, p1, p2, glm::vec4(color, 1.0f), id); } - void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec4& color, int id = UNKNOWN_ID) + void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec4& color, int id) { renderLine(batch, p1, p2, color, color, id); } void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, - const glm::vec3& color1, const glm::vec3& color2, int id = UNKNOWN_ID) + const glm::vec3& color1, const glm::vec3& color2, int id) { renderLine(batch, p1, p2, glm::vec4(color1, 1.0f), glm::vec4(color2, 1.0f), id); } void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, - const glm::vec4& color1, const glm::vec4& color2, int id = UNKNOWN_ID); + const glm::vec4& color1, const glm::vec4& color2, int id); void updateVertices(int id, const QVector& points, const glm::vec4& color); void updateVertices(int id, const QVector& points, const QVector& colors); @@ -380,41 +387,32 @@ private: int _nextID{ 1 }; QHash _lastRegisteredQuad3DTexture; - QHash _quad3DTextures; QHash _registeredQuad3DTextures; QHash _lastRegisteredQuad2DTexture; - QHash _quad2DTextures; QHash _registeredQuad2DTextures; QHash _lastRegisteredQuad3D; - QHash _quad3D; QHash _registeredQuad3D; QHash _lastRegisteredQuad2D; - QHash _quad2D; QHash _registeredQuad2D; QHash _lastRegisteredBevelRects; - QHash _bevelRects; QHash _registeredBevelRects; QHash _lastRegisteredLine3D; - QHash _line3DVBOs; QHash _registeredLine3DVBOs; QHash _lastRegisteredLine2D; - QHash _line2DVBOs; QHash _registeredLine2DVBOs; QHash _registeredVertices; QHash _lastRegisteredDashedLines; - QHash _dashedLines; QHash _registeredDashedLines; QHash _lastRegisteredGridBuffer; - QHash _gridBuffers; QHash _registeredGridBuffers; gpu::ShaderPointer _simpleShader; diff --git a/libraries/render-utils/src/HitEffect.cpp b/libraries/render-utils/src/HitEffect.cpp index febfd1942e..9fb4c5bcd4 100644 --- a/libraries/render-utils/src/HitEffect.cpp +++ b/libraries/render-utils/src/HitEffect.cpp @@ -33,6 +33,14 @@ HitEffect::HitEffect() { + _geometryId = DependencyManager::get()->allocateID(); +} + +HitEffect::~HitEffect() { + auto geometryCache = DependencyManager::get(); + if (_geometryId && geometryCache) { + geometryCache->releaseID(_geometryId); + } } const gpu::PipelinePointer& HitEffect::getHitEffectPipeline() { @@ -77,10 +85,10 @@ void HitEffect::run(const render::SceneContextPointer& sceneContext, const rende batch.setPipeline(getHitEffectPipeline()); - glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); - glm::vec2 bottomLeft(-1.0f, -1.0f); - glm::vec2 topRight(1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, color); + static const glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); + static const glm::vec2 bottomLeft(-1.0f, -1.0f); + static const glm::vec2 topRight(1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, color, _geometryId); }); } diff --git a/libraries/render-utils/src/HitEffect.h b/libraries/render-utils/src/HitEffect.h index 5252e63726..ea14ac231d 100644 --- a/libraries/render-utils/src/HitEffect.h +++ b/libraries/render-utils/src/HitEffect.h @@ -24,12 +24,14 @@ public: using JobModel = render::Job::Model; HitEffect(); + ~HitEffect(); void configure(const Config& config) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); const gpu::PipelinePointer& getHitEffectPipeline(); private: + int _geometryId { 0 }; gpu::PipelinePointer _hitEffectPipeline; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 28a1c3d579..6e1f57778b 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -862,8 +862,12 @@ void Model::setURL(const QUrl& url) { { render::PendingChanges pendingChanges; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - removeFromScene(scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + if (scene) { + removeFromScene(scene, pendingChanges); + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(renderutils) << "Model::setURL(), Unexpected null scene, possibly during application shutdown"; + } } _needsReload = true; diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 1fea7d6ced..188381b822 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -319,7 +319,7 @@ void diffuseProfileGPU(gpu::TexturePointer& profileMap, RenderArgs* args) { makePipeline = gpu::Pipeline::create(program, state); } - auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("diffuseProfile")); makeFramebuffer->setRenderBuffer(0, profileMap); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { @@ -356,7 +356,7 @@ void diffuseScatterGPU(const gpu::TexturePointer& profileMap, gpu::TexturePointe makePipeline = gpu::Pipeline::create(program, state); } - auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("diffuseScatter")); makeFramebuffer->setRenderBuffer(0, lut); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { @@ -393,7 +393,7 @@ void computeSpecularBeckmannGPU(gpu::TexturePointer& beckmannMap, RenderArgs* ar makePipeline = gpu::Pipeline::create(program, state); } - auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("computeSpecularBeckmann")); makeFramebuffer->setRenderBuffer(0, beckmannMap); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 1c12bcf996..f0ac56ac26 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -74,22 +74,19 @@ void LinearDepthFramebuffer::allocate() { // For Linear Depth: _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _linearDepthTexture->setSource("LinearDepthFramebuffer::_linearDepthTexture"); - _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _halfLinearDepthTexture->setSource("LinearDepthFramebuffer::_halfLinearDepthTexture"); _halfLinearDepthTexture->autoGenerateMips(5); _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _halfNormalTexture->setSource("LinearDepthFramebuffer::_halfNormalTexture"); - _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); _downsampleFramebuffer->setRenderBuffer(0, _halfLinearDepthTexture); _downsampleFramebuffer->setRenderBuffer(1, _halfNormalTexture); } @@ -308,18 +305,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto height = _frameSize.y; _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _curvatureTexture->setSource("SurfaceGeometryFramebuffer::_curvatureTexture"); - _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _lowCurvatureTexture->setSource("SurfaceGeometryFramebuffer::_lowCurvatureTexture"); - _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _blurringTexture->setSource("SurfaceGeometryFramebuffer::_blurringTexture"); - _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 144e1e0058..f8b5546b92 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -101,7 +101,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra } if (!_blurredFramebuffer) { - _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("blur")); // attach depthStencil if present in source //if (sourceFramebuffer->hasDepthStencil()) { @@ -124,7 +124,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra // The job output the blur result in a new Framebuffer spawning here. // Let s make sure it s ready for this if (!_outputFramebuffer) { - _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("blurOutput")); // attach depthStencil if present in source /* if (sourceFramebuffer->hasDepthStencil()) { diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index e3cfc9fa63..221251dae6 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -86,7 +86,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - const auto& key = item.getShapeKey(); + const auto key = item.getShapeKey(); if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 0b1d8fb55f..6bc42ac656 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -48,6 +48,10 @@ Scene::Scene(glm::vec3 origin, float size) : _items.push_back(Item()); // add the itemID #0 to nothing } +Scene::~Scene() { + qDebug() << "Scene::~Scene()"; +} + ItemID Scene::allocateID() { // Just increment and return the proevious value initialized at 0 return _IDAllocator.fetch_add(1); diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index b7cfc367b8..6b57a22a36 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -55,7 +55,7 @@ typedef std::queue PendingChangesQueue; class Scene { public: Scene(glm::vec3 origin, float size); - ~Scene() {} + ~Scene(); // This call is thread safe, can be called from anywhere to allocate a new ID ItemID allocateID(); diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 1065cc2e6a..00fa1f3ba5 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -17,6 +17,7 @@ #include #include #include +#include AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) : _engine(engine) @@ -86,3 +87,13 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb assetRequest->start(); } + +#if (PR_BUILD || DEV_BUILD) +void AssetScriptingInterface::sendFakedHandshake() { + auto nodeList = DependencyManager::get(); + SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); + + nodeList->sendFakedHandshakeRequestToNode(assetServer); +} + +#endif diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 85746ad36e..82d220ab34 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -28,6 +28,10 @@ public: Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); +#if (PR_BUILD || DEV_BUILD) + Q_INVOKABLE void sendFakedHandshake(); +#endif + protected: QSet _pendingRequests; QScriptEngine* _engine; diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index 15b3b6c853..964807894e 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -38,33 +38,47 @@ void BatchLoader::start() { _started = true; + if (_urls.size() == 0) { + _finished = true; + emit finished(_data); + 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(); + + // 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(scriptCache.data()); + + 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; } - checkFinished(); + + 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); } - - checkFinished(); } -void BatchLoader::checkFinished() { - if (!_finished && _urls.size() == _data.size()) { - _finished = true; - emit finished(_data); - } +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 cda040d219..a03a8d80c6 100644 --- a/libraries/script-engine/src/BatchLoader.h +++ b/libraries/script-engine/src/BatchLoader.h @@ -19,10 +19,22 @@ #include #include +#include + +class ScriptCacheSignalProxy : public QObject { + Q_OBJECT +public: + ScriptCacheSignalProxy(QObject* parent) : QObject(parent) { } + 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; }; diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index e9f20b5164..96e3d7e914 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -36,6 +36,19 @@ void ScriptCache::clearCache() { _scriptCache.clear(); } +void ScriptCache::clearATPScriptsFromCache() { + Lock lock(_containerLock); + qCDebug(scriptengine) << "Clearing ATP scripts from ScriptCache"; + for (auto it = _scriptCache.begin(); it != _scriptCache.end();) { + if (it.key().scheme() == "atp") { + qCDebug(scriptengine) << "Removing: " << it.key(); + it = _scriptCache.erase(it); + } else { + ++it; + } + } +} + QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) { QUrl url = ResourceManager::normalizeURL(unnormalizedURL); QString scriptContents; diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 17ba5c4b0a..5aac62b08b 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -39,6 +39,7 @@ class ScriptCache : public QObject, public Dependency { public: void clearCache(); + Q_INVOKABLE void clearATPScriptsFromCache(); void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 7a4265829b..776c7cfec6 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -1136,6 +1137,10 @@ QUrl ScriptEngine::resolvePath(const QString& include) const { return url; } +QUrl ScriptEngine::resourcesPath() const { + return QUrl::fromLocalFile(PathUtils::resourcesPath()); +} + void ScriptEngine::print(const QString& message) { emit printedMessage(message); } @@ -1199,6 +1204,11 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac } } + // If there are no URLs left to download, don't bother attempting to download anything and return early + if (urls.size() == 0) { + return; + } + BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 62db99a431..01088660ff 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -133,6 +133,7 @@ public: Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } Q_INVOKABLE void print(const QString& message); Q_INVOKABLE QUrl resolvePath(const QString& path) const; + Q_INVOKABLE QUrl resourcesPath() const; // Entity Script Related methods static void loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); @@ -145,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/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 818521ecc5..4e528ec52c 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -143,13 +143,13 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) { auto accountManager = DependencyManager::get(); - if (accountManager->hasValidAccessToken()) { - QUrlQuery urlQuery(_url.query()); - urlQuery.addQueryItem("access_token", accountManager->getAccountInfo().getAccessToken().token); - _url.setQuery(urlQuery); + if (_url.scheme() == "https" && accountManager->hasValidAccessToken()) { + static const QString HTTP_AUTHORIZATION_HEADER = "Authorization"; + QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; + _request.setRawHeader(HTTP_AUTHORIZATION_HEADER.toLocal8Bit(), bearerString.toLocal8Bit()); } - } + if (!username.isEmpty()) { _url.setUserName(username); } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 0c02cd5b03..3520a4dc44 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -34,7 +34,9 @@ const vec3& Vectors::UP = Vectors::UNIT_Y; const vec3& Vectors::FRONT = Vectors::UNIT_NEG_Z; const quat Quaternions::IDENTITY{ 1.0f, 0.0f, 0.0f, 0.0f }; +const quat Quaternions::X_180{ 0.0f, 1.0f, 0.0f, 0.0f }; const quat Quaternions::Y_180{ 0.0f, 0.0f, 1.0f, 0.0f }; +const quat Quaternions::Z_180{ 0.0f, 0.0f, 0.0f, 1.0f }; // Safe version of glm::mix; based on the code in Nick Bobick's article, // http://www.gamasutra.com/features/19980703/quaternions_01.htm (via Clyde, diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 898dfa873d..4aac913768 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -58,7 +58,9 @@ glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); class Quaternions { public: static const quat IDENTITY; + static const quat X_180; static const quat Y_180; + static const quat Z_180; }; class Vectors { diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index f003506aa7..d142e3f839 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -21,6 +21,8 @@ #include "LogHandler.h" +QMutex LogHandler::_mutex; + LogHandler& LogHandler::getInstance() { static LogHandler staticInstance; return staticInstance; @@ -63,8 +65,29 @@ const QString DATE_STRING_FORMAT = "MM/dd hh:mm:ss"; // the following will produce 11/18 13:55:36.999 const QString DATE_STRING_FORMAT_WITH_MILLISECONDS = "MM/dd hh:mm:ss.zzz"; +void LogHandler::setTargetName(const QString& targetName) { + QMutexLocker lock(&_mutex); + _targetName = targetName; +} + +void LogHandler::setShouldOutputProcessID(bool shouldOutputProcessID) { + QMutexLocker lock(&_mutex); + _shouldOutputProcessID = shouldOutputProcessID; +} + +void LogHandler::setShouldOutputThreadID(bool shouldOutputThreadID) { + QMutexLocker lock(&_mutex); + _shouldOutputThreadID = shouldOutputThreadID; +} + +void LogHandler::setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds) { + QMutexLocker lock(&_mutex); + _shouldDisplayMilliseconds = shouldDisplayMilliseconds; +} + + void LogHandler::flushRepeatedMessages() { - QMutexLocker locker(&_repeatedMessageLock); + QMutexLocker lock(&_mutex); QHash::iterator message = _repeatMessageCountHash.begin(); while (message != _repeatMessageCountHash.end()) { @@ -73,7 +96,9 @@ void LogHandler::flushRepeatedMessages() { .arg(message.value()).arg(message.key()).arg(_lastRepeatedMessage.value(message.key())); QMessageLogContext emptyContext; + lock.unlock(); printMessage(LogSuppressed, emptyContext, repeatMessage); + lock.relock(); } _lastRepeatedMessage.remove(message.key()); @@ -82,13 +107,13 @@ void LogHandler::flushRepeatedMessages() { } QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& context, const QString& message) { + QMutexLocker lock(&_mutex); if (message.isEmpty()) { return QString(); } if (type == LogDebug) { // for debug messages, check if this matches any of our regexes for repeated log messages - QMutexLocker locker(&_repeatedMessageLock); foreach(const QString& regexString, getInstance()._repeatedMessageRegexes) { QRegExp repeatRegex(regexString); if (repeatRegex.indexIn(message) != -1) { @@ -111,7 +136,6 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont } } if (type == LogDebug) { - QMutexLocker locker(&_onlyOnceMessageLock); // see if this message is one we should only print once foreach(const QString& regexString, getInstance()._onlyOnceMessageRegexes) { QRegExp onlyOnceRegex(regexString); @@ -163,11 +187,11 @@ void LogHandler::verboseMessageHandler(QtMsgType type, const QMessageLogContext& } const QString& LogHandler::addRepeatedMessageRegex(const QString& regexString) { - QMutexLocker locker(&_repeatedMessageLock); + QMutexLocker lock(&_mutex); return *_repeatedMessageRegexes.insert(regexString); } const QString& LogHandler::addOnlyOnceMessageRegex(const QString& regexString) { - QMutexLocker locker(&_onlyOnceMessageLock); + QMutexLocker lock(&_mutex); return *_onlyOnceMessageRegexes.insert(regexString); } diff --git a/libraries/shared/src/LogHandler.h b/libraries/shared/src/LogHandler.h index d346913dd3..890497b891 100644 --- a/libraries/shared/src/LogHandler.h +++ b/libraries/shared/src/LogHandler.h @@ -38,11 +38,11 @@ public: /// sets the target name to output via the verboseMessageHandler, called once before logging begins /// \param targetName the desired target name to output in logs - void setTargetName(const QString& targetName) { _targetName = targetName; } + void setTargetName(const QString& targetName); - void setShouldOutputProcessID(bool shouldOutputProcessID) { _shouldOutputProcessID = shouldOutputProcessID; } - void setShouldOutputThreadID(bool shouldOutputThreadID) { _shouldOutputThreadID = shouldOutputThreadID; } - void setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds) { _shouldDisplayMilliseconds = shouldDisplayMilliseconds; } + void setShouldOutputProcessID(bool shouldOutputProcessID); + void setShouldOutputThreadID(bool shouldOutputThreadID); + void setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds); QString printMessage(LogMsgType type, const QMessageLogContext& context, const QString &message); @@ -64,11 +64,11 @@ private: QSet _repeatedMessageRegexes; QHash _repeatMessageCountHash; QHash _lastRepeatedMessage; - QMutex _repeatedMessageLock; QSet _onlyOnceMessageRegexes; QHash _onlyOnceMessageCountHash; - QMutex _onlyOnceMessageLock; + + static QMutex _mutex; }; #endif // hifi_LogHandler_h diff --git a/libraries/shared/src/PerfStat.cpp b/libraries/shared/src/PerfStat.cpp index 615330e13f..d202fdb248 100644 --- a/libraries/shared/src/PerfStat.cpp +++ b/libraries/shared/src/PerfStat.cpp @@ -31,8 +31,8 @@ bool PerformanceWarning::_suppressShortTimings = false; // Destructor handles recording all of our stats PerformanceWarning::~PerformanceWarning() { quint64 end = usecTimestampNow(); - quint64 elapsedusec = (end - _start); - double elapsedmsec = elapsedusec / 1000.0; + quint64 elapsedUsec = (end - _start); + double elapsedmsec = elapsedUsec / 1000.0; if ((_alwaysDisplay || _renderWarningsOn) && elapsedmsec > 1) { if (elapsedmsec > 1000) { double elapsedsec = (end - _start) / 1000000.0; @@ -53,7 +53,7 @@ PerformanceWarning::~PerformanceWarning() { } // if the caller gave us a pointer to store the running total, track it now. if (_runningTotal) { - *_runningTotal += elapsedusec; + *_runningTotal += elapsedUsec; } if (_totalCalls) { *_totalCalls += 1; @@ -65,11 +65,11 @@ PerformanceWarning::~PerformanceWarning() { // ---------------------------------------------------------------------------- const quint64 STALE_STAT_PERIOD = 4 * USECS_PER_SECOND; -void PerformanceTimerRecord::tallyResult(const quint64& now) { +void PerformanceTimerRecord::tallyResult(const quint64& now) { if (_numAccumulations > 0) { - _numTallies++; - _movingAverage.updateAverage(_runningTotal - _lastTotal); - _lastTotal = _runningTotal; + _numTallies++; + _movingAverage.updateAverage(_runningTotal - _lastTotal); + _lastTotal = _runningTotal; _numAccumulations = 0; _expiry = now + STALE_STAT_PERIOD; } @@ -96,10 +96,10 @@ PerformanceTimer::PerformanceTimer(const QString& name) { PerformanceTimer::~PerformanceTimer() { if (_isActive && _start != 0) { - quint64 elapsedusec = (usecTimestampNow() - _start); + quint64 elapsedUsec = (usecTimestampNow() - _start); QString& fullName = _fullNames[QThread::currentThread()]; PerformanceTimerRecord& namedRecord = _records[fullName]; - namedRecord.accumulateResult(elapsedusec); + namedRecord.accumulateResult(elapsedUsec); fullName.resize(fullName.size() - (_name.size() + 1)); } } @@ -109,6 +109,17 @@ bool PerformanceTimer::isActive() { return _isActive; } +// static +QString PerformanceTimer::getContextName() { + return _fullNames[QThread::currentThread()]; +} + +// static +void PerformanceTimer::addTimerRecord(const QString& fullName, quint64 elapsedUsec) { + PerformanceTimerRecord& namedRecord = _records[fullName]; + namedRecord.accumulateResult(elapsedUsec); +} + // static void PerformanceTimer::setActive(bool active) { if (active != _isActive) { @@ -117,7 +128,7 @@ void PerformanceTimer::setActive(bool active) { _fullNames.clear(); _records.clear(); } - + qDebug() << "PerformanceTimer has been turned" << ((active) ? "on" : "off"); } } @@ -142,7 +153,7 @@ void PerformanceTimer::dumpAllTimerRecords() { QMapIterator i(_records); while (i.hasNext()) { i.next(); - qCDebug(shared) << i.key() << ": average " << i.value().getAverage() + qCDebug(shared) << i.key() << ": average " << i.value().getAverage() << " [" << i.value().getMovingAverage() << "]" << "usecs over" << i.value().getCount() << "calls"; } diff --git a/libraries/shared/src/PerfStat.h b/libraries/shared/src/PerfStat.h index 5a66990b16..785920779e 100644 --- a/libraries/shared/src/PerfStat.h +++ b/libraries/shared/src/PerfStat.h @@ -45,7 +45,7 @@ public: _alwaysDisplay(alwaysDisplay), _runningTotal(runningTotal), _totalCalls(totalCalls) { } - + quint64 elapsed() const { return (usecTimestampNow() - _start); }; ~PerformanceWarning(); @@ -56,14 +56,14 @@ public: class PerformanceTimerRecord { public: PerformanceTimerRecord() : _runningTotal(0), _lastTotal(0), _numAccumulations(0), _numTallies(0), _expiry(0) {} - + void accumulateResult(const quint64& elapsed) { _runningTotal += elapsed; ++_numAccumulations; } void tallyResult(const quint64& now); bool isStale(const quint64& now) const { return now > _expiry; } quint64 getAverage() const { return (_numTallies == 0) ? 0 : _runningTotal / _numTallies; } quint64 getMovingAverage() const { return (_numTallies == 0) ? 0 : _movingAverage.getAverage(); } quint64 getCount() const { return _numTallies; } - + private: quint64 _runningTotal; quint64 _lastTotal; @@ -81,7 +81,9 @@ public: static bool isActive(); static void setActive(bool active); - + + static QString getContextName(); + static void addTimerRecord(const QString& fullName, quint64 elapsedUsec); static const PerformanceTimerRecord& getTimerRecord(const QString& name) { return _records[name]; }; static const QMap& getAllTimerRecords() { return _records; }; static void tallyAllTimerRecords(); diff --git a/libraries/shared/src/PortableHighResolutionClock.h b/libraries/shared/src/PortableHighResolutionClock.h index ec1b068e66..5650a27f3c 100644 --- a/libraries/shared/src/PortableHighResolutionClock.h +++ b/libraries/shared/src/PortableHighResolutionClock.h @@ -21,6 +21,8 @@ #include +#include + #if defined(_MSC_VER) && _MSC_VER < 1900 #define WIN32_LEAN_AND_MEAN @@ -47,5 +49,11 @@ using p_high_resolution_clock = std::chrono::high_resolution_clock; #endif +Q_DECLARE_METATYPE(p_high_resolution_clock::time_point); + +#if defined(__GNUC__) && !defined(__clang__) +__attribute__((unused)) +#endif +static const int timePointMetaTypeID = qRegisterMetaType(); #endif // hifi_PortableHighResolutionClock_h 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/RunningMarker.cpp b/libraries/shared/src/RunningMarker.cpp new file mode 100644 index 0000000000..89fb3ada23 --- /dev/null +++ b/libraries/shared/src/RunningMarker.cpp @@ -0,0 +1,79 @@ +// +// RunningMarker.cpp +// libraries/shared/src +// +// Created by Brad Hefta-Gaub on 2016-10-16 +// 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 +// + +#include "RunningMarker.h" + +#include +#include +#include +#include + +#include "NumericalConstants.h" +#include "PathUtils.h" + +RunningMarker::RunningMarker(QObject* parent, QString name) : + _parent(parent), + _name(name) +{ +} + +void RunningMarker::startRunningMarker() { + static const int RUNNING_STATE_CHECK_IN_MSECS = MSECS_PER_SECOND; + + // start the nodeThread so its event loop is running + _runningMarkerThread = new QThread(_parent); + _runningMarkerThread->setObjectName("Running Marker Thread"); + _runningMarkerThread->start(); + + writeRunningMarkerFiler(); // write the first file, even before timer + + _runningMarkerTimer = new QTimer(); + QObject::connect(_runningMarkerTimer, &QTimer::timeout, [=](){ + writeRunningMarkerFiler(); + }); + _runningMarkerTimer->start(RUNNING_STATE_CHECK_IN_MSECS); + + // put the time on the thread + _runningMarkerTimer->moveToThread(_runningMarkerThread); +} + +RunningMarker::~RunningMarker() { + deleteRunningMarkerFile(); + QMetaObject::invokeMethod(_runningMarkerTimer, "stop", Qt::BlockingQueuedConnection); + _runningMarkerThread->quit(); + _runningMarkerTimer->deleteLater(); + _runningMarkerThread->deleteLater(); +} + +void RunningMarker::writeRunningMarkerFiler() { + QFile runningMarkerFile(getFilePath()); + + // always write, even it it exists, so that it touches the files + if (runningMarkerFile.open(QIODevice::WriteOnly)) { + runningMarkerFile.close(); + } +} + +void RunningMarker::deleteRunningMarkerFile() { + QFile runningMarkerFile(getFilePath()); + if (runningMarkerFile.exists()) { + runningMarkerFile.remove(); + } +} + +QString RunningMarker::getFilePath() { + return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + _name; +} + +QString RunningMarker::getMarkerFilePath(QString name) { + return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + name; +} + diff --git a/libraries/shared/src/RunningMarker.h b/libraries/shared/src/RunningMarker.h new file mode 100644 index 0000000000..16763d27e6 --- /dev/null +++ b/libraries/shared/src/RunningMarker.h @@ -0,0 +1,40 @@ +// +// RunningMarker.h +// interface/src +// +// Created by Brad Hefta-Gaub on 2016-10-15. +// 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 +// + +#ifndef hifi_RunningMarker_h +#define hifi_RunningMarker_h + +#include +#include + +class QThread; +class QTimer; + +class RunningMarker { +public: + RunningMarker(QObject* parent, QString name); + ~RunningMarker(); + + void startRunningMarker(); + + QString getFilePath(); + static QString getMarkerFilePath(QString name); +protected: + void writeRunningMarkerFiler(); + void deleteRunningMarkerFile(); + + QObject* _parent { nullptr }; + QString _name; + QThread* _runningMarkerThread { nullptr }; + QTimer* _runningMarkerTimer { nullptr }; +}; + +#endif // hifi_RunningMarker_h diff --git a/libraries/shared/src/SimpleMovingAverage.cpp b/libraries/shared/src/SimpleMovingAverage.cpp index e1c9a27390..f75180afb5 100644 --- a/libraries/shared/src/SimpleMovingAverage.cpp +++ b/libraries/shared/src/SimpleMovingAverage.cpp @@ -19,15 +19,29 @@ SimpleMovingAverage::SimpleMovingAverage(int numSamplesToAverage) : _eventDeltaAverage(0.0f), WEIGHTING(1.0f / numSamplesToAverage), ONE_MINUS_WEIGHTING(1 - WEIGHTING) { - } +SimpleMovingAverage::SimpleMovingAverage(const SimpleMovingAverage& other) { + *this = other; +} + +SimpleMovingAverage& SimpleMovingAverage::operator=(const SimpleMovingAverage& other) { + _numSamples = (int)other._numSamples; + _lastEventTimestamp = (uint64_t)other._lastEventTimestamp; + _average = (float)other._average; + _eventDeltaAverage = (float)other._eventDeltaAverage; + WEIGHTING = other.WEIGHTING; + ONE_MINUS_WEIGHTING = other.ONE_MINUS_WEIGHTING; + return *this; +} + + int SimpleMovingAverage::updateAverage(float sample) { if (_numSamples > 0) { _average = (ONE_MINUS_WEIGHTING * _average) + (WEIGHTING * sample); - + float eventDelta = (usecTimestampNow() - _lastEventTimestamp) / 1000000.0f; - + if (_numSamples > 1) { _eventDeltaAverage = (ONE_MINUS_WEIGHTING * _eventDeltaAverage) + (WEIGHTING * eventDelta); diff --git a/libraries/shared/src/SimpleMovingAverage.h b/libraries/shared/src/SimpleMovingAverage.h index dd25705f7e..858ee21371 100644 --- a/libraries/shared/src/SimpleMovingAverage.h +++ b/libraries/shared/src/SimpleMovingAverage.h @@ -16,27 +16,31 @@ #include #include +#include class SimpleMovingAverage { public: SimpleMovingAverage(int numSamplesToAverage = 100); - + SimpleMovingAverage(const SimpleMovingAverage& other); + SimpleMovingAverage& operator=(const SimpleMovingAverage& other); + int updateAverage(float sample); void reset(); - + int getSampleCount() const { return _numSamples; }; float getAverage() const { return _average; }; float getEventDeltaAverage() const; // returned in seconds float getAverageSampleValuePerSecond() const { return _average * (1.0f / getEventDeltaAverage()); } - + uint64_t getUsecsSinceLastEvent() const; + private: - int _numSamples; - uint64_t _lastEventTimestamp; - float _average; - float _eventDeltaAverage; - + std::atomic _numSamples; + std::atomic _lastEventTimestamp; + std::atomic _average; + std::atomic _eventDeltaAverage; + float WEIGHTING; float ONE_MINUS_WEIGHTING; }; @@ -44,10 +48,20 @@ private: template class MovingAverage { public: + MovingAverage() {} + MovingAverage(const MovingAverage& other) { + *this = other; + } + MovingAverage& operator=(const MovingAverage& other) { + numSamples = (int)other.numSamples; + average = (T)other.average; + return *this; + } + const float WEIGHTING = 1.0f / (float)MAX_NUM_SAMPLES; const float ONE_MINUS_WEIGHTING = 1.0f - WEIGHTING; - int numSamples{ 0 }; - T average; + std::atomic numSamples{ 0 }; + std::atomic average; void clear() { numSamples = 0; @@ -72,7 +86,7 @@ public: _samples = 0; } - bool isAverageValid() const { + bool isAverageValid() const { std::unique_lock lock(_lock); return (_samples > 0); } @@ -87,7 +101,7 @@ public: _samples++; } - T getAverage() const { + T getAverage() const { std::unique_lock lock(_lock); return _average; } 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/OffscreenQmlElement.h b/libraries/ui/src/OffscreenQmlElement.h index 87b404a4bd..4e07fcccd9 100644 --- a/libraries/ui/src/OffscreenQmlElement.h +++ b/libraries/ui/src/OffscreenQmlElement.h @@ -22,6 +22,7 @@ private: \ public: \ static void registerType(); \ static void show(std::function f = [](QQmlContext*, QObject*) {}); \ + static void hide(); \ static void toggle(std::function f = [](QQmlContext*, QObject*) {}); \ static void load(std::function f = [](QQmlContext*, QObject*) {}); \ private: @@ -33,6 +34,7 @@ protected: \ public: \ static void registerType(); \ static void show(); \ + static void hide(); \ static void toggle(); \ static void load(); \ private: @@ -50,6 +52,11 @@ private: offscreenUi->show(QML, NAME, f); \ } \ \ + void x::hide() { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->hide(NAME); \ + } \ + \ void x::toggle(std::function f) { \ auto offscreenUi = DependencyManager::get(); \ offscreenUi->toggle(QML, NAME, f); \ @@ -70,6 +77,11 @@ private: auto offscreenUi = DependencyManager::get(); \ offscreenUi->show(QML, NAME, f); \ } \ + void x::hide() { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->hide(NAME); \ + } \ + \ void x::toggle() { \ auto offscreenUi = DependencyManager::get(); \ offscreenUi->toggle(QML, NAME, f); \ diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 8cae137f67..ca7d3f7c17 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -616,6 +616,28 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, return fileDialog(map); } +QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(this, "existingDirectoryDialog", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, result), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return result; + } + + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + map.insert("selectDirectory", true); + return fileDialog(map); +} + QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { return DependencyManager::get()->fileOpenDialog(caption, dir, filter, selectedFilter, options); } @@ -624,6 +646,10 @@ QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, cons return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); } +QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + return DependencyManager::get()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options); +} + bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { if (!filterEnabled(originalDestination, event)) { return false; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index bc0a6bf544..2e6e853336 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -115,11 +115,14 @@ public: Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getSaveFileName static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getExistingDirectory + static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 84d0aa0489..2130e84220 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -86,13 +86,10 @@ QString QmlWebWindowClass::getURL() const { return result.toString(); } -// HACK find a good place to declare and store this -extern QString fixupHifiUrl(const QString& urlString); - void QmlWebWindowClass::setURL(const QString& urlString) { DependencyManager::get()->executeOnUiThread([=] { if (!_qmlWindow.isNull()) { - _qmlWindow->setProperty(URL_PROPERTY, fixupHifiUrl(urlString)); + _qmlWindow->setProperty(URL_PROPERTY, urlString); } }); } diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index e32c6d5a04..731afb3acb 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -19,7 +19,7 @@ class QmlWebWindowClass : public QmlWindowClass { public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); - public slots: +public slots: QString getURL() const; void setURL(const QString& url); diff --git a/plugins/hifiNeuron/src/NeuronProvider.cpp b/plugins/hifiNeuron/src/NeuronProvider.cpp index b171c5150d..5315ff105e 100644 --- a/plugins/hifiNeuron/src/NeuronProvider.cpp +++ b/plugins/hifiNeuron/src/NeuronProvider.cpp @@ -38,6 +38,10 @@ public: return _inputPlugins; } + virtual void destroyInputPlugins() override { + _inputPlugins.clear(); + } + private: InputPluginList _inputPlugins; }; diff --git a/plugins/hifiSdl2/src/SDL2Provider.cpp b/plugins/hifiSdl2/src/SDL2Provider.cpp index 1c387a9886..c3315dee07 100644 --- a/plugins/hifiSdl2/src/SDL2Provider.cpp +++ b/plugins/hifiSdl2/src/SDL2Provider.cpp @@ -37,6 +37,10 @@ public: return _inputPlugins; } + virtual void destroyInputPlugins() override { + _inputPlugins.clear(); + } + private: InputPluginList _inputPlugins; }; diff --git a/plugins/hifiSixense/src/SixenseProvider.cpp b/plugins/hifiSixense/src/SixenseProvider.cpp index 2958e47848..aded33db24 100644 --- a/plugins/hifiSixense/src/SixenseProvider.cpp +++ b/plugins/hifiSixense/src/SixenseProvider.cpp @@ -38,6 +38,9 @@ public: return _inputPlugins; } + virtual void destroyInputPlugins() override { + _inputPlugins.clear(); + } private: InputPluginList _inputPlugins; }; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 377050064a..7209104a85 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -126,3 +126,7 @@ void OculusBaseDisplayPlugin::updatePresentPose() { //_currentPresentFrameInfo.presentPose = toGlm(trackingState.HeadPose.ThePose); _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; } + +OculusBaseDisplayPlugin::~OculusBaseDisplayPlugin() { + qDebug() << "Destroying OculusBaseDisplayPlugin"; +} diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 25629f004a..e5dc75095d 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -16,8 +16,12 @@ class OculusBaseDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; 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/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 838a4121cd..415965e948 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -46,7 +46,7 @@ void OculusDisplayPlugin::cycleDebugOutput() { void OculusDisplayPlugin::customizeContext() { Parent::customizeContext(); - _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y)); + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OculusOutput", gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y)); ovrTextureSwapChainDesc desc = { }; desc.Type = ovrTexture_2D; desc.ArraySize = 1; @@ -59,13 +59,15 @@ void OculusDisplayPlugin::customizeContext() { ovrResult result = ovr_CreateTextureSwapChainGL(_session, &desc, &_textureSwapChain); if (!OVR_SUCCESS(result)) { - logFatal("Failed to create swap textures"); + logCritical("Failed to create swap textures"); + return; } int length = 0; result = ovr_GetTextureSwapChainLength(_session, _textureSwapChain, &length); if (!OVR_SUCCESS(result) || !length) { - qFatal("Unable to count swap chain textures"); + logCritical("Unable to count swap chain textures"); + return; } for (int i = 0; i < length; ++i) { GLuint chainTexId; @@ -83,6 +85,7 @@ void OculusDisplayPlugin::customizeContext() { _sceneLayer.ColorTexture[0] = _textureSwapChain; // not needed since the structure was zeroed on init, but explicit _sceneLayer.ColorTexture[1] = nullptr; + _customized = true; } void OculusDisplayPlugin::uncustomizeContext() { @@ -97,10 +100,15 @@ void OculusDisplayPlugin::uncustomizeContext() { ovr_DestroyTextureSwapChain(_session, _textureSwapChain); _textureSwapChain = nullptr; + _outputFramebuffer.reset(); + _customized = false; Parent::uncustomizeContext(); } void OculusDisplayPlugin::hmdPresent() { + if (!_customized) { + return; + } PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) @@ -163,3 +171,7 @@ QString OculusDisplayPlugin::getPreferredAudioOutDevice() const { } return AudioClient::friendlyNameForAudioDevice(buffer); } + +OculusDisplayPlugin::~OculusDisplayPlugin() { + qDebug() << "Destroying OculusDisplayPlugin"; +} diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index 80705319c6..0c7d57c4f4 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -12,6 +12,7 @@ class OculusDisplayPlugin : public OculusBaseDisplayPlugin { using Parent = OculusBaseDisplayPlugin; public: + ~OculusDisplayPlugin(); const QString& getName() const override { return NAME; } void init() override; @@ -31,5 +32,6 @@ private: static const QString NAME; ovrTextureSwapChain _textureSwapChain; gpu::FramebufferPointer _outputFramebuffer; + bool _customized { false }; }; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 80390fd538..5fbc0db7d1 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -39,12 +39,12 @@ void logWarning(const char* what) { qWarning(oculus) << what << ":" << getError().ErrorString; } -void logFatal(const char* what) { +void logCritical(const char* what) { std::string error("[oculus] "); error += what; error += ": "; error += getError().ErrorString; - qFatal(error.c_str()); + qCritical(error.c_str()); } diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index ba0547ae0a..e55dde1034 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -15,7 +15,7 @@ #include void logWarning(const char* what); -void logFatal(const char* what); +void logCritical(const char* what); bool oculusAvailable(); ovrSession acquireOculusSession(); void releaseOculusSession(); diff --git a/plugins/oculus/src/OculusProvider.cpp b/plugins/oculus/src/OculusProvider.cpp index e723fa839a..47ccc5304e 100644 --- a/plugins/oculus/src/OculusProvider.cpp +++ b/plugins/oculus/src/OculusProvider.cpp @@ -62,6 +62,14 @@ public: return _inputPlugins; } + virtual void destroyInputPlugins() override { + _inputPlugins.clear(); + } + + virtual void destroyDisplayPlugins() override { + _displayPlugins.clear(); + } + private: DisplayPluginList _displayPlugins; InputPluginList _inputPlugins; diff --git a/plugins/oculusLegacy/src/OculusProvider.cpp b/plugins/oculusLegacy/src/OculusProvider.cpp index 606563e0ad..fbfc66b432 100644 --- a/plugins/oculusLegacy/src/OculusProvider.cpp +++ b/plugins/oculusLegacy/src/OculusProvider.cpp @@ -38,6 +38,10 @@ public: return _displayPlugins; } + virtual void destroyDisplayPlugins() override { + _displayPlugins.clear(); + } + private: DisplayPluginList _displayPlugins; }; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index c0fa0db214..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(); @@ -377,9 +380,6 @@ void OpenVrDisplayPlugin::init() { } bool OpenVrDisplayPlugin::internalActivate() { - _openVrDisplayActive = true; - _container->setIsOptionChecked(StandingHMDSensorMode, true); - if (!_system) { _system = acquireOpenVrSystem(); } @@ -388,6 +388,28 @@ bool OpenVrDisplayPlugin::internalActivate() { return false; } + // If OpenVR isn't running, then the compositor won't be accessible + // FIXME find a way to launch the compositor? + if (!vr::VRCompositor()) { + qWarning() << "Failed to acquire OpenVR compositor"; + releaseOpenVrSystem(); + _system = nullptr; + 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); + _system->GetRecommendedRenderTargetSize(&_renderTargetSize.x, &_renderTargetSize.y); // Recommended render target size is per-eye, so double the X size for // left + right eyes @@ -425,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(); } @@ -445,8 +467,9 @@ void OpenVrDisplayPlugin::internalDeactivate() { _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { - // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and + // TODO: Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and // we don't want ViveControllerManager to consider old values to be valid. + _container->makeRenderingContextCurrent(); releaseOpenVrSystem(); _system = nullptr; } @@ -463,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() { @@ -572,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 { @@ -674,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 ccf318a629..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 @@ -109,8 +111,12 @@ void releaseOpenVrSystem() { vr::Texture_t vrTexture{ (void*)INVALID_GL_TEXTURE_HANDLE, vr::API_OpenGL, vr::ColorSpace_Auto }; 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 }; - vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); - vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); + + auto compositor = vr::VRCompositor(); + if (compositor) { + compositor->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); + compositor->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); + } vr::VR_Shutdown(); _openVrQuitRequested = false; @@ -295,10 +301,12 @@ controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; - static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches - static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); + // this needs to match the leftBasePosition in tutorial/viveControllerConfiguration.js:21 + static const float CONTROLLER_LATERAL_OFFSET = 0.0381f; + static const float CONTROLLER_VERTICAL_OFFSET = 0.0495f; + static const float CONTROLLER_FORWARD_OFFSET = 0.1371f; + static const glm::vec3 CONTROLLER_OFFSET(CONTROLLER_LATERAL_OFFSET, CONTROLLER_VERTICAL_OFFSET, CONTROLLER_FORWARD_OFFSET); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; @@ -318,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/OpenVrProvider.cpp b/plugins/openvr/src/OpenVrProvider.cpp index 66227a9543..944322373a 100644 --- a/plugins/openvr/src/OpenVrProvider.cpp +++ b/plugins/openvr/src/OpenVrProvider.cpp @@ -51,6 +51,13 @@ public: return _inputPlugins; } + virtual void destroyInputPlugins() override { + _inputPlugins.clear(); + } + + virtual void destroyDisplayPlugins() override { + _displayPlugins.clear(); + } private: DisplayPluginList _displayPlugins; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index bcfc7170dc..ff8fc64474 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -132,6 +132,7 @@ void ViveControllerManager::deactivate() { _container->removeMenu(MENU_PATH); if (_system) { + _container->makeRenderingContextCurrent(); releaseOpenVrSystem(); _system = nullptr; } @@ -209,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 f9cd32be46..718b5f3d3e 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -23,6 +23,7 @@ var DEFAULT_SCRIPTS = [ "system/mod.js", "system/selectAudioDevice.js", "system/notifications.js", + "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrab.js", "system/controllers/handControllerPointer.js", "system/controllers/squeezeHands.js", @@ -125,4 +126,4 @@ Script.scriptEnding.connect(function() { removeMenuItem(); }); -Menu.menuItemEvent.connect(menuItemEvent); \ No newline at end of file +Menu.menuItemEvent.connect(menuItemEvent); diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js index 6b73d66c4f..b87d418643 100644 --- a/scripts/developer/tests/performance/crowd-agent.js +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -16,12 +16,13 @@ var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; -print('crowd-agent version 2'); +print('crowd-agent version 4'); /* Observations: - File urls for AC scripts silently fail. Use a local server (e.g., python SimpleHTTPServer) for development. - URLs are cached regardless of server headers. Must use cache-defeating query parameters. - JSON.stringify(Avatar) silently fails (even when Agent.isAvatar) +- If you run from a dev build directory, you must link assignment-client//resources to ../../interface//resources */ function messageSend(message) { @@ -33,17 +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; } @@ -53,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); @@ -60,15 +105,17 @@ function startAgent(parameters) { // Can also be used to update. }); } if (parameters.animationData) { - data = parameters.animationData; + var data = parameters.animationData; Avatar.startAnimation(data.url, data.fps || 30, 1.0, (data.loopFlag === undefined) ? true : data.loopFlag, false, data.startFrame || 0, data.endFrame); } + if (parameters.attachment) { + attachment = parameters.attachment; + Avatar.attach(attachment.modelURL, attachment.jointName, attachment.translation, attachment.rotation, attachment.scale, attachment.isSoft); + } else { + attachment = undefined; + } print('crowd-agent avatars started'); } -function stopAgent(parameters) { - Agent.isAvatar = false; - print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); -} function messageHandler(channel, messageString, senderID) { if (channel !== MESSAGE_CHANNEL) { @@ -89,6 +136,7 @@ function messageHandler(channel, messageString, senderID) { messageSend({key: 'hello'}); // Allow the coordinator to count responses and make assignments. break; case 'hello': // ignore responses (e.g., from other agents) + case 'finishedSound': break; case "SUMMON": if (message.rcpt === Agent.sessionUUID) { diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index f085c3f685..398bc4fd0a 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, MyAvatar, Quat, Render, ScriptDiscoveryService, Window, LODManager, Entities, print*/ +/*globals Script, MyAvatar, Quat, Vec3, Render, ScriptDiscoveryService, Window, LODManager, Entities, Messages, AvatarList, Menu, Stats, HMD, location, print*/ // // loadedMachine.js // scripts/developer/tests/ @@ -17,37 +17,168 @@ var MINIMUM_DESKTOP_FRAMERATE = 57; // frames per second var MINIMUM_HMD_FRAMERATE = 86; var EXPECTED_DESKTOP_FRAMERATE = 60; var EXPECTED_HMD_FRAMERATE = 90; -var MAXIMUM_LOAD_TIME = 60; // seconds -var MINIMUM_AVATARS = 25; // FIXME: not implemented yet. Requires agent scripts. Idea is to have them organize themselves to the right number. +var NOMINAL_LOAD_TIME = 30; // seconds +var MAXIMUM_LOAD_TIME = NOMINAL_LOAD_TIME * 2; +var MINIMUM_AVATARS = 25; // changeable by prompt -var version = 2; +// 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. +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://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, + "loopFlag": true +}; + +var version = 4; function debug() { print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify))); } +function canonicalizePlacename(name) { + var prefix = 'dev-'; + name = name.toLowerCase(); + if (name.indexOf(prefix) === 0) { + name = name.slice(prefix.length); + } + return name; +} +var cachePlaces = ['localhost', 'welcome'].map(canonicalizePlacename); // For now, list the lighter weight one first. +var defaultPlace = location.hostname; +var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?"; +debug(cachePlaces, defaultPlace, prompt); +var entryPlace = Window.prompt(prompt, defaultPlace); +var runTribbles = Window.confirm("Run tribbles?\n\n\ +At most, only one participant should say yes."); +MINIMUM_AVATARS = parseInt(Window.prompt("Total avatars (including yourself and any already present)?", MINIMUM_AVATARS.toString()) || "0", 10); +AVATARS_CHATTERING_AT_ONCE = MINIMUM_AVATARS ? parseInt(Window.prompt("Number making sound?", Math.min(MINIMUM_AVATARS - 1, AVATARS_CHATTERING_AT_ONCE).toString()) || "0", 10) : 0; + +function placesMatch(a, b) { // handling case and 'dev-' variations + return canonicalizePlacename(a) === canonicalizePlacename(b); +} function isNowIn(place) { // true if currently in specified place - return location.hostname.toLowerCase() === place.toLowerCase(); + placesMatch(location.hostname, place); } -var cachePlaces = ['dev-Welcome', 'localhost']; // For now, list the lighter weight one first. -var isInCachePlace = cachePlaces.some(isNowIn); -var defaultPlace = isInCachePlace ? 'dev-Playa' : location.hostname; -var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?"; -debug(cachePlaces, isInCachePlace, defaultPlace, prompt); -var entryPlace = Window.prompt(prompt, defaultPlace); +function go(place) { // handle (dev-)welcome in the appropriate version-specific way + debug('go', place); + if (placesMatch(place, 'welcome')) { + location.goToEntry(); + } else { + location.handleLookupString(place); + } +} + +var spread = Math.sqrt(MINIMUM_AVATARS * DENSITY); // meters +var turnSpread = 90; // How many degrees should turn from front range over. + +function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2. +function contains(array, item) { return array.indexOf(item) >= 0; } +function without(array, itemsToRemove) { return array.filter(function (item) { return !contains(itemsToRemove, item); }); } +function nextAfter(array, id) { // Wrapping next element in array after id. + var index = array.indexOf(id) + 1; + return array[(index >= array.length) ? 0 : index]; +} + +var summonedAgents = []; +var chattering = []; +var accumulatedDelay = 0; +var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + debug('message', channel, messageString, senderID); + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}, avatarIdentifiers; + try { + message = JSON.parse(messageString); + } catch (e) { + print(e); + } + switch (message.key) { + case "hello": + 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 + }); + } + }, 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]); + Script.setTimeout(function () { + messageSend({ + key: 'SUMMON', + rcpt: nextAfter(without(summonedAgents, chattering), senderID), + soundData: SOUND_DATA + }); + }, Math.random() * NEXT_SOUND_SPREAD); + break; + case "HELO": + Window.alert("Someone else is summoning avatars."); + break; + default: + print("crowd-agent received unrecognized message:", messageString); + } +} +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); +Script.scriptEnding.connect(function () { + debug('stopping agents', summonedAgents); + 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'); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); +}); var fail = false, results = ""; -function addResult(label, actual, minimum, maximum) { +function addResult(label, actual, nominal, minimum, maximum) { if ((minimum !== undefined) && (actual < minimum)) { - fail = true; + fail = ' FAILED: ' + label + ' below ' + minimum; } if ((maximum !== undefined) && (actual > maximum)) { - fail = true; + fail = ' FAILED: ' + label + ' above ' + maximum; } - results += "\n" + label + ": " + actual + " (" + ((100 * actual) / (maximum || minimum)).toFixed(0) + "%)"; + results += "\n" + label + ": " + actual.toFixed(0) + " (" + ((100 * actual) / nominal).toFixed(0) + "%)"; } function giveReport() { - Window.alert(entryPlace + (fail ? " FAILED" : " OK") + "\n" + results); + Window.alert(entryPlace + (fail || " OK") + "\n" + results + "\nwith " + summonedAgents.length + " avatars added,\nand " + AVATARS_CHATTERING_AT_ONCE + " making noise."); } // Tests are performed domain-wide, at full LOD @@ -122,9 +253,8 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti } }; - debug('go', place); location.hostChanged.connect(waitForLoad); - location.handleLookupString(place); + go(place); } var config = Render.getConfig("Stats"); @@ -133,48 +263,59 @@ function doRender(continuation) { function onNewStats() { // Accumulates frames on signal during load test frames++; } + if (MINIMUM_AVATARS) { + messageSend({key: 'HELO'}); // Ask agents to report in now. + } + 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), - HMD.active ? MINIMUM_HMD_FRAMERATE : MINIMUM_DESKTOP_FRAMERATE, - HMD.active ? EXPECTED_HMD_FRAMERATE : EXPECTED_DESKTOP_FRAMERATE); + HMD.active ? EXPECTED_HMD_FRAMERATE : EXPECTED_DESKTOP_FRAMERATE, + HMD.active ? MINIMUM_HMD_FRAMERATE : MINIMUM_DESKTOP_FRAMERATE); + var total = AvatarList.getAvatarIdentifiers().length; + if (MINIMUM_AVATARS && !fail) { + if (0 === summonedAgents.length) { + fail = "FAIL: No agents reported.\nPlease run " + MINIMUM_AVATARS + " instances of\n\ +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 + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total."; + } + } continuation(); }); } var TELEPORT_PAUSE = 500; -function maybePrepareCache(continuation) { - var prepareCache = Window.confirm("Prepare cache?\n\n\ -Should we start with all and only those items cached that are encountered when visiting:\n" + cachePlaces.join(', ') + "\n\ -If 'yes', cache will be cleared and we will visit these two, with a turn in each, and wait for everything to be loaded.\n\ -You would want to say 'no' (and make other preparations) if you were testing these places."); - - if (prepareCache) { - function loadNext() { - var place = cachePlaces.shift(); - doLoad(place, function (prepTime) { - debug(place, 'ready', prepTime); - if (cachePlaces.length) { - loadNext(); - } else { - continuation(); - } - }); - } - location.handleLookupString(cachePlaces[cachePlaces.length - 1]); +function prepareCache(continuation) { + function loadNext() { + var place = cachePlaces.shift(); + doLoad(place, function (prepTime) { + debug(place, 'ready', prepTime); + if (cachePlaces.length) { + loadNext(); + } else { + continuation(); + } + }); + } + // remove entryPlace target from cachePlaces + var targetInCache = cachePlaces.indexOf(canonicalizePlacename(entryPlace)); + if (targetInCache !== -1) { + cachePlaces.splice(targetInCache, 1); + } + debug('cachePlaces', cachePlaces); + go(cachePlaces[1] || entryPlace); // Not quite right for entryPlace case (allows some qt pre-caching), but close enough. + Script.setTimeout(function () { Menu.triggerOption("Reload Content (Clears all caches)"); Script.setTimeout(loadNext, TELEPORT_PAUSE); - } else { - location.handleLookupString(isNowIn(cachePlaces[0]) ? cachePlaces[1] : cachePlaces[0]); - Script.setTimeout(continuation, TELEPORT_PAUSE); - } + }, TELEPORT_PAUSE); } function maybeRunTribbles(continuation) { - if (Window.confirm("Run tribbles?\n\n\ -At most, only one participant should say yes.")) { + if (runTribbles) { Script.load('http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/tribbles.js'); Script.setTimeout(continuation, 3000); } else { @@ -186,10 +327,11 @@ if (!entryPlace) { Window.alert("domain-check.js cancelled"); Script.stop(); } else { - maybePrepareCache(function (prepTime) { + prepareCache(function (prepTime) { debug('cache ready', prepTime); doLoad(entryPlace, function (loadTime) { - addResult("load time", loadTime, undefined, MAXIMUM_LOAD_TIME); + addResult("load time", loadTime, NOMINAL_LOAD_TIME, undefined, MAXIMUM_LOAD_TIME); + LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); // after loading, restore lod. maybeRunTribbles(function () { doRender(function () { giveReport(); diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js index 8118f553f1..69bf0860ae 100644 --- a/scripts/developer/tests/performance/summon.js +++ b/scripts/developer/tests/performance/summon.js @@ -13,16 +13,31 @@ // // 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://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, + "loopFlag": true +}; var spread = Math.sqrt(MINIMUM_AVATARS * DENSITY); // meters var turnSpread = 90; // How many degrees should turn from front range over. @@ -37,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)); @@ -57,34 +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/* - // No need to specify skeletonModelURL - //skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/being_of_light/being_of_light.fbx", - //skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/defaultAvatar_full.fst"/, - animationData: { // T-pose until we get animations working again. - "url": "file:///C:/Program Files/High Fidelity Release/resources/avatar/animations/idle.fbx", - //"url": "file:///c:/Program Files/High Fidelity Release/resources/avatar/animations/walk_fwd.fbx", - "startFrame": 0.0, - "endFrame": 300.0, - "timeScale": 1.0, - "loopFlag": true - }*/ - }); - } + }, 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]); @@ -100,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. @@ -121,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/developer/tests/webSpawnTool.js b/scripts/developer/tests/webSpawnTool.js new file mode 100644 index 0000000000..6496fc445a --- /dev/null +++ b/scripts/developer/tests/webSpawnTool.js @@ -0,0 +1,106 @@ +// webSpawnTool.js +// +// Stress tests the rendering of web surfaces over time +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +ENTITY_SPAWNER = function (properties) { + properties = properties || {}; + var RADIUS = properties.radius || 5.0; // Spawn within this radius (square) + var TEST_ENTITY_NAME = properties.entityName || "WebEntitySpawnTest"; + var NUM_ENTITIES = properties.count || 10000; // number of entities to spawn + var ENTITY_SPAWN_LIMIT = properties.spawnLimit || 1; + var ENTITY_SPAWN_INTERVAL = properties.spawnInterval || properties.interval || 2; + var ENTITY_LIFETIME = properties.lifetime || 10; // Entity timeout (when/if we crash, we need the entities to delete themselves) + + function makeEntity(properties) { + var entity = Entities.addEntity(properties); + return { + destroy: function () { + Entities.deleteEntity(entity) + }, + getAge: function () { + return Entities.getEntityProperties(entity).age; + } + }; + } + + function randomPositionXZ(center, radius) { + return { + x: center.x + (Math.random() * radius * 2.0) - radius, + y: center.y, + z: center.z + (Math.random() * radius * 2.0) - radius + }; + } + + var entities = []; + var entitiesToCreate = 0; + var entitiesSpawned = 0; + var spawnTimer = 0.0; + var keepAliveTimer = 0.0; + + function clear () { + var ids = Entities.findEntities(MyAvatar.position, 50); + var that = this; + ids.forEach(function(id) { + var properties = Entities.getEntityProperties(id); + if (properties.name == TEST_ENTITY_NAME) { + Entities.deleteEntity(id); + } + }, this); + } + + function createEntities () { + entitiesToCreate = NUM_ENTITIES; + Script.update.connect(spawnEntities); + } + + function spawnEntities (dt) { + if (entitiesToCreate <= 0) { + Script.update.disconnect(spawnEntities); + print("Finished spawning entities"); + } + else if ((spawnTimer -= dt) < 0.0){ + spawnTimer = ENTITY_SPAWN_INTERVAL; + + var n = Math.min(entitiesToCreate, ENTITY_SPAWN_LIMIT); + print("Spawning " + n + " entities (" + (entitiesSpawned += n) + ")"); + + entitiesToCreate -= n; + + var center = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: RADIUS * -1.5 })); + for (; n > 0; --n) { + entities.push(makeEntity({ + type: "Web", + //sourceUrl: "https://www.reddit.com/r/random/", + sourceUrl: "https://en.wikipedia.org/wiki/Special:Random", + name: TEST_ENTITY_NAME, + position: randomPositionXZ(center, RADIUS), + rotation: MyAvatar.orientation, + dimensions: { x: .8 + Math.random() * 0.8, y: 0.45 + Math.random() * 0.45, z: 0.01 }, + lifetime: ENTITY_LIFETIME + })); + } + } + } + + function despawnEntities () { + print("despawning entities"); + entities.forEach(function (entity) { + entity.destroy(); + }); + entities = []; + } + + function init () { + Script.update.disconnect(init); + clear(); + createEntities(); + Script.scriptEnding.connect(despawnEntities); + } + Script.update.connect(init); +}; + +ENTITY_SPAWNER(); diff --git a/scripts/developer/utilities/audio/Jitter.qml b/scripts/developer/utilities/audio/Jitter.qml new file mode 100644 index 0000000000..91f197a919 --- /dev/null +++ b/scripts/developer/utilities/audio/Jitter.qml @@ -0,0 +1,32 @@ +// +// Jitter.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +ColumnLayout { + id: jitter + property var max + property var avg + property bool showGraphs: false + + MovingValue { + label: "Jitter" + color: "red" + source: max - avg + showGraphs: jitter.showGraphs + } + Value { + label: "Average" + source: avg + } +} + diff --git a/scripts/developer/utilities/audio/MovingValue.qml b/scripts/developer/utilities/audio/MovingValue.qml new file mode 100644 index 0000000000..bbd9c31d6b --- /dev/null +++ b/scripts/developer/utilities/audio/MovingValue.qml @@ -0,0 +1,51 @@ +// +// MovingValue.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 +import "../lib/plotperf" + +RowLayout { + id: value + property string label + property var source + property string unit: "ms" + property bool showGraphs: false + property color color: "darkslategrey" + property int decimals: 0 + + width: parent.width + + Label { + Layout.preferredWidth: 100 + color: value.color + text: value.label + } + Label { + visible: !value.showGraphs + Layout.preferredWidth: 50 + horizontalAlignment: Text.AlignRight + color: value.color + text: value.source.toFixed(decimals) + ' ' + unit + } + PlotPerf { + visible: value.showGraphs + Layout.fillWidth: true + height: 70 + + valueUnit: value.unit + valueNumDigits: 0 + backgroundOpacity: 0.2 + + plots: [{ binding: "source", color: value.color }] + } +} + diff --git a/scripts/developer/utilities/audio/Section.qml b/scripts/developer/utilities/audio/Section.qml new file mode 100644 index 0000000000..100d05f6b2 --- /dev/null +++ b/scripts/developer/utilities/audio/Section.qml @@ -0,0 +1,56 @@ +// +// Section.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +Rectangle { + id: section + property string label: "Section" + property string description: "Description" + property alias control : loader.sourceComponent + + width: parent.width + height: content.height + border.width * 2 + content.spacing * 2 + border.color: "black" + border.width: 5 + radius: border.width * 2 + + ColumnLayout { + id: content + x: section.radius; y: section.radius + spacing: section.border.width + width: section.width - 2 * x + + // label + Label { + Layout.alignment: Qt.AlignCenter + text: hoverArea.containsMouse ? section.description : section.label + font.bold: true + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + } + } + + // spacer + Item { } + + // control + Loader { + id: loader + Layout.preferredWidth: parent.width + } + } +} + diff --git a/scripts/developer/utilities/audio/Stream.qml b/scripts/developer/utilities/audio/Stream.qml new file mode 100644 index 0000000000..e9383b627a --- /dev/null +++ b/scripts/developer/utilities/audio/Stream.qml @@ -0,0 +1,69 @@ +// +// Stream.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +ColumnLayout { + id: root + property var stream + property bool showGraphs: false + + Label { + Layout.alignment: Qt.AlignCenter + text: "Ring Buffer" + font.italic: true + } + MovingValue { + label: "Minimum Depth" + color: "limegreen" + source: stream.framesDesired + unit: "frames" + showGraphs: root.showGraphs + } + MovingValue { + label: "Buffer Depth" + color: "darkblue" + source: stream.unplayedMsMax + showGraphs: root.showGraphs + } + Value { + label: "Available (avg)" + source: stream.framesAvailable + " (" + stream.framesAvailableAvg + ") frames" + } + + Label { + Layout.alignment: Qt.AlignCenter + text: "Jitter" + font.italic: true + } + Jitter { + max: stream.timegapMsMaxWindow + avg: stream.timegapMsAvgWindow + showGraphs: root.showGraphs + } + + Label { + Layout.alignment: Qt.AlignCenter + text: "Packet Loss" + font.italic: true + } + Value { + label: "Window" + source: stream.lossRateWindow.toFixed(2) + "% (" + stream.lossCountWindow + " lost)" + } + Value { + label: "Overall" + color: "dimgrey" + source: stream.lossRate.toFixed(2) + "% (" + stream.lossCount + " lost)" + } +} + diff --git a/scripts/developer/utilities/audio/Value.qml b/scripts/developer/utilities/audio/Value.qml new file mode 100644 index 0000000000..70df7695bc --- /dev/null +++ b/scripts/developer/utilities/audio/Value.qml @@ -0,0 +1,36 @@ +// +// Value.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +RowLayout { + id: value + property string label + property var source + property color color: "darkslategrey" + + width: parent.width + property int dataPixelWidth: 150 + + Label { + Layout.preferredWidth: dataPixelWidth + color: value.color + text: value.label + } + Label { + Layout.preferredWidth: 0 + horizontalAlignment: Text.AlignRight + color: value.color + text: value.source + } +} + diff --git a/scripts/developer/utilities/audio/stats.js b/scripts/developer/utilities/audio/stats.js new file mode 100644 index 0000000000..493271ac99 --- /dev/null +++ b/scripts/developer/utilities/audio/stats.js @@ -0,0 +1,25 @@ +// +// stats.js +// scripts/developer/utilities/audio +// +// Zach Pomerantz, created on 9/22/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 +// + +var INITIAL_WIDTH = 400; +var INITIAL_OFFSET = 50; + +// Set up the qml ui +var qml = Script.resolvePath('stats.qml'); +var window = new OverlayWindow({ + title: 'Audio Interface Statistics', + source: qml, + width: 500, height: 520 // stats.qml may be too large for some screens +}); +window.setPosition(INITIAL_OFFSET, INITIAL_OFFSET); + +window.closed.connect(function() { Script.stop(); }); + diff --git a/scripts/developer/utilities/audio/stats.qml b/scripts/developer/utilities/audio/stats.qml new file mode 100644 index 0000000000..346e5e3544 --- /dev/null +++ b/scripts/developer/utilities/audio/stats.qml @@ -0,0 +1,95 @@ +// +// stats.qml +// scripts/developer/utilities/audio +// +// Created by Zach Pomerantz on 9/22/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +Column { + id: stats + width: parent.width + height: parent.height + property bool showGraphs: toggleGraphs.checked + + RowLayout { + width: parent.width + height: 30 + + Button { + id: toggleGraphs + property bool checked: false + + Layout.alignment: Qt.AlignCenter + + text: checked ? "Hide graphs" : "Show graphs" + onClicked: function() { checked = !checked; } + } + } + + Grid { + width: parent.width + height: parent.height - 30 + + Column { + width: parent.width / 2 + height: parent.height + + Section { + label: "Latency" + description: "Audio pipeline latency, broken out and summed" + control: ColumnLayout { + MovingValue { label: "Input Read"; source: AudioStats.inputReadMsMax; showGraphs: stats.showGraphs } + MovingValue { label: "Input Ring"; source: AudioStats.inputUnplayedMsMax; showGraphs: stats.showGraphs } + MovingValue { label: "Network (up)"; source: AudioStats.pingMs / 2; showGraphs: stats.showGraphs; decimals: 1 } + MovingValue { label: "Mixer Ring"; source: AudioStats.mixerStream.unplayedMsMax; showGraphs: stats.showGraphs } + MovingValue { label: "Network (down)"; source: AudioStats.pingMs / 2; showGraphs: stats.showGraphs; decimals: 1 } + MovingValue { label: "Output Ring"; source: AudioStats.clientStream.unplayedMsMax; showGraphs: stats.showGraphs } + MovingValue { label: "Output Read"; source: AudioStats.outputUnplayedMsMax; showGraphs: stats.showGraphs } + MovingValue { label: "TOTAL"; color: "black"; showGraphs: stats.showGraphs + source: AudioStats.inputReadMsMax + + AudioStats.inputUnplayedMsMax + + AudioStats.outputUnplayedMsMax + + AudioStats.mixerStream.unplayedMsMax + + AudioStats.clientStream.unplayedMsMax + + AudioStats.pingMs + } + } + } + + Section { + label: "Upstream Jitter" + description: "Timegaps in packets sent to the mixer" + control: Jitter { + max: AudioStats.sentTimegapMsMaxWindow + avg: AudioStats.sentTimegapMsAvgWindow + showGraphs: stats.showGraphs + } + } + } + + Column { + width: parent.width / 2 + height: parent.height + + Section { + label: "Mixer (upstream)" + description: "This client's remote audio stream, as seen by the server's mixer" + control: Stream { stream: AudioStats.mixerStream; showGraphs: stats.showGraphs } + } + + Section { + label: "Client (downstream)" + description: "This client's received audio stream, between the network and the OS" + control: Stream { stream: AudioStats.clientStream; showGraphs: stats.showGraphs } + } + } + } +} + diff --git a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml index c6b95dca30..13d9053adf 100644 --- a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml +++ b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml @@ -16,14 +16,14 @@ Item { width: parent.width height: 100 - property int hitboxExtension : 20 - // The title of the graph property string title // The object used as the default source object for the prop plots property var object + property var backgroundOpacity: 0.6 + // Plots is an array of plot descriptor // a default plot descriptor expects the following object: // prop: [ { @@ -55,9 +55,17 @@ Item { function createValues() { for (var i =0; i < plots.length; i++) { var plot = plots[i]; + var object = plot["object"] || root.object; + var value = plot["prop"]; + var isBinding = plot["binding"]; + if (isBinding) { + object = root.parent; + value = isBinding; + } _values.push( { - object: (plot["object"] !== undefined ? plot["object"] : root.object), - value: plot["prop"], + object: object, + value: value, + fromBinding: isBinding, valueMax: 1, numSamplesConstantMax: 0, valueHistory: new Array(), @@ -179,7 +187,7 @@ Item { ctx.fillText(text, 0, lineHeight); } function displayBackground(ctx) { - ctx.fillStyle = Qt.rgba(0, 0, 0, 0.6); + ctx.fillStyle = Qt.rgba(0, 0, 0, root.backgroundOpacity); ctx.fillRect(0, 0, width, height); ctx.strokeStyle= "grey"; @@ -210,15 +218,9 @@ Item { MouseArea { id: hitbox - anchors.fill:mycanvas - - anchors.topMargin: -hitboxExtension - anchors.bottomMargin: -hitboxExtension - anchors.leftMargin: -hitboxExtension - anchors.rightMargin: -hitboxExtension + anchors.fill: mycanvas onClicked: { - print("PerfPlot clicked!") resetMax(); } } 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/away.js b/scripts/system/away.js index f4a1a22bf6..96813031f1 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -54,7 +54,6 @@ var AWAY_INTRO = { var isEnabled = true; var wasMuted; // unknonwn? var isAway = false; // we start in the un-away state -var wasOverlaysVisible = Menu.isOptionChecked("Overlays"); var eventMappingName = "io.highfidelity.away"; // goActive on hand controller button events, too. var eventMapping = Controller.newMapping(eventMappingName); var avatarPosition = MyAvatar.position; @@ -178,11 +177,7 @@ function goAway(fromStartup) { playAwayAnimation(); // animation is still seen by others showOverlay(); - // remember the View > Overlays state... - wasOverlaysVisible = Menu.isOptionChecked("Overlays"); - - // show overlays so that people can see the "Away" message - Menu.setIsOptionChecked("Overlays", true); + HMD.requestShowHandControllers(); // tell the Reticle, we want to stop capturing the mouse until we come back Reticle.allowMouseCapture = false; @@ -222,6 +217,8 @@ function goActive() { MyAvatar.setEnableMeshVisible(true); // IWBNI we respected Developer->Avatar->Draw Mesh setting. stopAwayAnimation(); + HMD.requestHideHandControllers(); + // update the UI sphere to be centered about the current HMD orientation. HMD.centerUI(); @@ -233,9 +230,6 @@ function goActive() { hideOverlay(); - // restore overlays state to what it was when we went "away" - Menu.setIsOptionChecked("Overlays", wasOverlaysVisible); - // tell the Reticle, we are ready to capture the mouse again and it should be visible Reticle.allowMouseCapture = true; Reticle.visible = true; @@ -310,9 +304,9 @@ function setEnabled(value) { var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; var handleMessage = function(channel, message, sender) { - print("Got away message"); - if (channel == CHANNEL_AWAY_ENABLE) { - setEnabled(message == 'enable'); + if (channel === CHANNEL_AWAY_ENABLE && sender === MyAvatar.sessionUUID) { + print("away.js | Got message on Hifi-Away-Enable: ", message); + setEnabled(message === 'enable'); } } Messages.subscribe(CHANNEL_AWAY_ENABLE); @@ -349,6 +343,8 @@ Script.scriptEnding.connect(function () { Controller.disableMapping(eventMappingName); 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/tutorial/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js similarity index 74% rename from tutorial/controllerDisplay.js rename to scripts/system/controllers/controllerDisplay.js index 3bc0e05a96..6135f18426 100644 --- a/tutorial/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -1,26 +1,55 @@ -var VISIBLE_BY_DEFAULT = false; +// +// controllerDisplay.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* globals createControllerDisplay:true deleteControllerDisplay:true */ + var PARENT_ID = "{00000000-0000-0000-0000-000000000001}"; -var DEBUG = false; +function clamp(value, min, max) { + if (value < min) { + return min; + } else if (value > max) { + return max; + } + return value; +} + +function resolveHardware(path) { + var parts = path.split("."); + function resolveInner(base, path, i) { + if (i >= path.length) { + return base; + } + return resolveInner(base[path[i]], path, ++i); + } + return resolveInner(Controller.Hardware, parts, 0); +} + +var DEBUG = true; function debug() { if (DEBUG) { - print.apply(self, arguments); + 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", + partOverlays: {}, + parts: {}, + mappingName: "mapping-display-" + Math.random(), setVisible: function(visible) { - print("CONTROLLER_DISPLAY::Setting visible", this.overlays.length); + debug("Setting visible", this.overlays.length); for (var i = 0; i < this.overlays.length; ++i) { - print("i", i, this.overlays[i]); Overlays.editOverlay(this.overlays[i], { visible: visible }); @@ -30,7 +59,6 @@ createControllerDisplay = function(config) { setPartVisible: function(partName, visible) { return; if (partName in this.partOverlays) { - debug("Setting part visible", partName, visible); for (var i = 0; i < this.partOverlays[partName].length; ++i) { Overlays.editOverlay(this.partOverlays[partName][i], { //visible: visible @@ -41,7 +69,6 @@ createControllerDisplay = function(config) { setLayerForPart: function(partName, layerName) { if (partName in this.parts) { - debug("Setting layer...", partName, layerName); var part = this.parts[partName]; if (part.textureLayers && layerName in part.textureLayers) { var layer = part.textureLayers[layerName]; @@ -50,6 +77,9 @@ createControllerDisplay = function(config) { textures[part.textureName] = layer.defaultTextureURL; } for (var i = 0; i < this.partOverlays[partName].length; ++i) { + + // AJT: REMOVE + print("AJT: Overlays.editOverlays(" + partName + ", " + i + ", { textures: " + JSON.stringify(textures) + " })"); Overlays.editOverlay(this.partOverlays[partName][i], { textures: textures }); @@ -64,8 +94,7 @@ createControllerDisplay = function(config) { var position = controller.position; if (controller.naturalPosition) { - position = Vec3.sum(Vec3.multiplyQbyV( - controller.rotation, controller.naturalPosition), position); + position = Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position); } var overlayID = Overlays.addOverlay("model", { @@ -75,26 +104,17 @@ createControllerDisplay = function(config) { localPosition: position, parentID: PARENT_ID, parentJointIndex: controller.jointIndex, - ignoreRayIntersection: true, + ignoreRayIntersection: true }); controllerDisplay.overlays.push(overlayID); overlayID = null; - function clamp(value, min, max) { - if (value < min) { - return min; - } else if (value > max) { - return max - } - return value; - } - if (controller.parts) { for (var partName in controller.parts) { var part = controller.parts[partName]; var partPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); - var innerRotation = controller.rotation + var innerRotation = controller.rotation; controllerDisplay.parts[partName] = controller.parts[partName]; @@ -104,7 +124,7 @@ createControllerDisplay = function(config) { localRotation: innerRotation, parentID: PARENT_ID, parentJointIndex: controller.jointIndex, - ignoreRayIntersection: true, + ignoreRayIntersection: true }; if (part.defaultTextureLayer) { @@ -113,10 +133,9 @@ createControllerDisplay = function(config) { properties['textures'] = textures; } - var overlayID = Overlays.addOverlay("model", properties); + overlayID = Overlays.addOverlay("model", properties); - if (part.type == "rotational") { - var range = part.maxValue - part.minValue; + if (part.type === "rotational") { mapping.from([part.input]).peek().to(function(controller, overlayID, part) { return function(value) { value = clamp(value, part.minValue, part.maxValue); @@ -127,7 +146,7 @@ createControllerDisplay = function(config) { var offset = { x: 0, y: 0, z: 0 }; if (part.origin) { - var offset = Vec3.multiplyQbyV(rotation, part.origin); + offset = Vec3.multiplyQbyV(rotation, part.origin); offset = Vec3.subtract(offset, part.origin); } @@ -138,20 +157,9 @@ createControllerDisplay = function(config) { localPosition: partPosition, localRotation: Quat.multiply(controller.rotation, rotation) }); - } + }; }(controller, overlayID, part)); - } else if (part.type == "touchpad") { - function resolveHardware(path) { - var parts = path.split("."); - function resolveInner(base, path, i) { - if (i >= path.length) { - return base; - } - return resolveInner(base[path[i]], path, ++i); - } - return resolveInner(Controller.Hardware, parts, 0); - } - + } else if (part.type === "touchpad") { var visibleInput = resolveHardware(part.visibleInput); var xinput = resolveHardware(part.xInput); var yinput = resolveHardware(part.yInput); @@ -165,9 +173,10 @@ createControllerDisplay = function(config) { }); mapping.from([yinput]).peek().invert().to(function(value) { }); - } else if (part.type == "static") { + } else if (part.type === "static") { + // do nothing } else { - print("TYPE NOT SUPPORTED: ", part.type); + debug("TYPE NOT SUPPORTED: ", part.type); } controllerDisplay.overlays.push(overlayID); @@ -180,16 +189,11 @@ createControllerDisplay = function(config) { } Controller.enableMapping(controllerDisplay.mappingName); return controllerDisplay; -} - -ControllerDisplay = function() { }; deleteControllerDisplay = function(controllerDisplay) { - print("Deleting controller display"); for (var i = 0; i < controllerDisplay.overlays.length; ++i) { Overlays.deleteOverlay(controllerDisplay.overlays[i]); } Controller.disableMapping(controllerDisplay.mappingName); -} - +}; diff --git a/tutorial/viveHandsv2.js b/scripts/system/controllers/controllerDisplayManager.js similarity index 53% rename from tutorial/viveHandsv2.js rename to scripts/system/controllers/controllerDisplayManager.js index 890a5e1588..550357d659 100644 --- a/tutorial/viveHandsv2.js +++ b/scripts/system/controllers/controllerDisplayManager.js @@ -1,49 +1,26 @@ -if (!Function.prototype.bind) { - Function.prototype.bind = function(oThis) { - if (typeof this !== 'function') { - // closest thing possible to the ECMAScript 5 - // internal IsCallable function - throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); - } +// +// controllerDisplayManager.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function() {}, - fBound = function() { - return fToBind.apply(this instanceof fNOP - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; +/* globals ControllerDisplayManager:true createControllerDisplay deleteControllerDisplay + VIVE_CONTROLLER_CONFIGURATION_LEFT VIVE_CONTROLLER_CONFIGURATION_RIGHT */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ - if (this.prototype) { - // Function.prototype doesn't have a prototype property - fNOP.prototype = this.prototype; - } - fBound.prototype = new fNOP(); - - return fBound; - }; -} - -Script.setTimeout(function() { print('timeout') }, 100); +(function () { Script.include("controllerDisplay.js"); Script.include("viveControllerConfiguration.js"); -function debug() { - var args = Array.prototype.slice.call(arguments); - args.unshift("CONTROLLER DEBUG:"); - print.apply(this, args); -} +var HIDE_CONTROLLERS_ON_EQUIP = false; -var zeroPosition = { x: 0, y: 0, z: 0 }; -var zeroRotation = { x: 0, y: 0, z: 0, w: 1 }; - -/////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -// Management of controller display // -/////////////////////////////////////////////////////////////////////////////// +// +// Management of controller display +// ControllerDisplayManager = function() { var self = this; var controllerLeft = null; @@ -51,30 +28,24 @@ ControllerDisplayManager = function() { var controllerCheckerIntervalID = null; this.setLeftVisible = function(visible) { - print("settings controller display to visible"); if (controllerLeft) { - print("doign it...", visible); controllerLeft.setVisible(visible); } }; this.setRightVisible = function(visible) { - print("settings controller display to visible"); if (controllerRight) { - print("doign it...", visible); controllerRight.setVisible(visible); } }; function updateControllers() { - if (HMD.active) { + if (HMD.active && HMD.shouldShowHandControllers()) { if ("Vive" in Controller.Hardware) { if (!controllerLeft) { - debug("Found vive left!"); controllerLeft = createControllerDisplay(VIVE_CONTROLLER_CONFIGURATION_LEFT); } if (!controllerRight) { - debug("Found vive right!"); controllerRight = createControllerDisplay(VIVE_CONTROLLER_CONFIGURATION_RIGHT); } // We've found the controllers, we no longer need to look for active controllers @@ -83,17 +54,14 @@ ControllerDisplayManager = function() { controllerCheckerIntervalID = null; } } else { - debug("HMD active, but no controllers found"); self.deleteControllerDisplays(); - if (controllerCheckerIntervalID == null) { + if (!controllerCheckerIntervalID) { controllerCheckerIntervalID = Script.setInterval(updateControllers, 1000); } } } else { - debug("HMD inactive"); // We aren't in HMD mode, we no longer need to look for active controllers if (controllerCheckerIntervalID) { - debug("Clearing controller checker interval"); Script.clearInterval(controllerCheckerIntervalID); controllerCheckerIntervalID = null; } @@ -101,41 +69,35 @@ ControllerDisplayManager = function() { } } - Messages.subscribe('Controller-Display'); var handleMessages = function(channel, message, sender) { + var i, data, name, visible; if (!controllerLeft && !controllerRight) { return; } if (sender === MyAvatar.sessionUUID) { if (channel === 'Controller-Display') { - var data = JSON.parse(message); - var name = data.name; - var visible = data.visible; - //c.setDisplayAnnotation(name, visible); + data = JSON.parse(message); + name = data.name; + visible = data.visible; if (controllerLeft) { if (name in controllerLeft.annotations) { - debug("hiding"); - for (var i = 0; i < controllerLeft.annotations[name].length; ++i) { - debug("hiding", i); + for (i = 0; i < controllerLeft.annotations[name].length; ++i) { Overlays.editOverlay(controllerLeft.annotations[name][i], { visible: visible }); } } } if (controllerRight) { if (name in controllerRight.annotations) { - debug("hiding"); - for (var i = 0; i < controllerRight.annotations[name].length; ++i) { - debug("hiding", i); + for (i = 0; i < controllerRight.annotations[name].length; ++i) { Overlays.editOverlay(controllerRight.annotations[name][i], { visible: visible }); } } } } else if (channel === 'Controller-Display-Parts') { - debug('here part'); - var data = JSON.parse(message); - for (var name in data) { - var visible = data[name]; + data = JSON.parse(message); + for (name in data) { + visible = data[name]; if (controllerLeft) { controllerLeft.setPartVisible(name, visible); } @@ -144,8 +106,8 @@ ControllerDisplayManager = function() { } } } else if (channel === 'Controller-Set-Part-Layer') { - var data = JSON.parse(message); - for (var name in data) { + data = JSON.parse(message); + for (name in data) { var layer = data[name]; if (controllerLeft) { controllerLeft.setLayerForPart(name, layer); @@ -154,20 +116,19 @@ ControllerDisplayManager = function() { controllerRight.setLayerForPart(name, layer); } } - } else if (channel == 'Hifi-Object-Manipulation') {// && sender == MyAvatar.sessionUUID) { - //print("got manip"); - var data = JSON.parse(message); - //print("post data", data); - var visible = data.action != 'equip'; - //print("Calling..."); - if (data.joint == "LeftHand") { - self.setLeftVisible(visible); - } else if (data.joint == "RightHand") { - self.setRightVisible(visible); - } + } else if (channel === 'Hifi-Object-Manipulation') { + if (HIDE_CONTROLLERS_ON_EQUIP) { + data = JSON.parse(message); + visible = data.action !== 'equip'; + if (data.joint === "LeftHand") { + self.setLeftVisible(visible); + } else if (data.joint === "RightHand") { + self.setRightVisible(visible); + } + } } } - } + }; Messages.messageReceived.connect(handleMessages); @@ -183,12 +144,24 @@ ControllerDisplayManager = function() { }; this.destroy = function() { - print("Destroying controller display"); Messages.messageReceived.disconnect(handleMessages); + + HMD.displayModeChanged.disconnect(updateControllers); + HMD.shouldShowHandControllersChanged.disconnect(updateControllers); + self.deleteControllerDisplays(); }; HMD.displayModeChanged.connect(updateControllers); + HMD.shouldShowHandControllersChanged.connect(updateControllers); updateControllers(); -} +}; + +var controllerDisplayManager = new ControllerDisplayManager(); + +Script.scriptEnding.connect(function () { + controllerDisplayManager.destroy(); +}); + +}()); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8f54e3b62e..1ffa993d01 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -11,13 +11,16 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global setEntityCustomData, getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset */ + +/* global setEntityCustomData, getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, + Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE -Script.include("/~/system/libraries/utils.js"); -Script.include("/~/system/libraries/Xform.js"); -Script.include("/~/system/libraries/controllers.js"); +Script.include("../libraries/utils.js"); +Script.include("../libraries/Xform.js"); +Script.include("../libraries/controllers.js"); // // add lines where the hand ray picking is happening @@ -27,7 +30,7 @@ var WANT_DEBUG_STATE = false; var WANT_DEBUG_SEARCH_NAME = null; var FORCE_IGNORE_IK = false; -var SHOW_GRAB_POINT_SPHERE = true; +var SHOW_GRAB_POINT_SPHERE = false; // // these tune time-averaging and "on" value for analog trigger @@ -101,7 +104,7 @@ var MAX_EQUIP_HOTSPOT_RADIUS = 1.0; var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var NEAR_GRAB_RADIUS = 0.04; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_RADIUS = 0.1; // radius used for palm vs object for near grabbing. var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. @@ -792,7 +795,7 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - + setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_NEAR_GRABBING)); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); @@ -835,7 +838,7 @@ function MyController(hand) { this.grabPointSphere = Overlays.addOverlay("sphere", { localPosition: getGrabPointSphereOffset(this.handToController()), localRotation: { x: 0, y: 0, z: 0, w: 1 }, - dimensions: GRAB_POINT_SPHERE_RADIUS, + dimensions: GRAB_POINT_SPHERE_RADIUS * 2, color: GRAB_POINT_SPHERE_COLOR, alpha: GRAB_POINT_SPHERE_ALPHA, solid: true, @@ -2091,12 +2094,12 @@ function MyController(hand) { var TEAR_AWAY_DISTANCE = 0.1; var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); if (dist > TEAR_AWAY_DISTANCE) { - this.autoUnequipCounter += 1; + this.autoUnequipCounter += deltaTime; } else { this.autoUnequipCounter = 0; } - if (this.autoUnequipCounter > 1) { + if (this.autoUnequipCounter > 0.25) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + ", dist = " + dist); @@ -2661,6 +2664,15 @@ mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController. Controller.enableMapping(MAPPING_NAME); +function handleMenuEvent(menuItem) { + if (menuItem === "Show Grab Sphere") { + SHOW_GRAB_POINT_SPHERE = Menu.isOptionChecked("Show Grab Sphere"); + } +} + +Menu.addMenuItem({ menuName: "Developer", menuItemName: "Show Grab Sphere", isCheckable: true, isChecked: false }); +Menu.menuItemEvent.connect(handleMenuEvent); + // the section below allows the grab script to listen for messages // that disable either one or both hands. useful for two handed items var handToDisable = 'none'; @@ -2770,7 +2782,14 @@ var handleHandMessages = function(channel, message, sender) { Messages.messageReceived.connect(handleHandMessages); +var BASIC_TIMER_INTERVAL_MS = 20; // 20ms = 50hz good enough +var updateIntervalTimer = Script.setInterval(function(){ + update(BASIC_TIMER_INTERVAL_MS / 1000); +}, BASIC_TIMER_INTERVAL_MS); + function cleanup() { + Menu.removeMenuItem("Developer", "Show Grab Sphere"); + Script.clearInterval(updateIntervalTimer); rightController.cleanup(); leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); @@ -2778,6 +2797,5 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 5bb8ea8a90..c2deaa2fac 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -20,7 +20,7 @@ // When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand // controller beam intersects the HUD. -Script.include("/~/system/libraries/controllers.js"); +Script.include("../libraries/controllers.js"); // UTILITIES ------------- // @@ -204,7 +204,7 @@ function overlayFromWorldPoint(point) { } function activeHudPoint2d(activeHand) { // if controller is valid, update reticle position and answer 2d point. Otherwise falsey. - var controllerPose = getControllerWorldLocation(activeHand, true); + var controllerPose = getControllerWorldLocation(activeHand, true); // note: this will return head pose if hand pose is invalid (third eye) if (!controllerPose.valid) { return; // Controller is cradled. } @@ -447,12 +447,20 @@ function clearSystemLaser() { return; } HMD.disableHandLasers(BOTH_HUD_LASERS); + HMD.disableExtraLaser(); systemLaserOn = false; weMovedReticle = true; Reticle.position = { x: -1, y: -1 }; } function setColoredLaser() { // answer trigger state if lasers supported, else falsey. var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; + + if (!HMD.isHandControllerAvailable()) { + var position = MyAvatar.getHeadPosition(); + var direction = Quat.getUp(Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 }))); + return HMD.setExtraLaser(position, true, color, direction); + } + return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; } @@ -471,7 +479,12 @@ function update() { if (!Menu.isOptionChecked("First Person")) { return off(); // What to do? menus can be behind hand! } - if (!Window.hasFocus() || !Reticle.allowMouseCapture) { + if ((!Window.hasFocus() && !HMD.active) || !Reticle.allowMouseCapture) { + // In desktop it's pretty clear when another app is on top. In that case we bail, because + // hand controllers might be sputtering "valid" data and that will keep someone from deliberately + // using the mouse on another app. (Fogbugz case 546.) + // However, in HMD, you might not realize you're not on top, and you wouldn't be able to operate + // other apps anyway. So in that case, we DO keep going even though we're not on top. (Fogbugz 1831.) return off(); // Don't mess with other apps or paused mouse activity } leftTrigger.update(); @@ -479,15 +492,28 @@ function update() { if (!activeTrigger.state) { return off(); // No trigger } + if (getGrabCommunications()) { + return off(); + } var hudPoint2d = activeHudPoint2d(activeHand); if (!hudPoint2d) { return off(); } + + // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { if (HMD.active) { Reticle.depth = hudReticleDistance(); + + if (!HMD.isHandControllerAvailable()) { + var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; + var position = MyAvatar.getHeadPosition(); + var direction = Quat.getUp(Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 }))); + HMD.setExtraLaser(position, true, color, direction); + } } + if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. systemLaserOn = setColoredLaser(); @@ -502,10 +528,15 @@ function update() { clearSystemLaser(); Reticle.visible = false; } -setupHandler(Script.update, update); + +var BASIC_TIMER_INTERVAL = 20; // 20ms = 50hz good enough +var updateIntervalTimer = Script.setInterval(function(){ + update(); +}, BASIC_TIMER_INTERVAL); + // Check periodically for changes to setup. -var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds +var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // 10 seconds function checkSettings() { updateFieldOfView(); updateRecommendedArea(); @@ -515,6 +546,7 @@ checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); + Script.clearInterval(updateIntervalTimer); OffscreenFlags.navigationFocusDisabled = false; }); diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 2a7dbb000b..d3284352bf 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -91,7 +91,6 @@ function Teleporter() { this.tooClose = false; this.inCoolIn = false; - this.initialize = function() { this.createMappings(); }; @@ -142,7 +141,10 @@ function Teleporter() { }; - this.createTargetOverlay = function() { + this.createTargetOverlay = function(visible) { + if (visible == undefined) { + visible = true; + } if (_this.targetOverlay !== null) { return; @@ -150,20 +152,17 @@ function Teleporter() { var targetOverlayProps = { url: TARGET_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, - visible: true - }; - - var cancelOverlayProps = { - url: TOO_CLOSE_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: true + visible: visible }; _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; - this.createCancelOverlay = function() { + this.createCancelOverlay = function(visible) { + if (visible == undefined) { + visible = true; + } if (_this.cancelOverlay !== null) { return; @@ -172,7 +171,7 @@ function Teleporter() { var cancelOverlayProps = { url: TOO_CLOSE_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, - visible: true + visible: visible }; _this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps); @@ -187,6 +186,23 @@ function Teleporter() { this.cancelOverlay = null; } + this.hideCancelOverlay = function() { + if (this.cancelOverlay === null) { + return; + } + + this.intersection = null; + Overlays.editOverlay(this.cancelOverlay, { visible: false }); + } + + this.showCancelOverlay = function() { + if (this.cancelOverlay === null) { + return this.createCancelOverlay(); + } + Overlays.editOverlay(this.cancelOverlay, { visible: true }); + } + + this.deleteTargetOverlay = function() { if (this.targetOverlay === null) { return; @@ -197,6 +213,22 @@ function Teleporter() { this.targetOverlay = null; } + this.hideTargetOverlay = function() { + if (this.targetOverlay === null) { + return; + } + + this.intersection = null; + Overlays.editOverlay(this.targetOverlay, { visible: false }); + } + + this.showTargetOverlay = function() { + if (this.targetOverlay === null) { + return this.createTargetOverlay(); + } + Overlays.editOverlay(this.targetOverlay, { visible: true }); + } + this.turnOffOverlayBeams = function() { this.rightOverlayOff(); this.leftOverlayOff(); @@ -232,8 +264,8 @@ function Teleporter() { if ((leftPad.buttonValue === 0) && inTeleportMode === true) { if (_this.inCoolIn === true) { _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - _this.deleteCancelOverlay(); + _this.hideTargetOverlay(); + _this.hideCancelOverlay(); } else { _this.teleport(); } @@ -248,8 +280,8 @@ function Teleporter() { if ((rightPad.buttonValue === 0) && inTeleportMode === true) { if (_this.inCoolIn === true) { _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - _this.deleteCancelOverlay(); + _this.hideTargetOverlay(); + _this.hideCancelOverlay(); } else { _this.teleport(); } @@ -283,7 +315,7 @@ function Teleporter() { if (rightIntersection.intersects) { if (this.tooClose === true) { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); if (this.cancelOverlay !== null) { @@ -293,7 +325,7 @@ function Teleporter() { } } else { if (this.inCoolIn === true) { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); if (this.cancelOverlay !== null) { this.updateCancelOverlay(rightIntersection); @@ -301,7 +333,7 @@ function Teleporter() { this.createCancelOverlay(); } } else { - this.deleteCancelOverlay(); + this.hideCancelOverlay(); this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); if (this.targetOverlay !== null) { @@ -316,7 +348,7 @@ function Teleporter() { } else { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.rightLineOn(rightPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); } } @@ -347,7 +379,7 @@ function Teleporter() { if (leftIntersection.intersects) { if (this.tooClose === true) { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); if (this.cancelOverlay !== null) { this.updateCancelOverlay(leftIntersection); @@ -356,7 +388,7 @@ function Teleporter() { } } else { if (this.inCoolIn === true) { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); if (this.cancelOverlay !== null) { this.updateCancelOverlay(leftIntersection); @@ -364,7 +396,7 @@ function Teleporter() { this.createCancelOverlay(); } } else { - this.deleteCancelOverlay(); + this.hideCancelOverlay(); this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); if (this.targetOverlay !== null) { @@ -380,7 +412,7 @@ function Teleporter() { } else { - this.deleteTargetOverlay(); + this.hideTargetOverlay(); this.leftLineOn(leftPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); } }; @@ -463,6 +495,7 @@ function Teleporter() { var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); Overlays.editOverlay(this.targetOverlay, { + visible: true, position: position, rotation: towardUs }); @@ -484,6 +517,7 @@ function Teleporter() { var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); Overlays.editOverlay(this.cancelOverlay, { + visible: true, position: position, rotation: towardUs }); @@ -503,7 +537,7 @@ function Teleporter() { if (this.intersection !== null) { if (this.tooClose === true) { this.exitTeleportMode(); - this.deleteCancelOverlay(); + this.hideCancelOverlay(); return; } var offset = getAvatarFootOffset(); @@ -512,8 +546,8 @@ function Teleporter() { // Disable smooth arrival, possibly temporarily //this.smoothArrival(); MyAvatar.position = _this.intersection.intersection; - _this.deleteTargetOverlay(); - _this.deleteCancelOverlay(); + _this.hideTargetOverlay(); + _this.hideCancelOverlay(); HMD.centerUI(); } }; @@ -556,14 +590,16 @@ function Teleporter() { MyAvatar.position = landingPoint; if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { - _this.deleteTargetOverlay(); - _this.deleteCancelOverlay(); + _this.hideTargetOverlay(); + _this.hideCancelOverlay(); } }, SMOOTH_ARRIVAL_SPACING); - - } + + this.createTargetOverlay(false); + this.createCancelOverlay(false); + } //related to repositioning the avatar after you teleport diff --git a/tutorial/viveControllerConfiguration.js b/scripts/system/controllers/viveControllerConfiguration.js similarity index 59% rename from tutorial/viveControllerConfiguration.js rename to scripts/system/controllers/viveControllerConfiguration.js index b8197d6cd2..b49c3e1d04 100644 --- a/tutorial/viveControllerConfiguration.js +++ b/scripts/system/controllers/viveControllerConfiguration.js @@ -1,3 +1,16 @@ +// +// viveControllerConfiguration.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals VIVE_CONTROLLER_CONFIGURATION_LEFT:true VIVE_CONTROLLER_CONFIGURATION_RIGHT:true */ +/* eslint camelcase: ["error", { "properties": "never" }] */ + var LEFT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_LEFTHAND"); var RIGHT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_RIGHTHAND"); @@ -16,16 +29,20 @@ var rightBaseRotation = Quat.multiply( Quat.fromPitchYawRollDegrees(0, 0, -90) ) ); -var CONTROLLER_LENGTH_OFFSET = 0.0762; + +// keep these in sync with the values from plugins/openvr/src/OpenVrHelpers.cpp:303 +var CONTROLLER_LATERAL_OFFSET = 0.0381; +var CONTROLLER_VERTICAL_OFFSET = 0.0495; +var CONTROLLER_FORWARD_OFFSET = 0.1371; var leftBasePosition = { - x: CONTROLLER_LENGTH_OFFSET / 2, - y: CONTROLLER_LENGTH_OFFSET * 2, - z: CONTROLLER_LENGTH_OFFSET / 2 + x: CONTROLLER_VERTICAL_OFFSET, + y: CONTROLLER_FORWARD_OFFSET, + z: CONTROLLER_LATERAL_OFFSET }; var rightBasePosition = { - x: -CONTROLLER_LENGTH_OFFSET / 2, - y: CONTROLLER_LENGTH_OFFSET * 2, - z: CONTROLLER_LENGTH_OFFSET / 2 + x: -CONTROLLER_VERTICAL_OFFSET, + y: CONTROLLER_FORWARD_OFFSET, + z: CONTROLLER_LATERAL_OFFSET }; var viveNaturalDimensions = { @@ -40,8 +57,12 @@ var viveNaturalPosition = { z: 0.06380049744620919 }; -var viveModelURL = "atp:/controller/vive_body.fbx"; -var viveTipsModelURL = "atp:/controller/vive_tips.fbx" +var BASE_URL = Script.resourcesPath(); +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", @@ -66,20 +87,20 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { defaultTextureLayer: "blank", textureLayers: { blank: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Blank.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" }, trigger: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Trigger.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" }, arrows: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Rotate.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png" }, grip: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Grip.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png" }, teleport: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Teleport.png", - }, + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png" + } } }, @@ -87,7 +108,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { // and swaps in textures based on the thumb position. touchpad: { type: "touchpad", - modelURL: "atp:/controller/vive_trackpad.fbx", + modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx", visibleInput: "Vive.RSTouch", xInput: "Vive.LX", yInput: "Vive.LY", @@ -101,64 +122,74 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { disable_defaultTextureLayer: "blank", disable_textureLayers: { blank: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg" }, teleport: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg" }, arrows: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows.jpg" } } }, trigger: { type: "rotational", - modelURL: "atp:/controller/vive_trigger.fbx", + modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx", input: Controller.Standard.LT, naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763}, origin: { x: 0, y: -0.015, z: -0.00 }, 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: { type: "static", - modelURL: "atp:/controller/vive_l_grip.fbx", - naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx", + naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226} }, r_grip: { type: "static", - modelURL: "atp:/controller/vive_r_grip.fbx", - naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx", + naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226} }, sys_button: { type: "static", - modelURL: "atp:/controller/vive_sys_button.fbx", - naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311}, + modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx", + naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311} }, button: { type: "static", - modelURL: "atp:/controller/vive_button.fbx", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564} }, button2: { type: "static", - modelURL: "atp:/controller/vive_button.fbx", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564} - }, - }, - }, + } + } + } ] }; - VIVE_CONTROLLER_CONFIGURATION_RIGHT = { name: "Vive Right", controllers: [ @@ -188,20 +219,20 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { defaultTextureLayer: "blank", textureLayers: { blank: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Blank.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" }, trigger: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Trigger.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" }, arrows: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Rotate.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png" }, grip: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Grip.png", + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png" }, teleport: { - defaultTextureURL: viveTipsModelURL + "/Controller-Tips.fbm/Teleport.png", - }, + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png" + } } }, @@ -209,7 +240,7 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { // and swaps in textures based on the thumb position. touchpad: { type: "touchpad", - modelURL: "atp:/controller/vive_trackpad.fbx", + modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx", visibleInput: "Vive.RSTouch", xInput: "Vive.RX", yInput: "Vive.RY", @@ -223,20 +254,20 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { disable_defaultTextureLayer: "blank", disable_textureLayers: { blank: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg" }, teleport: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg" }, arrows: { - defaultTextureURL: "atp:/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows-active.jpg", + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows-active.jpg" } } }, trigger: { type: "rotational", - modelURL: "atp:/controller/vive_trigger.fbx", + modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx", input: Controller.Standard.RT, naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763}, origin: { x: 0, y: -0.015, z: -0.00 }, @@ -244,37 +275,48 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { maxValue: 1.0, axis: { x: -1, y: 0, z: 0 }, 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: { type: "static", - modelURL: "atp:/controller/vive_l_grip.fbx", - naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx", + naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226} }, r_grip: { type: "static", - modelURL: "atp:/controller/vive_r_grip.fbx", - naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx", + naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226} }, sys_button: { type: "static", - modelURL: "atp:/controller/vive_sys_button.fbx", - naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311}, + modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx", + naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311} }, button: { type: "static", - modelURL: "atp:/controller/vive_button.fbx", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564} }, button2: { type: "static", - modelURL: "atp:/controller/vive_button.fbx", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564} - }, - }, + } + } } ] }; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index d673bb4653..1382c94f9c 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1155,7 +1155,7 @@ function getPositionToImportEntity() { return position; } function importSVO(importURL) { - if (!Entities.canAdjustLocks()) { + if (!Entities.canRez() && !Entities.canRezTmp()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); return; } diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 5cc8b67b44..1de59ad135 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -312,15 +312,11 @@
-
- +
M diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 0bcae7f409..a7dfb048b8 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -591,10 +591,7 @@ function loaded() { var elLifetime = document.getElementById("property-lifetime"); var elScriptURL = document.getElementById("property-script-url"); - /* - FIXME: See FIXME for property-script-url. var elScriptTimestamp = document.getElementById("property-script-timestamp"); - */ var elReloadScriptButton = document.getElementById("reload-script-button"); var elUserData = document.getElementById("property-user-data"); var elClearUserData = document.getElementById("userdata-clear"); @@ -851,10 +848,7 @@ function loaded() { elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; elScriptURL.value = properties.script; - /* - FIXME: See FIXME for property-script-url. elScriptTimestamp.value = properties.scriptTimestamp; - */ var json = null; try { @@ -1150,11 +1144,7 @@ function loaded() { elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); - /* - FIXME: See FIXME for property-script-url. elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); - */ - elClearUserData.addEventListener("click", function() { deleteJSONEditor(); @@ -1171,11 +1161,8 @@ function loaded() { properties: properties, }) ); - - }); - elSaveUserData.addEventListener("click", function() { saveJSONUserData(true); }); @@ -1410,15 +1397,12 @@ function loaded() { percentage: parseInt(elRescaleDimensionsPct.value), })); }); - /* - FIXME: See FIXME for property-script-url. elReloadScriptButton.addEventListener("click", function() { EventBridge.emitWebEvent(JSON.stringify({ type: "action", action: "reloadScript" })); }); - */ window.onblur = function() { // Fake a change event diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index a255b7a494..0c21ea22ae 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -39,7 +39,7 @@ function calcSpawnInfo() { } // ctor -WebTablet = function (url, width, dpi) { +WebTablet = function (url, width, dpi, clientOnly) { var ASPECT = 4.0 / 3.0; var WIDTH = width || DEFAULT_WIDTH; @@ -63,7 +63,7 @@ WebTablet = function (url, width, dpi) { dimensions: {x: WIDTH, y: HEIGHT, z: DEPTH}, parentID: MyAvatar.sessionUUID, parentJointIndex: -2 - }); + }, clientOnly); var WEB_ENTITY_REDUCTION_FACTOR = {x: 0.78, y: 0.85}; var WEB_ENTITY_Z_OFFSET = -0.01; @@ -84,7 +84,7 @@ WebTablet = function (url, width, dpi) { dpi: DPI, parentID: this.tabletEntityID, parentJointIndex: -1 - }); + }, clientOnly); this.state = "idle"; }; @@ -93,4 +93,14 @@ WebTablet.prototype.destroy = function () { Entities.deleteEntity(this.webEntityID); Entities.deleteEntity(this.tabletEntityID); }; - +WebTablet.prototype.pickle = function () { + return JSON.stringify({webEntityID: this.webEntityID, tabletEntityID: this.tabletEntityID}); +}; +WebTablet.unpickle = function (string) { + if (!string) { + return; + } + var tablet = JSON.parse(string); + tablet.__proto__ = WebTablet.prototype; + return tablet; +}; diff --git a/scripts/system/libraries/controllers.js b/scripts/system/libraries/controllers.js index c77ce4578c..38febf2de4 100644 --- a/scripts/system/libraries/controllers.js +++ b/scripts/system/libraries/controllers.js @@ -7,13 +7,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global MyAvatar, Vec3, Controller, Quat */ +var GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing"; +setGrabCommunications = function setFarGrabCommunications(on) { + Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : ""); +} +getGrabCommunications = function getFarGrabCommunications() { + return !!Settings.getValue(GRAB_COMMUNICATIONS_SETTING, ""); +} -// var GRAB_POINT_SPHERE_OFFSET = { x: 0, y: 0.2, z: 0 }; -// var GRAB_POINT_SPHERE_OFFSET = { x: 0.1, y: 0.175, z: 0.04 }; -// var GRAB_POINT_SPHERE_OFFSET = { x: 0.1, y: 0.32, z: 0.04 }; - -// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp -var GRAB_POINT_SPHERE_OFFSET = { x: 0.0, y: 0.175, z: 0.0 }; +// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378 +var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; // x = upward, y = forward, z = lateral getGrabPointSphereOffset = function(handController) { if (handController === Controller.Standard.RightHand) { @@ -31,6 +34,7 @@ getControllerWorldLocation = function (handController, doOffset) { var orientation; var position; var pose = Controller.getPoseValue(handController); + var valid = pose.valid; if (pose.valid) { orientation = Quat.multiply(MyAvatar.orientation, pose.rotation); position = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); @@ -38,10 +42,15 @@ getControllerWorldLocation = function (handController, doOffset) { if (doOffset) { position = Vec3.sum(position, Vec3.multiplyQbyV(orientation, getGrabPointSphereOffset(handController))); } + } else if (!HMD.isHandControllerAvailable()) { + position = MyAvatar.getHeadPosition(); + orientation = Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 })); + valid = true; } + return {position: position, translation: position, orientation: orientation, rotation: orientation, - valid: pose.valid}; + valid: valid}; }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9d98a19305..d31f4cb722 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -16,6 +16,8 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; SPACE_LOCAL = "local"; SPACE_WORLD = "world"; +Script.include("./controllers.js"); + function objectTranslationPlanePoint(position, dimensions) { var newPosition = { x: position.x, y: position.y, z: position.z }; newPosition.y -= dimensions.y / 2.0; @@ -1046,12 +1048,11 @@ SelectionDisplay = (function() { that.triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); that.triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); function controllerComputePickRay() { - var controllerPose = Controller.getPoseValue(activeHand); + var controllerPose = getControllerWorldLocation(activeHand, true); if (controllerPose.valid && that.triggered) { - var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), - MyAvatar.position); + var controllerPosition = controllerPose.translation; // This gets point direction right, but if you want general quaternion it would be more complicated: - var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + var controllerDirection = Quat.getUp(controllerPose.rotation); return {origin: controllerPosition, direction: controllerDirection}; } } diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index 563a5289fc..894dae7eac 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -30,6 +30,10 @@ var TOOLBAR_MARGIN_Y = 0; var marketplaceVisible = false; var marketplaceWebTablet; +// We persist clientOnly data in the .ini file, and reconsistitute it on restart. +// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change. +var persistenceKey = "io.highfidelity.lastDomainTablet"; + function shouldShowWebTablet() { var rightPose = Controller.getPoseValue(Controller.Standard.RightHand); var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand); @@ -40,7 +44,8 @@ function shouldShowWebTablet() { function showMarketplace(marketplaceID) { if (shouldShowWebTablet()) { updateButtonState(true); - marketplaceWebTablet = new WebTablet("https://metaverse.highfidelity.com/marketplace"); + marketplaceWebTablet = new WebTablet("https://metaverse.highfidelity.com/marketplace", null, null, true); + Settings.setValue(persistenceKey, marketplaceWebTablet.pickle()); } else { var url = MARKETPLACE_URL; if (marketplaceID) { @@ -54,14 +59,25 @@ function showMarketplace(marketplaceID) { UserActivityLogger.openedMarketplace(); } +function hideTablet(tablet) { + if (!tablet) { + return; + } + updateButtonState(false); + tablet.destroy(); + marketplaceWebTablet = null; + Settings.setValue(persistenceKey, ""); +} +function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated + var tablet = WebTablet.unpickle(Settings.getValue(persistenceKey, "")); + hideTablet(tablet); +} function hideMarketplace() { if (marketplaceWindow.visible) { marketplaceWindow.setVisible(false); marketplaceWindow.setURL("about:blank"); } else if (marketplaceWebTablet) { - updateButtonState(false); - marketplaceWebTablet.destroy(); - marketplaceWebTablet = null; + hideTablet(marketplaceWebTablet); } marketplaceVisible = false; } @@ -102,6 +118,10 @@ function onClick() { browseExamplesButton.clicked.connect(onClick); marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged); +clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash. +// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}), +// but the HUD version stays around, so lets do the same. + Script.scriptEnding.connect(function () { toolBar.removeButton("marketplace"); browseExamplesButton.clicked.disconnect(onClick); 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/scripts/system/users.js b/scripts/system/users.js index af88e73362..1e4cf042f3 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -381,7 +381,7 @@ var usersWindow = (function () { isLoggedIn = false, isVisible = true, - isMinimized = true, + isMinimized = false, isBorderVisible = false, viewport, @@ -418,7 +418,9 @@ var usersWindow = (function () { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + + (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) + + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; @@ -485,16 +487,22 @@ var usersWindow = (function () { y: scrollbarBarPosition.y }); + x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + y = windowPosition.y + - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; - Overlays.editOverlay(friendsButton, { - x: x, - y: y - }); + if (shouldShowFriendsButton()) { + y -= FRIENDS_BUTTON_HEIGHT; + Overlays.editOverlay(friendsButton, { + x: x, + y: y + }); + y += FRIENDS_BUTTON_HEIGHT; + } - y += FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER; + y += DISPLAY_SPACER; displayControl.updatePosition(x, y); y += windowLineHeight + VISIBILITY_SPACER; @@ -563,6 +571,10 @@ var usersWindow = (function () { }); } + function shouldShowFriendsButton() { + return isVisible && isLoggedIn && !isMinimized; + } + function updateOverlayVisibility() { Overlays.editOverlay(windowBorder, { visible: isVisible && isBorderVisible @@ -574,7 +586,7 @@ var usersWindow = (function () { visible: isVisible }); Overlays.editOverlay(minimizeButton, { - visible: isLoggedIn && isVisible + visible: isVisible }); Overlays.editOverlay(scrollbarBackground, { visible: isVisible && isUsingScrollbars && !isMinimized @@ -583,7 +595,7 @@ var usersWindow = (function () { visible: isVisible && isUsingScrollbars && !isMinimized }); Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized + visible: shouldShowFriendsButton() }); displayControl.setVisible(isVisible && !isMinimized); visibilityControl.setVisible(isVisible && !isMinimized); @@ -659,12 +671,11 @@ var usersWindow = (function () { } } + checkLoggedIn(); calculateWindowHeight(); updateUsersDisplay(); updateOverlayPositions(); - checkLoggedIn(); - } else { print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. @@ -1182,7 +1193,7 @@ var usersWindow = (function () { pollUsers(); // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false))); + setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, true))); } function tearDown() { diff --git a/server-console/package.json b/server-console/package.json index d97f72609b..f72ffc347f 100644 --- a/server-console/package.json +++ b/server-console/package.json @@ -1,6 +1,6 @@ { - "name": "hf-console", - "description": "High Fidelity Console", + "name": "HighFidelitySandbox", + "description": "High Fidelity Sandbox", "author": "High Fidelity", "license": "Apache-2.0", "version": "1.0.0", @@ -33,6 +33,7 @@ "request": "^2.67.0", "request-progress": "1.0.2", "tar-fs": "^1.12.0", - "yargs": "^3.30.0" + "yargs": "^3.30.0", + "electron-log": "1.1.1" } } diff --git a/server-console/src/content-update.html b/server-console/src/content-update.html index c4ed14473a..279b72fbc0 100644 --- a/server-console/src/content-update.html +++ b/server-console/src/content-update.html @@ -6,7 +6,7 @@ -
+

We backed up your old Sandbox content, just in case.

To restore it, follow these steps: @@ -45,9 +45,9 @@

-
diff --git a/server-console/src/content-update.js b/server-console/src/content-update.js index c77cfc92c6..2835444841 100644 --- a/server-console/src/content-update.js +++ b/server-console/src/content-update.js @@ -6,6 +6,11 @@ function ready() { electron.ipcRenderer.on('update', function(event, message) { $('#directory').html(message); + + electron.ipcRenderer.send('setSize', { + width: $(window).width(), + height: $('#content').height() + 50 + }); }); electron.ipcRenderer.send('ready'); diff --git a/server-console/src/main.js b/server-console/src/main.js index 92bb549fbb..d8a6f30ac1 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -42,7 +42,7 @@ const appIcon = path.join(__dirname, '../resources/console.png'); const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; -const HOME_CONTENT_URL = "http://cachefly.highfidelity.com/home-tutorial-9.tar.gz"; +const HOME_CONTENT_URL = "http://cachefly.highfidelity.com/home-tutorial-release-5572.tar.gz"; function getBuildInfo() { var buildInfoPath = null; @@ -64,7 +64,6 @@ function getBuildInfo() { var buildInfo = DEFAULT_BUILD_INFO; if (buildInfoPath) { - console.log('Build info path:', buildInfoPath); try { buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); } catch (e) { @@ -74,11 +73,8 @@ function getBuildInfo() { return buildInfo; } - const buildInfo = getBuildInfo(); -console.log("build info", buildInfo); - function getRootHifiDataDirectory() { var organization = "High Fidelity"; if (buildInfo.releaseType != "PRODUCTION") { @@ -105,16 +101,26 @@ function getApplicationDataDirectory() { return path.join(getRootHifiDataDirectory(), '/Server Console'); } -console.log("Root hifi directory is: ", getRootHifiDataDirectory()); +// Configure log +global.log = require('electron-log'); +const logFile = getApplicationDataDirectory() + '/log.txt'; +fs.ensureFileSync(logFile); // Ensure file exists +log.transports.file.maxSize = 5 * 1024 * 1024; +log.transports.file.file = logFile; + +log.debug("build info", buildInfo); +log.debug("Root hifi directory is: ", getRootHifiDataDirectory()); const ipcMain = electron.ipcMain; var isShuttingDown = false; function shutdown() { + log.debug("Normal shutdown (isShuttingDown: " + isShuttingDown + ")"); if (!isShuttingDown) { // if the home server is running, show a prompt before quit to ask if the user is sure if (homeServer.state == ProcessGroupStates.STARTED) { + log.debug("Showing shutdown dialog."); dialog.showMessageBox({ type: 'question', buttons: ['Yes', 'No'], @@ -128,16 +134,27 @@ function shutdown() { } } +function forcedShutdown() { + log.debug("Forced shutdown (isShuttingDown: " + isShuttingDown + ")"); + if (!isShuttingDown) { + shutdownCallback(0); + } +} + function shutdownCallback(idx) { + log.debug("Entering shutdown callback."); if (idx == 0 && !isShuttingDown) { isShuttingDown = true; + log.debug("Saving user config"); userConfig.save(configPath); if (logWindow) { + log.debug("Closing log window"); logWindow.close(); } if (homeServer) { + log.debug("Stoping home server"); homeServer.stop(); } @@ -145,15 +162,20 @@ function shutdownCallback(idx) { if (homeServer.state == ProcessGroupStates.STOPPED) { // if the home server is already down, take down the server console now - app.quit(); + log.debug("Quitting."); + app.exit(0); } else { // if the home server is still running, wait until we get a state change or timeout // before quitting the app - var timeoutID = setTimeout(app.quit, 5000); + log.debug("Server still shutting down. Waiting"); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); homeServer.on('state-update', function(processGroup) { if (processGroup.state == ProcessGroupStates.STOPPED) { clearTimeout(timeoutID); - app.quit(); + log.debug("Quitting."); + app.exit(0); } }); } @@ -161,36 +183,36 @@ function shutdownCallback(idx) { } function deleteOldFiles(directoryPath, maxAgeInSeconds, filenameRegex) { - console.log("Deleting old log files in " + directoryPath); + log.debug("Deleting old log files in " + directoryPath); var filenames = []; try { filenames = fs.readdirSync(directoryPath); } catch (e) { - console.warn("Error reading contents of log file directory", e); + log.warn("Error reading contents of log file directory", e); return; } for (const filename of filenames) { - console.log("Checking", filename); + log.debug("Checking", filename); const absolutePath = path.join(directoryPath, filename); var stat = null; try { stat = fs.statSync(absolutePath); } catch (e) { - console.log("Error stat'ing file", absolutePath, e); + log.debug("Error stat'ing file", absolutePath, e); continue; } const curTime = Date.now(); if (stat.isFile() && filename.search(filenameRegex) >= 0) { const ageInSeconds = (curTime - stat.mtime.getTime()) / 1000.0; if (ageInSeconds >= maxAgeInSeconds) { - console.log("\tDeleting:", filename, ageInSeconds); + log.debug("\tDeleting:", filename, ageInSeconds); try { fs.unlinkSync(absolutePath); } catch (e) { if (e.code != 'EBUSY') { - console.warn("\tError deleting:", e); + log.warn("\tError deleting:", e); } } } @@ -200,8 +222,8 @@ function deleteOldFiles(directoryPath, maxAgeInSeconds, filenameRegex) { var logPath = path.join(getApplicationDataDirectory(), '/logs'); -console.log("Log directory:", logPath); -console.log("Data directory:", getRootHifiDataDirectory()); +log.debug("Log directory:", logPath); +log.debug("Data directory:", getRootHifiDataDirectory()); const configPath = path.join(getApplicationDataDirectory(), 'config.json'); var userConfig = new Config(); @@ -209,8 +231,8 @@ userConfig.load(configPath); // print out uncaught exceptions in the console process.on('uncaughtException', function(err) { - console.error(err); - console.error(err.stack); + log.error(err); + log.error(err.stack); }); var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) { @@ -219,13 +241,14 @@ var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) }); if (shouldQuit) { - console.warn("Another instance of the Sandbox is already running - this instance will quit."); - app.quit(); + log.warn("Another instance of the Sandbox is already running - this instance will quit."); + app.exit(0); return; } // Check command line arguments to see how to find binaries var argv = require('yargs').argv; + var pathFinder = require('./modules/path-finder.js'); var interfacePath = null; @@ -267,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) { @@ -499,7 +522,7 @@ const httpStatusPort = 60332; function backupResourceDirectories(folder) { try { fs.mkdirSync(folder); - console.log("Created directory " + folder); + log.debug("Created directory " + folder); var dsBackup = path.join(folder, '/domain-server'); var acBackup = path.join(folder, '/assignment-client'); @@ -512,7 +535,7 @@ function backupResourceDirectories(folder) { return true; } catch (e) { - console.log(e); + log.debug(e); return false; } } @@ -530,8 +553,11 @@ function openBackupInstructions(folder) { } window.show(); + electron.ipcMain.on('setSize', function(event, obj) { + window.setSize(obj.width, obj.height); + }); electron.ipcMain.on('ready', function() { - console.log("got ready"); + log.debug("got ready"); window.webContents.send('update', folder); }); } @@ -565,9 +591,9 @@ function checkNewContent() { var wantDebug = false; if (wantDebug) { - console.log('Last Modified: ' + response.headers['last-modified']); - console.log(localContent + " " + remoteContent + " " + shouldUpdate + " " + new Date()); - console.log("Remote content is " + (shouldUpdate ? "newer" : "older") + " that local content."); + log.debug('Last Modified: ' + response.headers['last-modified']); + log.debug(localContent + " " + remoteContent + " " + shouldUpdate + " " + new Date()); + log.debug("Remote content is " + (shouldUpdate ? "newer" : "older") + " that local content."); } if (shouldUpdate) { @@ -609,46 +635,46 @@ function maybeInstallDefaultContentSet(onComplete) { // Check for existing data const acResourceDirectory = getAssignmentClientResourcesDirectory(); - console.log("Checking for existence of " + acResourceDirectory); + log.debug("Checking for existence of " + acResourceDirectory); var userHasExistingACData = true; try { fs.accessSync(acResourceDirectory); - console.log("Found directory " + acResourceDirectory); + log.debug("Found directory " + acResourceDirectory); } catch (e) { - console.log(e); + log.debug(e); userHasExistingACData = false; } const dsResourceDirectory = getDomainServerClientResourcesDirectory(); - console.log("checking for existence of " + dsResourceDirectory); + log.debug("checking for existence of " + dsResourceDirectory); var userHasExistingDSData = true; try { fs.accessSync(dsResourceDirectory); - console.log("Found directory " + dsResourceDirectory); + log.debug("Found directory " + dsResourceDirectory); } catch (e) { - console.log(e); + log.debug(e); userHasExistingDSData = false; } if (userHasExistingACData || userHasExistingDSData) { - console.log("User has existing data, suppressing downloader"); + log.debug("User has existing data, suppressing downloader"); onComplete(); checkNewContent(); return; } - console.log("Found contentPath:" + argv.contentPath); + log.debug("Found contentPath:" + argv.contentPath); if (argv.contentPath) { fs.copy(argv.contentPath, getRootHifiDataDirectory(), function (err) { if (err) { - console.log('Could not copy home content: ' + err); - return console.error(err) + log.debug('Could not copy home content: ' + err); + return log.error(err) } - console.log('Copied home content over to: ' + getRootHifiDataDirectory()); + log.debug('Copied home content over to: ' + getRootHifiDataDirectory()); userConfig.set('homeContentLastModified', new Date()); onComplete(); }); @@ -675,11 +701,11 @@ function maybeInstallDefaultContentSet(onComplete) { window.on('closed', onComplete); electron.ipcMain.on('ready', function() { - console.log("got ready"); + log.debug("got ready"); var currentState = ''; function sendStateUpdate(state, args) { - // console.log(state, window, args); + // log.debug(state, window, args); window.webContents.send('update', { state: state, args: args }); currentState = state; } @@ -713,10 +739,10 @@ function maybeInstallDefaultContentSet(onComplete) { }); function extractError(err) { - console.log("Aborting request because gunzip/untar failed"); + log.debug("Aborting request because gunzip/untar failed"); aborted = true; req.abort(); - console.log("ERROR" + err); + log.debug("ERROR" + err); sendStateUpdate('error', { message: "Error installing resources." @@ -728,7 +754,7 @@ function maybeInstallDefaultContentSet(onComplete) { req.pipe(gunzip).pipe(tar.extract(getRootHifiDataDirectory())).on('error', extractError).on('finish', function(){ // response and decompression complete, return - console.log("Finished unarchiving home content set"); + log.debug("Finished unarchiving home content set"); userConfig.set('homeContentLastModified', new Date()); sendStateUpdate('complete'); }); @@ -771,6 +797,7 @@ function maybeShowSplash() { } } + const trayIconOS = (osType == "Darwin") ? "osx" : "win"; var trayIcons = {}; trayIcons[ProcessGroupStates.STARTED] = "console-tray-" + trayIconOS + ".png"; @@ -787,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; @@ -813,7 +841,7 @@ function onContentLoaded() { } }); notifier.on('click', function(notifierObject, options) { - console.log("Got click", options.url); + log.debug("Got click", options.url); shell.openExternal(options.url); }); } @@ -839,6 +867,34 @@ function onContentLoaded() { // start the home server homeServer.start(); } + + // 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. + if (argv.shutdownWatcher) { + log.debug("Shutdown watcher requested... argv.shutdownWatcher:", argv.shutdownWatcher); + var MAX_TIME_SINCE_EDIT = 5000; // 5 seconds between updates + var firstAttemptToCheck = new Date().getTime(); + var shutdownWatchInterval = setInterval(function(){ + var stats = fs.stat(argv.shutdownWatcher, function(err, stats) { + if (err) { + var sinceFirstCheck = new Date().getTime() - firstAttemptToCheck; + if (sinceFirstCheck > MAX_TIME_SINCE_EDIT) { + log.debug("Running state file is missing, assume interface has shutdown... shutting down snadbox."); + forcedShutdown(); + clearTimeout(shutdownWatchInterval); + } + } else { + var sinceEdit = new Date().getTime() - stats.mtime.getTime(); + if (sinceEdit > MAX_TIME_SINCE_EDIT) { + log.debug("Running state of interface hasn't updated in MAX time... shutting down."); + forcedShutdown(); + clearTimeout(shutdownWatchInterval); + } + } + }); + }, 1000); + } } diff --git a/server-console/src/modules/config.js b/server-console/src/modules/config.js index df44dcfafe..438c377154 100644 --- a/server-console/src/modules/config.js +++ b/server-console/src/modules/config.js @@ -10,7 +10,7 @@ Config.prototype = { try { rawData = fs.readFileSync(filePath); } catch(e) { - console.log("Config file not found"); + log.debug("Config file not found"); } var configData = {}; @@ -21,7 +21,7 @@ Config.prototype = { configData = {}; } } catch(e) { - console.error("Error parsing config file", filePath) + log.error("Error parsing config file", filePath) } this.data = {}; @@ -37,7 +37,7 @@ Config.prototype = { return defaultValue; }, set: function(key, value) { - console.log("Setting", key, "to", value); + log.debug("Setting", key, "to", value); this.data[key] = value; } }; diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js old mode 100755 new mode 100644 index ce20e385f3..20b8966148 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -43,7 +43,7 @@ ProcessGroup.prototype = extend(ProcessGroup.prototype, { }, start: function() { if (this.state != ProcessGroupStates.STOPPED) { - console.warn("Can't start process group that is not stopped."); + log.warn("Can't start process group that is not stopped."); return; } @@ -56,7 +56,7 @@ ProcessGroup.prototype = extend(ProcessGroup.prototype, { }, stop: function() { if (this.state != ProcessGroupStates.STARTED) { - console.warn("Can't stop process group that is not started."); + log.warn("Can't stop process group that is not started."); return; } for (let process of this.processes) { @@ -120,10 +120,10 @@ util.inherits(Process, events.EventEmitter); Process.prototype = extend(Process.prototype, { start: function() { if (this.state != ProcessStates.STOPPED) { - console.warn("Can't start process that is not stopped."); + log.warn("Can't start process that is not stopped."); return; } - console.log("Starting " + this.command + " " + this.commandArgs.join(' ')); + log.debug("Starting " + this.command + " " + this.commandArgs.join(' ')); var logStdout = 'ignore', logStderr = 'ignore'; @@ -138,7 +138,7 @@ Process.prototype = extend(Process.prototype, { if (e.code == 'EEXIST') { logDirectoryCreated = true; } else { - console.error("Error creating log directory"); + log.error("Error creating log directory"); } } @@ -151,13 +151,13 @@ Process.prototype = extend(Process.prototype, { try { logStdout = fs.openSync(tmpLogStdout, 'ax'); } catch(e) { - console.log("Error creating stdout log file", e); + log.debug("Error creating stdout log file", e); logStdout = 'ignore'; } try { logStderr = fs.openSync(tmpLogStderr, 'ax'); } catch(e) { - console.log("Error creating stderr log file", e); + log.debug("Error creating stderr log file", e); logStderr = 'ignore'; } } @@ -169,7 +169,7 @@ Process.prototype = extend(Process.prototype, { stdio: ['ignore', logStdout, logStderr] }); } catch (e) { - console.log("Got error starting child process for " + this.name, e); + log.debug("Got error starting child process for " + this.name, e); this.child = null; this.updateState(ProcessStates.STOPPED); return; @@ -179,7 +179,7 @@ Process.prototype = extend(Process.prototype, { var pidLogStdout = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stdout.txt"); fs.rename(tmpLogStdout, pidLogStdout, function(e) { if (e !== null) { - console.log("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); + log.debug("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); } }); this.logStdout = pidLogStdout; @@ -190,7 +190,7 @@ Process.prototype = extend(Process.prototype, { var pidLogStderr = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stderr.txt"); fs.rename(tmpLogStderr, pidLogStderr, function(e) { if (e !== null) { - console.log("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); + log.debug("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); } }); this.logStderr = pidLogStderr; @@ -201,13 +201,13 @@ Process.prototype = extend(Process.prototype, { this.child.on('error', this.onChildStartError.bind(this)); this.child.on('close', this.onChildClose.bind(this)); - console.log("Child process started"); + log.debug("Child process started"); this.updateState(ProcessStates.STARTED); this.emit('logs-updated'); }, stop: function(force) { if (this.state == ProcessStates.STOPPED) { - console.warn("Can't stop process that is not started or stopping."); + log.warn("Can't stop process that is not started or stopping."); return; } if (os.type() == "Windows_NT") { @@ -217,7 +217,7 @@ Process.prototype = extend(Process.prototype, { } childProcess.exec(command, {}, function(error) { if (error) { - console.error('Error executing taskkill:', error); + log.error('Error executing taskkill:', error); } }); } else { @@ -225,12 +225,12 @@ Process.prototype = extend(Process.prototype, { this.child.kill(signal); } - console.log("Stopping child process:", this.child.pid, this.name); + log.debug("Stopping child process:", this.child.pid, this.name); if (!force) { this.stoppingTimeoutID = setTimeout(function() { if (this.state == ProcessStates.STOPPING) { - console.log("Force killling", this.name, this.child.pid); + log.debug("Force killling", this.name, this.child.pid); this.stop(true); } }.bind(this), 2500); @@ -257,11 +257,11 @@ Process.prototype = extend(Process.prototype, { // Events onChildStartError: function(error) { - console.log("Child process error ", error); + log.debug("Child process error ", error); this.updateState(ProcessStates.STOPPED); }, onChildClose: function(code) { - console.log("Child process closed with code ", code, this.name); + log.debug("Child process closed with code ", code, this.name); if (this.stoppingTimeoutID) { clearTimeout(this.stoppingTimeoutID); this.stoppingTimeoutID = null; @@ -332,7 +332,7 @@ ACMonitorProcess.prototype = extend(ACMonitorProcess.prototype, { this.pendingRequest = null; if (error) { - console.error('ERROR Getting AC Monitor status', error); + log.error('ERROR Getting AC Monitor status', error); } else { this.childServers = body.servers; } diff --git a/server-console/src/modules/hf-updater.js b/server-console/src/modules/hf-updater.js index 38e409b3a2..489364f655 100644 --- a/server-console/src/modules/hf-updater.js +++ b/server-console/src/modules/hf-updater.js @@ -11,7 +11,7 @@ const BUILDS_URL = 'https://highfidelity.com/builds.xml'; function UpdateChecker(currentVersion, checkForUpdatesEveryXSeconds) { this.currentVersion = currentVersion; - console.log('cur', currentVersion); + log.debug('cur', currentVersion); setInterval(this.checkForUpdates.bind(this), checkForUpdatesEveryXSeconds * 1000); this.checkForUpdates(); @@ -19,10 +19,10 @@ function UpdateChecker(currentVersion, checkForUpdatesEveryXSeconds) { util.inherits(UpdateChecker, events.EventEmitter); UpdateChecker.prototype = extend(UpdateChecker.prototype, { checkForUpdates: function() { - console.log("Checking for updates"); + log.debug("Checking for updates"); request(BUILDS_URL, (error, response, body) => { if (error) { - console.log("Error", error); + log.debug("Error", error); return; } if (response.statusCode == 200) { @@ -30,13 +30,13 @@ UpdateChecker.prototype = extend(UpdateChecker.prototype, { var $ = cheerio.load(body, { xmlMode: true }); const latestBuild = $('project[name="interface"] platform[name="' + platform + '"]').children().first(); const latestVersion = parseInt(latestBuild.find('version').text()); - console.log("Latest version is:", latestVersion, this.currentVersion); + log.debug("Latest version is:", latestVersion, this.currentVersion); if (latestVersion > this.currentVersion) { const url = latestBuild.find('url').text(); this.emit('update-available', latestVersion, url); } } catch (e) { - console.warn("Error when checking for updates", e); + log.warn("Error when checking for updates", e); } } }); diff --git a/server-console/src/modules/path-finder.js b/server-console/src/modules/path-finder.js index d0e4639e4a..5ecd2c74ff 100644 --- a/server-console/src/modules/path-finder.js +++ b/server-console/src/modules/path-finder.js @@ -72,11 +72,11 @@ exports.discoveredPath = function (name, binaryType, releaseType) { var extension = platformExtension(name); if (stats.isFile() || (stats.isDirectory() && extension == ".app")) { - console.log("Found " + name + " at " + testPath); + log.debug("Found " + name + " at " + testPath); return testPath; } } catch (e) { - console.log("Executable with name " + name + " not found at path " + testPath); + log.debug("Executable with name " + name + " not found at path " + testPath); } } 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 008f363e53..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 @@ -164,11 +165,59 @@ class MyTestWindow : public TestWindow { } }; +extern bool needsSparseRectification(const uvec2& size); +extern uvec2 rectifyToSparseSize(const uvec2& size); -int main(int argc, char** argv) { +void testSparseRectify() { + std::vector> NEEDS_SPARSE_TESTS {{ + // Already sparse + { {1024, 1024 }, false }, + { { 128, 128 }, false }, + // Too small in one dimension + { { 127, 127 }, false }, + { { 1, 1 }, false }, + { { 1000, 1 }, false }, + { { 1024, 1 }, false }, + { { 100, 100 }, false }, + // needs rectification + { { 1000, 1000 }, true }, + { { 1024, 1000 }, true }, + } }; + + for (const auto& test : NEEDS_SPARSE_TESTS) { + const auto& size = test.first; + const auto& expected = test.second; + auto result = needsSparseRectification(size); + Q_ASSERT(expected == result); + result = needsSparseRectification(uvec2(size.y, size.x)); + Q_ASSERT(expected == result); + } + + std::vector> SPARSE_SIZE_TESTS { { + // needs rectification + { { 1000, 1000 }, { 1024, 1024 } }, + { { 1024, 1000 }, { 1024, 1024 } }, + } }; + + for (const auto& test : SPARSE_SIZE_TESTS) { + const auto& size = test.first; + const auto& expected = test.second; + auto result = rectifyToSparseSize(size); + Q_ASSERT(expected == result); + result = rectifyToSparseSize(uvec2(size.y, size.x)); + Q_ASSERT(expected == uvec2(result.y, result.x)); + } +} + +int main(int argc, char** argv) { + testSparseRectify(); + + // FIXME this test appears to be broken +#if 0 QGuiApplication app(argc, argv); MyTestWindow window; app.exec(); +#endif return 0; } diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index d040601b07..aab45536bd 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -18,10 +18,12 @@ #include #include #include +#include #include #include #include + #include #include #include @@ -423,6 +425,7 @@ protected: return vec3(); } + bool isAboutToQuit() const override { return false; } void postLambdaEvent(std::function f) override {} qreal getDevicePixelRatio() override { @@ -638,7 +641,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/udt-test/src/UDTTest.cpp b/tools/udt-test/src/UDTTest.cpp index af94d2294b..6161dbfdbc 100644 --- a/tools/udt-test/src/UDTTest.cpp +++ b/tools/udt-test/src/UDTTest.cpp @@ -25,7 +25,7 @@ const QCommandLineOption TARGET_OPTION { "IP:PORT or HOSTNAME:PORT" }; const QCommandLineOption PACKET_SIZE { - "packet-size", "size for sent packets in bytes (defaults to " + QString(MAX_PACKET_SIZE) + ")", "bytes", + "packet-size", "size for sent packets in bytes (defaults to " + QString::number(udt::MAX_PACKET_SIZE) + ")", "bytes", QString(udt::MAX_PACKET_SIZE) }; const QCommandLineOption MIN_PACKET_SIZE { 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 c8b3bd38c1..4908465779 100644 --- a/tutorial/tutorial.js +++ b/tutorial/tutorial.js @@ -9,7 +9,6 @@ // Script.include("entityData.js"); -Script.include("viveHandsv2.js"); Script.include("lighter/createButaneLighter.js"); Script.include("tutorialEntityIDs.js"); @@ -33,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(); @@ -41,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"; @@ -107,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)); } @@ -192,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); + } } } } @@ -206,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 @@ -223,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) { - controllerDisplayManager = new ControllerDisplayManager(); disableEverything(); + HMD.requestShowHandControllers(); + onFinish(); }, cleanup: function() { @@ -259,6 +274,7 @@ function disableEverything() { setControllerPartLayer('tips', 'blank'); hideEntitiesWithTag('finish'); + setAwayEnabled(false); } @@ -276,10 +292,6 @@ function reenableEverything() { setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); MyAvatar.shouldRenderLocally = true; - if (controllerDisplayManager) { - controllerDisplayManager.destroy(); - controllerDisplayManager = null; - } setAwayEnabled(true); } @@ -297,6 +309,7 @@ var stepEnableControllers = function(name) { stepEnableControllers.prototype = { start: function(onFinish) { reenableEverything(); + HMD.requestHideHandControllers(); onFinish(); }, cleanup: function() { @@ -344,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; @@ -363,6 +374,7 @@ stepOrient.prototype = { this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); }, cleanup: function() { + debug("Orient | Cleanup"); if (this.active) { this.active = false; } @@ -379,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); - } -}; /////////////////////////////////////////////////////////////////////////////// @@ -455,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) { @@ -486,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(); @@ -497,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) { @@ -534,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, })); @@ -544,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) { @@ -566,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(); @@ -576,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); @@ -590,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 } @@ -607,6 +563,7 @@ function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) { PositionWatcher.prototype = { destroy: function() { + debug("Destroying position watcher"); Script.clearInterval(this.watcherIntervalID); } }; @@ -634,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, })); @@ -648,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; } @@ -691,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(); @@ -718,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; @@ -756,9 +719,6 @@ var stepTurnAround = function(name) { } stepTurnAround.prototype = { start: function(onFinish) { - setControllerVisible("left", true); - setControllerVisible("right", true); - setControllerPartLayer('touchpad', 'arrows'); setControllerPartLayer('tips', 'arrows'); @@ -768,8 +728,9 @@ stepTurnAround.prototype = { 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.numTimesTurnPressed); + var FORWARD_THRESHOLD = 90; + var REQ_NUM_TIMES_PRESSED = 3; var dir = Quat.getFront(MyAvatar.orientation); var angle = Math.atan2(dir.z, dir.x); @@ -786,18 +747,17 @@ stepTurnAround.prototype = { onAction: function(action, value) { var STEP_YAW_ACTION = 6; if (action == STEP_YAW_ACTION && value != 0) { + debug("TurnAround | Got yaw action"); this.numTimesTurnPressed += 1; } }, 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'); @@ -830,22 +790,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(); @@ -857,6 +816,7 @@ stepTeleport.prototype = { showEntitiesWithTag(this.tag); }, cleanup: function() { + debug("Teleport | Cleanup"); setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); @@ -907,6 +867,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) { @@ -925,6 +889,7 @@ function showEntitiesWithTag(tag) { collisionless: collisionless, userData: JSON.stringify(data), }; + debug("Showing: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -949,6 +914,7 @@ function showEntitiesWithTag(tag) { Entities.editEntity(entityID, newProperties); }); } + function hideEntitiesWithTag(tag) { var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; if (entities) { @@ -964,6 +930,8 @@ function hideEntitiesWithTag(tag) { ignoreForCollisions: 1, userData: JSON.stringify(data), }; + + debug("Hiding: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -986,6 +954,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; @@ -995,16 +972,23 @@ TutorialManager = function() { var startedTutorialAt = 0; var startedLastStepAt = 0; + var wentToEntryStepNum; + var VERSION = 1; + var tutorialID; + var self = this; this.startTutorial = 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"), @@ -1013,6 +997,7 @@ TutorialManager = function() { new stepFinish("finish"), new stepEnableControllers("enableControllers"), ]; + wentToEntryStepNum = STEPS.length; for (var i = 0; i < STEPS.length; ++i) { STEPS[i].cleanup(); } @@ -1021,11 +1006,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(); @@ -1038,6 +1021,12 @@ 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"); @@ -1047,7 +1036,6 @@ TutorialManager = function() { } else { info("Starting step", currentStepNum); currentStep = STEPS[currentStepNum]; - startedLastStepAt = Date.now(); currentStep.start(this.onFinish); return true; } @@ -1063,11 +1051,27 @@ 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, tracking"); + 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/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 6ee3b9645b..6c1533371b 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -6,7 +6,7 @@ // (function() { - var baseURL = "https://hifi-production.s3.amazonaws.com/hifi-production/DomainContent/CellScience/"; + var baseURL = "https://hifi-production.s3.amazonaws.com/DomainContent/CellScience/"; var self = this; this.buttonImageURL = baseURL + "GUI/play_audio.svg?2"; @@ -116,4 +116,4 @@ Controller.mousePressEvent.connect(this.onClick); Script.update.connect(this.update); -}); \ No newline at end of file +}); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js index a7a74fa79c..c33caf8b37 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js @@ -47,8 +47,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.035, - position: properties.position + volume: 0.45, }; self.sound = SoundCache.getSound(self.soundURL); self.buttonImageURL = baseURL + "GUI/GUI_audio.png?" + version; @@ -142,6 +141,7 @@ Overlays.editOverlay(self.button, { visible: false }); + self.soundOptions.position = MyAvatar.position; this.soundPlaying = Audio.playSound(self.sound, self.soundOptions); } else { // print("not downloaded"); @@ -162,4 +162,4 @@ Controller.mousePressEvent.connect(this.onClick); -}); \ No newline at end of file +}); diff --git a/unpublishedScripts/DomainContent/CellScience/moveCellsAC.js b/unpublishedScripts/DomainContent/CellScience/moveCellsAC.js index d696d464bf..7b0669c616 100644 --- a/unpublishedScripts/DomainContent/CellScience/moveCellsAC.js +++ b/unpublishedScripts/DomainContent/CellScience/moveCellsAC.js @@ -39,11 +39,13 @@ var THROTTLE = true; var THROTTLE_RATE = 5000; var sinceLastUpdate = 0; +var entitiesToMove = []; //print('cells script') function findCells() { var results = Entities.findEntities(basePosition, 60000); + Script.clearInterval(octreeQueryInterval); // we don't need it any more if (results.length === 0) { // print('no entities found') @@ -55,9 +57,7 @@ function findCells() { // print('name is:: ' + name) if (name === 'Cell') { // print('found a cell!!' + v) - Script.setTimeout(function() { - moveCell(v); - }, Math.random() * THROTTLE_RATE); + entitiesToMove.push(v); } }); } @@ -93,6 +93,7 @@ function update(deltaTime) { Entities.setPacketsPerSecond(6000); print("PPS:" + Entities.getPacketsPerSecond()); initialized = true; + Script.setTimeout(findCells, 20 * 1000); // After 20 seconds of getting entities, look for cells. } return; } @@ -102,7 +103,11 @@ function update(deltaTime) { if (sinceLastUpdate > THROTTLE_RATE) { // print('SHOULD FIND CELLS!!!') sinceLastUpdate = 0; - findCells(); + entitiesToMove.forEach(function (v) { + Script.setTimeout(function() { + moveCell(v); + }, Math.random() * THROTTLE_RATE); // don't move all of them every five seconds, but at random times over interval + }); } else { // print('returning in update ' + sinceLastUpdate) return; @@ -116,4 +121,4 @@ function unload() { } Script.update.connect(update); -Script.scriptEnding.connect(unload); \ No newline at end of file +Script.scriptEnding.connect(unload); diff --git a/unpublishedScripts/DomainContent/CellScience/moveVesiclesAC.js b/unpublishedScripts/DomainContent/CellScience/moveVesiclesAC.js index 4d4ce76d74..114abd4984 100644 --- a/unpublishedScripts/DomainContent/CellScience/moveVesiclesAC.js +++ b/unpublishedScripts/DomainContent/CellScience/moveVesiclesAC.js @@ -39,11 +39,13 @@ var THROTTLE = true; var THROTTLE_RATE = 5000; var sinceLastUpdate = 0; +var entitiesToMove = []; //print('vesicle script') function findVesicles() { var results = Entities.findEntities(basePosition, 60000); + Script.clearInterval(octreeQueryInterval); // we don't need it any more if (results.length === 0) { // print('no entities found'); @@ -54,9 +56,7 @@ function findVesicles() { var name = Entities.getEntityProperties(v, 'name').name; if (name === 'vesicle') { //print('found a vesicle!!' + v) - Script.setTimeout(function() { - moveVesicle(v); - }, Math.random() * THROTTLE_RATE); + entitiesToMove.push(v); } }); } @@ -100,6 +100,7 @@ function update(deltaTime) { Entities.setPacketsPerSecond(6000); print("PPS:" + Entities.getPacketsPerSecond()); initialized = true; + Script.setTimeout(findVesicles, 20 * 1000); // After 20 seconds of getting entities, look for cells. } return; } @@ -108,7 +109,11 @@ function update(deltaTime) { sinceLastUpdate = sinceLastUpdate + deltaTime * 1000; if (sinceLastUpdate > THROTTLE_RATE) { sinceLastUpdate = 0; - findVesicles(); + entitiesToMove.forEach(function (v) { + Script.setTimeout(function() { + moveVesicle(v); + }, Math.random() * THROTTLE_RATE); // don't move all of them every five seconds, but at random times over interval + }); } else { return; } @@ -121,4 +126,4 @@ function unload() { } Script.update.connect(update); -Script.scriptEnding.connect(unload); \ No newline at end of file +Script.scriptEnding.connect(unload); diff --git a/unpublishedScripts/DomainContent/Home/portal.js b/unpublishedScripts/DomainContent/Home/portal.js new file mode 100644 index 0000000000..ea6241265c --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/portal.js @@ -0,0 +1,52 @@ +(function(){ + var teleport; + var portalDestination; + var thisEntityID; + + function playSound() { + 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, '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, 'userData'); // in case the userData/portalURL has changed + if (properties) { + portalDestination = properties.userData; + + print("portal.js | enterEntity() .... The portal destination is " + 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 { + location.goToEntry(); // going forward: no data means go to appropriate entry point + } + } + }; + + this.leaveEntity = function(entityID) { + print("portal.js | leaveEntity"); + + playSound(); + }; +})