diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index d40576a75d..038f53154c 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -1,7 +1,96 @@ +# Linux build guide + Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file. -### Qt5 Dependencies +## Qt5 Dependencies Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required: libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev + +## Ubuntu 16.04 specific build guide + +### Prepare environment + +Install qt: +```bash +wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb +sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb +``` + +Install build dependencies: +```bash +sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev +``` + +To compile interface in a server you must install: +```bash +sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 +``` + +Install build tools: +```bash +sudo apt install cmake +``` + +### Get code and checkout the tag you need + +Clone this repository: +```bash +git clone https://github.com/highfidelity/hifi.git +``` + +To compile a RELEASE version checkout the tag you need getting a list of all tags: +```bash +git fetch -a +git tags +``` + +Then checkout last tag with: +```bash +git checkout tags/RELEASE-6819 +``` + +Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type: +```bash +git checkout tags/RELEASE-6731 +``` + +### Compiling + +Create the build directory: +```bash +mkdir -p hifi/build +cd hifi/build +``` + +Prepare makefiles: +```bash +cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake .. +``` + +Start compilation and get a cup of coffee: +```bash +make domain-server assignment-client interface +``` + +In a server does not make sense to compile interface + +### Running the software + +Running domain server: +```bash +./domain-server/domain-server +``` + +Running assignment client: +```bash +./assignment-client/assignment-client -n 6 +``` + +Running interface: +```bash +./interface/interface +``` + +Go to localhost in running interface. diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 8aec5adb1f..c44fdf74ff 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -50,14 +51,14 @@ #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" -#include "AvatarAudioTimer.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), - _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO) + _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO), + _avatarAudioTimer(this) { _entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -81,6 +82,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -92,6 +96,14 @@ Agent::Agent(ReceivedMessage& message) : this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); + + + // 100Hz timer for audio + const int TARGET_INTERVAL_MSEC = 10; // 10ms + connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio); + _avatarAudioTimer.setSingleShot(false); + _avatarAudioTimer.setInterval(TARGET_INTERVAL_MSEC); + _avatarAudioTimer.setTimerType(Qt::PreciseTimer); } void Agent::playAvatarSound(SharedSoundPointer sound) { @@ -471,14 +483,7 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); - // 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(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); // Agents should run at 45hz static const int AVATAR_DATA_HZ = 45; @@ -557,7 +562,7 @@ void Agent::setIsAvatar(bool isAvatar) { _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets // tell the avatarAudioTimer to start ticking - emit startAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); } @@ -586,7 +591,7 @@ void Agent::setIsAvatar(bool isAvatar) { nodeList->sendPacket(std::move(packet), *node); }); } - emit stopAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); } } @@ -604,6 +609,24 @@ void Agent::processAgentAvatar() { AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData; QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail); + + int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; + avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; + avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; + return; + } + } + } + scriptedAvatar->doneEncoding(true); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -796,8 +819,7 @@ void Agent::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); - emit stopAvatarAudioTimer(); - _avatarAudioTimerThread.quit(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); // cleanup codec & encoder if (_codec && _encoder) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 549a0858b7..a549df5fb3 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -81,9 +81,6 @@ private slots: void processAgentAvatar(); void processAgentAvatarAudio(); -signals: - void startAvatarAudioTimer(); - void stopAvatarAudioTimer(); private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); @@ -118,7 +115,7 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; - QThread _avatarAudioTimerThread; + QTimer _avatarAudioTimer; bool _flushEncoder { false }; }; diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index abfc66ac55..efced972a0 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -141,7 +142,7 @@ void AssignmentClient::stopAssignmentClient() { QThread* currentAssignmentThread = _currentAssignment->thread(); // ask the current assignment to stop - QMetaObject::invokeMethod(_currentAssignment, "stop", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(_currentAssignment, "stop"); // ask the current assignment to delete itself on its thread _currentAssignment->deleteLater(); diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp index bd656ceb09..dd3050ba4e 100644 --- a/assignment-client/src/AssignmentClientApp.cpp +++ b/assignment-client/src/AssignmentClientApp.cpp @@ -17,8 +17,8 @@ #include #include -#include #include +#include #include #include "Assignment.h" diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp deleted file mode 100644 index d031b9d9f6..0000000000 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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() { - 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 deleted file mode 100644 index 1f6381b030..0000000000 --- a/assignment-client/src/AvatarAudioTimer.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// 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/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 643361ac5d..d2c19d97ba 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -76,7 +76,7 @@ void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { _function = &AudioMixerSlave::mix; - _configure = [&](AudioMixerSlave& slave) { + _configure = [=](AudioMixerSlave& slave) { slave.configureMix(_begin, _end, _frame, _throttlingRatio); }; _frame = frame; diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index b4aae40c10..eea44f031e 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -85,7 +85,22 @@ void AvatarMixer::handleReplicatedPacket(QSharedPointer message auto nodeList = DependencyManager::get(); auto nodeID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID)); - auto replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr()); + SharedNodePointer replicatedNode; + + if (message->getType() == PacketType::ReplicatedKillAvatar) { + // this is a kill packet, which we should only process if we already have the node in our list + // since it of course does not make sense to add a node just to remove it an instant later + replicatedNode = nodeList->nodeWithUUID(nodeID); + + if (!replicatedNode) { + return; + } + } else { + replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr()); + } + + // we better have a node to work with at this point + assert(replicatedNode); if (message->getType() == PacketType::ReplicatedAvatarIdentity) { handleAvatarIdentityPacket(message, replicatedNode); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4d80bc7d17..a4bf8fa253 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -108,9 +108,6 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) { if (isRadiusIgnoring(other)) { _radiusIgnoredOthers.erase(other); - auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID); - exitingSpaceBubblePacket->write(other.toRfc4122()); - DependencyManager::get()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self); } } diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 7e37f583ff..34feafbd4d 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -383,11 +383,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData"; bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther, hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); - } - if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { - qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!"; - includeThisAvatar = false; + if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { + qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!"; + includeThisAvatar = false; + } } } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index cb5ae7735a..8afbc1cfe4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -69,7 +69,7 @@ static AvatarMixerSlave slave; void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { _function = &AvatarMixerSlave::processIncomingPackets; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configure(begin, end); }; run(begin, end); @@ -79,7 +79,7 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); }; run(begin, end); diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 57456b00c3..c7715d4014 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -13,12 +13,13 @@ #include #include +#include #include #include #include "ScriptableAvatar.h" -QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { _globalPosition = getPosition(); return AvatarData::toByteArrayStateful(dataDetail); } @@ -49,7 +50,7 @@ void ScriptableAvatar::stopAnimation() { AnimationDetails ScriptableAvatar::getAnimationDetails() { if (QThread::currentThread() != thread()) { AnimationDetails result; - QMetaObject::invokeMethod(this, "getAnimationDetails", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "getAnimationDetails", Q_RETURN_ARG(AnimationDetails, result)); return result; } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 1028912e55..b1039b5ac0 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -28,7 +28,7 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; private slots: diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index ac686e2e0a..2c8f8a9e37 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -50,6 +50,12 @@ EntityServer::~EntityServer() { tree->removeNewlyCreatedHook(this); } +void EntityServer::aboutToFinish() { + DependencyManager::get()->cleanup(); + + OctreeServer::aboutToFinish(); +} + void EntityServer::handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode) { if (_octreeInboundPacketProcessor) { _octreeInboundPacketProcessor->queueReceivedPacket(message, senderNode); diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 40676e79bd..26c2f149aa 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -59,6 +59,8 @@ public: virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override; virtual void trackViewerGone(const QUuid& sessionID) override; + virtual void aboutToFinish() override; + public slots: virtual void nodeAdded(SharedNodePointer node) override; virtual void nodeKilled(SharedNodePointer node) override; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 7db06f12c0..868b377ced 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -81,7 +81,6 @@ bool OctreeSendThread::process() { // don't do any send processing until the initial load of the octree is complete... if (_myServer->isInitialLoadComplete()) { if (auto node = _node.lock()) { - _nodeMissingCount = 0; OctreeQueryNode* nodeData = static_cast(node->getLinkedData()); // Sometimes the node data has not yet been linked, in which case we can't really do anything @@ -129,8 +128,7 @@ AtomicUIntStat OctreeSendThread::_totalSpecialBytes { 0 }; AtomicUIntStat OctreeSendThread::_totalSpecialPackets { 0 }; -int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, - int& truePacketsSent, bool dontSuppressDuplicate) { +int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate) { OctreeServer::didHandlePacketSend(this); // if we're shutting down, then exit early @@ -141,15 +139,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* bool debug = _myServer->wantsDebugSending(); quint64 now = usecTimestampNow(); - bool packetSent = false; // did we send a packet? - int packetsSent = 0; + int numPackets = 0; // Here's where we check to see if this packet is a duplicate of the last packet. If it is, we will silently // obscure the packet and not send it. This allows the callers and upper level logic to not need to know about // this rate control savings. if (!dontSuppressDuplicate && nodeData->shouldSuppressDuplicatePacket()) { nodeData->resetOctreePacket(); // we still need to reset it though! - return packetsSent; // without sending... + return numPackets; // without sending... } // If we've got a stats message ready to send, then see if we can piggyback them together @@ -163,12 +160,15 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // copy octree message to back of stats message statsPacket.write(nodeData->getPacket().getData(), nodeData->getPacket().getDataSize()); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -191,18 +191,22 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // actually send it OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - packetSent = true; } else { // not enough room in the packet, send two packets + + // first packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -221,19 +225,18 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } - trueBytesSent += statsPacket.getDataSize(); - truePacketsSent++; - packetsSent++; - + // second packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += nodeData->getPacket().getDataSize(); + numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + // we count wasted bytes here because we were unable to fit the stats packet + thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -259,13 +262,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // just send the octree packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - int thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += packetSizeWithHeader; + int numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + int thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + numPackets++; + _trueBytesSent += numBytes; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -280,23 +284,21 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* qDebug() << "Sending packet at " << now << " [" << _totalPackets <<"]: sequence: " << sequence << " timestamp: " << timestamp << - " size: " << packetSizeWithHeader << " [" << _totalBytes << + " size: " << numBytes << " [" << _totalBytes << "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } } } // remember to track our stats - if (packetSent) { + if (numPackets > 0) { nodeData->stats.packetSent(nodeData->getPacket().getPayloadSize()); - trueBytesSent += nodeData->getPacket().getPayloadSize(); - truePacketsSent++; - packetsSent++; nodeData->octreePacketSent(); nodeData->resetOctreePacket(); } - return packetsSent; + _truePacketsSent += numPackets; + return numPackets; } /// Version of octree element distributor that sends the deepest LOD level at once @@ -315,13 +317,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* preDistributionProcessing(); } - // calculate max number of packets that can be sent during this interval - int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); - int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); - - int truePacketsSent = 0; - int trueBytesSent = 0; - int packetsSentThisInterval = 0; + _truePacketsSent = 0; + _trueBytesSent = 0; + _packetsSentThisInterval = 0; bool isFullScene = nodeData->shouldForceFullScene(); if (isFullScene) { @@ -334,17 +332,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* && ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged())); } - bool somethingToSend = true; // assume we have something - - // If our packet already has content in it, then we must use the color choice of the waiting packet. - // If we're starting a fresh packet, then... - // If we're moving, and the client asked for low res, then we force monochrome, otherwise, use - // the clients requested color state. - - // If we have a packet waiting, and our desired want color, doesn't match the current waiting packets color - // then let's just send that waiting packet. if (nodeData->isPacketWaiting()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); + // send the waiting packet + _packetsSentThisInterval += handlePacketSend(node, nodeData); } else { nodeData->resetOctreePacket(); } @@ -375,8 +365,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* //unsigned long encodeTime = nodeData->stats.getTotalEncodeTime(); //unsigned long elapsedTime = nodeData->stats.getElapsedTime(); - int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent, isFullScene); - packetsSentThisInterval += packetsJustSent; + _packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene); // If we're starting a full scene, then definitely we want to empty the elementBag if (isFullScene) { @@ -404,185 +393,44 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If we have something in our elementBag, then turn them into packets and send them out... if (!nodeData->elementBag.isEmpty()) { - int bytesWritten = 0; quint64 start = usecTimestampNow(); - // TODO: add these to stats page - //quint64 startCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //quint64 startCompressCalls = OctreePacketData::getCompressContentCalls(); - - int extraPackingAttempts = 0; - bool completedScene = false; - - while (somethingToSend && packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { - float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; - float encodeElapsedUsec = OctreeServer::SKIP_TIME; - float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; - float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; - - quint64 startInside = usecTimestampNow(); - - bool lastNodeDidntFit = false; // assume each node fits - if (!nodeData->elementBag.isEmpty()) { - - quint64 lockWaitStart = usecTimestampNow(); - _myServer->getOctree()->withReadLock([&]{ - quint64 lockWaitEnd = usecTimestampNow(); - lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); - quint64 encodeStart = usecTimestampNow(); - - OctreeElementPointer subTree = nodeData->elementBag.extract(); - if (!subTree) { - return; - } - - float octreeSizeScale = nodeData->getOctreeSizeScale(); - int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); - - int boundaryLevelAdjust = boundaryLevelAdjustClient + - (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - - EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, - viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, - isFullScene, _myServer->getJurisdiction(), nodeData); - nodeData->copyCurrentViewFrustum(params.viewFrustum); - if (viewFrustumChanged) { - nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); - } - - // Our trackSend() function is implemented by the server subclass, and will be called back - // during the encodeTreeBitstream() as new entities/data elements are sent - params.trackSend = [this, node](const QUuid& dataID, quint64 dataEdited) { - _myServer->trackSend(dataID, dataEdited, node->getUUID()); - }; - - // TODO: should this include the lock time or not? This stat is sent down to the client, - // it seems like it may be a good idea to include the lock time as part of the encode time - // are reported to client. Since you can encode without the lock - nodeData->stats.encodeStarted(); - - bytesWritten = _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); - - quint64 encodeEnd = usecTimestampNow(); - encodeElapsedUsec = (float)(encodeEnd - encodeStart); - - // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've - // sent the entire scene. We want to know this below so we'll actually write this content into - // the packet and send it - completedScene = nodeData->elementBag.isEmpty(); - - if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { - lastNodeDidntFit = true; - extraPackingAttempts++; - } - - nodeData->stats.encodeStopped(); - }); - } else { - // If the bag was empty then we didn't even attempt to encode, and so we know the bytesWritten were 0 - bytesWritten = 0; - somethingToSend = false; // this will cause us to drop out of the loop... - } - - // If the last node didn't fit, but we're in compressed mode, then we actually want to see if we can fit a - // little bit more in this packet. To do this we write into the packet, but don't send it yet, we'll - // keep attempting to write in compressed mode to add more compressed segments - - // We only consider sending anything if there is something in the _packetData to send... But - // if bytesWritten == 0 it means either the subTree couldn't fit or we had an empty bag... Both cases - // mean we should send the previous packet contents and reset it. - if (completedScene || lastNodeDidntFit) { - - if (_packetData.hasContent()) { - quint64 compressAndWriteStart = usecTimestampNow(); - - // if for some reason the finalized size is greater than our available size, then probably the "compressed" - // form actually inflated beyond our padding, and in this case we will send the current packet, then - // write to out new packet... - unsigned int writtenSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - - if (writtenSize > nodeData->getAvailable()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - } - - nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); - quint64 compressAndWriteEnd = usecTimestampNow(); - compressAndWriteElapsedUsec = (float)(compressAndWriteEnd - compressAndWriteStart); - } - - // If we're not running compressed, then we know we can just send now. Or if we're running compressed, but - // the packet doesn't have enough space to bother attempting to pack more... - bool sendNow = true; - - if (!completedScene && (nodeData->getAvailable() >= MINIMUM_ATTEMPT_MORE_PACKING && - extraPackingAttempts <= REASONABLE_NUMBER_OF_PACKING_ATTEMPTS)) { - sendNow = false; // try to pack more - } - - int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; - if (sendNow) { - quint64 packetSendingStart = usecTimestampNow(); - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - quint64 packetSendingEnd = usecTimestampNow(); - packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); - - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - extraPackingAttempts = 0; - } else { - // If we're in compressed mode, then we want to see if we have room for more in this wire packet. - // but we've finalized the _packetData, so we want to start a new section, we will do that by - // resetting the packet settings with the max uncompressed size of our current available space - // in the wire packet. We also include room for our section header, and a little bit of padding - // to account for the fact that whenc compressing small amounts of data, we sometimes end up with - // a larger compressed size then uncompressed size - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; - } - _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed - - } - OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); - OctreeServer::trackEncodeTime(encodeElapsedUsec); - OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); - OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); - - quint64 endInside = usecTimestampNow(); - quint64 elapsedInsideUsecs = endInside - startInside; - OctreeServer::trackInsideTime((float)elapsedInsideUsecs); - } - - if (somethingToSend && _myServer->wantsVerboseDebug()) { - qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << packetsSentThisInterval - << " maxPacketsPerInterval = " << maxPacketsPerInterval - << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; - } + traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); // Here's where we can/should allow the server to send other data... // send the environment packet // TODO: should we turn this into a while loop to better handle sending multiple special packets if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) { int specialPacketsSent = 0; - trueBytesSent += _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); + int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed - truePacketsSent += specialPacketsSent; - packetsSentThisInterval += specialPacketsSent; + _truePacketsSent += specialPacketsSent; + _trueBytesSent += specialBytesSent; + _packetsSentThisInterval += specialPacketsSent; _totalPackets += specialPacketsSent; - _totalBytes += trueBytesSent; + _totalBytes += specialBytesSent; _totalSpecialPackets += specialPacketsSent; - _totalSpecialBytes += trueBytesSent; + _totalSpecialBytes += specialBytesSent; } + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + // Re-send packets that were nacked by the client - while (nodeData->hasNextNackedPacket() && packetsSentThisInterval < maxPacketsPerInterval) { + while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) { const NLPacket* packet = nodeData->getNextNackedPacket(); if (packet) { DependencyManager::get()->sendUnreliablePacket(*packet, *node); - truePacketsSent++; - packetsSentThisInterval++; + int numBytes = packet->getDataSize(); + _truePacketsSent++; + _trueBytesSent += numBytes; + _packetsSentThisInterval++; - _totalBytes += packet->getDataSize(); _totalPackets++; + _totalBytes += numBytes; _totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize(); } } @@ -591,12 +439,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* int elapsedmsec = (end - start) / USECS_PER_MSEC; OctreeServer::trackLoopTime(elapsedmsec); - // TODO: add these to stats page - //quint64 endCompressCalls = OctreePacketData::getCompressContentCalls(); - //int elapsedCompressCalls = endCompressCalls - startCompressCalls; - //quint64 endCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //int elapsedCompressTimeMsecs = endCompressTimeMsecs - startCompressTimeMsecs; - // if after sending packets we've emptied our bag, then we want to remember that we've sent all // the octree elements from the current view frustum if (nodeData->elementBag.isEmpty()) { @@ -606,17 +448,147 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If this was a full scene then make sure we really send out a stats packet at this point so that // the clients will know the scene is stable if (isFullScene) { - int thisTrueBytesSent = 0; - int thisTruePacketsSent = 0; nodeData->stats.sceneCompleted(); - int packetsJustSent = handlePacketSend(node, nodeData, thisTrueBytesSent, thisTruePacketsSent, true); - _totalBytes += thisTrueBytesSent; - _totalPackets += thisTruePacketsSent; - truePacketsSent += packetsJustSent; + handlePacketSend(node, nodeData, true); } } } // end if bag wasn't empty, and so we sent stuff... - return truePacketsSent; + return _truePacketsSent; +} + +void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + + int extraPackingAttempts = 0; + bool completedScene = false; + + bool somethingToSend = true; // assume we have something + while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { + float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; + float encodeElapsedUsec = OctreeServer::SKIP_TIME; + float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; + float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; + + quint64 startInside = usecTimestampNow(); + + bool lastNodeDidntFit = false; // assume each node fits + if (!nodeData->elementBag.isEmpty()) { + + quint64 lockWaitStart = usecTimestampNow(); + _myServer->getOctree()->withReadLock([&]{ + quint64 lockWaitEnd = usecTimestampNow(); + lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); + quint64 encodeStart = usecTimestampNow(); + + OctreeElementPointer subTree = nodeData->elementBag.extract(); + if (!subTree) { + return; + } + + float octreeSizeScale = nodeData->getOctreeSizeScale(); + int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); + + int boundaryLevelAdjust = boundaryLevelAdjustClient + + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + + EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, + viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, + isFullScene, _myServer->getJurisdiction(), nodeData); + nodeData->copyCurrentViewFrustum(params.viewFrustum); + if (viewFrustumChanged) { + nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); + } + + // Our trackSend() function is implemented by the server subclass, and will be called back + // during the encodeTreeBitstream() as new entities/data elements are sent + params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { + _myServer->trackSend(dataID, dataEdited, _nodeUuid); + }; + + // TODO: should this include the lock time or not? This stat is sent down to the client, + // it seems like it may be a good idea to include the lock time as part of the encode time + // are reported to client. Since you can encode without the lock + nodeData->stats.encodeStarted(); + + // NOTE: this is where the tree "contents" are actaully packed + _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); + + quint64 encodeEnd = usecTimestampNow(); + encodeElapsedUsec = (float)(encodeEnd - encodeStart); + + // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've + // sent the entire scene. We want to know this below so we'll actually write this content into + // the packet and send it + completedScene = nodeData->elementBag.isEmpty(); + + if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { + lastNodeDidntFit = true; + extraPackingAttempts++; + } + + nodeData->stats.encodeStopped(); + }); + } else { + somethingToSend = false; // this will cause us to drop out of the loop... + } + + if (completedScene || lastNodeDidntFit) { + // we probably want to flush what has accumulated in nodeData but: + // do we have more data to send? and is there room? + if (_packetData.hasContent()) { + // yes, more data to send + quint64 compressAndWriteStart = usecTimestampNow(); + unsigned int additionalSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + if (additionalSize > nodeData->getAvailable()) { + // no room --> flush what we've got + _packetsSentThisInterval += handlePacketSend(node, nodeData); + } + + // either there is room, or we've flushed and reset nodeData's data buffer + // so we can transfer whatever is in _packetData to nodeData + nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); + compressAndWriteElapsedUsec = (float)(usecTimestampNow()- compressAndWriteStart); + } + + bool sendNow = completedScene || + nodeData->getAvailable() < MINIMUM_ATTEMPT_MORE_PACKING || + extraPackingAttempts > REASONABLE_NUMBER_OF_PACKING_ATTEMPTS; + + int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; + if (sendNow) { + quint64 packetSendingStart = usecTimestampNow(); + _packetsSentThisInterval += handlePacketSend(node, nodeData); + quint64 packetSendingEnd = usecTimestampNow(); + packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); + + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + extraPackingAttempts = 0; + } else { + // We want to see if we have room for more in this wire packet but we've copied the _packetData, + // so we want to start a new section. We will do that by resetting the packet settings with the max + // size of our current available space in the wire packet plus room for our section header and a + // little bit of padding. + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; + } + _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed + } + OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); + OctreeServer::trackEncodeTime(encodeElapsedUsec); + OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); + OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); + + quint64 endInside = usecTimestampNow(); + quint64 elapsedInsideUsecs = endInside - startInside; + OctreeServer::trackInsideTime((float)elapsedInsideUsecs); + } + + if (somethingToSend && _myServer->wantsVerboseDebug()) { + qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << _packetsSentThisInterval + << " maxPacketsPerInterval = " << maxPacketsPerInterval + << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; + } } diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 06c9b5f1d6..d158539f57 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -34,7 +34,7 @@ public: void setIsShuttingDown(); bool isShuttingDown() { return _isShuttingDown; } - + QUuid getNodeUuid() const { return _nodeUuid; } static AtomicUIntStat _totalBytes; @@ -53,20 +53,23 @@ protected: /// Called before a packetDistributor pass to allow for pre-distribution processing virtual void preDistributionProcessing() {}; + virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); OctreeServer* _myServer { nullptr }; QWeakPointer _node; private: - int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent, bool dontSuppressDuplicate = false); + int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); - + QUuid _nodeUuid; OctreePacketData _packetData; - int _nodeMissingCount { 0 }; + int _truePacketsSent { 0 }; // available for debug stats + int _trueBytesSent { 0 }; // available for debug stats + int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition bool _isShuttingDown { false }; }; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 489478ff9a..e7433e7c05 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,9 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::set(); DependencyManager::set(ScriptEngine::ENTITY_SERVER_SCRIPT); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); diff --git a/cmake/modules/FindFBX.cmake b/cmake/modules/FindFBX.cmake index 2e84d1ea19..9a1d08a010 100644 --- a/cmake/modules/FindFBX.cmake +++ b/cmake/modules/FindFBX.cmake @@ -56,19 +56,17 @@ elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") endif() function(_fbx_find_library _name _lib _suffix) - if (MSVC12) + if (MSVC_VERSION EQUAL 1910) + set(VS_PREFIX vs2017) + elseif (MSVC_VERSION EQUAL 1900) + set(VS_PREFIX vs2015) + elseif (MSVC_VERSION EQUAL 1800) set(VS_PREFIX vs2013) - endif() - - if (MSVC11) + elseif (MSVC_VERSION EQUAL 1700) set(VS_PREFIX vs2012) - endif() - - if (MSVC10) + elseif (MSVC_VERSION EQUAL 1600) set(VS_PREFIX vs2010) - endif() - - if (MSVC90) + elseif (MSVC_VERSION EQUAL 1500) set(VS_PREFIX vs2008) endif() diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 710fd81316..f44c8185d8 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -49,7 +49,7 @@ Var STR_CONTAINS_VAR_3 Var STR_CONTAINS_VAR_4 Var STR_RETURN_VAR - + Function StrContains Exch $STR_NEEDLE Exch 1 @@ -438,6 +438,7 @@ Var DesktopServerCheckbox Var ServerStartupCheckbox Var LaunchServerNowCheckbox Var LaunchClientNowCheckbox +Var CleanInstallCheckbox Var CurrentOffset Var OffsetUnits Var CopyFromProductionCheckbox @@ -475,27 +476,18 @@ Function PostInstallOptionsPage ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@" Pop $DesktopClientCheckbox IntOp $CurrentOffset $CurrentOffset + 15 - + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} ${EndIf} - + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@" Pop $DesktopServerCheckbox - + IntOp $CurrentOffset $CurrentOffset + 15 + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" - Pop $ServerStartupCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} @@ -511,17 +503,33 @@ Function PostInstallOptionsPage IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" - Pop $LaunchClientNowCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} - ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE - ${IfNot} $substringResult == "" - ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} - ${EndIf} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" + Pop $LaunchClientNowCheckbox + IntOp $CurrentOffset $CurrentOffset + 30 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE + ${IfNot} $substringResult == "" + ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} + ${EndIf} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Perform a clean install (Delete older settings and content)" + Pop $CleanInstallCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} @PR_BUILD@ == 1 @@ -543,7 +551,7 @@ Function PostInstallOptionsPage ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} ${EndIf} - + nsDialogs::Show FunctionEnd @@ -558,6 +566,7 @@ Var ServerStartupState Var LaunchServerNowState Var LaunchClientNowState Var CopyFromProductionState +Var CleanInstallState Function ReadPostInstallOptions ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} @@ -579,13 +588,18 @@ Function ReadPostInstallOptions ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ; check if we need to launch the server post-install - ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState + ; check if we need to launch the server post-install + ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ; check if we need to launch the client post-install - ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ; check if we need to launch the client post-install + ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${NSD_GetState} $CleanInstallCheckbox $CleanInstallState ${EndIf} FunctionEnd @@ -628,6 +642,15 @@ Function HandlePostInstallOptions !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO ${EndIf} ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${If} $CleanInstallState == ${BST_CHECKED} + SetShellVarContext current + RMDir /r "$APPDATA\@BUILD_ORGANIZATION@" + RMDir /r "$LOCALAPPDATA\@BUILD_ORGANIZATION@" + ${EndIf} + ${EndIf} ${If} @PR_BUILD@ == 1 diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index bc67a31c02..c1eff76d78 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -50,6 +50,7 @@ { "label": "Places / Paths", "html_id": "places_paths", + "restart": false, "settings": [ { "name": "paths", @@ -75,6 +76,7 @@ { "name": "descriptors", "label": "Description", + "restart": false, "help": "This data will be queryable from your server. It may be collected by High Fidelity and used to share your domain with others.", "settings": [ { diff --git a/domain-server/resources/web/js/tables.js b/domain-server/resources/web/js/tables.js index 09f85a7047..600e5a5f8e 100644 --- a/domain-server/resources/web/js/tables.js +++ b/domain-server/resources/web/js/tables.js @@ -2,11 +2,11 @@ $(document).ready(function(){ // setup the underscore templates var nodeTemplate = _.template($('#nodes-template').html()); var queuedTemplate = _.template($('#queued-template').html()); - + // setup a function to grab the assignments function getNodesAndAssignments() { $.getJSON("nodes.json", function(json){ - + json.nodes.sort(function(a, b){ if (a.type === b.type) { if (a.uptime < b.uptime) { @@ -16,36 +16,50 @@ $(document).ready(function(){ } else { return 0; } - } - + } + if (a.type === "agent" && b.type !== "agent") { return 1; } else if (b.type === "agent" && a.type !== "agent") { return -1; } - + if (a.type > b.type) { return 1; } - + if (a.type < b.type) { return -1; - } + } }); - + $('#nodes-table tbody').html(nodeTemplate(json)); + }).fail(function(jqXHR, textStatus, errorThrown) { + // we assume a 401 means the DS has restarted + // and no longer has our OAuth produced uuid + // so just reload and re-auth + if (jqXHR.status == 401) { + location.reload(); + } }); - - $.getJSON("assignments.json", function(json){ + + $.getJSON("assignments.json", function(json){ $('#assignments-table tbody').html(queuedTemplate(json)); + }).fail(function(jqXHR, textStatus, errorThrown) { + // we assume a 401 means the DS has restarted + // and no longer has our OAuth produced uuid + // so just reload and re-auth + if (jqXHR.status == 401) { + location.reload(); + } }); } - + // do the first GET on page load getNodesAndAssignments(); // grab the new assignments JSON every two seconds var getNodesAndAssignmentsInterval = setInterval(getNodesAndAssignments, 2000); - + // hook the node delete to the X button $(document.body).on('click', '.glyphicon-remove', function(){ // fire off a delete for this node @@ -57,10 +71,10 @@ $(document).ready(function(){ } }); }); - + $(document.body).on('click', '#kill-all-btn', function() { var confirmed_kill = confirm("Are you sure?"); - + if (confirmed_kill == true) { $.ajax({ url: "/nodes/", diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 095613a473..c5171620de 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -40,11 +40,11 @@ #include #include #include +#include +#include #include "DomainServerNodeData.h" #include "NodeConnectionData.h" -#include -#include int const DomainServer::EXIT_CODE_REBOOT = 234923; @@ -162,8 +162,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request + //send signal to DomainMetadata when descriptors changed _metadata = new DomainMetadata(this); - + connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated, + _metadata, &DomainMetadata::descriptorsChanged); qDebug() << "domain-server is running"; static const QString AC_SUBNET_WHITELIST_SETTING_PATH = "security.ac_subnet_whitelist"; @@ -1972,7 +1974,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return _settingsManager.handleAuthenticatedHTTPRequest(connection, url); } -const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID"; +static const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID"; +static const QString STATE_QUERY_KEY = "state"; bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url, bool skipSubHandler) { qDebug() << "HTTPS request received at" << url.toString(); @@ -1983,10 +1986,9 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u const QString CODE_QUERY_KEY = "code"; QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY); - const QString STATE_QUERY_KEY = "state"; QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY)); - if (!authorizationCode.isEmpty() && !stateUUID.isNull()) { + if (!authorizationCode.isEmpty() && !stateUUID.isNull() && _webAuthenticationStateSet.remove(stateUUID)) { // fire off a request with this code and state to get an access token for the user const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token"; @@ -2004,47 +2006,83 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply* tokenReply = NetworkAccessManager::getInstance().post(tokenRequest, tokenPostBody.toLocal8Bit()); + connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::tokenGrantFinished); - if (_webAuthenticationStateSet.remove(stateUUID)) { - // this is a web user who wants to auth to access web interface - // we hold the response back to them until we get their profile information - // and can decide if they are let in or not + // add this connection to our list of pending connections so that we can hold the response + _pendingOAuthConnections.insert(stateUUID, connection); - QEventLoop loop; - connect(tokenReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + // set the state UUID on the reply so that we can associate the response with the connection later + tokenReply->setProperty(STATE_QUERY_KEY.toLocal8Bit(), stateUUID); - // start the loop for the token request - loop.exec(); + return true; + } else { + connection->respond(HTTPConnection::StatusCode400); - QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply); + return true; + } + } else { + return false; + } +} - // stop the loop once the profileReply is complete - connect(profileReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); +HTTPSConnection* DomainServer::connectionFromReplyWithState(QNetworkReply* reply) { + // grab the UUID state property from the reply + QUuid stateUUID = reply->property(STATE_QUERY_KEY.toLocal8Bit()).toUuid(); - // restart the loop for the profile request - loop.exec(); + if (!stateUUID.isNull()) { + return _pendingOAuthConnections.take(stateUUID); + } else { + return nullptr; + } +} +void DomainServer::tokenGrantFinished() { + auto tokenReply = qobject_cast(sender()); + + if (tokenReply) { + if (tokenReply->error() == QNetworkReply::NoError) { + // now that we have a token for this profile, send off a profile request + QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply); + + // forward along the state UUID that we kept with the token request + profileReply->setProperty(STATE_QUERY_KEY.toLocal8Bit(), tokenReply->property(STATE_QUERY_KEY.toLocal8Bit())); + + connect(profileReply, &QNetworkReply::finished, this, &DomainServer::profileRequestFinished); + } else { + // the token grant failed, send back a 500 (assuming the connection is still around) + auto connection = connectionFromReplyWithState(tokenReply); + + if (connection) { + connection->respond(HTTPConnection::StatusCode500); + } + } + + tokenReply->deleteLater(); + } +} + +void DomainServer::profileRequestFinished() { + + auto profileReply = qobject_cast(sender()); + + if (profileReply) { + auto connection = connectionFromReplyWithState(profileReply); + + if (connection) { + if (profileReply->error() == QNetworkReply::NoError) { // call helper method to get cookieHeaders Headers cookieHeaders = setupCookieHeadersFromProfileReply(profileReply); connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, cookieHeaders); - delete tokenReply; - delete profileReply; - - // we've redirected the user back to our homepage - return true; - + } else { + // the profile request failed, send back a 500 (assuming the connection is still around) + connection->respond(HTTPConnection::StatusCode500); } } - // respond with a 200 code indicating that login is complete - connection->respond(HTTPConnection::StatusCode200); - - return true; - } else { - return false; + profileReply->deleteLater(); } } @@ -2104,22 +2142,31 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl // the user does not have allowed username or role, return 401 return false; } else { - // re-direct this user to OAuth page + static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With"; + static const QString XML_REQUESTED_WITH = "XMLHttpRequest"; - // generate a random state UUID to use - QUuid stateUUID = QUuid::createUuid(); + if (connection->requestHeaders().value(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) { + // unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR + // path to OAuth authorize + connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); + } else { + // re-direct this user to OAuth page - // add it to the set so we can handle the callback from the OAuth provider - _webAuthenticationStateSet.insert(stateUUID); + // generate a random state UUID to use + QUuid stateUUID = QUuid::createUuid(); - QUrl authURL = oauthAuthorizationURL(stateUUID); + // add it to the set so we can handle the callback from the OAuth provider + _webAuthenticationStateSet.insert(stateUUID); - Headers redirectHeaders; + QUrl authURL = oauthAuthorizationURL(stateUUID); - redirectHeaders.insert("Location", authURL.toEncoded()); + Headers redirectHeaders; - connection->respond(HTTPConnection::StatusCode302, - QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); + redirectHeaders.insert("Location", authURL.toEncoded()); + + connection->respond(HTTPConnection::StatusCode302, + QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); + } // we don't know about this user yet, so they are not yet authenticated return false; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 8851e3380b..4808297c89 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -111,6 +111,9 @@ private slots: void updateDownstreamNodes(); void updateUpstreamNodes(); + void tokenGrantFinished(); + void profileRequestFinished(); + signals: void iceServerChanged(); void userConnected(); @@ -178,6 +181,8 @@ private: void updateReplicationNodes(ReplicationServerDirection direction); + HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply); + SubnetList _acSubnetWhitelist; std::vector _replicatedUsernames; @@ -235,6 +240,8 @@ private: bool _sendICEServerAddressToMetaverseAPIInProgress { false }; bool _sendICEServerAddressToMetaverseAPIRedo { false }; + + QHash> _pendingOAuthConnections; }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 9279648319..7a2cfa645a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1198,6 +1198,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; + static const QString DESCRIPTION_ROOT_KEY = "descriptors"; auto& settingsVariant = _configMap.getConfig(); bool needRestart = false; @@ -1249,7 +1250,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject); - if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY) { + if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != SETTINGS_PATHS_KEY ) { needRestart = true; } } else { @@ -1265,7 +1266,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { const QJsonValue& settingValue = rootValue.toObject()[settingKey]; updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject); - if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY) + if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != DESCRIPTION_ROOT_KEY) || settingKey == AC_SUBNET_WHITELIST_KEY) { needRestart = true; } diff --git a/gvr-interface/src/RenderingClient.cpp b/gvr-interface/src/RenderingClient.cpp index b7c6f30a73..f04be5cb7f 100644 --- a/gvr-interface/src/RenderingClient.cpp +++ b/gvr-interface/src/RenderingClient.cpp @@ -63,10 +63,7 @@ void RenderingClient::sendAvatarPacket() { } void RenderingClient::cleanupBeforeQuit() { - - QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); - + DependencyManager::get()->cleanupBeforeQuit(); // destroy the AudioClient so it and its thread will safely go down DependencyManager::destroy(); } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index a0e9bd30d4..8f58ef3c98 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -34,36 +34,32 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, - { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, + { "from": "Vive.LeftHand", "to": "Standard.LeftHand"}, + { "from": "Vive.RightHand", "to": "Standard.RightHand"}, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", - "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}], - "when": [ "Application.InHMD" ] + "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}] }, { "from": "Vive.RightFoot", "to" : "Standard.RightFoot", - "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}], - "when": [ "Application.InHMD" ] + "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}] }, { "from": "Vive.Hips", "to" : "Standard.Hips", - "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}], - "when": [ "Application.InHMD" ] + "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}] }, { "from": "Vive.Spine2", "to" : "Standard.Spine2", - "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}], - "when": [ "Application.InHMD" ] + "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}] }, - { "from": "Vive.Head", "to" : "Standard.Head", "when": [ "Application.InHMD" ] }, + { "from": "Vive.Head", "to" : "Standard.Head"}, - { "from": "Vive.RightArm", "to" : "Standard.RightArm", "when": [ "Application.InHMD" ] }, - { "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when": [ "Application.InHMD" ] } + { "from": "Vive.RightArm", "to" : "Standard.RightArm"}, + { "from": "Vive.LeftArm", "to" : "Standard.LeftArm"} ] } diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index ddb0743bf8..8733349227 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/icons/tablet-icons/edit-disabled.svg b/interface/resources/icons/tablet-icons/edit-disabled.svg new file mode 100644 index 0000000000..4869b30dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-disabled.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-a.svg b/interface/resources/icons/tablet-icons/spectator-a.svg new file mode 100644 index 0000000000..22ebde999b --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-a.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-i.svg b/interface/resources/icons/tablet-icons/spectator-i.svg new file mode 100644 index 0000000000..3e6c1a7dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/interface/resources/images/calibration-help.png b/interface/resources/images/calibration-help.png new file mode 100644 index 0000000000..e3734a7d9c Binary files /dev/null and b/interface/resources/images/calibration-help.png differ diff --git a/interface/resources/images/static.gif b/interface/resources/images/static.gif new file mode 100644 index 0000000000..fbe46f48e6 Binary files /dev/null and b/interface/resources/images/static.gif differ diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 36161e3c3e..b279b7ca8d 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -18,7 +18,7 @@ Original.CheckBox { id: checkBox property int colorScheme: hifi.colorSchemes.light - property string color: hifi.colors.lightGray + property string color: hifi.colors.lightGrayText readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool isRedCheck: false property int boxSize: 14 diff --git a/interface/resources/qml/controls-uit/ImageMessageBox.qml b/interface/resources/qml/controls-uit/ImageMessageBox.qml new file mode 100644 index 0000000000..95c753aab4 --- /dev/null +++ b/interface/resources/qml/controls-uit/ImageMessageBox.qml @@ -0,0 +1,64 @@ +// +// ImageMessageBox.qml +// +// Created by Dante Ruiz on 7/5/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" + +Item { + id: imageBox + visible: false + anchors.fill: parent + property alias source: image.source + property alias imageWidth: image.width + property alias imageHeight: image.height + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.3 + } + + Image { + id: image + anchors.centerIn: parent + + HiFiGlyphs { + id: closeGlyphButton + text: hifi.glyphs.close + size: 25 + + anchors { + top: parent.top + topMargin: 15 + right: parent.right + rightMargin: 15 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + + onExited: { + parent.text = hifi.glyphs.close; + } + + onClicked: { + imageBox.visible = false; + } + } + } + } + +} diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml new file mode 100644 index 0000000000..5a775221f6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -0,0 +1,38 @@ +// +// Separator.qml +// +// Created by Zach Fox on 2017-06-06 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import "../styles-uit" + +Item { + // Size + height: 2; + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: height; + // Style + color: hifi.colors.baseGrayShadow; + } + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.baseGrayHighlight; + } +} diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index 39831546e1..89bae9bcde 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -36,7 +36,7 @@ Slider { Rectangle { width: parent.height - 2 - height: slider.value * (slider.width/(slider.maximumValue - slider.minimumValue)) - 1 + height: slider.width * (slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) - 1 radius: height / 2 anchors { top: parent.top diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml new file mode 100644 index 0000000000..d54f986717 --- /dev/null +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -0,0 +1,156 @@ +// +// Switch.qml +// +// Created by Zach Fox on 2017-06-06 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" + +Item { + id: rootSwitch; + + property int colorScheme: hifi.colorSchemes.light; + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light; + property int switchWidth: 70; + readonly property int switchRadius: height/2; + property string labelTextOff: ""; + property string labelGlyphOffText: ""; + property int labelGlyphOffSize: 32; + property string labelTextOn: ""; + property string labelGlyphOnText: ""; + property int labelGlyphOnSize: 32; + property alias checked: originalSwitch.checked; + signal onCheckedChanged; + signal clicked; + + Original.Switch { + id: originalSwitch; + activeFocusOnPress: true; + anchors.top: rootSwitch.top; + anchors.left: rootSwitch.left; + anchors.leftMargin: rootSwitch.width/2 - rootSwitch.switchWidth/2; + onCheckedChanged: rootSwitch.onCheckedChanged(); + onClicked: rootSwitch.clicked(); + + style: SwitchStyle { + + padding { + top: 3; + left: 3; + right: 3; + bottom: 3; + } + + groove: Rectangle { + color: "#252525"; + implicitWidth: rootSwitch.switchWidth; + implicitHeight: rootSwitch.height; + radius: rootSwitch.switchRadius; + } + + handle: Rectangle { + id: switchHandle; + implicitWidth: rootSwitch.height - padding.top - padding.bottom; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: hifi.colors.lightGrayText; + color: hifi.colors.lightGray; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.lightGray; + } + } + } + } + + // OFF Label + Item { + anchors.right: originalSwitch.left; + anchors.rightMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOff; + text: labelTextOff; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; + anchors.top: parent.top; + anchors.right: parent.right; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOff; + text: labelGlyphOffText; + size: labelGlyphOffSize; + color: labelOff.color; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.right: labelOff.left; + anchors.rightMargin: 4; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelGlyphOff.left; + anchors.right: labelOff.right; + onClicked: { + originalSwitch.checked = false; + } + } + } + + // ON Label + Item { + anchors.left: originalSwitch.right; + anchors.leftMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOn; + text: labelTextOn; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOn; + text: labelGlyphOnText; + size: labelGlyphOnSize; + color: labelOn.color; + anchors.top: parent.top; + anchors.left: labelOn.right; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelOn.left; + anchors.right: labelGlyphOn.right; + onClicked: { + originalSwitch.checked = true; + } + } + } +} diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index a813dc6b5f..3985c7d6f6 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -72,6 +72,7 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -86,7 +87,7 @@ Preference { } function buildPreference(preference) { - console.log("\tPreference type " + preference.type + " name " + preference.name) + console.log("\tPreference type " + preference.type + " name " + preference.name); var builder; switch (preference.type) { case Preference.Editable: @@ -128,6 +129,11 @@ Preference { checkBoxCount = 0; builder = comboBoxBuilder; break; + + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml new file mode 100644 index 0000000000..3cba67bc82 --- /dev/null +++ b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml @@ -0,0 +1,111 @@ +// +// SpinnerSliderPreference.qml +// +// Created by Cain Kilgore on 11th July 2017 +// 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.5 + +import "../../dialogs" +import "../../controls-uit" + +Preference { + id: root + property alias slider: slider + property alias spinner: spinner + height: control.height + hifi.dimensions.controlInterlineHeight + + Component.onCompleted: { + slider.value = preference.value; + spinner.value = preference.value; + } + + function save() { + preference.value = slider.value; + preference.save(); + } + + Item { + id: control + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Math.max(labelText.height, slider.height, spinner.height, button.height) + + Label { + id: labelText + text: root.label + ":" + colorScheme: hifi.colorSchemes.dark + anchors { + left: parent.left + right: slider.left + rightMargin: hifi.dimensions.labelPadding + verticalCenter: parent.verticalCenter + } + horizontalAlignment: Text.AlignRight + wrapMode: Text.Wrap + } + + Slider { + id: slider + value: preference.value + width: 100 + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + stepSize: preference.step + onValueChanged: { + spinner.value = value + } + anchors { + right: spinner.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + SpinBox { + id: spinner + decimals: preference.decimals + value: preference.value + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + width: 100 + onValueChanged: { + slider.value = value; + } + anchors { + right: button.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + GlyphButton { + id: button + onClicked: { + if (spinner.maximumValue >= 1) { + spinner.value = 1 + slider.value = 1 + } else { + spinner.value = spinner.maximumValue + slider.value = spinner.maximumValue + } + } + width: 30 + glyph: hifi.glyphs.reload + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index 754876b2c1..fa9d7aa6f0 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -32,14 +32,15 @@ Item { radius: popupRadius } Rectangle { - width: Math.max(parent.width * 0.75, 400) + id: textContainer; + width: Math.max(parent.width * 0.8, 400) height: contentContainer.height + 50 anchors.centerIn: parent radius: popupRadius color: "white" Item { id: contentContainer - width: parent.width - 60 + width: parent.width - 50 height: childrenRect.height anchors.centerIn: parent Item { @@ -92,7 +93,7 @@ Item { anchors.top: parent.top anchors.topMargin: -20 anchors.right: parent.right - anchors.rightMargin: -25 + anchors.rightMargin: -20 MouseArea { anchors.fill: closeGlyphButton hoverEnabled: true @@ -127,11 +128,51 @@ Item { color: hifi.colors.darkGray wrapMode: Text.WordWrap textFormat: Text.StyledText + onLinkActivated: { + Qt.openUrlExternally(link) + } } } } + // Left gray MouseArea MouseArea { - anchors.fill: parent + anchors.left: parent.left; + anchors.right: textContainer.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Right gray MouseArea + MouseArea { + anchors.left: textContainer.left; + anchors.right: parent.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Top gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: textContainer.top; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Bottom gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: textContainer.bottom; + anchors.bottom: parent.bottom; acceptedButtons: Qt.LeftButton onClicked: { letterbox.visible = false diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8db04a0f5b..c98cfba1ba 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1101,9 +1101,9 @@ Rectangle { case 'nearbyUsers': var data = message.params; var index = -1; + iAmAdmin = Users.canKick; index = findNearbySessionIndex('', data); if (index !== -1) { - iAmAdmin = Users.canKick; myData = data[index]; data.splice(index, 1); } else { diff --git a/interface/resources/qml/hifi/SpectatorCamera.qml b/interface/resources/qml/hifi/SpectatorCamera.qml new file mode 100644 index 0000000000..3a8559ab1e --- /dev/null +++ b/interface/resources/qml/hifi/SpectatorCamera.qml @@ -0,0 +1,374 @@ +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera +// +// Created by Zach Fox on 2017-06-05 +// 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 Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls + +// references HMD, XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: spectatorCamera; + // Style + color: hifi.colors.baseGray; + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // Force the popup on top of everything else + } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: spectatorCamera.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // "Spectator" text + RalewaySemiBold { + id: titleBarText; + text: "Spectator"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // SPECTATOR APP DESCRIPTION START + // + Item { + id: spectatorDescriptionContainer; + // Size + width: spectatorCamera.width; + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.top: titleBarContainer.bottom; + + // (i) Glyph + HiFiGlyphs { + id: spectatorDescriptionGlyph; + text: hifi.glyphs.info; + // Size + width: 20; + height: parent.height; + size: 60; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + anchors.topMargin: 0; + // Style + color: hifi.colors.lightGrayText; + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignTop; + } + + // "Spectator" app description text + RalewayLight { + id: spectatorDescriptionText; + text: "Spectator lets you change what your monitor displays while you're using a VR headset. Use Spectator when streaming and recording video."; + // Text size + size: 14; + // Size + width: 350; + height: paintedHeight; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: spectatorDescriptionGlyph.right; + anchors.leftMargin: 40; + // Style + color: hifi.colors.lightGrayText; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 14; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: spectatorDescriptionText.anchors.left; + anchors.leftMargin: spectatorDescriptionText.anchors.leftMargin; + // Style + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + font.underline: true; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + letterbox(hifi.glyphs.question, + "Spectator Camera", + "By default, your monitor shows a preview of what you're seeing in VR. " + + "Using the Spectator Camera app, your monitor can display the view " + + "from a virtual hand-held camera - perfect for taking selfies or filming " + + "your friends!
" + + "

Streaming and Recording

" + + "We recommend OBS for streaming and recording the contents of your monitor to services like " + + "Twitch, YouTube Live, and Facebook Live.

" + + "To get started using OBS, click this link now. The page will open in an external browser:
" + + 'OBS Official Overview Guide'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: spectatorLearnMoreText.bottom; + anchors.topMargin: spectatorDescriptionText.anchors.topMargin; + } + } + // + // SPECTATOR APP DESCRIPTION END + // + + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Size + height: spectatorCamera.height - spectatorDescriptionContainer.height - titleBarContainer.height; + // Anchors + anchors.top: spectatorDescriptionContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 25; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + + // "Camera On" Checkbox + HifiControlsUit.CheckBox { + id: cameraToggleCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: parent.top; + text: "Spectator Camera On"; + boxSize: 24; + onClicked: { + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + spectatorCameraPreview.ready = checked; + } + } + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: cameraToggleCheckBox.bottom; + anchors.topMargin: 20; + anchors.right: parent.right; + height: 250; + color: cameraToggleCheckBox.checked ? "transparent" : "black"; + + AnimatedImage { + source: "../../images/static.gif" + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof what your monitor shows."; + size: 16; + color: hifi.colors.lightGrayText; + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: cameraToggleCheckbox.checked; + url: monitorShowsSwitch.checked ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: cameraToggleCheckBox.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = cameraToggleCheckBox.checked; + update(); + } + } + } + + + // "Monitor Shows" Switch Label Glyph + HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.blueHighlight; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 13; + anchors.left: parent.left; + } + // "Monitor Shows" Switch Label + RalewayLight { + id: monitorShowsSwitchLabel; + text: "MONITOR SHOWS:"; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 20; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 6; + size: 16; + width: paintedWidth; + height: paintedHeight; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + // "Monitor Shows" Switch + HifiControlsUit.Switch { + id: monitorShowsSwitch; + height: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: monitorShowsSwitchLabel.bottom; + anchors.topMargin: 10; + labelTextOff: "HMD Preview"; + labelTextOn: "Camera View"; + labelGlyphOnText: hifi.glyphs.alert; + onCheckedChanged: { + sendToScript({method: 'setMonitorShowsCameraView', params: checked}); + } + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: monitorShowsSwitch.bottom; + anchors.topMargin: 25; + text: ""; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + } + // + // SPECTATOR CONTROLS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the SpectatorCamera JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from spectatorCamera.js. + // + function fromScript(message) { + switch (message.method) { + case 'updateSpectatorCameraCheckbox': + cameraToggleCheckBox.checked = message.params; + break; + case 'updateMonitorShowsSwitch': + monitorShowsSwitch.checked = message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.setting; + switchViewFromControllerCheckBox.enabled = true; + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Clicking Touch's Left Thumbstick Switches Monitor View"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Clicking Left Thumb Pad Switches Monitor View"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + } + break; + case 'showPreviewTextureNotInstructions': + console.log('showPreviewTextureNotInstructions recvd', JSON.stringify(message)); + spectatorCameraPreview.url = message.url; + spectatorCameraPreview.visible = message.setting; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c13bd3281a..03d27e3831 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -117,26 +117,28 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + text: display; + wrap: false; + checked: selected; + enabled: false; + } - RowLayout { - width: parent.width; + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setInputDevice(info); + } - AudioControls.CheckBox { - Layout.maximumWidth: parent.width - level.width - 40; - text: display; - wrap: false; - checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } - } - InputLevel { - id: level; - Layout.alignment: Qt.AlignRight; - Layout.rightMargin: 30; - visible: selected; - } + InputLevel { + id: level; + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 30 + visible: selected; } } } @@ -166,7 +168,7 @@ Rectangle { ListView { anchors { left: parent.left; right: parent.right; leftMargin: 70 } - height: 125; + height: Math.min(250, contentHeight); spacing: 0; snapMode: ListView.SnapToItem; clip: true; @@ -174,13 +176,19 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left text: display; checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } + enabled: false; + } + + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setOutputDevice(info); } } } diff --git a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml index 338a76989f..dd56bc96ab 100644 --- a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml +++ b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml @@ -25,11 +25,12 @@ Rectangle { signal canceled() signal restart() - property int count: 3 + property int count: 5 property string calibratingText: "CALIBRATING..." property string calibratingCountText: "CALIBRATION STARTING IN" property string calibrationSuccess: "CALIBRATION COMPLETED" property string calibrationFailed: "CALIBRATION FAILED" + property string instructionText: "Please stand in a T-Pose during calibration" HifiConstants { id: hifi } visible: true @@ -64,7 +65,7 @@ Rectangle { HiFiGlyphs { id: image - text: hifi.glyphs.avatar1 + text: hifi.glyphs.avatarTPose size: 190 color: hifi.colors.white @@ -158,6 +159,15 @@ Rectangle { onClicked: { restart(); + statusText.color = hifi.colors.blueHighlight; + statusText.text = info.calibratingCountText; + directions.text = instructionText; + countDown.visible = true; + busyIndicator.running = true; + busyRotation.from = 0 + busyRotation.to = 360 + busyIndicator.source = blueIndicator; + closeWindow.stop(); numberAnimation.stop(); info.count = (timer.interval / 1000); numberAnimation.start(); @@ -178,6 +188,7 @@ Rectangle { } } + function start(interval, countNumber) { countDown.visible = true; statusText.color = hifi.colors.blueHighlight; @@ -201,6 +212,7 @@ Rectangle { busyIndicator.running = false; statusText.text = info.calibrationSuccess statusText.color = hifi.colors.greenHighlight + directions.text = "SUCCESS" closeWindow.start(); } diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index e1ba93a840..4814eaf01c 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -16,6 +16,7 @@ import "../../controls-uit" as HifiControls StackView { id: stack initialItem: inputConfiguration + property alias messageVisible: imageMessageBox.visible Rectangle { id: inputConfiguration anchors.fill: parent @@ -26,6 +27,15 @@ StackView { property var pluginSettings: null + HifiControls.ImageMessageBox { + id: imageMessageBox + anchors.fill: parent + z: 2000 + imageWidth: 442 + imageHeight: 670 + source: "../../../images/calibration-help.png" + } + Rectangle { width: inputConfiguration.width height: 1 @@ -167,7 +177,7 @@ StackView { loader.item.pluginName = box.currentText; } } - + if (loader.item.hasOwnProperty("displayInformation")) { loader.item.displayConfiguration(); } @@ -183,20 +193,20 @@ StackView { return InputConfiguration.activeInputPlugins(); } } - + function initialize() { changeSource(); } - + function changeSource() { loader.source = ""; var source = ""; if (box.currentText == "Vive") { source = InputConfiguration.configurationLayout("OpenVR"); - } else { + } else { source = InputConfiguration.configurationLayout(box.currentText); } - + loader.source = source; if (source === "") { box.label = "(not configurable)"; @@ -204,14 +214,14 @@ StackView { box.label = ""; } } - + Timer { id: timer repeat: false interval: 300 onTriggered: initialize() } - + Component.onCompleted: { timer.start(); } diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 5040c043f4..d47c981440 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -65,7 +65,8 @@ Rectangle { onClicked: { newModelDialog.keyboardEnabled = HMD.active parent.focus = true; - parent.forceActiveFocus() + parent.forceActiveFocus(); + modelURL.cursorPosition = modelURL.positionAt(mouseX, mouseY, TextInput.CursorBetweenCharaters); } } } diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index 52a935ab19..90d6ba7022 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -50,9 +50,12 @@ Rectangle { readonly property int apply: 1 readonly property int applyAndCalibrate: 2 readonly property int calibrate: 3 - + } - + + + + MouseArea { id: mouseArea @@ -64,6 +67,7 @@ Rectangle { mouse.accepted = false; } } + color: hifi.colors.baseGray RalewayBold { @@ -146,6 +150,7 @@ Rectangle { label: "Y: offset" minimumValue: -10 stepSize: 0.0254 + value: -0.05 colorScheme: hifi.colorSchemes.dark onEditingFinished: { @@ -161,15 +166,16 @@ Rectangle { minimumValue: -10 stepSize: 0.0254 decimals: 4 + value: -0.05 colorScheme: hifi.colorSchemes.dark - + onEditingFinished: { sendConfigurationSettings(); } } } - - + + RalewayBold { id: hands @@ -245,7 +251,7 @@ Rectangle { anchors.left: openVrConfiguration.left anchors.leftMargin: leftMargin + 10 spacing: 10 - + HifiControls.SpinBox { id: handYOffset decimals: 4 @@ -269,7 +275,7 @@ Rectangle { stepSize: 0.0254 decimals: 4 colorScheme: hifi.colorSchemes.dark - + onEditingFinished: { sendConfigurationSettings(); } @@ -290,6 +296,52 @@ Rectangle { anchors.leftMargin: leftMargin } + RalewayRegular { + id: info + + text: "See Recommended Tracker Placement" + color: hifi.colors.blueHighlight + size: 10 + anchors { + left: additional.right + leftMargin: 10 + verticalCenter: additional.verticalCenter + } + + Rectangle { + id: selected + color: hifi.colors.blueHighlight + + width: info.width + height: 1 + + anchors { + top: info.bottom + topMargin: 1 + left: info.left + right: info.right + } + + visible: false + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: true + + onEntered: { + selected.visible = true; + } + + onExited: { + selected.visible = false; + } + onClicked: { + stack.messageVisible = true; + } + } + } + Row { id: feetConfig anchors.top: additional.bottom @@ -379,6 +431,7 @@ Rectangle { if (checked) { hipBox.checked = true; feetBox.checked = true; + shoulderBox.checked = false; } sendConfigurationSettings(); } @@ -416,6 +469,7 @@ Rectangle { if (checked) { hipBox.checked = true; feetBox.checked = true; + chestBox.checked = false; } sendConfigurationSettings(); } @@ -458,12 +512,12 @@ Rectangle { width: glyphButton.width + calibrationText.width + padding height: hifi.dimensions.controlLineHeight anchors.top: bottomSeperator.bottom - anchors.topMargin: 10 + anchors.topMargin: 15 anchors.left: parent.left anchors.leftMargin: leftMargin radius: hifi.buttons.radius - + gradient: Gradient { GradientStop { position: 0.2 @@ -479,7 +533,7 @@ Rectangle { } } } - + GradientStop { position: 1.0 color: { @@ -495,10 +549,10 @@ Rectangle { } } } - - + + HiFiGlyphs { id: glyphButton color: enabled ? hifi.buttons.textColor[calibrationButton.color] @@ -512,7 +566,7 @@ Rectangle { bottomMargin: 1 } } - + RalewayBold { id: calibrationText font.capitalization: Font.AllUppercase @@ -527,7 +581,7 @@ Rectangle { topMargin: 7 } } - + MouseArea { anchors.fill: parent @@ -549,19 +603,19 @@ Rectangle { } } } - + onPressed: { calibrationButton.pressed = true; } - + onReleased: { calibrationButton.pressed = false; } - + onEntered: { calibrationButton.hovered = true; } - + onExited: { calibrationButton.hovered = false; } @@ -590,16 +644,24 @@ Rectangle { lastConfiguration = composeConfigurationSettings(); } + Component.onDestruction: { + var settings = InputConfiguration.configurationSettings(pluginName); + var data = { + "num_pucks": settings["puckCount"] + } + UserActivityLogger.logAction("mocap_ui_close_dialog", data); + } + HifiControls.SpinBox { id: timeToCalibrate width: 70 anchors.top: calibrationButton.bottom - anchors.topMargin: 40 + anchors.topMargin: 20 anchors.left: parent.left anchors.leftMargin: leftMargin - minimumValue: 3 - value: 3 + minimumValue: 5 + value: 5 colorScheme: hifi.colorSchemes.dark onEditingFinished: { @@ -634,6 +696,57 @@ Rectangle { } } + Separator { + id: advanceSeperator + width: parent.width + anchors.top: timeToCalibrate.bottom + anchors.topMargin: 10 + } + + RalewayBold { + id: advanceSettings + + text: "Advanced Settings" + size: 12 + + color: hifi.colors.white + + anchors.top: advanceSeperator.bottom + anchors.topMargin: 10 + anchors.left: parent.left + anchors.leftMargin: leftMargin + } + + + HifiControls.CheckBox { + id: viveInDesktop + width: 15 + height: 15 + boxRadius: 7 + + anchors.top: advanceSettings.bottom + anchors.topMargin: 5 + anchors.left: openVrConfiguration.left + anchors.leftMargin: leftMargin + 10 + + onClicked: { + sendConfigurationSettings(); + } + } + + RalewayBold { + id: viveDesktopText + size: 10 + text: "Use Vive devices in desktop mode" + color: hifi.colors.white + + anchors { + left: viveInDesktop.right + leftMargin: 5 + verticalCenter: viveInDesktop.verticalCenter + } + } + NumberAnimation { id: numberAnimation target: openVrConfiguration @@ -641,17 +754,39 @@ Rectangle { to: 0 } + function logAction(action, status) { + console.log("calibrated from ui"); + var data = { + "num_pucks": status["puckCount"], + "puck_configuration": status["configuration"], + "head_puck": status["head_puck"], + "hand_puck": status["hand_pucks"] + } + UserActivityLogger.logAction(action, data); + } + function calibrationStatusInfo(status) { var calibrationScreen = stack.currentItem; - if (status["calibrated"]) { - calibrationScreen.success(); - } else if (!status["calibrated"]) { - var uncalibrated = status["success"]; - if (!uncalibrated) { - calibrationScreen.failure(); - } + + if (!status["UI"]) { + calibratingScreen = screen.createObject(); + stack.push(calibratingScreen); } + if (status["calibrated"]) { + calibrationScreen.success(); + + if (status["UI"]) { + logAction("mocap_ui_success", status); + } + + } else if (!status["calibrated"]) { + calibrationScreen.failure(); + + if (status["UI"]) { + logAction("mocap_ui_failed", status); + } + } updateCalibrationButton(); } @@ -698,6 +833,7 @@ Rectangle { var HmdHead = settings["HMDHead"]; var viveController = settings["handController"]; + var desktopMode = settings["desktopMode"]; if (HmdHead) { headBox.checked = true; @@ -715,8 +851,16 @@ Rectangle { handBox.checked = false; } + viveInDesktop.checked = desktopMode; + initializeButtonState(); updateCalibrationText(); + + var data = { + "num_pucks": settings["puckCount"] + }; + + UserActivityLogger.logAction("mocap_ui_open_dialog", data); } function displayTrackerConfiguration(type) { @@ -750,11 +894,11 @@ Rectangle { var handOverride = handSetting["override"]; var settingsChanged = false; - + if (lastConfiguration["bodyConfiguration"] !== bodySetting) { settingsChanged = true; } - + var lastHead = lastConfiguration["headConfiguration"]; if (lastHead["override"] !== headOverride) { settingsChanged = true; @@ -764,13 +908,13 @@ Rectangle { if (lastHand["override"] !== handOverride) { settingsChanged = true; } - + if (settingsChanged) { if ((!handOverride) && (!headOverride) && (bodySetting === "None")) { state = buttonState.apply; } else { state = buttonState.applyAndCalibrate; - } + } } else { if (state == buttonState.apply) { state = buttonState.disabled; @@ -778,7 +922,7 @@ Rectangle { state = buttonState.calibrate; } } - + lastConfiguration = settings; } @@ -795,7 +939,7 @@ Rectangle { state = buttonState.disabled; } else { state = buttonState.calibrate; - } + } } function updateCalibrationButton() { @@ -861,11 +1005,12 @@ Rectangle { "Y": handYOffset.value, "Z": handZOffset.value } - + var settingsObject = { "bodyConfiguration": trackerConfiguration, "headConfiguration": headObject, - "handConfiguration": handObject + "handConfiguration": handObject, + "desktopMode": viveInDesktop.checked } return settingsObject; diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 073f143dbe..8bf13bad76 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -94,10 +94,26 @@ StackView { property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false - + width: parent.width height: parent.height + MouseArea { + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: keyboard.top + } + + propagateComposedEvents: true + onPressed: { + parent.forceActiveFocus(); + addressBarDialog.keyboardEnabled = false; + mouse.accepted = false; + } + } + anchors { right: parent.right left: parent.left @@ -227,9 +243,9 @@ StackView { MouseArea { anchors.fill: parent; onClicked: { - if (!addressLine.focus || !HMD.active) { - addressLine.focus = true; - addressLine.forceActiveFocus(); + addressLine.focus = true; + addressLine.forceActiveFocus(); + if (HMD.active) { addressBarDialog.keyboardEnabled = HMD.active; } tabletRoot.playButtonClickSound(); diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index ea0c2844a1..fdfcfcf806 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -52,8 +52,10 @@ Windows.ScrollingWindow { // used to receive messages from interface script function fromScript(message) { - if (loader.item.hasOwnProperty("fromScript")) { - loader.item.fromScript(message); + if (loader.item !== null) { + if (loader.item.hasOwnProperty("fromScript")) { + loader.item.fromScript(message); + } } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 9dd0956000..af1fbd0070 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -81,6 +81,7 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -143,6 +144,10 @@ Preference { //to be not overlapped when drop down is active zpos = root.z + 1000 - itemNum break; + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index ca39326102..1556a9c0c0 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -50,7 +50,7 @@ Item { id: colors // Base colors - readonly property color baseGray: "#404040" + readonly property color baseGray: "#393939" readonly property color darkGray: "#121212" readonly property color baseGrayShadow: "#252525" readonly property color baseGrayHighlight: "#575757" @@ -336,5 +336,6 @@ Item { readonly property string source: "\ue01c" readonly property string playback_play: "\ue01d" readonly property string stop_square: "\ue01e" + readonly property string avatarTPose: "\ue01f" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ee7b0cfa82..06222d872e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,7 @@ #include +#include #include #include #include @@ -67,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -110,10 +113,7 @@ #include #include #include -#include -#include -#include -#include +#include #include #include #include @@ -168,6 +168,7 @@ #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif +#include "ui/ResourceImageItem.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" @@ -479,6 +480,12 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); bool previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); + // get dir to use for cache + static const auto CACHE_SWITCH = "--cache"; + QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); + if (!cacheDir.isEmpty()) { + qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); + } Setting::init(); @@ -582,6 +589,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -938,58 +946,68 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Make sure we don't time out during slow operations at startup updateHeartbeat(); - - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - static const QString TESTER = "HIFI_TESTER"; - auto gpuIdent = GPUIdent::getInstance(); - auto glContextData = getGLContextData(); - QJsonObject properties = { - { "version", applicationVersion() }, - { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) }, - { "previousSessionCrashed", _previousSessionCrashed }, - { "previousSessionRuntime", sessionRunTime.get() }, - { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, - { "kernel_type", QSysInfo::kernelType() }, - { "kernel_version", QSysInfo::kernelVersion() }, - { "os_type", QSysInfo::productType() }, - { "os_version", QSysInfo::productVersion() }, - { "gpu_name", gpuIdent->getName() }, - { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", static_cast(gpuIdent->getMemory()) }, - { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, - { "gl_version", glContextData["version"] }, - { "gl_vender", glContextData["vendor"] }, - { "gl_sl_version", glContextData["sl_version"] }, - { "gl_renderer", glContextData["renderer"] }, - { "ideal_thread_count", QThread::idealThreadCount() } - }; - auto macVersion = QSysInfo::macVersion(); - if (macVersion != QSysInfo::MV_None) { - properties["os_osx_version"] = QSysInfo::macVersion(); - } - auto windowsVersion = QSysInfo::windowsVersion(); - if (windowsVersion != QSysInfo::WV_None) { - properties["os_win_version"] = QSysInfo::windowsVersion(); - } - - ProcessorInfo procInfo; - if (getProcessorInfo(procInfo)) { - properties["processor_core_count"] = procInfo.numProcessorCores; - properties["logical_processor_count"] = procInfo.numLogicalProcessors; - properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; - properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; - properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; - } - - // add firstRun flag from settings to launch event Setting::Handle firstRun { Settings::firstRun, true }; - properties["first_run"] = firstRun.get(); - // add the user's machine ID to the launch event - properties["machine_fingerprint"] = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + // once the settings have been loaded, check if we need to flip the default for UserActivityLogger + auto& userActivityLogger = UserActivityLogger::getInstance(); + if (!userActivityLogger.isDisabledSettingSet()) { + // the user activity logger is opt-out for Interface + // but it's defaulted to disabled for other targets + // so we need to enable it here if it has never been disabled by the user + userActivityLogger.disable(false); + } - UserActivityLogger::getInstance().logAction("launch", properties); + if (userActivityLogger.isEnabled()) { + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + static const QString TESTER = "HIFI_TESTER"; + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "version", applicationVersion() }, + { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) }, + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["sl_version"] }, + { "gl_renderer", glContextData["renderer"] }, + { "ideal_thread_count", QThread::idealThreadCount() } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + + ProcessorInfo procInfo; + if (getProcessorInfo(procInfo)) { + properties["processor_core_count"] = procInfo.numProcessorCores; + properties["logical_processor_count"] = procInfo.numLogicalProcessors; + properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; + properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; + properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; + } + + properties["first_run"] = firstRun.get(); + + // add the user's machine ID to the launch event + properties["machine_fingerprint"] = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + + userActivityLogger.logAction("launch", properties); + } // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); @@ -1201,15 +1219,26 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Make sure we don't time out during slow operations at startup updateHeartbeat(); - int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now - connect(&_settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - connect(&_settingsThread, SIGNAL(started()), &_settingsTimer, SLOT(start())); - connect(&_settingsThread, SIGNAL(finished()), &_settingsTimer, SLOT(stop())); - _settingsTimer.moveToThread(&_settingsThread); - _settingsTimer.setSingleShot(false); - _settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable - _settingsThread.setPriority(QThread::LowestPriority); - _settingsThread.start(); + + QTimer* settingsTimer = new QTimer(); + moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ + connect(qApp, &Application::beforeAboutToQuit, [this, settingsTimer]{ + // Disconnect the signal from the save settings + QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + // Stop the settings timer + settingsTimer->stop(); + // Delete it (this will trigger the thread destruction + settingsTimer->deleteLater(); + // Mark the settings thread as finished, so we know we can safely save in the main application + // shutdown code + _settingsGuard.trigger(); + }); + + int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now + settingsTimer->setSingleShot(false); + settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable + QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + }, QThread::LowestPriority); if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. @@ -1434,6 +1463,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["atp_mapping_requests"] = atpMappingRequests; properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + QJsonObject bytesDownloaded; + bytesDownloaded["atp"] = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["http"] = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["file"] = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["total"] = bytesDownloaded["atp"].toInt() + bytesDownloaded["http"].toInt() + + bytesDownloaded["file"].toInt(); + properties["bytesDownloaded"] = bytesDownloaded; auto myAvatar = getMyAvatar(); glm::vec3 avatarPosition = myAvatar->getPosition(); @@ -1637,7 +1674,7 @@ QString Application::getUserAgent() { if (QThread::currentThread() != thread()) { QString userAgent; - QMetaObject::invokeMethod(this, "getUserAgent", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, userAgent)); + BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); return userAgent; } @@ -1672,9 +1709,7 @@ QString Application::getUserAgent() { void Application::toggleTabletUI(bool shouldOpen) const { auto tabletScriptingInterface = DependencyManager::get(); auto hmd = DependencyManager::get(); - TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - bool messageOpen = tablet->isMessageDialogOpen(); - if ((!messageOpen || (messageOpen && !hmd->getShouldShowTablet())) && !(shouldOpen && hmd->getShouldShowTablet())) { + if (!(shouldOpen && hmd->getShouldShowTablet())) { auto HMD = DependencyManager::get(); HMD->toggleShouldShowTablet(); } @@ -1802,11 +1837,13 @@ void Application::cleanupBeforeQuit() { locationUpdateTimer.stop(); identityPacketTimer.stop(); pingTimer.stop(); - QMetaObject::invokeMethod(&_settingsTimer, "stop", Qt::BlockingQueuedConnection); - // save state - _settingsThread.quit(); - saveSettings(); + // Wait for the settings thread to shut down, and save the settings one last time when it's safe + if (_settingsGuard.wait()) { + // save state + saveSettings(); + } + _window->saveGeometry(); // Destroy third party processes after scripts have finished using them. @@ -1830,8 +1867,7 @@ void Application::cleanupBeforeQuit() { // FIXME: something else is holding a reference to AudioClient, // so it must be explicitly synchronously stopped here - QMetaObject::invokeMethod(DependencyManager::get().data(), - "cleanupBeforeQuit", Qt::BlockingQueuedConnection); + DependencyManager::get()->cleanupBeforeQuit(); // destroy Audio so it and its threads have a chance to go down safely // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine @@ -1944,7 +1980,8 @@ void Application::initializeGL() { render::CullFunctor cullFunctor = LODManager::shouldRender; static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); - _renderEngine->addJob("SecondaryCameraFrame", cullFunctor); + _renderEngine->addJob("UpdateScene"); + _renderEngine->addJob("SecondaryCameraJob", cullFunctor); _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -1992,6 +2029,7 @@ void Application::initializeUi() { LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); auto offscreenUi = DependencyManager::get(); @@ -2087,6 +2125,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("HoverOverlay", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -2152,48 +2191,74 @@ void Application::paintGL() { return; } - auto displayPlugin = getActiveDisplayPlugin(); - // FIXME not needed anymore? - _offscreenContext->makeCurrent(); + DisplayPluginPointer displayPlugin; + { + PROFILE_RANGE(render, "/getActiveDisplayPlugin"); + displayPlugin = getActiveDisplayPlugin(); + } - // If a display plugin loses it's underlying support, it - // needs to be able to signal us to not use it - if (!displayPlugin->beginFrameRender(_frameCount)) { - _inPaint = false; - updateDisplayMode(); - return; + { + PROFILE_RANGE(render, "/offscreenMakeCurrent"); + // FIXME not needed anymore? + _offscreenContext->makeCurrent(); + } + + { + PROFILE_RANGE(render, "/pluginBeginFrameRender"); + // If a display plugin loses it's underlying support, it + // needs to be able to signal us to not use it + if (!displayPlugin->beginFrameRender(_frameCount)) { + _inPaint = false; + updateDisplayMode(); + return; + } } // update the avatar with a fresh HMD pose - getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); + { + PROFILE_RANGE(render, "/updateAvatar"); + getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); + } auto lodManager = DependencyManager::get(); + RenderArgs renderArgs; { - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.calculate(); - } - RenderArgs renderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - { - QMutexLocker viewLocker(&_viewMutex); - renderArgs.setViewFrustum(_viewFrustum); + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + _viewFrustum.calculate(); + } + renderArgs = RenderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + { + QMutexLocker viewLocker(&_viewMutex); + renderArgs.setViewFrustum(_viewFrustum); + } } - PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - - _gpuContext->beginFrame(getHMDSensorPose()); - // Reset the gpu::Context Stages - // Back to the default framebuffer; - gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { - batch.resetStages(); - }); + { + PROFILE_RANGE(render, "/resizeGL"); + PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } { + PROFILE_RANGE(render, "/gpuContextReset"); + _gpuContext->beginFrame(getHMDSensorPose()); + // Reset the gpu::Context Stages + // Back to the default framebuffer; + gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { + batch.resetStages(); + }); + } + + + { + PROFILE_RANGE(render, "/renderOverlay"); PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs // the ApplicationOverlay class assumes it's viewport is setup to be the device size @@ -2204,114 +2269,127 @@ void Application::paintGL() { glm::vec3 boomOffset; { - PerformanceTimer perfTimer("CameraUpdates"); + PROFILE_RANGE(render, "/updateCamera"); + { + PerformanceTimer perfTimer("CameraUpdates"); - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); - cameraMenuChanged(); - } - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glm::quat_cast(camMat)); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); + cameraMenuChanged(); } - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); - _myCamera.setPosition(extractTranslation(hmdWorldMat) + - myAvatar->getOrientation() * boomOffset); - } else { - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getOrientation() * boomOffset); - } - } - } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } else { - _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() - * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) - + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glm::quat_cast(camMat)); } else { - _myCamera.setOrientation(cameraEntity->getRotation()); - _myCamera.setPosition(cameraEntity->getPosition()); + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); + _myCamera.setPosition(extractTranslation(hmdWorldMat) + + myAvatar->getOrientation() * boomOffset); + } else { + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * boomOffset); + } + } + } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } else { + _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); + } else { + _myCamera.setOrientation(cameraEntity->getRotation()); + _myCamera.setPosition(cameraEntity->getPosition()); + } } } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); + // Update camera position + if (!isHMDMode()) { + _myCamera.update(1.0f / _frameCounter.rate()); + } } } - getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform()); + { + PROFILE_RANGE(render, "/updateCompositor"); + getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform()); + } - // Primary rendering pass - auto framebufferCache = DependencyManager::get(); - const QSize size = framebufferCache->getFrameBufferSize(); - // Final framebuffer that will be handled to the display-plugin - auto finalFramebuffer = framebufferCache->getFramebuffer(); + gpu::FramebufferPointer finalFramebuffer; + QSize finalFramebufferSize; + { + PROFILE_RANGE(render, "/getOutputFramebuffer"); + // Primary rendering pass + auto framebufferCache = DependencyManager::get(); + finalFramebufferSize = framebufferCache->getFrameBufferSize(); + // Final framebuffer that will be handled to the display-plugin + finalFramebuffer = framebufferCache->getFramebuffer(); + } { PROFILE_RANGE(render, "/mainRender"); PerformanceTimer perfTimer("mainRender"); renderArgs._boomOffset = boomOffset; + // FIXME is this ever going to be different from the size previously set in the render args + // in the overlay render? // Viewport is assigned to the size of the framebuffer - renderArgs._viewport = ivec4(0, 0, size.width(), size.height()); + renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); if (displayPlugin->isStereo()) { // Stereo modes will typically have a larger projection matrix overall, // so we ask for the 'mono' projection matrix, which for stereo and HMD @@ -2661,56 +2739,43 @@ bool Application::importSVOFromURL(const QString& urlString) { return true; } +bool _renderRequested { false }; + bool Application::event(QEvent* event) { if (!Menu::getInstance()) { return false; } - // Presentation/painting logic - // TODO: Decouple presentation and painting loops - static bool isPaintingThrottled = false; - if ((int)event->type() == (int)Present) { - if (isPaintingThrottled) { - // If painting (triggered by presentation) is hogging the main thread, - // repost as low priority to avoid hanging the GUI. - // This has the effect of allowing presentation to exceed the paint budget by X times and - // only dropping every (1/X) frames, instead of every ceil(X) frames - // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). - removePostedEvents(this, Present); - postEvent(this, new QEvent(static_cast(Present)), Qt::LowEventPriority); - isPaintingThrottled = false; + int type = event->type(); + switch (type) { + case Event::Lambda: + static_cast(event)->call(); return true; - } - float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); - if (shouldPaint(nsecsElapsed)) { - _lastTimeUpdated.start(); - idle(nsecsElapsed); - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); - } - isPaintingThrottled = true; + case Event::Present: + if (!_renderRequested) { + float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); + if (shouldPaint(nsecsElapsed)) { + _renderRequested = true; + _lastTimeUpdated.start(); + idle(nsecsElapsed); + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + } + } + return true; - return true; - } else if ((int)event->type() == (int)Paint) { - // NOTE: This must be updated as close to painting as possible, - // or AvatarInputs will mysteriously move to the bottom-right - AvatarInputs::getInstance()->update(); + case Event::Paint: + // NOTE: This must be updated as close to painting as possible, + // or AvatarInputs will mysteriously move to the bottom-right + AvatarInputs::getInstance()->update(); + paintGL(); + // wait for the next present event before starting idle / paint again + removePostedEvents(this, Present); + _renderRequested = false; + return true; - paintGL(); - - isPaintingThrottled = false; - - return true; - } else if ((int)event->type() == (int)Idle) { - float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); - idle(nsecsElapsed); - - return true; - } - - if ((int)event->type() == (int)Lambda) { - static_cast(event)->call(); - return true; + default: + break; } { @@ -3100,59 +3165,6 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; - - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - } - } - - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { - - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); - } - cameraMenuChanged(); - break; - } - - case Qt::Key_P: { - if (!(isShifted || isMeta || isOption)) { - bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); - cameraMenuChanged(); - } - break; - } - case Qt::Key_Slash: Menu::getInstance()->triggerOption(MenuOption::Stats); break; @@ -3613,6 +3625,133 @@ bool Application::shouldPaint(float nsecsElapsed) { #include #include #pragma comment(lib, "pdh.lib") +#pragma comment(lib, "ntdll.lib") + +extern "C" { + enum SYSTEM_INFORMATION_CLASS { + SystemBasicInformation = 0, + SystemProcessorPerformanceInformation = 8, + }; + + struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { + LARGE_INTEGER IdleTime; + LARGE_INTEGER KernelTime; + LARGE_INTEGER UserTime; + LARGE_INTEGER DpcTime; + LARGE_INTEGER InterruptTime; + ULONG InterruptCount; + }; + + struct SYSTEM_BASIC_INFORMATION { + ULONG Reserved; + ULONG TimerResolution; + ULONG PageSize; + ULONG NumberOfPhysicalPages; + ULONG LowestPhysicalPageNumber; + ULONG HighestPhysicalPageNumber; + ULONG AllocationGranularity; + ULONG_PTR MinimumUserModeAddress; + ULONG_PTR MaximumUserModeAddress; + ULONG_PTR ActiveProcessorsAffinityMask; + CCHAR NumberOfProcessors; + }; + + NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( + _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, + _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, + _In_ ULONG SystemInformationLength, + _Out_opt_ PULONG ReturnLength + ); + +} +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { + return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); +} + +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { + return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); +} + + +template +void updateValueAndDelta(std::pair& pair, T newValue) { + auto& value = pair.first; + auto& delta = pair.second; + delta = (value != 0) ? newValue - value : 0; + value = newValue; +} + +struct MyCpuInfo { + using ValueAndDelta = std::pair; + std::string name; + ValueAndDelta kernel { 0, 0 }; + ValueAndDelta user { 0, 0 }; + ValueAndDelta idle { 0, 0 }; + float kernelUsage { 0.0f }; + float userUsage { 0.0f }; + + void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { + updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); + updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); + updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); + auto totalTime = kernel.second + user.second + idle.second; + if (totalTime != 0) { + kernelUsage = (FLOAT)kernel.second / totalTime; + userUsage = (FLOAT)user.second / totalTime; + } else { + kernelUsage = userUsage = 0.0f; + } + } +}; + +void updateCpuInformation() { + static std::once_flag once; + static SYSTEM_BASIC_INFORMATION systemInfo {}; + static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; + static std::vector cpuInfos; + static std::vector myCpuInfos; + static MyCpuInfo myCpuTotals; + std::call_once(once, [&] { + NtQuerySystemInformation( SystemBasicInformation, systemInfo); + cpuInfos.resize(systemInfo.NumberOfProcessors); + myCpuInfos.resize(systemInfo.NumberOfProcessors); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + myCpuInfos[i].name = "cpu." + std::to_string(i); + } + myCpuTotals.name = "cpu.total"; + }); + NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); + + // Zero the CPU totals. + memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + auto& cpuInfo = cpuInfos[i]; + // KernelTime includes IdleTime. + cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; + + // Update totals + cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; + cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; + cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; + + // Update friendly structure + auto& myCpuInfo = myCpuInfos[i]; + myCpuInfo.update(cpuInfo); + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, + { "user", myCpuInfo.userUsage } + }); + } + + myCpuTotals.update(cpuTotals); + PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { + { "kernel", myCpuTotals.kernelUsage }, + { "user", myCpuTotals.userUsage } + }); +} + static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; static int numProcessors; @@ -3665,6 +3804,26 @@ void getCpuUsage(vec3& systemAndUser) { systemAndUser.z = (float)counterVal.doubleValue; } +void setupCpuMonitorThread() { + initCpuUsage(); + auto cpuMonitorThread = QThread::currentThread(); + + QTimer* timer = new QTimer(); + timer->setInterval(50); + QObject::connect(timer, &QTimer::timeout, [] { + updateCpuInformation(); + vec3 kernelUserAndSystem; + getCpuUsage(kernelUserAndSystem); + PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); + PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); + }); + QObject::connect(cpuMonitorThread, &QThread::finished, [=] { + timer->deleteLater(); + cpuMonitorThread->deleteLater(); + }); + timer->start(); +} + #endif @@ -3685,15 +3844,17 @@ void Application::idle(float nsecsElapsed) { } #ifdef Q_OS_WIN + // If tracing is enabled then monitor the CPU in a separate thread static std::once_flag once; - std::call_once(once, [] { - initCpuUsage(); + std::call_once(once, [&] { + if (trace_app().isDebugEnabled()) { + QThread* cpuMonitorThread = new QThread(qApp); + cpuMonitorThread->setObjectName("cpuMonitorThread"); + QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); + cpuMonitorThread->start(); + } }); - - vec3 kernelUserAndSystem; - getCpuUsage(kernelUserAndSystem); - PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); - PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); #endif @@ -5166,7 +5327,7 @@ namespace render { auto& batch = *args->_batch; DependencyManager::get()->bindSimpleProgram(batch); - renderWorldBox(batch); + renderWorldBox(args, batch); } } } @@ -5229,10 +5390,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } { - PerformanceTimer perfTimer("SceneProcessTransaction"); _main3DScene->enqueueTransaction(transaction); - - _main3DScene->processTransactionQueue(); } // For now every frame pass the renderContext @@ -5286,6 +5444,10 @@ void Application::updateWindowTitle() const { qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); #endif _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); } void Application::clearDomainOctreeDetails() { @@ -5671,6 +5833,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri auto entityScriptServerLog = DependencyManager::get(); scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("HoverOverlay", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue); @@ -6442,11 +6605,11 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - auto newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); if (QUrl(newScript).scheme() == "atp") { OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP"); } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript); + DependencyManager::get()->loadScript(newScript.trimmed()); } } @@ -6923,6 +7086,12 @@ void Application::updateDisplayMode() { // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); + // switch to first person if entering hmd and setting is checked + if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } diff --git a/interface/src/Application.h b/interface/src/Application.h index eb77855b2b..e82b6c6dc5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -299,7 +300,6 @@ public: void setAvatarOverrideUrl(const QUrl& url, bool save); QUrl getAvatarOverrideUrl() { return _avatarOverrideUrl; } bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; } - void setCacheOverrideDir(const QString& dirName) { _cacheDir = dirName; } signals: void svoImportRequested(const QString& url); @@ -597,8 +597,7 @@ private: bool _notifiedPacketVersionMismatchThisDomain; - QThread _settingsThread; - QTimer _settingsTimer; + ConditionalGuard _settingsGuard; GLCanvas* _glWidget{ nullptr }; @@ -680,7 +679,7 @@ private: QTimer _addAssetToWorldErrorTimer; FileScriptingInterface* _fileDownload; - AudioInjector* _snapshotSoundInjector { nullptr }; + AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; @@ -692,6 +691,5 @@ private: QUrl _avatarOverrideUrl; bool _saveAvatarOverrideUrl { false }; - QString _cacheDir; }; #endif // hifi_Application_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index c673b6bc43..81a74a7bdc 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -223,7 +223,7 @@ Menu::Menu() { // View > First Person cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::FirstPerson, 0, // QML Qt:: Key_P + MenuOption::FirstPerson, 0, true, qApp, SLOT(cameraMenuChanged()))); // View > Third Person @@ -233,7 +233,7 @@ Menu::Menu() { // View > Mirror cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::FullscreenMirror, 0, // QML Qt::Key_H, + MenuOption::FullscreenMirror, 0, false, qApp, SLOT(cameraMenuChanged()))); // View > Independent [advanced] @@ -258,6 +258,9 @@ Menu::Menu() { // View > Overlays addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true); + // View > Enter First Person Mode in HMD + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FirstPersonHMD, 0, true); + // Navigate menu ---------------------------------- MenuWrapper* navigateMenu = addMenu("Navigate"); @@ -319,7 +322,7 @@ Menu::Menu() { QString("../../hifi/tablet/TabletLodPreferences.qml"), "LodPreferencesDialog"); }); - action = addActionToQMenuAndActionHash(settingsMenu, "Controller Settings"); + action = addActionToQMenuAndActionHash(settingsMenu, "Controller Settings..."); connect(action, &QAction::triggered, [] { auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); auto hmd = DependencyManager::get(); @@ -682,7 +685,7 @@ Menu::Menu() { // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); { - auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig(); + auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.DrawStatus"); addCheckableActionToQMenuAndActionHash(physicsOptionsMenu, MenuOption::PhysicsShowOwned, 0, false, drawStatusConfig, SLOT(setShowNetwork(bool))); } diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 43611eab70..607708eb3e 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -105,6 +105,7 @@ namespace MenuOption { const QString ExpandPhysicsSimulationTiming = "Expand /physics"; const QString ExpandUpdateTiming = "Expand /update"; const QString FirstPerson = "First Person"; + const QString FirstPersonHMD = "Enter First Person Mode in HMD"; const QString FivePointCalibration = "5 Point Calibration"; const QString FixGaze = "Fix Gaze (no saccade)"; const QString Forward = "Forward"; diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index f59d2fcc7a..56b8b3ef85 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,9 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Application.h" #include "SecondaryCamera.h" #include #include +#include using RenderArgsPointer = std::shared_ptr; @@ -27,39 +29,32 @@ void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render } } -void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" - bool wasEnabled = isEnabled(); - setEnabled(false); - auto textureCache = DependencyManager::get(); - textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg - setEnabled(wasEnabled); -} - -void SecondaryCameraRenderTaskConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. - resetSize(width, height); -} - -class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view. +class SecondaryCameraJob { // Changes renderContext for our framebuffer and view. + QUuid _attachedEntityId{}; glm::vec3 _position{}; glm::quat _orientation{}; float _vFoV{}; float _nearClipPlaneDistance{}; float _farClipPlaneDistance{}; + EntityPropertyFlags _attachedEntityPropertyFlags; + QSharedPointer _entityScriptingInterface; public: - using Config = BeginSecondaryCameraFrameConfig; - using JobModel = render::Job::ModelO; - BeginSecondaryCameraFrame() { + using Config = SecondaryCameraJobConfig; + using JobModel = render::Job::ModelO; + SecondaryCameraJob() { _cachedArgsPointer = std::make_shared(_cachedArgs); + _entityScriptingInterface = DependencyManager::get(); + _attachedEntityPropertyFlags += PROP_POSITION; + _attachedEntityPropertyFlags += PROP_ROTATION; } void configure(const Config& config) { - if (config.enabled || config.alwaysEnabled) { - _position = config.position; - _orientation = config.orientation; - _vFoV = config.vFoV; - _nearClipPlaneDistance = config.nearClipPlaneDistance; - _farClipPlaneDistance = config.farClipPlaneDistance; - } + _attachedEntityId = config.attachedEntityId; + _position = config.position; + _orientation = config.orientation; + _vFoV = config.vFoV; + _nearClipPlaneDistance = config.nearClipPlaneDistance; + _farClipPlaneDistance = config.farClipPlaneDistance; } void run(const render::RenderContextPointer& renderContext, RenderArgsPointer& cachedArgs) { @@ -83,8 +78,14 @@ public: }); auto srcViewFrustum = args->getViewFrustum(); - srcViewFrustum.setPosition(_position); - srcViewFrustum.setOrientation(_orientation); + if (!_attachedEntityId.isNull()) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, _attachedEntityPropertyFlags); + srcViewFrustum.setPosition(entityProperties.getPosition()); + srcViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + srcViewFrustum.setPosition(_position); + srcViewFrustum.setOrientation(_orientation); + } srcViewFrustum.setProjection(glm::perspective(glm::radians(_vFoV), ((float)args->_viewport.z / (float)args->_viewport.w), _nearClipPlaneDistance, _farClipPlaneDistance)); // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, // which is not what we want here. @@ -99,6 +100,41 @@ protected: RenderArgsPointer _cachedArgsPointer; }; +void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { + if (attachedEntityId.isNull()) { + position = pos; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set position of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::setOrientation(glm::quat orient) { + if (attachedEntityId.isNull()) { + orientation = orient; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set orientation of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::enableSecondaryCameraRenderConfigs(bool enabled) { + qApp->getRenderEngine()->getConfiguration()->getConfig()->setEnabled(enabled); + setEnabled(enabled); +} + +void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. + qApp->getRenderEngine()->getConfiguration()->getConfig()->resetSize(width, height); +} + +void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" + bool wasEnabled = isEnabled(); + setEnabled(false); + auto textureCache = DependencyManager::get(); + textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg + setEnabled(wasEnabled); +} + class EndSecondaryCameraFrame { // Restores renderContext. public: using JobModel = render::Job::ModelI; @@ -119,7 +155,7 @@ public: }; void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor) { - const auto cachedArg = task.addJob("BeginSecondaryCamera"); + const auto cachedArg = task.addJob("SecondaryCamera"); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); task.addJob("RenderDeferredTask", items); diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 5ad19c9614..0941959c0a 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -28,34 +28,40 @@ public: void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); }; -class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. +class SecondaryCameraJobConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. Q_OBJECT - Q_PROPERTY(glm::vec3 position MEMBER position NOTIFY dirty) // of viewpoint to render from - Q_PROPERTY(glm::quat orientation MEMBER orientation NOTIFY dirty) // of viewpoint to render from + Q_PROPERTY(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation + Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from + Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees. Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters. public: + QUuid attachedEntityId{}; glm::vec3 position{}; glm::quat orientation{}; - float vFoV{ 45.0f }; - float nearClipPlaneDistance{ 0.1f }; - float farClipPlaneDistance{ 100.0f }; - BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {} + float vFoV{ DEFAULT_FIELD_OF_VIEW_DEGREES }; + float nearClipPlaneDistance{ DEFAULT_NEAR_CLIP }; + float farClipPlaneDistance{ DEFAULT_FAR_CLIP }; + SecondaryCameraJobConfig() : render::Task::Config(false) {} signals: void dirty(); +public slots: + glm::vec3 getPosition() { return position; } + void setPosition(glm::vec3 pos); + glm::quat getOrientation() { return orientation; } + void setOrientation(glm::quat orient); + void enableSecondaryCameraRenderConfigs(bool enabled); + void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTaskConfig : public render::Task::Config { Q_OBJECT public: SecondaryCameraRenderTaskConfig() : render::Task::Config(false) {} -private: void resetSize(int width, int height); signals: void dirty(); -public slots: - void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTask { diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 78a503bc71..7822b78244 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -34,7 +34,7 @@ using namespace std; -void renderWorldBox(gpu::Batch& batch) { +void renderWorldBox(RenderArgs* args, gpu::Batch& batch) { auto geometryCache = DependencyManager::get(); // Show center of world @@ -115,7 +115,7 @@ void renderWorldBox(gpu::Batch& batch) { geometryIds[17]); - geometryCache->renderWireCubeInstance(batch, GREY4); + geometryCache->renderWireCubeInstance(args, batch, GREY4); // Draw meter markers along the 3 axis to help with measuring things const float MARKER_DISTANCE = 1.0f; @@ -123,23 +123,23 @@ void renderWorldBox(gpu::Batch& batch) { transform = Transform().setScale(MARKER_RADIUS); batch.setModelTransform(transform); - geometryCache->renderSolidSphereInstance(batch, RED); + geometryCache->renderSolidSphereInstance(args, batch, RED); transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, 0.0f)).setScale(MARKER_RADIUS); batch.setModelTransform(transform); - geometryCache->renderSolidSphereInstance(batch, RED); + geometryCache->renderSolidSphereInstance(args, batch, RED); transform = Transform().setTranslation(glm::vec3(0.0f, MARKER_DISTANCE, 0.0f)).setScale(MARKER_RADIUS); batch.setModelTransform(transform); - geometryCache->renderSolidSphereInstance(batch, GREEN); + geometryCache->renderSolidSphereInstance(args, batch, GREEN); transform = Transform().setTranslation(glm::vec3(0.0f, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS); batch.setModelTransform(transform); - geometryCache->renderSolidSphereInstance(batch, BLUE); + geometryCache->renderSolidSphereInstance(args, batch, BLUE); transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS); batch.setModelTransform(transform); - geometryCache->renderSolidSphereInstance(batch, GREY); + geometryCache->renderSolidSphereInstance(args, batch, GREY); } // Do some basic timing tests and report the results diff --git a/interface/src/Util.h b/interface/src/Util.h index b1b4c78bcb..48acb53936 100644 --- a/interface/src/Util.h +++ b/interface/src/Util.h @@ -16,8 +16,9 @@ #include #include +#include -void renderWorldBox(gpu::Batch& batch); +void renderWorldBox(RenderArgs* args, gpu::Batch& batch); void runTimingTests(); void runUnitTests(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 20b3949bc6..bd545c64e0 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -25,6 +25,7 @@ #endif +#include #include #include #include @@ -62,7 +63,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::ExitingSpaceBubble, this, "processExitingSpaceBubble"); // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer @@ -319,9 +319,6 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); - } - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { - DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); @@ -433,8 +430,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - - _collisionInjectors.remove_if([](QPointer& injector) { + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); @@ -482,7 +478,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& const QScriptValue& avatarIdsToDiscard) { RayToAvatarIntersectionResult result; if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "findRayIntersection", Q_RETURN_ARG(RayToAvatarIntersectionResult, result), Q_ARG(const PickRay&, ray), Q_ARG(const QScriptValue&, avatarIdsToInclude), diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f1e71f7367..30801807d6 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -22,11 +22,11 @@ #include #include #include +#include #include "AvatarMotionState.h" #include "MyAvatar.h" -class AudioInjector; class AvatarManager : public AvatarHashMap { Q_OBJECT @@ -104,7 +104,7 @@ private: std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list> _collisionInjectors; + std::list _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d2d15ea708..83328f1549 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -21,6 +21,7 @@ #include +#include #include #include #include @@ -294,7 +295,7 @@ void MyAvatar::simulateAttachments(float deltaTime) { // don't update attachments here, do it in harvestResultsFromPhysicsSimulation() } -QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { CameraMode mode = qApp->getCamera().getMode(); _globalPosition = getPosition(); // This might not be right! Isn't the capsule local offset in avatar space, and don't we need to add the radius to the y as well? -HRS 5/26/17 @@ -897,7 +898,7 @@ void MyAvatar::restoreAnimation() { QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; - QMetaObject::invokeMethod(this, "getAnimationRoles", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QStringList, result)); + BLOCKING_INVOKE_METHOD(this, "getAnimationRoles", Q_RETURN_ARG(QStringList, result)); return result; } return _skeletonModel->getRig().getAnimationRoles(); @@ -1080,9 +1081,6 @@ void MyAvatar::loadData() { _yawSpeed = loadSetting(settings, "yawSpeed", _yawSpeed); _pitchSpeed = loadSetting(settings, "pitchSpeed", _pitchSpeed); - _targetScale = loadSetting(settings, "scale", 1.0f); - setScale(glm::vec3(_targetScale)); - _prefOverrideAnimGraphUrl.set(QUrl(settings.value("animGraphURL", "").toString())); _fullAvatarURLFromPreferences = settings.value("fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()).toUrl(); _fullAvatarModelName = settings.value("fullAvatarModelName", DEFAULT_FULL_AVATAR_MODEL_NAME).toString(); @@ -1374,6 +1372,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { Avatar::setSkeletonModelURL(skeletonModelURL); _skeletonModel->setVisibleInScene(true, qApp->getMain3DScene()); _headBoneSet.clear(); + emit skeletonChanged(); } @@ -1387,7 +1386,7 @@ void MyAvatar::resetFullAvatarURL() { void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "useFullAvatarURL", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", Q_ARG(const QUrl&, fullAvatarURL), Q_ARG(const QString&, modelName)); return; @@ -1413,7 +1412,7 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN void MyAvatar::setAttachmentData(const QVector& attachmentData) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setAttachmentData", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "setAttachmentData", Q_ARG(const QVector, attachmentData)); return; } @@ -1652,7 +1651,8 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setParentVelocity(parentVelocity); _characterController.setPositionAndOrientation(getPosition(), getOrientation()); - if (qApp->isHMDMode()) { + auto headPose = getHeadControllerPoseInAvatarFrame(); + if (headPose.isValid()) { _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); @@ -2243,6 +2243,14 @@ void MyAvatar::clampScaleChangeToDomainLimits(float desiredScale) { qCDebug(interfaceapp, "Changed scale to %f", (double)_targetScale); } +float MyAvatar::getDomainMinScale() { + return _domainMinimumScale; +} + +float MyAvatar::getDomainMaxScale() { + return _domainMaximumScale; +} + void MyAvatar::increaseSize() { // make sure we're starting from an allowable scale clampTargetScaleToDomainLimits(); @@ -2290,17 +2298,27 @@ void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettings if (_domainMinimumScale > _domainMaximumScale) { std::swap(_domainMinimumScale, _domainMaximumScale); } + // Set avatar current scale + Settings settings; + settings.beginGroup("Avatar"); + _targetScale = loadSetting(settings, "scale", 1.0f); - qCDebug(interfaceapp, "This domain requires a minimum avatar scale of %f and a maximum avatar scale of %f", - (double)_domainMinimumScale, (double)_domainMaximumScale); + qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumScale + << " and a maximum avatar scale of " << _domainMaximumScale + << ". Current avatar scale is " << _targetScale; // debug to log if this avatar's scale in this domain will be clamped - auto clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); + float clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); if (_targetScale != clampedScale) { - qCDebug(interfaceapp, "Avatar scale will be clamped to %f because %f is not allowed by current domain", - (double)clampedScale, (double)_targetScale); + qCDebug(interfaceapp) << "Current avatar scale is clamped to " << clampedScale + << " because " << _targetScale << " is not allowed by current domain"; + // The current scale of avatar should not be more than domain's max_avatar_scale and not less than domain's min_avatar_scale . + _targetScale = clampedScale; } + + setScale(glm::vec3(_targetScale)); + settings.endGroup(); } void MyAvatar::clearScaleRestriction() { @@ -2390,7 +2408,7 @@ bool MyAvatar::safeLanding(const glm::vec3& position) { if (QThread::currentThread() != thread()) { bool result; - QMetaObject::invokeMethod(this, "safeLanding", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const glm::vec3&, position)); + BLOCKING_INVOKE_METHOD(this, "safeLanding", Q_RETURN_ARG(bool, result), Q_ARG(const glm::vec3&, position)); return result; } glm::vec3 better; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d94d4f11b7..fa0b52d260 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -561,6 +561,8 @@ public slots: void increaseSize(); void decreaseSize(); void resetSize(); + float getDomainMinScale(); + float getDomainMaxScale(); void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), @@ -611,12 +613,13 @@ signals: void onLoadComplete(); void wentAway(); void wentActive(); + void skeletonChanged(); private: bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut); - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override; void simulate(float deltaTime); void updateFromTrackers(float deltaTime); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 42ceb756b9..a19055d4da 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -101,7 +101,7 @@ int main(int argc, const char* argv[]) { if (allowMultipleInstances) { instanceMightBeRunning = false; } - // this needs to be done here in main, as the mechanism for setting the + // this needs to be done here in main, as the mechanism for setting the // scripts directory appears not to work. See the bug report // https://highfidelity.fogbugz.com/f/cases/5759/Issues-changing-scripts-directory-in-ScriptsEngine if (parser.isSet(overrideScriptsPathOption)) { @@ -111,20 +111,6 @@ int main(int argc, const char* argv[]) { } } - if (parser.isSet(overrideAppLocalDataPathOption)) { - // get dir to use for cache - QString cacheDir = parser.value(overrideAppLocalDataPathOption); - if (!cacheDir.isEmpty()) { - // tell everyone to use the right cache location - // - // this handles data8 and prepared - DependencyManager::get()->setCacheDir(cacheDir); - - // this does the ktx_cache - PathUtils::getAppLocalDataPath(cacheDir); - } - } - if (instanceMightBeRunning) { // Try to connect and send message to existing interface instance QLocalSocket socket; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 4576190413..9719c23885 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -11,6 +11,8 @@ #include "Audio.h" +#include + #include "Application.h" #include "AudioClient.h" #include "ui/AvatarInputs.h" @@ -49,27 +51,22 @@ float Audio::loudnessToLevel(float loudness) { Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); connect(client, &AudioClient::muteToggled, this, &Audio::onMutedChanged); + connect(client, &AudioClient::noiseReductionChanged, this, &Audio::onNoiseReductionChanged); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); + connect(client, &AudioClient::inputVolumeChanged, this, &Audio::onInputVolumeChanged); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); - connect(&_devices._inputs, &AudioDeviceList::deviceChanged, this, &Audio::onInputChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); } void Audio::setMuted(bool isMuted) { if (_isMuted != isMuted) { auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "toggleMute", Qt::BlockingQueuedConnection); - - _isMuted = isMuted; - emit mutedChanged(_isMuted); + QMetaObject::invokeMethod(client, "toggleMute"); } } void Audio::onMutedChanged() { - auto client = DependencyManager::get().data(); - bool isMuted; - QMetaObject::invokeMethod(client, "isMuted", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isMuted)); - + bool isMuted = DependencyManager::get()->isMuted(); if (_isMuted != isMuted) { _isMuted = isMuted; emit mutedChanged(_isMuted); @@ -79,11 +76,16 @@ void Audio::onMutedChanged() { void Audio::enableNoiseReduction(bool enable) { if (_enableNoiseReduction != enable) { auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setNoiseReduction", Qt::BlockingQueuedConnection, Q_ARG(bool, enable)); - + QMetaObject::invokeMethod(client, "setNoiseReduction", Q_ARG(bool, enable)); enableNoiseReductionSetting.set(enable); - _enableNoiseReduction = enable; - emit noiseReductionChanged(enable); + } +} + +void Audio::onNoiseReductionChanged() { + bool noiseReductionEnabled = DependencyManager::get()->isNoiseReductionEnabled(); + if (_enableNoiseReduction != noiseReductionEnabled) { + _enableNoiseReduction = noiseReductionEnabled; + emit noiseReductionChanged(_enableNoiseReduction); } } @@ -93,19 +95,11 @@ void Audio::setInputVolume(float volume) { if (_inputVolume != volume) { auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setInputVolume", Qt::BlockingQueuedConnection, Q_ARG(float, volume)); - - _inputVolume = volume; - emit inputVolumeChanged(_inputVolume); + QMetaObject::invokeMethod(client, "setInputVolume", Q_ARG(float, volume)); } } -// different audio input devices may have different volumes -void Audio::onInputChanged() { - auto client = DependencyManager::get().data(); - float volume; - QMetaObject::invokeMethod(client, "getInputVolume", Qt::BlockingQueuedConnection, Q_RETURN_ARG(float, volume)); - +void Audio::onInputVolumeChanged(float volume) { if (_inputVolume != volume) { _inputVolume = volume; emit inputVolumeChanged(_inputVolume); @@ -139,4 +133,12 @@ void Audio::setReverb(bool enable) { void Audio::setReverbOptions(const AudioEffectOptions* options) { DependencyManager::get()->setReverbOptions(options); -} \ No newline at end of file +} + +void Audio::setInputDevice(const QAudioDeviceInfo& device) { + _devices.chooseInputDevice(device); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device) { + _devices.chooseOutputDevice(device); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 953727ede8..acf101159b 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -50,6 +50,8 @@ public: void showMicMeter(bool show); void setInputVolume(float volume); + Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device); + Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device); Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); @@ -62,9 +64,12 @@ signals: void contextChanged(const QString& context); public slots: - void onMutedChanged(); void onContextChanged(); - void onInputChanged(); + +private slots: + void onMutedChanged(); + void onNoiseReductionChanged(); + void onInputVolumeChanged(float volume); void onInputLoudnessChanged(float loudness); protected: diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index b0ea8226e8..a284e38dac 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -11,6 +11,8 @@ #include +#include + #include "AudioDevices.h" #include "Application.h" @@ -36,7 +38,8 @@ Setting::Handle& getSetting(bool contextIsHMD, QAudio::Mode mode) { QHash AudioDeviceList::_roles { { Qt::DisplayRole, "display" }, - { Qt::CheckStateRole, "selected" } + { Qt::CheckStateRole, "selected" }, + { Qt::UserRole, "info" } }; Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled }; @@ -49,74 +52,24 @@ QVariant AudioDeviceList::data(const QModelIndex& index, int role) const { return _devices.at(index.row()).display; } else if (role == Qt::CheckStateRole) { return _devices.at(index.row()).selected; + } else if (role == Qt::UserRole) { + return QVariant::fromValue(_devices.at(index.row()).info); } else { return QVariant(); } } -bool AudioDeviceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() >= _devices.size() || role != Qt::CheckStateRole) { - return false; - } - - // only allow switching to a new device, not deactivating an in-use device - auto selected = value.toBool(); - if (!selected) { - return false; - } - - return setDevice(index.row(), true); -} - -bool AudioDeviceList::setDevice(int row, bool fromUser) { - bool success = false; - auto& device = _devices[row]; - - // skip if already selected - if (!device.selected) { - auto client = DependencyManager::get(); - QMetaObject::invokeMethod(client.data(), "switchAudioDevice", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, success), - Q_ARG(QAudio::Mode, _mode), - Q_ARG(const QAudioDeviceInfo&, device.info)); - - if (success) { - device.selected = true; - if (fromUser) { - emit deviceSelected(device.info, _selectedDevice); - } - emit deviceChanged(device.info); - } - } - - emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); - return success; -} void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { - bool success { false }; - - // try to set the last selected device - if (!device.isNull()) { - auto i = 0; - for (; i < rowCount(); ++i) { - if (device == _devices[i].info.deviceName()) { - break; - } - } - if (i < rowCount()) { - success = setDevice(i, false); - } - - // the selection failed - reset it - if (!success) { - emit deviceSelected(); - } - } + auto client = DependencyManager::get().data(); + auto deviceName = getSetting(contextIsHMD, _mode).get(); + bool switchResult = false; + QMetaObject::invokeMethod(client, "switchAudioDevice", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, switchResult), + Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); // try to set to the default device for this mode - if (!success) { - auto client = DependencyManager::get().data(); + if (!switchResult) { if (contextIsHMD) { QString deviceName; if (_mode == QAudio::AudioInput) { @@ -135,17 +88,15 @@ void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { } void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { + auto oldDevice = _selectedDevice; _selectedDevice = device; - QModelIndex index; for (auto i = 0; i < _devices.size(); ++i) { AudioDevice& device = _devices[i]; - if (device.selected && device.info != _selectedDevice) { device.selected = false; } else if (device.info == _selectedDevice) { device.selected = true; - index = createIndex(i, 0); } } @@ -183,13 +134,6 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) { _outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput)); _inputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioInput)); _outputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioOutput)); - - connect(&_inputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioInput, device, previousDevice); - }); - connect(&_outputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioOutput, device, previousDevice); - }); } void AudioDevices::onContextChanged(const QString& context) { @@ -245,22 +189,40 @@ void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& de } void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList& devices) { - static bool initialized { false }; - auto initialize = [&]{ - if (initialized) { - onContextChanged(QString()); - } else { - initialized = true; - } - }; - + static std::once_flag once; if (mode == QAudio::AudioInput) { _inputs.onDevicesChanged(devices); - static std::once_flag inputFlag; - std::call_once(inputFlag, initialize); } else { // if (mode == QAudio::AudioOutput) _outputs.onDevicesChanged(devices); - static std::once_flag outputFlag; - std::call_once(outputFlag, initialize); + } + std::call_once(once, [&] { onContextChanged(QString()); }); +} + + +void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + bool success = false; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QAudio::Mode, QAudio::AudioInput), + Q_ARG(const QAudioDeviceInfo&, device)); + + if (success) { + onDeviceSelected(QAudio::AudioInput, device, _inputs._selectedDevice); + } +} + +void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + bool success = false; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QAudio::Mode, QAudio::AudioOutput), + Q_ARG(const QAudioDeviceInfo&, device)); + + if (success) { + onDeviceSelected(QAudio::AudioOutput, device, _outputs._selectedDevice); } } diff --git a/interface/src/scripting/AudioDevices.h b/interface/src/scripting/AudioDevices.h index cd47ab4191..a17c577535 100644 --- a/interface/src/scripting/AudioDevices.h +++ b/interface/src/scripting/AudioDevices.h @@ -37,14 +37,11 @@ public: // get/set devices through a QML ListView QVariant data(const QModelIndex& index, int role) const override; - bool setData(const QModelIndex& index, const QVariant &value, int role) override; // reset device to the last selected device in this context, or the default void resetDevice(bool contextIsHMD, const QString& device); signals: - void deviceSelected(const QAudioDeviceInfo& device = QAudioDeviceInfo(), - const QAudioDeviceInfo& previousDevice = QAudioDeviceInfo()); void deviceChanged(const QAudioDeviceInfo& device); private slots: @@ -54,12 +51,9 @@ private slots: private: friend class AudioDevices; - bool setDevice(int index, bool fromUser); - static QHash _roles; static Qt::ItemFlags _flags; - - QAudio::Mode _mode; + const QAudio::Mode _mode; QAudioDeviceInfo _selectedDevice; QList _devices; }; @@ -73,6 +67,8 @@ class AudioDevices : public QObject { public: AudioDevices(bool& contextIsHMD); + void chooseInputDevice(const QAudioDeviceInfo& device); + void chooseOutputDevice(const QAudioDeviceInfo& device); signals: void nop(); diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index b803080538..f8db061299 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -8,9 +8,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "Application.h" #include "ClipboardScriptingInterface.h" +#include + +#include "Application.h" + ClipboardScriptingInterface::ClipboardScriptingInterface() { } @@ -24,7 +27,7 @@ float ClipboardScriptingInterface::getClipboardContentsLargestDimension() { bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector& entityIDs) { bool retVal; - QMetaObject::invokeMethod(qApp, "exportEntities", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(qApp, "exportEntities", Q_RETURN_ARG(bool, retVal), Q_ARG(const QString&, filename), Q_ARG(const QVector&, entityIDs)); @@ -33,7 +36,7 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, const bool ClipboardScriptingInterface::exportEntities(const QString& filename, float x, float y, float z, float s) { bool retVal; - QMetaObject::invokeMethod(qApp, "exportEntities", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(qApp, "exportEntities", Q_RETURN_ARG(bool, retVal), Q_ARG(const QString&, filename), Q_ARG(float, x), @@ -45,7 +48,7 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, float bool ClipboardScriptingInterface::importEntities(const QString& filename) { bool retVal; - QMetaObject::invokeMethod(qApp, "importEntities", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(qApp, "importEntities", Q_RETURN_ARG(bool, retVal), Q_ARG(const QString&, filename)); return retVal; @@ -53,7 +56,7 @@ bool ClipboardScriptingInterface::importEntities(const QString& filename) { QVector ClipboardScriptingInterface::pasteEntities(glm::vec3 position) { QVector retVal; - QMetaObject::invokeMethod(qApp, "pasteEntities", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(qApp, "pasteEntities", Q_RETURN_ARG(QVector, retVal), Q_ARG(float, position.x), Q_ARG(float, position.y), diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 3c10475242..826732c777 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -13,6 +13,10 @@ #include +#include + +#include + /**jsdoc * @namespace Clipboard */ diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 883a6e758e..35f2e2aa86 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -152,22 +153,31 @@ QString HMDScriptingInterface::preferredAudioOutput() const { return qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); } -bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { +bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) { + if (QThread::currentThread() != thread()) { + bool result; + BLOCKING_INVOKE_METHOD(this, "setHandLasers", Q_RETURN_ARG(bool, result), + Q_ARG(int, hands), Q_ARG(bool, enabled), Q_ARG(glm::vec4, color), Q_ARG(glm::vec3, direction)); + return result; + } + auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([offscreenUi, enabled] { - offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); - }); + offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); return qApp->getActiveDisplayPlugin()->setHandLaser(hands, enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, 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); - }); +bool HMDScriptingInterface::setExtraLaser(const glm::vec3& worldStart, bool enabled, const glm::vec4& color, const glm::vec3& direction) { + if (QThread::currentThread() != thread()) { + bool result; + BLOCKING_INVOKE_METHOD(this, "setExtraLaser", Q_RETURN_ARG(bool, result), + Q_ARG(glm::vec3, worldStart), Q_ARG(bool, enabled), Q_ARG(glm::vec4, color), Q_ARG(glm::vec3, direction)); + return result; + } + auto offscreenUi = DependencyManager::get(); + offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); auto myAvatar = DependencyManager::get()->getMyAvatar(); auto sensorToWorld = myAvatar->getSensorToWorldMatrix(); @@ -179,11 +189,11 @@ bool HMDScriptingInterface::setExtraLaser(const glm::vec3& worldStart, bool enab color, sensorStart, sensorDirection); } -void HMDScriptingInterface::disableExtraLaser() const { +void HMDScriptingInterface::disableExtraLaser() { setExtraLaser(vec3(0), false, vec4(0), vec3(0)); } -void HMDScriptingInterface::disableHandLasers(int hands) const { +void HMDScriptingInterface::disableHandLasers(int hands) { setHandLasers(hands, false, vec4(0), vec3(0)); } diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 4657e61d05..3ed7db0232 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -51,11 +51,11 @@ public: 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 setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction); + Q_INVOKABLE void disableHandLasers(int hands); - Q_INVOKABLE bool setExtraLaser(const glm::vec3& worldStart, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; - Q_INVOKABLE void disableExtraLaser() const; + Q_INVOKABLE bool setExtraLaser(const glm::vec3& worldStart, bool enabled, const glm::vec4& color, const glm::vec3& direction); + Q_INVOKABLE void disableExtraLaser(); /// Suppress the activation of any on-screen keyboard so that a script operation will diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index 91fba1ce9e..d9372978e8 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include "Menu.h" @@ -43,7 +44,7 @@ bool MenuScriptingInterface::menuExists(const QString& menu) { return Menu::getInstance()->menuExists(menu); } bool result; - QMetaObject::invokeMethod(Menu::getInstance(), "menuExists", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, menu)); return result; @@ -86,7 +87,7 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString& return Menu::getInstance()->menuItemExists(menu, menuitem); } bool result; - QMetaObject::invokeMethod(Menu::getInstance(), "menuItemExists", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, menu), Q_ARG(const QString&, menuitem)); @@ -114,7 +115,7 @@ bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) { return Menu::getInstance()->isOptionChecked(menuOption); } bool result; - QMetaObject::invokeMethod(Menu::getInstance(), "isOptionChecked", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, menuOption)); return result; @@ -131,7 +132,7 @@ bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) { return Menu::getInstance()->isOptionChecked(menuOption); } bool result; - QMetaObject::invokeMethod(Menu::getInstance(), "isMenuEnabled", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, menuOption)); return result; @@ -157,7 +158,7 @@ bool MenuScriptingInterface::isInfoViewVisible(const QString& path) { } bool result; - QMetaObject::invokeMethod(Menu::getInstance(), "isInfoViewVisible", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isInfoViewVisible", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, path)); return result; } diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 84c742d0ab..d4b4ba1480 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -57,20 +58,25 @@ void TestScriptingInterface::waitIdle() { } bool TestScriptingInterface::loadTestScene(QString scene) { + if (QThread::currentThread() != thread()) { + bool result; + BLOCKING_INVOKE_METHOD(this, "loadTestScene", Q_RETURN_ARG(bool, result), Q_ARG(QString, scene)); + return result; + } + static const QString TEST_ROOT = "https://raw.githubusercontent.com/highfidelity/hifi_tests/master/"; static const QString TEST_BINARY_ROOT = "https://hifi-public.s3.amazonaws.com/test_scene_data/"; static const QString TEST_SCRIPTS_ROOT = TEST_ROOT + "scripts/"; static const QString TEST_SCENES_ROOT = TEST_ROOT + "scenes/"; - return DependencyManager::get()->returnFromUiThread([scene]()->QVariant { - DependencyManager::get()->setUrlPrefixOverride("atp:/", TEST_BINARY_ROOT + scene + ".atp/"); - auto tree = qApp->getEntities()->getTree(); - auto treeIsClient = tree->getIsClient(); - // Force the tree to accept the load regardless of permissions - tree->setIsClient(false); - auto result = tree->readFromURL(TEST_SCENES_ROOT + scene + ".json"); - tree->setIsClient(treeIsClient); - return result; - }).toBool(); + + DependencyManager::get()->setUrlPrefixOverride("atp:/", TEST_BINARY_ROOT + scene + ".atp/"); + auto tree = qApp->getEntities()->getTree(); + auto treeIsClient = tree->getIsClient(); + // Force the tree to accept the load regardless of permissions + tree->setIsClient(false); + auto result = tree->readFromURL(TEST_SCENES_ROOT + scene + ".json"); + tree->setIsClient(treeIsClient); + return result; } bool TestScriptingInterface::startTracing(QString logrules) { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 38f467f22b..84f4cbbbd8 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -9,11 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "WindowScriptingInterface.h" + #include #include #include #include +#include #include #include @@ -24,8 +27,6 @@ #include "Menu.h" #include "OffscreenUi.h" -#include "WindowScriptingInterface.h" - static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation"; @@ -316,7 +317,7 @@ bool WindowScriptingInterface::isPhysicsEnabled() { int WindowScriptingInterface::openMessageBox(QString title, QString text, int buttons, int defaultButton) { if (QThread::currentThread() != thread()) { int result; - QMetaObject::invokeMethod(this, "openMessageBox", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "openMessageBox", Q_RETURN_ARG(int, result), Q_ARG(QString, title), Q_ARG(QString, text), diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 4fb4829636..f8ed20f42f 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -18,6 +18,8 @@ #include #include +#include + class CustomPromptResult { public: QVariant value; diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 79314ce49a..443f11fb23 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -115,7 +116,7 @@ void JSConsole::executeCommand(const QString& command) { QScriptValue JSConsole::executeCommandInWatcher(const QString& command) { QScriptValue result; - QMetaObject::invokeMethod(_scriptEngine, "evaluate", Qt::ConnectionType::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(_scriptEngine, "evaluate", Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, command), Q_ARG(const QString&, _consoleFileName)); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 41a4ebdf68..87131e4f5c 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -184,12 +184,15 @@ void setupPreferences() { { auto getter = [=]()->float { return myAvatar->getUniformScale(); }; auto setter = [=](float value) { myAvatar->setTargetScale(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Avatar scale (default is 1.0)", getter, setter); - preference->setMin(0.01f); - preference->setMax(99.9f); + auto preference = new SpinnerSliderPreference(AVATAR_TUNING, "Avatar Scale", getter, setter); + preference->setStep(0.05f); preference->setDecimals(2); - preference->setStep(1); preferences->addPreference(preference); + + // When the Interface is first loaded, this section setupPreferences(); is loaded - + // causing the myAvatar->getDomainMinScale() and myAvatar->getDomainMaxScale() to get set to incorrect values + // which can't be changed across domain switches. Having these values loaded up when you load the Dialog each time + // is a way around this, therefore they're not specified here but in the QML. } { auto getter = []()->float { return DependencyManager::get()->getEyeClosingThreshold(); }; diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp new file mode 100644 index 0000000000..7b9592fa4c --- /dev/null +++ b/interface/src/ui/ResourceImageItem.cpp @@ -0,0 +1,114 @@ +// +// ResourceImageItem.cpp +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// Copyright 2017 High Fidelity, Inc. + +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +//#include "Application.h" +#include "ResourceImageItem.h" + +#include +#include +#include +#include + +ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { + auto textureCache = DependencyManager::get(); + connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); +} + +void ResourceImageItem::setUrl(const QString& url) { + if (url != m_url) { + m_url = url; + update(); + } +} + +void ResourceImageItem::setReady(bool ready) { + if (ready != m_ready) { + m_ready = ready; + update(); + } +} + +void ResourceImageItemRenderer::onUpdateTimer() { + if (_ready) { + if (_networkTexture && _networkTexture->isLoaded()) { + if(_fboMutex.tryLock()) { + invalidateFramebufferObject(); + qApp->getActiveDisplayPlugin()->copyTextureToQuickFramebuffer(_networkTexture, _copyFbo, &_fenceSync); + _fboMutex.unlock(); + } else { + qDebug() << "couldn't get a lock, using last frame"; + } + } else { + _networkTexture = DependencyManager::get()->getTexture(_url); + } + } + update(); +} + +ResourceImageItemRenderer::ResourceImageItemRenderer() : QQuickFramebufferObject::Renderer() { + connect(&_updateTimer, SIGNAL(timeout()), this, SLOT(onUpdateTimer())); + auto textureCache = DependencyManager::get(); +} + +void ResourceImageItemRenderer::synchronize(QQuickFramebufferObject* item) { + ResourceImageItem* resourceImageItem = static_cast(item); + + resourceImageItem->setFlag(QQuickItem::ItemHasContents); + + _url = resourceImageItem->getUrl(); + _ready = resourceImageItem->getReady(); + _visible = resourceImageItem->isVisible(); + _window = resourceImageItem->window(); + + _networkTexture = DependencyManager::get()->getTexture(_url); + static const int UPDATE_TIMER_DELAY_IN_MS = 100; // 100 ms = 10 hz for now + if (_ready && _visible && !_updateTimer.isActive()) { + _updateTimer.start(UPDATE_TIMER_DELAY_IN_MS); + } else if (!(_ready && _visible) && _updateTimer.isActive()) { + _updateTimer.stop(); + } +} + +QOpenGLFramebufferObject* ResourceImageItemRenderer::createFramebufferObject(const QSize& size) { + if (_copyFbo) { + delete _copyFbo; + } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + _copyFbo = new QOpenGLFramebufferObject(size, format); + _copyFbo->bind(); + return new QOpenGLFramebufferObject(size, format); +} + +void ResourceImageItemRenderer::render() { + auto f = QOpenGLContext::currentContext()->extraFunctions(); + + if (_fenceSync) { + f->glWaitSync(_fenceSync, 0, GL_TIMEOUT_IGNORED); + f->glDeleteSync(_fenceSync); + _fenceSync = 0; + } + if (_ready) { + _fboMutex.lock(); + _copyFbo->bind(); + QOpenGLFramebufferObject::blitFramebuffer(framebufferObject(), _copyFbo, GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + // this clears the copyFbo texture + // so next frame starts fresh - helps + // when aspect ratio changes + _copyFbo->takeTexture(); + _copyFbo->bind(); + _copyFbo->release(); + + _fboMutex.unlock(); + } + glFlush(); + _window->resetOpenGLState(); +} diff --git a/interface/src/ui/ResourceImageItem.h b/interface/src/ui/ResourceImageItem.h new file mode 100644 index 0000000000..985ab5a66e --- /dev/null +++ b/interface/src/ui/ResourceImageItem.h @@ -0,0 +1,63 @@ +// +// ResourceImageItem.h +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// Copyright 2017 High Fidelity, Inc. + +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_ResourceImageItem_h +#define hifi_ResourceImageItem_h + +#include "Application.h" + +#include +#include +#include + +#include + +class ResourceImageItemRenderer : public QObject, public QQuickFramebufferObject::Renderer { + Q_OBJECT +public: + ResourceImageItemRenderer(); + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override; + void synchronize(QQuickFramebufferObject* item) override; + void render() override; +private: + bool _ready; + QString _url; + bool _visible; + + NetworkTexturePointer _networkTexture; + QQuickWindow* _window; + QMutex _fboMutex; + QOpenGLFramebufferObject* _copyFbo { nullptr }; + GLsync _fenceSync { 0 }; + QTimer _updateTimer; +public slots: + void onUpdateTimer(); +}; + +class ResourceImageItem : public QQuickFramebufferObject { + Q_OBJECT + Q_PROPERTY(QString url READ getUrl WRITE setUrl) + Q_PROPERTY(bool ready READ getReady WRITE setReady) +public: + ResourceImageItem(); + QString getUrl() const { return m_url; } + void setUrl(const QString& url); + bool getReady() const { return m_ready; } + void setReady(bool ready); + QQuickFramebufferObject::Renderer* createRenderer() const override { return new ResourceImageItemRenderer; } + +private: + QString m_url; + bool m_ready { false }; + +}; + +#endif // hifi_ResourceImageItem_h diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index 182e7da978..f5668caa71 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -37,9 +37,11 @@ QVariant Billboard3DOverlay::getProperty(const QString &property) { return Planar3DOverlay::getProperty(property); } -void Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { +bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { + bool transformChanged = false; if (force || usecTimestampNow() > _transformExpiry) { - PanelAttachable::applyTransformTo(transform, true); - pointTransformAtCamera(transform, getOffsetRotation()); + transformChanged = PanelAttachable::applyTransformTo(transform, true); + transformChanged |= pointTransformAtCamera(transform, getOffsetRotation()); } + return transformChanged; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index d256a92afe..d429537b5b 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -27,7 +27,7 @@ public: QVariant getProperty(const QString& property) override; protected: - virtual void applyTransformTo(Transform& transform, bool force = false) override; + virtual bool applyTransformTo(Transform& transform, bool force = false) override; }; #endif // hifi_Billboard3DOverlay_h diff --git a/interface/src/ui/overlays/Billboardable.cpp b/interface/src/ui/overlays/Billboardable.cpp index a01d62bfd1..34a4ef6df5 100644 --- a/interface/src/ui/overlays/Billboardable.cpp +++ b/interface/src/ui/overlays/Billboardable.cpp @@ -28,7 +28,7 @@ QVariant Billboardable::getProperty(const QString &property) { return QVariant(); } -void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { +bool Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { if (isFacingAvatar()) { glm::vec3 billboardPos = transform.getTranslation(); glm::vec3 cameraPos = qApp->getCamera().getPosition(); @@ -38,5 +38,7 @@ void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offse glm::quat rotation(glm::vec3(elevation, azimuth, 0)); transform.setRotation(rotation); transform.postRotate(offsetRotation); + return true; } + return false; } diff --git a/interface/src/ui/overlays/Billboardable.h b/interface/src/ui/overlays/Billboardable.h index e2d29a2769..46d9ac6479 100644 --- a/interface/src/ui/overlays/Billboardable.h +++ b/interface/src/ui/overlays/Billboardable.h @@ -27,7 +27,7 @@ protected: void setProperties(const QVariantMap& properties); QVariant getProperty(const QString& property); - void pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); + bool pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); private: bool _isFacingAvatar = false; diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index ae0173f054..827417a912 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -80,8 +80,8 @@ void Circle3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); auto& batch = *args->_batch; - if (args->_pipeline) { - batch.setPipeline(args->_pipeline->pipeline); + if (args->_shapePipeline) { + batch.setPipeline(args->_shapePipeline->pipeline); } // FIXME: THe line width of _lineWidth is not supported anymore, we ll need a workaround diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 8af4c1d908..a353545245 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -65,15 +65,15 @@ void Cube3DOverlay::render(RenderArgs* args) { transform.setTranslation(position); transform.setRotation(rotation); auto geometryCache = DependencyManager::get(); - auto pipeline = args->_pipeline; - if (!pipeline) { - pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); + auto shapePipeline = args->_shapePipeline; + if (!shapePipeline) { + shapePipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { transform.setScale(dimensions); batch->setModelTransform(transform); - geometryCache->renderSolidCubeInstance(*batch, cubeColor, pipeline); + geometryCache->renderSolidCubeInstance(args, *batch, cubeColor, shapePipeline); } else { geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); if (getIsDashedLine()) { @@ -109,7 +109,7 @@ void Cube3DOverlay::render(RenderArgs* args) { } else { transform.setScale(dimensions); batch->setModelTransform(transform); - geometryCache->renderWireCubeInstance(*batch, cubeColor, pipeline); + geometryCache->renderWireCubeInstance(args, *batch, cubeColor, shapePipeline); } } } diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index 3e0bb74bc4..7dfee2c491 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -45,11 +45,13 @@ Image3DOverlay::~Image3DOverlay() { } void Image3DOverlay::update(float deltatime) { +#if OVERLAY_PANELS if (usecTimestampNow() > _transformExpiry) { Transform transform = getTransform(); applyTransformTo(transform); setTransform(transform); } +#endif } void Image3DOverlay::render(RenderArgs* args) { @@ -97,10 +99,14 @@ void Image3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; xColor color = getColor(); float alpha = getAlpha(); - + Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + bool transformChanged = applyTransformTo(transform, true); + // If the transform is not modified, setting the transform to + // itself will cause drift over time due to floating point errors. + if (transformChanged) { + setTransform(transform); + } transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 4ad1b070b1..494c287676 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -84,9 +84,9 @@ public: void setColorPulse(float value) { _colorPulse = value; } void setAlphaPulse(float value) { _alphaPulse = value; } - virtual void setProperties(const QVariantMap& properties); - virtual Overlay* createClone() const = 0; - virtual QVariant getProperty(const QString& property); + Q_INVOKABLE virtual void setProperties(const QVariantMap& properties); + Q_INVOKABLE virtual Overlay* createClone() const = 0; + Q_INVOKABLE virtual QVariant getProperty(const QString& property); render::ItemID getRenderItemID() const { return _renderItemID; } void setRenderItemID(render::ItemID renderItemID) { _renderItemID = renderItemID; } diff --git a/interface/src/ui/overlays/OverlayPanel.cpp b/interface/src/ui/overlays/OverlayPanel.cpp index df2b91c4ef..06480109ce 100644 --- a/interface/src/ui/overlays/OverlayPanel.cpp +++ b/interface/src/ui/overlays/OverlayPanel.cpp @@ -11,6 +11,8 @@ #include "OverlayPanel.h" +#if OVERLAY_PANELS + #include #include #include @@ -185,3 +187,4 @@ void OverlayPanel::applyTransformTo(Transform& transform, bool force) { pointTransformAtCamera(transform, getOffsetRotation()); } } +#endif \ No newline at end of file diff --git a/interface/src/ui/overlays/OverlayPanel.h b/interface/src/ui/overlays/OverlayPanel.h index 5bffe3851e..cff2bc224d 100644 --- a/interface/src/ui/overlays/OverlayPanel.h +++ b/interface/src/ui/overlays/OverlayPanel.h @@ -22,6 +22,7 @@ #include "Billboardable.h" #include "Overlay.h" +#if OVERLAY_PANELS class PropertyBinding { public: PropertyBinding() {} @@ -80,4 +81,6 @@ private: QScriptEngine* _scriptEngine; }; +#endif + #endif // hifi_OverlayPanel_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index a9efd51a3e..72682fcb8c 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -14,6 +14,7 @@ #include +#include #include #include #include @@ -39,34 +40,39 @@ Q_LOGGING_CATEGORY(trace_render_overlays, "trace.render.overlays") void Overlays::cleanupAllOverlays() { + QMap overlaysHUD; + QMap overlaysWorld; { - QWriteLocker lock(&_lock); - QWriteLocker deleteLock(&_deleteLock); - foreach(Overlay::Pointer overlay, _overlaysHUD) { - _overlaysToDelete.push_back(overlay); - } - foreach(Overlay::Pointer overlay, _overlaysWorld) { - _overlaysToDelete.push_back(overlay); - } - _overlaysHUD.clear(); - _overlaysWorld.clear(); - _panels.clear(); + QMutexLocker locker(&_mutex); + overlaysHUD.swap(_overlaysHUD); + overlaysWorld.swap(_overlaysWorld); } + + foreach(Overlay::Pointer overlay, overlaysHUD) { + _overlaysToDelete.push_back(overlay); + } + foreach(Overlay::Pointer overlay, overlaysWorld) { + _overlaysToDelete.push_back(overlay); + } +#if OVERLAY_PANELS + _panels.clear(); +#endif cleanupOverlaysToDelete(); } void Overlays::init() { +#if OVERLAY_PANELS _scriptEngine = new QScriptEngine(); +#endif } void Overlays::update(float deltatime) { - { - QWriteLocker lock(&_lock); - foreach(Overlay::Pointer thisOverlay, _overlaysHUD) { + QMutexLocker locker(&_mutex); + foreach(const auto& thisOverlay, _overlaysHUD) { thisOverlay->update(deltatime); } - foreach(Overlay::Pointer thisOverlay, _overlaysWorld) { + foreach(const auto& thisOverlay, _overlaysWorld) { thisOverlay->update(deltatime); } } @@ -80,8 +86,6 @@ void Overlays::cleanupOverlaysToDelete() { render::Transaction transaction; { - QWriteLocker lock(&_deleteLock); - do { Overlay::Pointer overlay = _overlaysToDelete.takeLast(); @@ -100,7 +104,6 @@ void Overlays::cleanupOverlaysToDelete() { void Overlays::renderHUD(RenderArgs* renderArgs) { PROFILE_RANGE(render_overlays, __FUNCTION__); - QReadLocker lock(&_lock); gpu::Batch& batch = *renderArgs->_batch; auto geometryCache = DependencyManager::get(); @@ -111,7 +114,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { int height = size.y; mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); - + QMutexLocker locker(&_mutex); foreach(Overlay::Pointer thisOverlay, _overlaysHUD) { // Reset all batch pipeline settings between overlay @@ -126,16 +129,17 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { } void Overlays::disable() { - QWriteLocker lock(&_lock); _enabled = false; } void Overlays::enable() { - QWriteLocker lock(&_lock); _enabled = true; } +// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder +// class on packet processing threads Overlay::Pointer Overlays::getOverlay(OverlayID id) const { + QMutexLocker locker(&_mutex); if (_overlaysHUD.contains(id)) { return _overlaysHUD[id]; } @@ -146,6 +150,13 @@ Overlay::Pointer Overlays::getOverlay(OverlayID id) const { } OverlayID Overlays::addOverlay(const QString& type, const QVariant& properties) { + if (QThread::currentThread() != thread()) { + OverlayID result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "addOverlay", Q_RETURN_ARG(OverlayID, result), Q_ARG(QString, type), Q_ARG(QVariant, properties)); + return result; + } + Overlay::Pointer thisOverlay = nullptr; if (type == ImageOverlay::TYPE) { @@ -185,19 +196,22 @@ OverlayID Overlays::addOverlay(const QString& type, const QVariant& properties) return UNKNOWN_OVERLAY_ID; } -OverlayID Overlays::addOverlay(Overlay::Pointer overlay) { - QWriteLocker lock(&_lock); +OverlayID Overlays::addOverlay(const Overlay::Pointer& overlay) { OverlayID thisID = OverlayID(QUuid::createUuid()); overlay->setOverlayID(thisID); overlay->setStackOrder(_stackOrder++); if (overlay->is3D()) { - _overlaysWorld[thisID] = overlay; + { + QMutexLocker locker(&_mutex); + _overlaysWorld[thisID] = overlay; + } render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; overlay->addToScene(overlay, scene, transaction); scene->enqueueTransaction(transaction); } else { + QMutexLocker locker(&_mutex); _overlaysHUD[thisID] = overlay; } @@ -205,14 +219,23 @@ OverlayID Overlays::addOverlay(Overlay::Pointer overlay) { } OverlayID Overlays::cloneOverlay(OverlayID id) { + if (QThread::currentThread() != thread()) { + OverlayID result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "cloneOverlay", Q_RETURN_ARG(OverlayID, result), Q_ARG(OverlayID, id)); + return result; + } + Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { OverlayID cloneId = addOverlay(Overlay::Pointer(thisOverlay->createClone())); +#if OVERLAY_PANELS auto attachable = std::dynamic_pointer_cast(thisOverlay); if (attachable && attachable->getParentPanel()) { attachable->getParentPanel()->addChild(cloneId); } +#endif return cloneId; } @@ -220,21 +243,32 @@ OverlayID Overlays::cloneOverlay(OverlayID id) { } bool Overlays::editOverlay(OverlayID id, const QVariant& properties) { - QWriteLocker lock(&_lock); + if (QThread::currentThread() != thread()) { + // NOTE editOverlay can be called very frequently in scripts and can't afford to + // block waiting on the main thread. Additionally, no script actually + // examines the return value and does something useful with it, so use a non-blocking + // invoke and just always return true + QMetaObject::invokeMethod(this, "editOverlay", Q_ARG(OverlayID, id), Q_ARG(QVariant, properties)); + return true; + } Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { thisOverlay->setProperties(properties.toMap()); - return true; } return false; } bool Overlays::editOverlays(const QVariant& propertiesById) { + if (QThread::currentThread() != thread()) { + // NOTE see comment on editOverlay for why this is not a blocking call + QMetaObject::invokeMethod(this, "editOverlays", Q_ARG(QVariant, propertiesById)); + return true; + } + QVariantMap map = propertiesById.toMap(); bool success = true; - QWriteLocker lock(&_lock); for (const auto& key : map.keys()) { OverlayID id = OverlayID(key); Overlay::Pointer thisOverlay = getOverlay(id); @@ -249,10 +283,15 @@ bool Overlays::editOverlays(const QVariant& propertiesById) { } void Overlays::deleteOverlay(OverlayID id) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "deleteOverlay", Q_ARG(OverlayID, id)); + return; + } + Overlay::Pointer overlayToDelete; { - QWriteLocker lock(&_lock); + QMutexLocker locker(&_mutex); if (_overlaysHUD.contains(id)) { overlayToDelete = _overlaysHUD.take(id); } else if (_overlaysWorld.contains(id)) { @@ -262,19 +301,27 @@ void Overlays::deleteOverlay(OverlayID id) { } } +#if OVERLAY_PANELS auto attachable = std::dynamic_pointer_cast(overlayToDelete); if (attachable && attachable->getParentPanel()) { attachable->getParentPanel()->removeChild(id); attachable->setParentPanel(nullptr); } +#endif + - QWriteLocker lock(&_deleteLock); _overlaysToDelete.push_back(overlayToDelete); - emit overlayDeleted(id); } -QString Overlays::getOverlayType(OverlayID overlayId) const { +QString Overlays::getOverlayType(OverlayID overlayId) { + if (QThread::currentThread() != thread()) { + QString result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "getOverlayType", Q_RETURN_ARG(QString, result), Q_ARG(OverlayID, overlayId)); + return result; + } + Overlay::Pointer overlay = getOverlay(overlayId); if (overlay) { return overlay->getType(); @@ -283,6 +330,13 @@ QString Overlays::getOverlayType(OverlayID overlayId) const { } QObject* Overlays::getOverlayObject(OverlayID id) { + if (QThread::currentThread() != thread()) { + QObject* result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(OverlayID, id)); + return result; + } + Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { return qobject_cast(&(*thisOverlay)); @@ -290,6 +344,7 @@ QObject* Overlays::getOverlayObject(OverlayID id) { return nullptr; } +#if OVERLAY_PANELS OverlayID Overlays::getParentPanel(OverlayID childId) const { Overlay::Pointer overlay = getOverlay(childId); auto attachable = std::dynamic_pointer_cast(overlay); @@ -330,33 +385,25 @@ void Overlays::setParentPanel(OverlayID childId, OverlayID panelId) { } } } +#endif OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) { - glm::vec2 pointCopy = point; - QReadLocker lock(&_lock); if (!_enabled) { return UNKNOWN_OVERLAY_ID; } - QMapIterator i(_overlaysHUD); - const float LARGE_NEGATIVE_FLOAT = -9999999; - glm::vec3 origin(pointCopy.x, pointCopy.y, LARGE_NEGATIVE_FLOAT); - glm::vec3 direction(0, 0, 1); - glm::vec3 thisSurfaceNormal; + QMutexLocker locker(&_mutex); + QMapIterator i(_overlaysHUD); unsigned int bestStackOrder = 0; OverlayID bestOverlayID = UNKNOWN_OVERLAY_ID; - while (i.hasNext()) { i.next(); - OverlayID thisID = i.key(); - if (!i.value()->is3D()) { - auto thisOverlay = std::dynamic_pointer_cast(i.value()); - if (thisOverlay && thisOverlay->getVisible() && thisOverlay->isLoaded() && - thisOverlay->getBoundingRect().contains(pointCopy.x, pointCopy.y, false)) { - if (thisOverlay->getStackOrder() > bestStackOrder) { - bestOverlayID = thisID; - bestStackOrder = thisOverlay->getStackOrder(); - } + auto thisOverlay = std::dynamic_pointer_cast(i.value()); + if (thisOverlay && thisOverlay->getVisible() && thisOverlay->isLoaded() && + thisOverlay->getBoundingRect().contains(point.x, point.y, false)) { + if (thisOverlay->getStackOrder() > bestStackOrder) { + bestOverlayID = i.key(); + bestStackOrder = thisOverlay->getStackOrder(); } } } @@ -365,15 +412,47 @@ OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) { } OverlayPropertyResult Overlays::getProperty(OverlayID id, const QString& property) { - OverlayPropertyResult result; Overlay::Pointer thisOverlay = getOverlay(id); - QReadLocker lock(&_lock); + OverlayPropertyResult result; if (thisOverlay && thisOverlay->supportsGetProperty()) { result.value = thisOverlay->getProperty(property); } return result; } +OverlayPropertyResult Overlays::getProperties(const OverlayID& id, const QStringList& properties) { + Overlay::Pointer thisOverlay = getOverlay(id); + OverlayPropertyResult result; + if (thisOverlay && thisOverlay->supportsGetProperty()) { + QVariantMap mapResult; + for (const auto& property : properties) { + mapResult.insert(property, thisOverlay->getProperty(property)); + } + result.value = mapResult; + } + return result; +} + +OverlayPropertyResult Overlays::getOverlaysProperties(const QVariant& propertiesById) { + QVariantMap map = propertiesById.toMap(); + OverlayPropertyResult result; + QVariantMap resultMap; + for (const auto& key : map.keys()) { + OverlayID id = OverlayID(key); + QVariantMap overlayResult; + Overlay::Pointer thisOverlay = getOverlay(id); + if (thisOverlay && thisOverlay->supportsGetProperty()) { + QStringList propertiesToFetch = map[key].toStringList(); + for (const auto& property : propertiesToFetch) { + overlayResult[property] = thisOverlay->getProperty(property); + } + } + resultMap[key] = overlayResult; + } + result.value = resultMap; + return result; +} + OverlayPropertyResult::OverlayPropertyResult() { } @@ -405,10 +484,10 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR const QVector& overlaysToInclude, const QVector& overlaysToDiscard, bool visibleOnly, bool collidableOnly) { - QReadLocker lock(&_lock); float bestDistance = std::numeric_limits::max(); bool bestIsFront = false; + QMutexLocker locker(&_mutex); RayToOverlayIntersectionResult result; QMapIterator i(_overlaysWorld); while (i.hasNext()) { @@ -448,16 +527,6 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR return result; } -RayToOverlayIntersectionResult::RayToOverlayIntersectionResult() : - intersects(false), - overlayID(UNKNOWN_OVERLAY_ID), - distance(0), - face(), - intersection(), - extraInfo() -{ -} - QScriptValue RayToOverlayIntersectionResultToScriptValue(QScriptEngine* engine, const RayToOverlayIntersectionResult& value) { auto obj = engine->newObject(); obj.setProperty("intersects", value.intersects); @@ -531,7 +600,13 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& objectVar } bool Overlays::isLoaded(OverlayID id) { - QReadLocker lock(&_lock); + if (QThread::currentThread() != thread()) { + bool result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "isLoaded", Q_RETURN_ARG(bool, result), Q_ARG(OverlayID, id)); + return result; + } + Overlay::Pointer thisOverlay = getOverlay(id); if (!thisOverlay) { return false; // not found @@ -539,21 +614,30 @@ bool Overlays::isLoaded(OverlayID id) { return thisOverlay->isLoaded(); } -QSizeF Overlays::textSize(OverlayID id, const QString& text) const { - Overlay::Pointer thisOverlay = _overlaysHUD[id]; +QSizeF Overlays::textSize(OverlayID id, const QString& text) { + if (QThread::currentThread() != thread()) { + QSizeF result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "textSize", Q_RETURN_ARG(QSizeF, result), Q_ARG(OverlayID, id), Q_ARG(QString, text)); + return result; + } + + Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { - if (auto textOverlay = std::dynamic_pointer_cast(thisOverlay)) { - return textOverlay->textSize(text); - } - } else { - thisOverlay = _overlaysWorld[id]; - if (auto text3dOverlay = std::dynamic_pointer_cast(thisOverlay)) { - return text3dOverlay->textSize(text); + if (thisOverlay->is3D()) { + if (auto text3dOverlay = std::dynamic_pointer_cast(thisOverlay)) { + return text3dOverlay->textSize(text); + } + } else { + if (auto textOverlay = std::dynamic_pointer_cast(thisOverlay)) { + return textOverlay->textSize(text); + } } } return QSizeF(0.0f, 0.0f); } +#if OVERLAY_PANELS OverlayID Overlays::addPanel(OverlayPanel::Pointer panel) { QWriteLocker lock(&_lock); @@ -607,8 +691,17 @@ void Overlays::deletePanel(OverlayID panelId) { emit panelDeleted(panelId); } +#endif bool Overlays::isAddedOverlay(OverlayID id) { + if (QThread::currentThread() != thread()) { + bool result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "isAddedOverlay", Q_RETURN_ARG(bool, result), Q_ARG(OverlayID, id)); + return result; + } + + QMutexLocker locker(&_mutex); return _overlaysHUD.contains(id) || _overlaysWorld.contains(id); } @@ -636,20 +729,46 @@ void Overlays::sendHoverLeaveOverlay(OverlayID id, PointerEvent event) { emit hoverLeaveOverlay(id, event); } -OverlayID Overlays::getKeyboardFocusOverlay() const { +OverlayID Overlays::getKeyboardFocusOverlay() { + if (QThread::currentThread() != thread()) { + OverlayID result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "getKeyboardFocusOverlay", Q_RETURN_ARG(OverlayID, result)); + return result; + } + return qApp->getKeyboardFocusOverlay(); } void Overlays::setKeyboardFocusOverlay(OverlayID id) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setKeyboardFocusOverlay", Q_ARG(OverlayID, id)); + return; + } + qApp->setKeyboardFocusOverlay(id); } -float Overlays::width() const { +float Overlays::width() { + if (QThread::currentThread() != thread()) { + float result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "width", Q_RETURN_ARG(float, result)); + return result; + } + auto offscreenUi = DependencyManager::get(); return offscreenUi->getWindow()->size().width(); } -float Overlays::height() const { +float Overlays::height() { + if (QThread::currentThread() != thread()) { + float result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "height", Q_RETURN_ARG(float, result)); + return result; + } + auto offscreenUi = DependencyManager::get(); return offscreenUi->getWindow()->size().height(); } @@ -705,7 +824,6 @@ PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay r auto thisOverlay = std::dynamic_pointer_cast(overlay); - QReadLocker lock(&_lock); auto position = thisOverlay->getPosition(); auto rotation = thisOverlay->getRotation(); auto dimensions = thisOverlay->getSize(); @@ -854,9 +972,15 @@ bool Overlays::mouseMoveEvent(QMouseEvent* event) { return false; } -QVector Overlays::findOverlays(const glm::vec3& center, float radius) const { +QVector Overlays::findOverlays(const glm::vec3& center, float radius) { QVector result; + //if (QThread::currentThread() != thread()) { + // PROFILE_RANGE(script, __FUNCTION__); + // BLOCKING_INVOKE_METHOD(this, "findOverlays", Q_RETURN_ARG(QVector, result), Q_ARG(glm::vec3, center), Q_ARG(float, radius)); + // return result; + //} + QMutexLocker locker(&_mutex); QMapIterator i(_overlaysWorld); int checked = 0; while (i.hasNext()) { diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index a1d4be8376..100f853a96 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -25,8 +25,9 @@ #include #include "Overlay.h" -#include "OverlayPanel.h" + #include "PanelAttachable.h" +#include "OverlayPanel.h" class PickRay; @@ -41,6 +42,8 @@ Q_DECLARE_METATYPE(OverlayPropertyResult); QScriptValue OverlayPropertyResultToScriptValue(QScriptEngine* engine, const OverlayPropertyResult& value); void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPropertyResult& value); +const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); + /**jsdoc * @typedef Overlays.RayToOverlayIntersectionResult * @property {bool} intersects True if the PickRay intersected with a 3D overlay. @@ -51,10 +54,9 @@ void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPro */ class RayToOverlayIntersectionResult { public: - RayToOverlayIntersectionResult(); - bool intersects; - OverlayID overlayID; - float distance; + bool intersects { false }; + OverlayID overlayID { UNKNOWN_OVERLAY_ID }; + float distance { 0 }; BoxFace face; glm::vec3 surfaceNormal; glm::vec3 intersection; @@ -77,8 +79,6 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, R * @namespace Overlays */ -const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); - class Overlays : public QObject { Q_OBJECT @@ -94,11 +94,13 @@ public: void enable(); Overlay::Pointer getOverlay(OverlayID id) const; +#if OVERLAY_PANELS OverlayPanel::Pointer getPanel(OverlayID id) const { return _panels[id]; } +#endif /// adds an overlay that's already been created OverlayID addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } - OverlayID addOverlay(Overlay::Pointer overlay); + OverlayID addOverlay(const Overlay::Pointer& overlay); bool mousePressEvent(QMouseEvent* event); bool mouseDoublePressEvent(QMouseEvent* event); @@ -156,7 +158,7 @@ public slots: * @param {Overlays.OverlayID} overlayID The ID of the overlay to get the type of. * @return {string} The type of the overlay if found, otherwise the empty string. */ - QString getOverlayType(OverlayID overlayId) const; + QString getOverlayType(OverlayID overlayId); /**jsdoc * Get the overlay Script object. @@ -188,6 +190,10 @@ public slots: */ OverlayPropertyResult getProperty(OverlayID id, const QString& property); + OverlayPropertyResult getProperties(const OverlayID& id, const QStringList& properties); + + OverlayPropertyResult getOverlaysProperties(const QVariant& overlaysProperties); + /*jsdoc * Find the closest 3D overlay hit by a pick ray. * @@ -215,7 +221,7 @@ public slots: * @param {float} radius search radius * @return {Overlays.OverlayID[]} list of overlays withing the radius */ - QVector findOverlays(const glm::vec3& center, float radius) const; + QVector findOverlays(const glm::vec3& center, float radius); /**jsdoc * Check whether an overlay's assets have been loaded. For example, if the @@ -237,7 +243,7 @@ public slots: * @param {string} The string to measure. * @return {Vec2} The size of the text. */ - QSizeF textSize(OverlayID id, const QString& text) const; + QSizeF textSize(OverlayID id, const QString& text); /**jsdoc * Get the width of the virtual 2D HUD. @@ -245,7 +251,7 @@ public slots: * @function Overlays.width * @return {float} The width of the 2D HUD. */ - float width() const; + float width(); /**jsdoc * Get the height of the virtual 2D HUD. @@ -253,11 +259,12 @@ public slots: * @function Overlays.height * @return {float} The height of the 2D HUD. */ - float height() const; + float height(); /// return true if there is an overlay with that id else false bool isAddedOverlay(OverlayID id); +#if OVERLAY_PANELS OverlayID getParentPanel(OverlayID childId) const; void setParentPanel(OverlayID childId, OverlayID panelId); @@ -279,6 +286,8 @@ public slots: /// return true if there is a panel with that id else false bool isAddedPanel(OverlayID id) { return _panels.contains(id); } +#endif + void sendMousePressOnOverlay(OverlayID overlayID, const PointerEvent& event); void sendMouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event); void sendMouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event); @@ -287,7 +296,7 @@ public slots: void sendHoverOverOverlay(OverlayID id, PointerEvent event); void sendHoverLeaveOverlay(OverlayID id, PointerEvent event); - OverlayID getKeyboardFocusOverlay() const; + OverlayID getKeyboardFocusOverlay(); void setKeyboardFocusOverlay(OverlayID id); signals: @@ -314,15 +323,18 @@ signals: private: void cleanupOverlaysToDelete(); + mutable QMutex _mutex; QMap _overlaysHUD; QMap _overlaysWorld; +#if OVERLAY_PANELS QMap _panels; +#endif QList _overlaysToDelete; unsigned int _stackOrder { 1 }; - QReadWriteLock _lock; - QReadWriteLock _deleteLock; +#if OVERLAY_PANELS QScriptEngine* _scriptEngine; +#endif bool _enabled = true; PointerEvent calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, RayToOverlayIntersectionResult rayPickResult, @@ -331,7 +343,7 @@ private: OverlayID _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID }; OverlayID _currentHoverOverOverlayID { UNKNOWN_OVERLAY_ID }; - RayToOverlayIntersectionResult findRayIntersectionInternal(const PickRay& ray, bool precisionPicking, + Q_INVOKABLE RayToOverlayIntersectionResult findRayIntersectionInternal(const PickRay& ray, bool precisionPicking, const QVector& overlaysToInclude, const QVector& overlaysToDiscard, bool visibleOnly = false, bool collidableOnly = false); diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 7f1c4e2e50..aae8893667 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -16,11 +16,15 @@ #include "OverlayPanel.h" bool PanelAttachable::getParentVisible() const { +#if OVERLAY_PANELS if (getParentPanel()) { return getParentPanel()->getVisible() && getParentPanel()->getParentVisible(); } else { return true; } +#else + return true; +#endif } QVariant PanelAttachable::getProperty(const QString& property) { @@ -57,15 +61,19 @@ void PanelAttachable::setProperties(const QVariantMap& properties) { } } -void PanelAttachable::applyTransformTo(Transform& transform, bool force) { +bool PanelAttachable::applyTransformTo(Transform& transform, bool force) { if (force || usecTimestampNow() > _transformExpiry) { const quint64 TRANSFORM_UPDATE_PERIOD = 100000; // frequency is 10 Hz _transformExpiry = usecTimestampNow() + TRANSFORM_UPDATE_PERIOD; +#if OVERLAY_PANELS if (getParentPanel()) { getParentPanel()->applyTransformTo(transform, true); transform.postTranslate(getOffsetPosition()); transform.postRotate(getOffsetRotation()); transform.postScale(getOffsetScale()); + return true; } +#endif } + return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 270addbfcf..1598aa4700 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -30,6 +30,8 @@ #ifndef hifi_PanelAttachable_h #define hifi_PanelAttachable_h +#define OVERLAY_PANELS 0 + #include #include @@ -39,18 +41,21 @@ #include class OverlayPanel; - class PanelAttachable { public: // getters +#if OVERLAY_PANELS std::shared_ptr getParentPanel() const { return _parentPanel; } +#endif glm::vec3 getOffsetPosition() const { return _offset.getTranslation(); } glm::quat getOffsetRotation() const { return _offset.getRotation(); } glm::vec3 getOffsetScale() const { return _offset.getScale(); } bool getParentVisible() const; // setters +#if OVERLAY_PANELS void setParentPanel(std::shared_ptr panel) { _parentPanel = panel; } +#endif void setOffsetPosition(const glm::vec3& position) { _offset.setTranslation(position); } void setOffsetRotation(const glm::quat& rotation) { _offset.setRotation(rotation); } void setOffsetScale(float scale) { _offset.setScale(scale); } @@ -62,11 +67,13 @@ protected: /// set position, rotation and scale on transform based on offsets, and parent panel offsets /// if force is false, only apply transform if it hasn't been applied in the last .1 seconds - virtual void applyTransformTo(Transform& transform, bool force = false); + virtual bool applyTransformTo(Transform& transform, bool force = false); quint64 _transformExpiry = 0; private: +#if OVERLAY_PANELS std::shared_ptr _parentPanel = nullptr; +#endif Transform _offset; }; diff --git a/interface/src/ui/overlays/QmlOverlay.cpp b/interface/src/ui/overlays/QmlOverlay.cpp index eb909de993..15e72cf1e3 100644 --- a/interface/src/ui/overlays/QmlOverlay.cpp +++ b/interface/src/ui/overlays/QmlOverlay.cpp @@ -32,21 +32,20 @@ QmlOverlay::QmlOverlay(const QUrl& url, const QmlOverlay* textOverlay) } void QmlOverlay::buildQmlElement(const QUrl& url) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "buildQmlElement", Q_ARG(QUrl, url)); + return; + } + auto offscreenUi = DependencyManager::get(); - offscreenUi->returnFromUiThread([=] { - offscreenUi->load(url, [=](QQmlContext* context, QObject* object) { - QQuickItem* rawPtr = dynamic_cast(object); - // Create a shared ptr with a custom deleter lambda, that calls deleteLater - _qmlElement = std::shared_ptr(rawPtr, [](QQuickItem* ptr) { - if (ptr) { - ptr->deleteLater(); - } - }); + offscreenUi->load(url, [=](QQmlContext* context, QObject* object) { + QQuickItem* rawPtr = dynamic_cast(object); + // Create a shared ptr with a custom deleter lambda, that calls deleteLater + _qmlElement = std::shared_ptr(rawPtr, [](QQuickItem* ptr) { + if (ptr) { + ptr->deleteLater(); + } }); - while (!_qmlElement) { - qApp->processEvents(); - } - return QVariant(); }); } @@ -55,20 +54,23 @@ QmlOverlay::~QmlOverlay() { } void QmlOverlay::setProperties(const QVariantMap& properties) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setProperties", Q_ARG(QVariantMap, properties)); + return; + } + Overlay2D::setProperties(properties); auto bounds = _bounds; std::weak_ptr weakQmlElement = _qmlElement; - DependencyManager::get()->executeOnUiThread([weakQmlElement, bounds, properties] { - // check to see if qmlElement still exists - auto qmlElement = weakQmlElement.lock(); - if (qmlElement) { - qmlElement->setX(bounds.left()); - qmlElement->setY(bounds.top()); - qmlElement->setWidth(bounds.width()); - qmlElement->setHeight(bounds.height()); - QMetaObject::invokeMethod(qmlElement.get(), "updatePropertiesFromScript", Qt::DirectConnection, Q_ARG(QVariant, properties)); - } - }); + // check to see if qmlElement still exists + auto qmlElement = weakQmlElement.lock(); + if (qmlElement) { + qmlElement->setX(bounds.left()); + qmlElement->setY(bounds.top()); + qmlElement->setWidth(bounds.width()); + qmlElement->setHeight(bounds.height()); + QMetaObject::invokeMethod(qmlElement.get(), "updatePropertiesFromScript", Qt::DirectConnection, Q_ARG(QVariant, properties)); + } } void QmlOverlay::render(RenderArgs* args) { diff --git a/interface/src/ui/overlays/QmlOverlay.h b/interface/src/ui/overlays/QmlOverlay.h index 736d3884b5..ced2b6fa1f 100644 --- a/interface/src/ui/overlays/QmlOverlay.h +++ b/interface/src/ui/overlays/QmlOverlay.h @@ -32,7 +32,7 @@ public: void render(RenderArgs* args) override; private: - void buildQmlElement(const QUrl& url); + Q_INVOKABLE void buildQmlElement(const QUrl& url); protected: std::shared_ptr _qmlElement; diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 2556bc84aa..a6fcacc769 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -45,17 +45,17 @@ void Shape3DOverlay::render(RenderArgs* args) { transform.setTranslation(position); transform.setRotation(rotation); auto geometryCache = DependencyManager::get(); - auto pipeline = args->_pipeline; - if (!pipeline) { - pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); + auto shapePipeline = args->_shapePipeline; + if (!shapePipeline) { + shapePipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } transform.setScale(dimensions); batch->setModelTransform(transform); if (_isSolid) { - geometryCache->renderSolidShapeInstance(*batch, _shape, cubeColor, pipeline); + geometryCache->renderSolidShapeInstance(args, *batch, _shape, cubeColor, shapePipeline); } else { - geometryCache->renderWireShapeInstance(*batch, _shape, cubeColor, pipeline); + geometryCache->renderWireShapeInstance(args, *batch, _shape, cubeColor, shapePipeline); } } } diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 07c2861f16..5bbf41eb94 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -44,15 +44,15 @@ void Sphere3DOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); auto geometryCache = DependencyManager::get(); - auto pipeline = args->_pipeline; - if (!pipeline) { - pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); + auto shapePipeline = args->_shapePipeline; + if (!shapePipeline) { + shapePipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { - geometryCache->renderSolidSphereInstance(*batch, sphereColor, pipeline); + geometryCache->renderSolidSphereInstance(args, *batch, sphereColor, shapePipeline); } else { - geometryCache->renderWireSphereInstance(*batch, sphereColor, pipeline); + geometryCache->renderWireSphereInstance(args, *batch, sphereColor, shapePipeline); } } } diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index ebc28ca86a..4b110b8099 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -137,8 +137,8 @@ void Text3DOverlay::render(RenderArgs* args) { // Text renderer sets its own pipeline, _textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), getDrawInFront()); // so before we continue, we must reset the pipeline - batch.setPipeline(args->_pipeline->pipeline); - args->_pipeline->prepare(batch); + batch.setPipeline(args->_shapePipeline->pipeline); + args->_shapePipeline->prepare(batch, args); } const render::ShapeKey Text3DOverlay::getShapeKey() { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 80f11fa552..acba15d2ec 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -451,7 +451,7 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { // In Qt 5.9 mouse events must be sent before touch events to make sure some QtQuick components will // receive mouse events #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) - if (!(this->_pressed && event.getType() == PointerEvent::Move)) { + if (event.getType() == PointerEvent::Move) { QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); } @@ -459,11 +459,10 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); #if QT_VERSION < QT_VERSION_CHECK(5, 9, 0) - if (this->_pressed && event.getType() == PointerEvent::Move) { - return; + if (event.getType() == PointerEvent::Move) { + QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); } - QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); #endif } diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index d7076a443e..57c00e7183 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -23,20 +23,23 @@ #include "CubicHermiteSpline.h" #include "AnimUtil.h" -static void lookupJointChainInfo(AnimInverseKinematics::JointChainInfo* jointChainInfos, size_t numJointChainInfos, +static const float JOINT_CHAIN_INTERP_TIME = 0.25f; + +static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo, int indexA, int indexB, - AnimInverseKinematics::JointChainInfo** jointChainInfoA, - AnimInverseKinematics::JointChainInfo** jointChainInfoB) { - *jointChainInfoA = nullptr; - *jointChainInfoB = nullptr; - for (size_t i = 0; i < numJointChainInfos; i++) { - if (jointChainInfos[i].jointIndex == indexA) { - *jointChainInfoA = jointChainInfos + i; + const AnimInverseKinematics::JointInfo** jointInfoA, + const AnimInverseKinematics::JointInfo** jointInfoB) { + *jointInfoA = nullptr; + *jointInfoB = nullptr; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const AnimInverseKinematics::JointInfo* jointInfo = &jointChainInfo.jointInfoVec[i]; + if (jointInfo->jointIndex == indexA) { + *jointInfoA = jointInfo; } - if (jointChainInfos[i].jointIndex == indexB) { - *jointChainInfoB = jointChainInfos + i; + if (jointInfo->jointIndex == indexB) { + *jointInfoB = jointInfo; } - if (*jointChainInfoA && *jointChainInfoB) { + if (*jointInfoA && *jointInfoB) { break; } } @@ -149,25 +152,28 @@ void AnimInverseKinematics::setTargetVars(const QString& jointName, const QStrin } void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses) { - // build a list of valid targets from _targetVarVec and animVars - _maxTargetIndex = -1; + _hipsTargetIndex = -1; - bool removeUnfoundJoints = false; + + targets.reserve(_targetVarVec.size()); for (auto& targetVar : _targetVarVec) { + + // update targetVar jointIndex cache if (targetVar.jointIndex == -1) { - // this targetVar hasn't been validated yet... int jointIndex = _skeleton->nameToJointIndex(targetVar.jointName); if (jointIndex >= 0) { // this targetVar has a valid joint --> cache the indices targetVar.jointIndex = jointIndex; } else { qCWarning(animation) << "AnimInverseKinematics could not find jointName" << targetVar.jointName << "in skeleton"; - removeUnfoundJoints = true; } - } else { - IKTarget target; + } + + IKTarget target; + if (targetVar.jointIndex != -1) { target.setType(animVars.lookup(targetVar.typeVar, (int)IKTarget::Type::RotationAndPosition)); + target.setIndex(targetVar.jointIndex); if (target.getType() != IKTarget::Type::Unknown) { AnimPose absPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, absPose.rot()); @@ -175,7 +181,6 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: float weight = animVars.lookup(targetVar.weightVar, targetVar.weight); target.setPose(rotation, translation); - target.setIndex(targetVar.jointIndex); target.setWeight(weight); target.setFlexCoefficients(targetVar.numFlexCoefficients, targetVar.flexCoefficients); @@ -188,39 +193,20 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: glm::vec3 poleReferenceVector = animVars.lookupRigToGeometryVector(targetVar.poleReferenceVectorVar, Vectors::UNIT_Z); target.setPoleReferenceVector(glm::normalize(poleReferenceVector)); - targets.push_back(target); - - if (targetVar.jointIndex > _maxTargetIndex) { - _maxTargetIndex = targetVar.jointIndex; - } - // record the index of the hips ik target. if (target.getIndex() == _hipsIndex) { - _hipsTargetIndex = (int)targets.size() - 1; + _hipsTargetIndex = (int)targets.size(); } } + } else { + target.setType((int)IKTarget::Type::Unknown); } - } - if (removeUnfoundJoints) { - int numVars = (int)_targetVarVec.size(); - int i = 0; - while (i < numVars) { - if (_targetVarVec[i].jointIndex == -1) { - if (numVars > 1) { - // swap i for last element - _targetVarVec[i] = _targetVarVec[numVars - 1]; - } - _targetVarVec.pop_back(); - --numVars; - } else { - ++i; - } - } + targets.push_back(target); } } -void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets) { +void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec) { // compute absolute poses that correspond to relative target poses AnimPoseVec absolutePoses; absolutePoses.resize(_relativePoses.size()); @@ -234,26 +220,75 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< accumulator.clearAndClean(); } - float maxError = FLT_MAX; + float maxError = 0.0f; int numLoops = 0; const int MAX_IK_LOOPS = 16; - const float MAX_ERROR_TOLERANCE = 0.1f; // cm - while (maxError > MAX_ERROR_TOLERANCE && numLoops < MAX_IK_LOOPS) { + while (numLoops < MAX_IK_LOOPS) { ++numLoops; bool debug = context.getEnableDebugDrawIKChains() && numLoops == MAX_IK_LOOPS; // solve all targets - for (auto& target: targets) { - if (target.getType() == IKTarget::Type::Spline) { - solveTargetWithSpline(context, target, absolutePoses, debug); - } else { - solveTargetWithCCD(context, target, absolutePoses, debug); + for (size_t i = 0; i < targets.size(); i++) { + switch (targets[i].getType()) { + case IKTarget::Type::Unknown: + break; + case IKTarget::Type::Spline: + solveTargetWithSpline(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + default: + solveTargetWithCCD(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + } + } + + // on last iteration, interpolate jointChains, if necessary + if (numLoops == MAX_IK_LOOPS) { + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + size_t chainSize = std::min(_prevJointChainInfoVec[i].jointInfoVec.size(), jointChainInfoVec[i].jointInfoVec.size()); + for (size_t j = 0; j < chainSize; j++) { + jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha); + jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha); + } + + // if joint chain was just disabled, ramp the weight toward zero. + if (_prevJointChainInfoVec[i].target.getType() != IKTarget::Type::Unknown && + jointChainInfoVec[i].target.getType() == IKTarget::Type::Unknown) { + IKTarget newTarget = _prevJointChainInfoVec[i].target; + newTarget.setWeight((1.0f - alpha) * _prevJointChainInfoVec[i].target.getWeight()); + jointChainInfoVec[i].target = newTarget; + } + } + } + } + + // copy jointChainInfoVecs into accumulators + for (size_t i = 0; i < targets.size(); i++) { + const std::vector& jointInfoVec = jointChainInfoVec[i].jointInfoVec; + + // don't accumulate disabled or rotation only ik targets. + IKTarget::Type type = jointChainInfoVec[i].target.getType(); + if (type != IKTarget::Type::Unknown && type != IKTarget::Type::RotationOnly) { + float weight = jointChainInfoVec[i].target.getWeight(); + if (weight > 0.0f) { + for (size_t j = 0; j < jointInfoVec.size(); j++) { + const JointInfo& info = jointInfoVec[j]; + if (info.jointIndex >= 0) { + _rotationAccumulators[info.jointIndex].add(info.rot, weight); + _translationAccumulators[info.jointIndex].add(info.trans, weight); + } + } + } } } // harvest accumulated rotations and apply the average for (int i = 0; i < (int)_relativePoses.size(); ++i) { + if (i == _hipsIndex) { + continue; // don't apply accumulators to hips + } if (_rotationAccumulators[i].size() > 0) { _relativePoses[i].rot() = _rotationAccumulators[i].getAverage(); _rotationAccumulators[i].clear(); @@ -289,7 +324,7 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< // finally set the relative rotation of each tip to agree with absolute target rotation for (auto& target: targets) { int tipIndex = target.getIndex(); - int parentIndex = _skeleton->getParentIndex(tipIndex); + int parentIndex = (tipIndex >= 0) ? _skeleton->getParentIndex(tipIndex) : -1; // update rotationOnly targets that don't lie on the ik chain of other ik targets. if (parentIndex != -1 && !_rotationAccumulators[tipIndex].isDirty() && target.getType() == IKTarget::Type::RotationOnly) { @@ -308,9 +343,34 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< absolutePoses[tipIndex].rot() = targetRotation; } } + + // copy jointChainInfoVec into _prevJointChainInfoVec, and update timers + for (size_t i = 0; i < jointChainInfoVec.size(); i++) { + _prevJointChainInfoVec[i].timer = _prevJointChainInfoVec[i].timer - dt; + if (_prevJointChainInfoVec[i].timer <= 0.0f) { + _prevJointChainInfoVec[i] = jointChainInfoVec[i]; + _prevJointChainInfoVec[i].target = targets[i]; + // store relative poses into unknown/rotation only joint chains. + // so we have something to interpolate from if this chain is activated. + IKTarget::Type type = _prevJointChainInfoVec[i].target.getType(); + if (type == IKTarget::Type::Unknown || type == IKTarget::Type::RotationOnly) { + for (size_t j = 0; j < _prevJointChainInfoVec[i].jointInfoVec.size(); j++) { + auto& info = _prevJointChainInfoVec[i].jointInfoVec[j]; + if (info.jointIndex >= 0) { + info.rot = _relativePoses[info.jointIndex].rot(); + info.trans = _relativePoses[info.jointIndex].trans(); + } else { + info.rot = Quaternions::IDENTITY; + info.trans = glm::vec3(0.0f); + } + } + } + } + } } -void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { +void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { size_t chainDepth = 0; IKTarget::Type targetType = target.getType(); @@ -338,9 +398,6 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // the tip's parent-relative as we proceed up the chain glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot(); - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; - // NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward // as the head is nodded. if (targetType == IKTarget::Type::HmdHead || @@ -368,7 +425,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 tipRelativeTranslation = _relativePoses[target.getIndex()].trans(); - jointChainInfos[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, target.getWeight(), tipIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, tipIndex, constrained }; } // cache tip absolute position @@ -379,7 +436,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // descend toward root, pivoting each joint to get tip closer to target position while (pivotIndex != _hipsIndex && pivotsParentIndex != -1) { - assert(chainDepth < MAX_CHAIN_DEPTH); + assert(chainDepth < jointChainInfoOut.jointInfoVec.size()); // compute the two lines that should be aligned glm::vec3 jointPosition = absolutePoses[pivotIndex].trans(); @@ -444,9 +501,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const glm::quat twistPart; glm::vec3 axis = glm::normalize(deltaRotation * leverArm); swingTwistDecomposition(missingRotation, axis, swingPart, twistPart); - float dotSign = copysignf(1.0f, twistPart.w); const float LIMIT_LEAK_FRACTION = 0.1f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation; + deltaRotation = safeLerp(glm::quat(), twistPart, LIMIT_LEAK_FRACTION); } } } @@ -455,9 +511,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // An HmdHead target slaves the orientation of the end-effector by distributing rotation // deltas up the hierarchy. Its target position is enforced later (by shifting the hips). deltaRotation = target.getRotation() * glm::inverse(tipOrientation); - float dotSign = copysignf(1.0f, deltaRotation.w); const float ANGLE_DISTRIBUTION_FACTOR = 0.45f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * deltaRotation, ANGLE_DISTRIBUTION_FACTOR)); + deltaRotation = safeLerp(glm::quat(), deltaRotation, ANGLE_DISTRIBUTION_FACTOR); } // compute joint's new parent-relative rotation after swing @@ -480,7 +535,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 newTrans = _relativePoses[pivotIndex].trans(); - jointChainInfos[chainDepth] = { newRot, newTrans, target.getWeight(), pivotIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { newRot, newTrans, pivotIndex, constrained }; // keep track of tip's new transform as we descend towards root tipPosition = jointPosition + deltaRotation * (tipPosition - jointPosition); @@ -502,24 +557,25 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const int baseParentJointIndex = _skeleton->getParentIndex(baseJointIndex); AnimPose topPose, midPose, basePose; int topChainIndex = -1, baseChainIndex = -1; + const size_t MAX_CHAIN_DEPTH = 30; AnimPose postAbsPoses[MAX_CHAIN_DEPTH]; AnimPose accum = absolutePoses[_hipsIndex]; AnimPose baseParentPose = absolutePoses[_hipsIndex]; for (int i = (int)chainDepth - 1; i >= 0; i--) { - accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfos[i].relRot, jointChainInfos[i].relTrans); + accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); postAbsPoses[i] = accum; - if (jointChainInfos[i].jointIndex == topJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == topJointIndex) { topChainIndex = i; topPose = accum; } - if (jointChainInfos[i].jointIndex == midJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == midJointIndex) { midPose = accum; } - if (jointChainInfos[i].jointIndex == baseJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseJointIndex) { baseChainIndex = i; basePose = accum; } - if (jointChainInfos[i].jointIndex == baseParentJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseParentJointIndex) { baseParentPose = accum; } } @@ -599,21 +655,16 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::quat newBaseRelRot = glm::inverse(baseParentPose.rot()) * poleRot * basePose.rot(); - jointChainInfos[baseChainIndex].relRot = newBaseRelRot; + jointChainInfoOut.jointInfoVec[baseChainIndex].rot = newBaseRelRot; glm::quat newTopRelRot = glm::inverse(midPose.rot()) * glm::inverse(poleRot) * topPose.rot(); - jointChainInfos[topChainIndex].relRot = newTopRelRot; + jointChainInfoOut.jointInfoVec[topChainIndex].rot = newTopRelRot; } } } - for (size_t i = 0; i < chainDepth; i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, chainDepth, context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -628,7 +679,7 @@ static CubicHermiteSplineFunctorWithArcLength computeSplineFromTipAndBase(const } // pre-compute information about each joint influeced by this spline IK target. -void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) { +void AnimInverseKinematics::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const { std::vector splineJointInfoVec; // build spline between the default poses. @@ -681,13 +732,13 @@ void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext _splineJointInfoMap[target.getIndex()] = splineJointInfoVec; } -const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) { +const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const { // find or create splineJointInfo for this target auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); } else { - computeSplineJointInfosForIKTarget(context, target); + computeAndCacheSplineJointInfosForIKTarget(context, target); auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); @@ -697,10 +748,8 @@ const std::vector* AnimInverseKinematics return nullptr; } -void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { - - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; +void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { const int baseIndex = _hipsIndex; @@ -720,7 +769,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co // This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way) // when the head is arched backwards very far. - glm::quat halfRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), 0.5f)); + glm::quat halfRot = safeLerp(basePose.rot(), tipPose.rot(), 0.5f); if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { tipPose.rot() = -tipPose.rot(); } @@ -743,7 +792,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co if (target.getIndex() == _headIndex) { rotT = t * t; } - glm::quat twistRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), rotT)); + glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT); // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis glm::vec3 y = glm::normalize(spline.d(t)); @@ -783,19 +832,14 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co } } - jointChainInfos[i] = { relPose.rot(), relPose.trans(), target.getWeight(), splineJointInfo.jointIndex, constrained }; + jointChainInfoOut.jointInfoVec[i] = { relPose.rot(), relPose.trans(), splineJointInfo.jointIndex, constrained }; parentAbsPose = flexedAbsPose; } } - for (size_t i = 0; i < splineJointInfoVec->size(); i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, splineJointInfoVec->size(), context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -806,6 +850,24 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar return _relativePoses; } +AnimPose AnimInverseKinematics::applyHipsOffset() const { + glm::vec3 hipsOffset = _hipsOffset; + AnimPose relHipsPose = _relativePoses[_hipsIndex]; + float offsetLength = glm::length(hipsOffset); + const float MIN_HIPS_OFFSET_LENGTH = 0.03f; + if (offsetLength > MIN_HIPS_OFFSET_LENGTH) { + float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); + glm::vec3 scaledHipsOffset = scaleFactor * hipsOffset; + if (_hipsParentIndex == -1) { + relHipsPose.trans() = _relativePoses[_hipsIndex].trans() + scaledHipsOffset; + } else { + AnimPose absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); + absHipsPose.trans() += scaledHipsOffset; + relHipsPose = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + } + } + return relHipsPose; +} //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { @@ -850,33 +912,88 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _relativePoses = underPoses; } else { + JointChainInfoVec jointChainInfoVec(targets.size()); + { + PROFILE_RANGE_EX(simulation_animation, "ik/jointChainInfo", 0xffff00ff, 0); + + // initialize a new jointChainInfoVec, this will hold the results for solving each ik chain. + JointInfo defaultJointInfo = { glm::quat(), glm::vec3(), -1, false }; + for (size_t i = 0; i < targets.size(); i++) { + size_t chainDepth = (size_t)_skeleton->getChainDepth(targets[i].getIndex()); + jointChainInfoVec[i].jointInfoVec.reserve(chainDepth); + jointChainInfoVec[i].target = targets[i]; + int index = targets[i].getIndex(); + for (size_t j = 0; j < chainDepth; j++) { + jointChainInfoVec[i].jointInfoVec.push_back(defaultJointInfo); + jointChainInfoVec[i].jointInfoVec[j].jointIndex = index; + index = _skeleton->getParentIndex(index); + } + } + + // identify joint chains that have changed types this frame. + _prevJointChainInfoVec.resize(jointChainInfoVec.size()); + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer <= 0.0f && + (jointChainInfoVec[i].target.getType() != _prevJointChainInfoVec[i].target.getType() || + jointChainInfoVec[i].target.getPoleVectorEnabled() != _prevJointChainInfoVec[i].target.getPoleVectorEnabled())) { + _prevJointChainInfoVec[i].timer = JOINT_CHAIN_INTERP_TIME; + } + } + } + { PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0); - if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) { + if (_hipsTargetIndex >= 0) { + assert(_hipsTargetIndex < (int)targets.size()); + // slam the hips to match the _hipsTarget + AnimPose absPose = targets[_hipsTargetIndex].getPose(); + int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex()); - if (parentIndex != -1) { - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose; - } else { - _relativePoses[_hipsIndex] = absPose; + AnimPose parentAbsPose = _skeleton->getAbsolutePose(parentIndex, _relativePoses); + + // do smooth interpolation of hips, if necessary. + if (_prevJointChainInfoVec[_hipsTargetIndex].timer > 0.0f && _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec.size() > 0) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[_hipsTargetIndex].timer) / JOINT_CHAIN_INTERP_TIME; + + auto& info = _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec[0]; + AnimPose prevHipsRelPose(info.rot, info.trans); + AnimPose prevHipsAbsPose = parentAbsPose * prevHipsRelPose; + ::blend(1, &prevHipsAbsPose, &absPose, alpha, &absPose); } - } else { + + _relativePoses[_hipsIndex] = parentAbsPose.inverse() * absPose; + _relativePoses[_hipsIndex].scale() = glm::vec3(1.0f); + _hipsOffset = Vectors::ZERO; + + } else if (_hipsIndex >= 0) { + // if there is no hips target, shift hips according to the _hipsOffset from the previous frame - float offsetLength = glm::length(_hipsOffset); - const float MIN_HIPS_OFFSET_LENGTH = 0.03f; - if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) { - float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); - glm::vec3 hipsOffset = scaleFactor * _hipsOffset; - if (_hipsParentIndex == -1) { - _relativePoses[_hipsIndex].trans() = _relativePoses[_hipsIndex].trans() + hipsOffset; - } else { - auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); - absHipsPose.trans() += hipsOffset; - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + AnimPose relHipsPose = applyHipsOffset(); + + // determine if we should begin interpolating the hips. + for (size_t i = 0; i < targets.size(); i++) { + if (_prevJointChainInfoVec[i].target.getIndex() == _hipsIndex) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + // smoothly lerp in hipsOffset + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + AnimPose prevRelHipsPose(_prevJointChainInfoVec[i].jointInfoVec[0].rot, _prevJointChainInfoVec[i].jointInfoVec[0].trans); + ::blend(1, &prevRelHipsPose, &relHipsPose, alpha, &relHipsPose); + } + break; } } + + _relativePoses[_hipsIndex] = relHipsPose; + } + + // if there is an active jointChainInfo for the hips store the post shifted hips into it. + // This is so we have a valid pose to interplate from when the hips target is disabled. + if (_hipsTargetIndex >= 0) { + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].rot = _relativePoses[_hipsIndex].rot(); + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].trans = _relativePoses[_hipsIndex].trans(); } // update all HipsRelative targets to account for the hips shift/ik target. @@ -920,15 +1037,14 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars { PROFILE_RANGE_EX(simulation_animation, "ik/ccd", 0xffff00ff, 0); + preconditionRelativePosesToAvoidLimbLock(context, targets); - solve(context, targets); + solve(context, targets, dt, jointChainInfoVec); } if (_hipsTargetIndex < 0) { PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0); - computeHipsOffset(targets, underPoses, dt); - } else { - _hipsOffset = Vectors::ZERO; + _hipsOffset = computeHipsOffset(targets, underPoses, dt, _hipsOffset); } } @@ -937,23 +1053,15 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } } - if (_leftHandIndex > -1) { - _uncontrolledLeftHandPose = _skeleton->getAbsolutePose(_leftHandIndex, underPoses); - } - if (_rightHandIndex > -1) { - _uncontrolledRightHandPose = _skeleton->getAbsolutePose(_rightHandIndex, underPoses); - } - if (_hipsIndex > -1) { - _uncontrolledHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); - } - return _relativePoses; } -void AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt) { +glm::vec3 AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const { + // measure new _hipsOffset for next frame // by looking for discrepancies between where a targeted endEffector is // and where it wants to be (after IK solutions are done) + glm::vec3 hipsOffset = prevHipsOffset; glm::vec3 newHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); @@ -969,9 +1077,9 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } else if (target.getType() == IKTarget::Type::HmdHead) { // we want to shift the hips to bring the head to its designated position glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - _hipsOffset += target.getTranslation() - actual; + hipsOffset += target.getTranslation() - actual; // and ignore all other targets - newHipsOffset = _hipsOffset; + newHipsOffset = hipsOffset; break; } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); @@ -991,16 +1099,18 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } } - // smooth transitions by relaxing _hipsOffset toward the new value + // smooth transitions by relaxing hipsOffset toward the new value const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += (newHipsOffset - _hipsOffset) * tau; + hipsOffset += (newHipsOffset - hipsOffset) * tau; // clamp the hips offset - float hipsOffsetLength = glm::length(_hipsOffset); + float hipsOffsetLength = glm::length(hipsOffset); if (hipsOffsetLength > _maxHipsOffsetLength) { - _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; } + + return hipsOffset; } void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { @@ -1414,8 +1524,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele targetVar.jointIndex = -1; } - _maxTargetIndex = -1; - for (auto& accumulator: _rotationAccumulators) { accumulator.clearAndClean(); } @@ -1446,10 +1554,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _leftHandIndex = -1; _rightHandIndex = -1; } - - _uncontrolledLeftHandPose = AnimPose(); - _uncontrolledRightHandPose = AnimPose(); - _uncontrolledHipsPose = AnimPose(); } static glm::vec3 sphericalToCartesian(float phi, float theta) { @@ -1495,14 +1599,14 @@ void AnimInverseKinematics::debugDrawRelativePoses(const AnimContext& context) c } } -void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const { +void AnimInverseKinematics::debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const { AnimPoseVec poses = _relativePoses; // copy debug joint rotations into the relative poses - for (size_t i = 0; i < numJointChainInfos; i++) { - const JointChainInfo& info = jointChainInfos[i]; - poses[info.jointIndex].rot() = info.relRot; - poses[info.jointIndex].trans() = info.relTrans; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const JointInfo& info = jointChainInfo.jointInfoVec[i]; + poses[info.jointIndex].rot() = info.rot; + poses[info.jointIndex].trans() = info.trans; } // convert relative poses to absolute @@ -1519,9 +1623,9 @@ void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, si // draw each pose for (int i = 0; i < (int)poses.size(); i++) { int parentIndex = _skeleton->getParentIndex(i); - JointChainInfo* jointInfo = nullptr; - JointChainInfo* parentJointInfo = nullptr; - lookupJointChainInfo(jointChainInfos, numJointChainInfos, i, parentIndex, &jointInfo, &parentJointInfo); + const JointInfo* jointInfo = nullptr; + const JointInfo* parentJointInfo = nullptr; + lookupJointInfo(jointChainInfo, i, parentIndex, &jointInfo, &parentJointInfo); if (jointInfo && parentJointInfo) { // transform local axes into world space. @@ -1608,7 +1712,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_Y); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1626,7 +1730,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_X); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1666,10 +1770,9 @@ void AnimInverseKinematics::blendToPoses(const AnimPoseVec& targetPoses, const A // relax toward poses int numJoints = (int)_relativePoses.size(); for (int i = 0; i < numJoints; ++i) { - float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), targetPoses[i].rot())); if (_rotationAccumulators[i].isDirty()) { // this joint is affected by IK --> blend toward the targetPoses rotation - _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * targetPoses[i].rot(), blendFactor)); + _relativePoses[i].rot() = safeLerp(_relativePoses[i].rot(), targetPoses[i].rot(), blendFactor); } else { // this joint is NOT affected by IK --> slam to underPoses rotation _relativePoses[i].rot() = underPoses[i].rot(); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index d473ae3698..7f7640aa24 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -26,14 +26,21 @@ class RotationConstraint; class AnimInverseKinematics : public AnimNode { public: - struct JointChainInfo { - glm::quat relRot; - glm::vec3 relTrans; - float weight; + struct JointInfo { + glm::quat rot; + glm::vec3 trans; int jointIndex; bool constrained; }; + struct JointChainInfo { + std::vector jointInfoVec; + IKTarget target; + float timer { 0.0f }; + }; + + using JointChainInfoVec = std::vector; + explicit AnimInverseKinematics(const QString& id); virtual ~AnimInverseKinematics() override; @@ -66,23 +73,22 @@ public: void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; } - const AnimPose& getUncontrolledLeftHandPose() { return _uncontrolledLeftHandPose; } - const AnimPose& getUncontrolledRightHandPose() { return _uncontrolledRightHandPose; } - const AnimPose& getUncontrolledHipPose() { return _uncontrolledHipsPose; } - protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); - void solve(const AnimContext& context, const std::vector& targets); - void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); - void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); + void solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec); + void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; + void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; - void debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const; + void debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const; void debugDrawRelativePoses(const AnimContext& context) const; void debugDrawConstraints(const AnimContext& context) const; void debugDrawSpineSplines(const AnimContext& context, const std::vector& targets) const; void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); void preconditionRelativePosesToAvoidLimbLock(const AnimContext& context, const std::vector& targets); + AnimPose applyHipsOffset() const; // used to pre-compute information about each joint influeced by a spline IK target. struct SplineJointInfo { @@ -91,8 +97,8 @@ protected: AnimPose offsetPose; // local offset from the spline to the joint. }; - void computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target); - const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target); + void computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const; + const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const; // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } @@ -101,7 +107,7 @@ protected: void clearConstraints(); void initConstraints(); void initLimitCenterPoses(); - void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); + glm::vec3 computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const; // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; @@ -136,7 +142,7 @@ protected: AnimPoseVec _relativePoses; // current relative poses AnimPoseVec _limitCenterPoses; // relative - std::map> _splineJointInfoMap; + mutable std::map> _splineJointInfoMap; // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; @@ -148,18 +154,12 @@ protected: int _leftHandIndex { -1 }; int _rightHandIndex { -1 }; - // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses - // during the the cyclic coordinate descent algorithm - int _maxTargetIndex { 0 }; - float _maxErrorOnLastSolve { FLT_MAX }; bool _previousEnableDebugIKTargets { false }; SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; QString _solutionSourceVar; - AnimPose _uncontrolledLeftHandPose { AnimPose() }; - AnimPose _uncontrolledRightHandPose { AnimPose() }; - AnimPose _uncontrolledHipsPose { AnimPose() }; + JointChainInfoVec _prevJointChainInfoVec; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 062e016660..804ffb0583 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -42,6 +42,20 @@ int AnimSkeleton::getNumJoints() const { return _jointsSize; } +int AnimSkeleton::getChainDepth(int jointIndex) const { + if (jointIndex >= 0) { + int chainDepth = 0; + int index = jointIndex; + do { + chainDepth++; + index = _joints[index].parentIndex; + } while (index != -1); + return chainDepth; + } else { + return 0; + } +} + const AnimPose& AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { return _absoluteBindPoses[jointIndex]; } diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 6315f2d62b..99c9a148f7 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -28,6 +28,7 @@ public: int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; + int getChainDepth(int jointIndex) const; // absolute pose, not relative to parent const AnimPose& getAbsoluteBindPose(int jointIndex) const; diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index a4659f1e76..bcf30642e8 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -28,7 +28,7 @@ void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, A } result[i].scale() = lerp(aPose.scale(), bPose.scale(), alpha); - result[i].rot() = glm::normalize(glm::lerp(aPose.rot(), q2, alpha)); + result[i].rot() = safeLerp(aPose.rot(), bPose.rot(), alpha); result[i].trans() = lerp(aPose.trans(), bPose.trans(), alpha); } } diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 055fd630eb..d215fdc654 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -21,4 +21,14 @@ glm::quat averageQuats(size_t numQuats, const glm::quat* quats); float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, const QString& id, AnimNode::Triggers& triggersOut); +inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) { + // adjust signs if necessary + glm::quat bTemp = b; + float dot = glm::dot(a, bTemp); + if (dot < 0.0f) { + bTemp = -bTemp; + } + return glm::normalize(glm::lerp(a, bTemp, alpha)); +} + #endif diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 7d4c0f4e92..9aa315beb8 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -9,15 +9,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AnimationCache.h" + #include #include -#include "AnimationCache.h" -#include "AnimationLogging.h" +#include #include #include #include +#include "AnimationLogging.h" + int animationPointerMetaTypeId = qRegisterMetaType(); AnimationCache::AnimationCache(QObject* parent) : @@ -31,7 +34,7 @@ AnimationCache::AnimationCache(QObject* parent) : AnimationPointer AnimationCache::getAnimation(const QUrl& url) { if (QThread::currentThread() != thread()) { AnimationPointer result; - QMetaObject::invokeMethod(this, "getAnimation", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "getAnimation", Q_RETURN_ARG(AnimationPointer, result), Q_ARG(const QUrl&, url)); return result; } @@ -97,7 +100,7 @@ bool Animation::isLoaded() const { QStringList Animation::getJointNames() const { if (QThread::currentThread() != thread()) { QStringList result; - QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointNames", Q_RETURN_ARG(QStringList, result)); return result; } @@ -111,7 +114,7 @@ QStringList Animation::getJointNames() const { QVector Animation::getFrames() const { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), "getFrames", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getFrames", Q_RETURN_ARG(QVector, result)); return result; } diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 5567539659..325a1b40b6 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -56,8 +56,8 @@ private: glm::vec3 _poleReferenceVector; bool _poleVectorEnabled { false }; int _index { -1 }; - Type _type { Type::RotationAndPosition }; - float _weight; + Type _type { Type::Unknown }; + float _weight { 0.0f }; float _flexCoefficients[MAX_FLEX_COEFFICIENTS]; size_t _numFlexCoefficients; }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 3d04b0b26f..fc0ca73c96 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -404,8 +404,18 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { - if (isIndexValid(jointIndex)) { - position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; + return true; + } else { + return false; + } + } + + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { + position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; return true; } else { return false; @@ -413,17 +423,31 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm: } bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { - if (isIndexValid(jointIndex)) { - position = _internalPoseSet._absolutePoses[jointIndex].trans(); - return true; + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + position = _internalPoseSet._absolutePoses[jointIndex].trans(); + return true; + } else { + return false; + } } else { - return false; + return getAbsoluteJointTranslationInRigFrame(jointIndex, position); } } bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const glm::quat& rotation) const { - if (isIndexValid(jointIndex)) { - result = rotation * _internalPoseSet._absolutePoses[jointIndex].rot(); + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + result = rotation * _internalPoseSet._absolutePoses[jointIndex].rot(); + return true; + } else { + return false; + } + } + + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { + result = rotation * _externalPoseSet._absolutePoses[jointIndex].rot(); return true; } else { return false; @@ -431,6 +455,15 @@ bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const } bool Rig::getJointRotation(int jointIndex, glm::quat& rotation) const { + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + rotation = _internalPoseSet._relativePoses[jointIndex].rot(); + return true; + } else { + return false; + } + } + QReadLocker readLock(&_externalPoseSetLock); if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._relativePoses.size()) { rotation = _externalPoseSet._relativePoses[jointIndex].rot(); @@ -1082,36 +1115,13 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, bodyCapsuleHalfHeight, 0); const float HAND_RADIUS = 0.05f; - - const float RELAX_DURATION = 0.6f; - const float CONTROL_DURATION = 0.4f; - const bool TO_CONTROLLED = true; - const bool FROM_CONTROLLED = false; - const bool LEFT_HAND = true; - const bool RIGHT_HAND = false; - const float ELBOW_POLE_VECTOR_BLEND_FACTOR = 0.95f; if (leftHandEnabled) { - if (!_isLeftHandControlled) { - _leftHandControlTimeRemaining = CONTROL_DURATION; - _isLeftHandControlled = true; - } glm::vec3 handPosition = leftHandPose.trans(); glm::quat handRotation = leftHandPose.rot(); - if (_leftHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _leftHandControlTimeRemaining = std::max(_leftHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_leftHandControlTimeRemaining, CONTROL_DURATION, handPose, - LEFT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1124,9 +1134,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("leftHandRotation", handRotation); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - _lastLeftHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isLeftHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("LeftHand"); int armJointIndex = _animSkeleton->nameToJointIndex("LeftArm"); @@ -1154,47 +1161,17 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevLeftHandPoleVectorValid = false; _animVars.set("leftHandPoleVectorEnabled", false); - if (_isLeftHandControlled) { - _leftHandRelaxTimeRemaining = RELAX_DURATION; - _isLeftHandControlled = false; - } + _animVars.unset("leftHandPosition"); + _animVars.unset("leftHandRotation"); + _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - if (_leftHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _leftHandRelaxTimeRemaining = std::max(_leftHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_leftHandRelaxTimeRemaining, RELAX_DURATION, _lastLeftHandControlledPose, - LEFT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("leftHandPosition", handPose.trans()); - _animVars.set("leftHandRotation", handPose.rot()); - _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("leftHandPosition"); - _animVars.unset("leftHandRotation"); - _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } } if (rightHandEnabled) { - if (!_isRightHandControlled) { - _rightHandControlTimeRemaining = CONTROL_DURATION; - _isRightHandControlled = true; - } glm::vec3 handPosition = rightHandPose.trans(); glm::quat handRotation = rightHandPose.rot(); - if (_rightHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _rightHandControlTimeRemaining = std::max(_rightHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_rightHandControlTimeRemaining, CONTROL_DURATION, handPose, RIGHT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1207,9 +1184,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("rightHandRotation", handRotation); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - _lastRightHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isRightHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("RightHand"); int armJointIndex = _animSkeleton->nameToJointIndex("RightArm"); @@ -1237,25 +1211,9 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevRightHandPoleVectorValid = false; _animVars.set("rightHandPoleVectorEnabled", false); - if (_isRightHandControlled) { - _rightHandRelaxTimeRemaining = RELAX_DURATION; - _isRightHandControlled = false; - } - - if (_rightHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _rightHandRelaxTimeRemaining = std::max(_rightHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_rightHandRelaxTimeRemaining, RELAX_DURATION, _lastRightHandControlledPose, RIGHT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("rightHandPosition", handPose.trans()); - _animVars.set("rightHandRotation", handPose.rot()); - _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("rightHandPosition"); - _animVars.unset("rightHandRotation"); - _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } + _animVars.unset("rightHandPosition"); + _animVars.unset("rightHandRotation"); + _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); } } @@ -1704,39 +1662,38 @@ void Rig::computeAvatarBoundingCapsule( ikNode.setTargetVars("RightFoot", "rightFootPosition", "rightFootRotation", "rightFootType", "rightFootWeight", 1.0f, {}, QString(), QString(), QString()); - - AnimPose geometryToRig = _modelOffset * _geometryOffset; - - AnimPose hips(glm::vec3(1), glm::quat(), glm::vec3()); + glm::vec3 hipsPosition(0.0f); int hipsIndex = indexOfJoint("Hips"); if (hipsIndex >= 0) { - hips = geometryToRig * _animSkeleton->getAbsoluteBindPose(hipsIndex); + hipsPosition = transformPoint(_geometryToRigTransform, _animSkeleton->getAbsoluteDefaultPose(hipsIndex).trans()); } AnimVariantMap animVars; + animVars.setRigToGeometryTransform(_rigToGeometryTransform); glm::quat handRotation = glm::angleAxis(PI, Vectors::UNIT_X); - animVars.set("leftHandPosition", hips.trans()); + animVars.set("leftHandPosition", hipsPosition); animVars.set("leftHandRotation", handRotation); animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightHandPosition", hips.trans()); + animVars.set("rightHandPosition", hipsPosition); animVars.set("rightHandRotation", handRotation); animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); int rightFootIndex = indexOfJoint("RightFoot"); int leftFootIndex = indexOfJoint("LeftFoot"); if (rightFootIndex != -1 && leftFootIndex != -1) { - glm::vec3 foot = Vectors::ZERO; + glm::vec3 geomFootPosition = glm::vec3(0.0f, _animSkeleton->getAbsoluteDefaultPose(rightFootIndex).trans().y, 0.0f); + glm::vec3 footPosition = transformPoint(_geometryToRigTransform, geomFootPosition); glm::quat footRotation = glm::angleAxis(0.5f * PI, Vectors::UNIT_X); - animVars.set("leftFootPosition", foot); + animVars.set("leftFootPosition", footPosition); animVars.set("leftFootRotation", footRotation); animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightFootPosition", foot); + animVars.set("rightFootPosition", footPosition); animVars.set("rightFootRotation", footRotation); animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); } // call overlay twice: once to verify AnimPoseVec joints and again to do the IK AnimNode::Triggers triggersOut; - AnimContext context(false, false, false, glm::mat4(), glm::mat4()); + AnimContext context(false, false, false, _geometryToRigTransform, _rigToGeometryTransform); float dt = 1.0f; // the value of this does not matter ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); @@ -1769,34 +1726,13 @@ void Rig::computeAvatarBoundingCapsule( // compute bounding shape parameters // NOTE: we assume that the longest side of totalExtents is the yAxis... - glm::vec3 diagonal = (geometryToRig * totalExtents.maximum) - (geometryToRig * totalExtents.minimum); + glm::vec3 diagonal = (transformPoint(_geometryToRigTransform, totalExtents.maximum) - + transformPoint(_geometryToRigTransform, totalExtents.minimum)); // ... and assume the radiusOut is half the RMS of the X and Z sides: radiusOut = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); heightOut = diagonal.y - 2.0f * radiusOut; glm::vec3 rootPosition = finalPoses[geometry.rootJointIndex].trans(); - glm::vec3 rigCenter = (geometryToRig * (0.5f * (totalExtents.maximum + totalExtents.minimum))); - localOffsetOut = rigCenter - (geometryToRig * rootPosition); -} - -bool Rig::transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose) { - auto ikNode = getAnimInverseKinematicsNode(); - if (ikNode) { - float alpha = 1.0f - deltaTime / totalDuration; - const AnimPose geometryToRigTransform(_geometryToRigTransform); - AnimPose uncontrolledHandPose; - if (isLeftHand) { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); - } else { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); - } - if (isToControlled) { - ::blend(1, &uncontrolledHandPose, &controlledHandPose, alpha, &returnHandPose); - } else { - ::blend(1, &controlledHandPose, &uncontrolledHandPose, alpha, &returnHandPose); - } - return true; - } - return false; + glm::vec3 rigCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); + localOffsetOut = rigCenter - transformPoint(_geometryToRigTransform, rootPosition); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c17a7b9c8f..5293fa1fe7 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -340,18 +340,6 @@ protected: int _nextStateHandlerId { 0 }; QMutex _stateMutex; - bool transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose); - - bool _isLeftHandControlled { false }; - bool _isRightHandControlled { false }; - float _leftHandControlTimeRemaining { 0.0f }; - float _rightHandControlTimeRemaining { 0.0f }; - float _leftHandRelaxTimeRemaining { 0.0f }; - float _rightHandRelaxTimeRemaining { 0.0f }; - AnimPose _lastLeftHandControlledPose; - AnimPose _lastRightHandControlledPose; - glm::vec3 _prevRightFootPoleVector { Vectors::UNIT_Z }; bool _prevRightFootPoleVectorValid { false }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 43af7afdef..bc02da1cc4 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -92,6 +92,7 @@ void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudio::AudioInput); auto outputDevices = getAvailableDevices(QAudio::AudioOutput); + Lock lock(_deviceMutex); if (inputDevices != _inputDevices) { _inputDevices.swap(inputDevices); emit devicesChanged(QAudio::AudioInput, _inputDevices); @@ -210,9 +211,9 @@ AudioClient::AudioClient() : connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, Qt::DirectConnection); - connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { + connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { qCDebug(audioclient) << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; - switchOutputToAudioDevice(outputDeviceInfo); + switchOutputToAudioDevice(outputDeviceInfo); }); connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); @@ -259,10 +260,21 @@ void AudioClient::customDeleter() { void AudioClient::cleanupBeforeQuit() { // FIXME: this should be put in customDeleter, but there is still a reference to this when it is called, // so this must be explicitly, synchronously stopped + static ConditionalGuard guard; + if (QThread::currentThread() != thread()) { + // This will likely be called from the main thread, but we don't want to do blocking queued calls + // from the main thread, so we use a normal auto-connection invoke, and then use a conditional to wait + // for completion + // The effect is the same, yes, but we actually want to avoid the use of Qt::BlockingQueuedConnection + // in the code + QMetaObject::invokeMethod(this, "cleanupBeforeQuit"); + guard.wait(); + return; + } stop(); - _checkDevicesTimer->stop(); + guard.trigger(); } void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { @@ -619,7 +631,7 @@ void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointerreadPrimitive(&bitset); bool hasReverb = oneAtBit(bitset, HAS_REVERB_BIT); - + if (hasReverb) { float reverbTime, wetLevel; message->readPrimitive(&reverbTime); @@ -717,7 +729,7 @@ void AudioClient::Gate::flush() { void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_muted) { toggleMute(); - + // have the audio scripting interface emit a signal to say we were muted by the mixer emit mutedByMixer(); } @@ -726,7 +738,7 @@ void AudioClient::handleNoisyMutePacket(QSharedPointer message) void AudioClient::handleMuteEnvironmentPacket(QSharedPointer message) { glm::vec3 position; float radius; - + message->readPrimitive(&position); message->readPrimitive(&radius); @@ -759,7 +771,7 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess } void AudioClient::selectAudioFormat(const QString& selectedCodecName) { - + _selectedCodecName = selectedCodecName; qCDebug(audioclient) << "Selected Codec:" << _selectedCodecName; @@ -776,7 +788,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { _codec = plugin; - _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); qCDebug(audioclient) << "Selected Codec Plugin:" << _codec.get(); break; @@ -784,7 +796,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { } } - + bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; @@ -1192,11 +1204,11 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // lock the injectors Lock lock(_injectorsMutex); - QVector injectorsToRemove; + QVector injectorsToRemove; memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); - for (AudioInjector* injector : _activeLocalAudioInjectors) { + for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { @@ -1209,7 +1221,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - + if (injector->isAmbisonic()) { // no distance attenuation @@ -1238,36 +1250,36 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } - + } else { // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = gainForSource(distance, injector->getVolume()); + float gain = gainForSource(distance, injector->getVolume()); float azimuth = azimuthForSource(relativePosition); - + // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - + } else { - + qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - + qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } - - for (AudioInjector* injector : injectorsToRemove) { + + for (const AudioInjectorPointer& injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1335,6 +1347,14 @@ void AudioClient::toggleMute() { emit muteToggled(); } +void AudioClient::setNoiseReduction(bool enable) { + if (_isNoiseGateEnabled != enable) { + _isNoiseGateEnabled = enable; + emit noiseReductionChanged(); + } +} + + void AudioClient::setIsStereoInput(bool isStereoInput) { if (isStereoInput != _isStereoInput) { _isStereoInput = isStereoInput; @@ -1350,7 +1370,7 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { } } -bool AudioClient::outputLocalInjector(AudioInjector* injector) { +bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access @@ -1446,6 +1466,8 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn _audioInput = new QAudioInput(inputDeviceInfo, _inputFormat, this); _numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); _audioInput->setBufferSize(_numInputCallbackBytes); + // different audio input devices may have different volumes + emit inputVolumeChanged(_audioInput->volume()); // how do we want to handle input working, but output not working? int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes); @@ -1690,9 +1712,9 @@ int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(_orientationGetter()); - + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - + // project the rotated source position vector onto the XZ plane rotatedSourcePosition.y = 0.0f; @@ -1700,15 +1722,15 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - + // produce an oriented angle about the y-axis glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); - float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" + float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" return (direction.x < 0.0f) ? -angle : angle; - } else { + } else { // no azimuth if they are in same spot - return 0.0f; + return 0.0f; } } @@ -1847,3 +1869,10 @@ void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 sca void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); } + +void AudioClient::setInputVolume(float volume) { + if (_audioInput && volume != (float)_audioInput->volume()) { + _audioInput->setVolume(volume); + emit inputVolumeChanged(_audioInput->volume()); + } +} diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 54ce3aa6c2..31e36671c7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -143,7 +143,7 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - bool outputLocalInjector(AudioInjector* injector) override; + bool outputLocalInjector(const AudioInjectorPointer& injector) override; QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; QList getAudioDevices(QAudio::Mode mode) const; @@ -180,7 +180,8 @@ public slots: virtual void setIsStereoInput(bool stereo) override; - void setNoiseReduction(bool isNoiseGateEnabled) { _isNoiseGateEnabled = isNoiseGateEnabled; } + void setNoiseReduction(bool isNoiseGateEnabled); + bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } @@ -197,7 +198,7 @@ public slots: bool switchAudioDevice(QAudio::Mode mode, const QString& deviceName); float getInputVolume() const { return (_audioInput) ? (float)_audioInput->volume() : 0.0f; } - void setInputVolume(float volume) { if (_audioInput) _audioInput->setVolume(volume); } + void setInputVolume(float volume); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); @@ -207,7 +208,9 @@ public slots: void saveSettings(); signals: - bool muteToggled(); + void inputVolumeChanged(float volume); + void muteToggled(); + void noiseReductionChanged(); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness); @@ -377,7 +380,7 @@ private: bool _hasReceivedFirstPacket { false }; - QVector _activeLocalAudioInjectors; + QVector _activeLocalAudioInjectors; bool _isPlayingBackRecording { false }; diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 2e14b9956b..8b48b55206 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -18,6 +18,7 @@ #include #include "AudioInjectorOptions.h" +#include "AudioInjector.h" class AudioInjector; class AudioInjectorLocalBuffer; @@ -35,7 +36,7 @@ public: // threadsafe // moves injector->getLocalBuffer() to another thread (so removes its parent) // take care to delete it when ~AudioInjector, as parenting Qt semantics will not work - virtual bool outputLocalInjector(AudioInjector* injector) = 0; + virtual bool outputLocalInjector(const AudioInjectorPointer& injector) = 0; public slots: virtual bool shouldLoopbackInjectors() { return false; } diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 47e6c98144..ee57e42e77 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -92,11 +92,6 @@ void AudioInjector::finish() { emit finished(); deleteLocalBuffer(); - - if (stateHas(AudioInjectorState::PendingDelete)) { - // we've been asked to delete after finishing, trigger a deleteLater here - deleteLater(); - } } void AudioInjector::restart() { @@ -132,7 +127,7 @@ void AudioInjector::restart() { } } -bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector*)) { +bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { _state = AudioInjectorState::NotFinished; int byteOffset = 0; @@ -150,7 +145,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector* bool success = true; if (!_options.localOnly) { auto injectorManager = DependencyManager::get(); - if (!(*injectorManager.*injection)(this)) { + if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; finishNetworkInjection(); } @@ -173,7 +168,7 @@ bool AudioInjector::injectLocally() { // call this function on the AudioClient's thread // this will move the local buffer's thread to the LocalInjectorThread - success = _localAudioInterface->outputLocalInjector(this); + success = _localAudioInterface->outputLocalInjector(sharedFromThis()); if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; @@ -418,20 +413,16 @@ void AudioInjector::triggerDeleteAfterFinish() { } if (stateHas(AudioInjectorState::Finished)) { - stopAndDeleteLater(); + stop(); } else { _state |= AudioInjectorState::PendingDelete; } } -void AudioInjector::stopAndDeleteLater() { - stop(); - QMetaObject::invokeMethod(this, "deleteLater", Qt::QueuedConnection); -} - -AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position) { +AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position) { if (!sound || !sound->isReady()) { - return nullptr; + return AudioInjectorPointer(); } AudioInjectorOptions options; @@ -462,8 +453,8 @@ AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float vo return playSoundAndDelete(resampled, options); } -AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* sound = playSound(buffer, options); +AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer sound = playSound(buffer, options); if (sound) { sound->_state |= AudioInjectorState::PendingDelete; @@ -473,8 +464,9 @@ AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const } -AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* injector = new AudioInjector(buffer, options); +AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); + if (!injector->inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::playSound failed to thread injector"; } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index a901c2520f..aed51c5f85 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -32,6 +32,8 @@ class AbstractAudioInterface; class AudioInjectorManager; +class AudioInjector; +using AudioInjectorPointer = QSharedPointer; enum class AudioInjectorState : uint8_t { @@ -46,19 +48,19 @@ AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs); // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object -// until it dies. -class AudioInjector : public QObject { +// until it dies. +class AudioInjector : public QObject, public QEnableSharedFromThis { Q_OBJECT public: AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); ~AudioInjector(); - + bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } - + int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - + AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } @@ -72,36 +74,36 @@ public: bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position); + static AudioInjectorPointer playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position); public slots: void restart(); - + void stop(); void triggerDeleteAfterFinish(); - void stopAndDeleteLater(); - + const AudioInjectorOptions& getOptions() const { return _options; } void setOptions(const AudioInjectorOptions& options); - + float getLoudness() const { return _loudness; } bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); void finishLocalInjection(); void finishNetworkInjection(); - + signals: void finished(); void restarting(); - + private: int64_t injectNextFrame(); - bool inject(bool(AudioInjectorManager::*injection)(AudioInjector*)); + bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); void deleteLocalBuffer(); - + static AbstractAudioInterface* _localAudioInterface; QByteArray _audioData; @@ -112,17 +114,17 @@ private: int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; AudioInjectorLocalBuffer* _localBuffer { nullptr }; - + int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; quint16 _outgoingSequenceNumber { 0 }; - + // when the injector is local, we need this AudioHRTF _localHRTF; AudioFOA _localFOA; friend class AudioInjectorManager; }; -Q_DECLARE_METATYPE(AudioInjector*) - +Q_DECLARE_METATYPE(AudioInjectorPointer) + #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index c66e209ea9..f30d3093ec 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -21,26 +21,26 @@ AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; - + Lock lock(_injectorsMutex); - + // make sure any still living injectors are stopped and deleted while (!_injectors.empty()) { // grab the injector at the front auto& timePointerPair = _injectors.top(); - + // ask it to stop and be deleted - timePointerPair.second->stopAndDeleteLater(); - + timePointerPair.second->stop(); + _injectors.pop(); } - + // get rid of the lock now that we've stopped all living injectors lock.unlock(); - + // in case the thread is waiting for injectors wake it up now _injectorReady.notify_one(); - + // quit and wait on the manager thread, if we ever created it if (_thread) { _thread->quit(); @@ -51,10 +51,10 @@ AudioInjectorManager::~AudioInjectorManager() { void AudioInjectorManager::createThread() { _thread = new QThread; _thread->setObjectName("Audio Injector Thread"); - + // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); - + // start the thread _thread->start(); } @@ -63,20 +63,20 @@ void AudioInjectorManager::run() { while (!_shouldStop) { // wait until the next injector is ready, or until we get a new injector given to us Lock lock(_injectorsMutex); - + if (_injectors.size() > 0) { // when does the next injector need to send a frame? // do we get to wait or should we just go for it now? - + auto timeInjectorPair = _injectors.top(); - + auto nextTimestamp = timeInjectorPair.first; int64_t difference = int64_t(nextTimestamp - usecTimestampNow()); - + if (difference > 0) { _injectorReady.wait_for(lock, std::chrono::microseconds(difference)); } - + if (_injectors.size() > 0) { // loop through the injectors in the map and send whatever frames need to go out auto front = _injectors.top(); @@ -90,7 +90,7 @@ void AudioInjectorManager::run() { // either way we're popping this injector off - get a copy first auto injector = front.second; _injectors.pop(); - + if (!injector.isNull()) { // this is an injector that's ready to go, have it send a frame now auto nextCallDelta = injector->injectNextFrame(); @@ -100,7 +100,7 @@ void AudioInjectorManager::run() { heldInjectors.emplace(heldInjectors.end(), usecTimestampNow() + nextCallDelta, injector); } } - + if (_injectors.size() > 0) { front = _injectors.top(); } else { @@ -120,10 +120,10 @@ void AudioInjectorManager::run() { // we have no current injectors, wait until we get at least one before we do anything _injectorReady.wait(lock); } - + // unlock the lock in case something in process events needs to modify the queue lock.unlock(); - + QCoreApplication::processEvents(); } } @@ -139,36 +139,36 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a return false; } -bool AudioInjectorManager::threadInjector(AudioInjector* injector) { +bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } - + // guard the injectors vector with a mutex Lock lock(_injectorsMutex); - + if (wouldExceedLimits()) { return false; } else { if (!_thread) { createThread(); } - + // move the injector to the QThread injector->moveToThread(_thread); - + // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); - + return true; } } -bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { +bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; @@ -181,8 +181,8 @@ bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { return false; } else { // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); } diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index de5537856e..9aca3014e3 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -23,7 +23,7 @@ #include -class AudioInjector; +#include "AudioInjector.h" class AudioInjectorManager : public QObject, public Dependency { Q_OBJECT @@ -33,39 +33,38 @@ public: private slots: void run(); private: - - using InjectorQPointer = QPointer; - using TimeInjectorPointerPair = std::pair; - + + using TimeInjectorPointerPair = std::pair; + struct greaterTime { bool operator() (const TimeInjectorPointerPair& x, const TimeInjectorPointerPair& y) const { return x.first > y.first; } }; - + using InjectorQueue = std::priority_queue, greaterTime>; using Mutex = std::mutex; using Lock = std::unique_lock; - - bool threadInjector(AudioInjector* injector); - bool restartFinishedInjector(AudioInjector* injector); + + bool threadInjector(const AudioInjectorPointer& injector); + bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - + AudioInjectorManager() {}; AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; - + void createThread(); - + QThread* _thread { nullptr }; bool _shouldStop { false }; InjectorQueue _injectors; Mutex _injectorsMutex; std::condition_variable _injectorReady; - + friend class AudioInjector; }; diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 1646540da6..162e833da2 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -9,10 +9,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include "SoundCache.h" + +#include + +#include #include "AudioLogging.h" -#include "SoundCache.h" static const int SOUNDS_LOADING_PRIORITY { -7 }; // Make sure sounds load after the low rez texture mips @@ -29,7 +32,7 @@ SoundCache::SoundCache(QObject* parent) : SharedSoundPointer SoundCache::getSound(const QUrl& url) { if (QThread::currentThread() != thread()) { SharedSoundPointer result; - QMetaObject::invokeMethod(this, "getSound", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "getSound", Q_RETURN_ARG(SharedSoundPointer, result), Q_ARG(const QUrl&, url)); return result; } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 02b31235de..51a08d737c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -609,7 +610,7 @@ void Avatar::render(RenderArgs* renderArgs) { if (showCollisionShapes && shouldRenderHead(renderArgs) && _skeletonModel->isRenderable()) { PROFILE_RANGE_BATCH(batch, __FUNCTION__":skeletonBoundingCollisionShapes"); const float BOUNDING_SHAPE_ALPHA = 0.7f; - _skeletonModel->renderBoundingCollisionShapes(*renderArgs->_batch, getUniformScale(), BOUNDING_SHAPE_ALPHA); + _skeletonModel->renderBoundingCollisionShapes(renderArgs, *renderArgs->_batch, getUniformScale(), BOUNDING_SHAPE_ALPHA); } if (showReceiveStats || showNamesAboveHeads) { @@ -1007,49 +1008,87 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } -int Avatar::getJointIndex(const QString& name) const { - if (QThread::currentThread() != thread()) { - int result; - QMetaObject::invokeMethod(const_cast(this), "getJointIndex", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(int, result), Q_ARG(const QString&, name)); - return result; +void Avatar::invalidateJointIndicesCache() const { + QWriteLocker writeLock(&_modelJointIndicesCacheLock); + _modelJointsCached = false; +} + +void Avatar::withValidJointIndicesCache(std::function const& worker) const { + QReadLocker readLock(&_modelJointIndicesCacheLock); + if (_modelJointsCached) { + worker(); + } else { + readLock.unlock(); + { + QWriteLocker writeLock(&_modelJointIndicesCacheLock); + if (!_modelJointsCached) { + _modelJointIndicesCache.clear(); + if (_skeletonModel && _skeletonModel->isActive()) { + _modelJointIndicesCache = _skeletonModel->getFBXGeometry().jointIndices; + _modelJointsCached = true; + } + } + worker(); + } } +} + +int Avatar::getJointIndex(const QString& name) const { int result = getFauxJointIndex(name); if (result != -1) { return result; } - return _skeletonModel->isActive() ? _skeletonModel->getFBXGeometry().getJointIndex(name) : -1; + + withValidJointIndicesCache([&]() { + if (_modelJointIndicesCache.contains(name)) { + result = _modelJointIndicesCache[name] - 1; + } + }); + return result; } QStringList Avatar::getJointNames() const { - if (QThread::currentThread() != thread()) { - QStringList result; - QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QStringList, result)); - return result; - } - return _skeletonModel->isActive() ? _skeletonModel->getFBXGeometry().getJointNames() : QStringList(); + QStringList result; + withValidJointIndicesCache([&]() { + // find out how large the vector needs to be + int maxJointIndex = -1; + QHashIterator k(_modelJointIndicesCache); + while (k.hasNext()) { + k.next(); + int index = k.value(); + if (index > maxJointIndex) { + maxJointIndex = index; + } + } + // iterate through the hash and put joint names + // into the vector at their indices + QVector resultVector(maxJointIndex+1); + QHashIterator i(_modelJointIndicesCache); + while (i.hasNext()) { + i.next(); + int index = i.value(); + resultVector[index] = i.key(); + } + // convert to QList and drop out blanks + result = resultVector.toList(); + QMutableListIterator j(result); + while (j.hasNext()) { + QString jointName = j.next(); + if (jointName.isEmpty()) { + j.remove(); + } + } + }); + return result; } glm::vec3 Avatar::getJointPosition(int index) const { - if (QThread::currentThread() != thread()) { - glm::vec3 position; - QMetaObject::invokeMethod(const_cast(this), "getJointPosition", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(glm::vec3, position), Q_ARG(const int, index)); - return position; - } glm::vec3 position; _skeletonModel->getJointPositionInWorldFrame(index, position); return position; } glm::vec3 Avatar::getJointPosition(const QString& name) const { - if (QThread::currentThread() != thread()) { - glm::vec3 position; - QMetaObject::invokeMethod(const_cast(this), "getJointPosition", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(glm::vec3, position), Q_ARG(const QString&, name)); - return position; - } glm::vec3 position; _skeletonModel->getJointPositionInWorldFrame(getJointIndex(name), position); return position; @@ -1070,6 +1109,8 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { } void Avatar::setModelURLFinished(bool success) { + invalidateJointIndicesCache(); + if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) { const int MAX_SKELETON_DOWNLOAD_ATTEMPTS = 4; // NOTE: we don't want to be as generous as ResourceCache is, we only want 4 attempts if (_skeletonModel->getResourceDownloadAttemptsRemaining() <= 0 || @@ -1105,7 +1146,7 @@ static std::shared_ptr allocateAttachmentModel(bool isSoft, const Rig& ri void Avatar::setAttachmentData(const QVector& attachmentData) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setAttachmentData", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "setAttachmentData", Q_ARG(const QVector, attachmentData)); return; } @@ -1439,8 +1480,7 @@ void Avatar::addToScene(AvatarSharedPointer myHandle, const render::ScenePointer if (scene) { auto nodelist = DependencyManager::get(); if (showAvatars - && !nodelist->isIgnoringNode(getSessionUUID()) - && !nodelist->isRadiusIgnoringNode(getSessionUUID())) { + && !nodelist->isIgnoringNode(getSessionUUID())) { render::Transaction transaction; addToScene(myHandle, scene, transaction); scene->enqueueTransaction(transaction); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1724d42510..89db519abc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -267,6 +267,13 @@ protected: virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter! SkeletonModelPointer _skeletonModel; + + void invalidateJointIndicesCache() const; + void withValidJointIndicesCache(std::function const& worker) const; + mutable QHash _modelJointIndicesCache; + mutable QReadWriteLock _modelJointIndicesCacheLock; + mutable bool _modelJointsCached { false }; + glm::vec3 _skeletonOffset; std::vector> _attachmentModels; std::vector> _attachmentsToRemove; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 2a2817e68b..9651951b46 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -322,20 +322,20 @@ void SkeletonModel::computeBoundingShape() { _boundingCapsuleLocalOffset = invScale * offset; } -void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float scale, float alpha) { +void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& batch, float scale, float alpha) { auto geometryCache = DependencyManager::get(); // draw a blue sphere at the capsule top point glm::vec3 topPoint = _translation + getRotation() * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); batch.setModelTransform(Transform().setTranslation(topPoint).postScale(scale * _boundingCapsuleRadius)); - geometryCache->renderSolidSphereInstance(batch, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); + geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); glm::vec3 axis = topPoint - bottomPoint; batch.setModelTransform(Transform().setTranslation(bottomPoint).postScale(scale * _boundingCapsuleRadius)); - geometryCache->renderSolidSphereInstance(batch, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); + geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points glm::vec3 origin(0.0f); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index db87a37477..e48884c581 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -96,7 +96,7 @@ public: /// \return whether or not the head was found. glm::vec3 getDefaultEyeModelPosition() const; - void renderBoundingCollisionShapes(gpu::Batch& batch, float scale, float alpha); + void renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& batch, float scale, float alpha); float getBoundingCapsuleRadius() const { return _boundingCapsuleRadius; } float getBoundingCapsuleHeight() const { return _boundingCapsuleHeight; } const glm::vec3 getBoundingCapsuleOffset() const { return _boundingCapsuleLocalOffset; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 1631de3307..0fa5d86822 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,27 @@ static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water #define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) +size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients) { + return FACE_TRACKER_INFO_SIZE + numBlendshapeCoefficients * sizeof(float); +} + +size_t AvatarDataPacket::maxJointDataSize(size_t numJoints) { + const size_t validityBitsSize = (size_t)std::ceil(numJoints / (float)BITS_IN_BYTE); + + size_t totalSize = sizeof(uint8_t); // numJoints + + totalSize += validityBitsSize; // Orientations mask + totalSize += numJoints * sizeof(SixByteQuat); // Orientations + totalSize += validityBitsSize; // Translations mask + totalSize += numJoints * sizeof(SixByteTrans); // Translations + + size_t NUM_FAUX_JOINT = 2; + totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints + + return totalSize; +} + + AvatarData::AvatarData() : SpatiallyNestable(NestableType::Avatar, QUuid()), _handPosition(0.0f), @@ -73,19 +95,6 @@ AvatarData::AvatarData() : setBodyYaw(-90.0f); setBodyRoll(0.0f); - ASSERT(sizeof(AvatarDataPacket::Header) == AvatarDataPacket::HEADER_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarGlobalPosition) == AvatarDataPacket::AVATAR_GLOBAL_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarLocalPosition) == AvatarDataPacket::AVATAR_LOCAL_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarBoundingBox) == AvatarDataPacket::AVATAR_BOUNDING_BOX_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarOrientation) == AvatarDataPacket::AVATAR_ORIENTATION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarScale) == AvatarDataPacket::AVATAR_SCALE_SIZE); - ASSERT(sizeof(AvatarDataPacket::LookAtPosition) == AvatarDataPacket::LOOK_AT_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AudioLoudness) == AvatarDataPacket::AUDIO_LOUDNESS_SIZE); - ASSERT(sizeof(AvatarDataPacket::SensorToWorldMatrix) == AvatarDataPacket::SENSOR_TO_WORLD_SIZE); - ASSERT(sizeof(AvatarDataPacket::AdditionalFlags) == AvatarDataPacket::ADDITIONAL_FLAGS_SIZE); - ASSERT(sizeof(AvatarDataPacket::ParentInfo) == AvatarDataPacket::PARENT_INFO_SIZE); - ASSERT(sizeof(AvatarDataPacket::FaceTrackerInfo) == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); - connect(this, &AvatarData::lookAtSnappingChanged, this, &AvatarData::markIdentityDataChanged); } @@ -170,12 +179,12 @@ float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPositio // we want to track outbound data in this case... -QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { AvatarDataPacket::HasFlags hasFlagsOut; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), - hasFlagsOut, false, false, glm::vec3(0), nullptr, + hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr, &_outboundDataRate); } @@ -190,15 +199,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent lazyInitHeadData(); - QByteArray avatarDataByteArray(udt::MAX_PACKET_SIZE, 0); - unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); - unsigned char* startPosition = destinationBuffer; - // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { AvatarDataPacket::HasFlags packetStateFlags = 0; - memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags)); - return avatarDataByteArray.left(sizeof(packetStateFlags)); + QByteArray avatarDataByteArray(reinterpret_cast(&packetStateFlags), sizeof(packetStateFlags)); + return avatarDataByteArray; } // FIXME - @@ -259,6 +264,15 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent hasJointData = sendAll || !sendMinimum; } + + const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + + (hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getNumSummedBlendshapeCoefficients()) : 0) + + (hasJointData ? AvatarDataPacket::maxJointDataSize(_jointData.size()) : 0); + + QByteArray avatarDataByteArray((int)byteArraySize, 0); + unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); + unsigned char* startPosition = destinationBuffer; + // Leading flags, to indicate how much data is actually included in the packet... AvatarDataPacket::HasFlags packetStateFlags = (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) @@ -479,12 +493,15 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* validityPosition = destinationBuffer; unsigned char validity = 0; int validityBit = 0; + int numValidityBytes = (int)std::ceil(numJoints / (float)BITS_IN_BYTE); #ifdef WANT_DEBUG int rotationSentCount = 0; unsigned char* beforeRotations = destinationBuffer; #endif + destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + if (sentJointDataOut) { sentJointDataOut->resize(_jointData.size()); // Make sure the destination is resized before using it } @@ -504,6 +521,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent #ifdef WANT_DEBUG rotationSentCount++; #endif + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); + if (sentJointDataOut) { auto jointDataOut = *sentJointDataOut; jointDataOut[i].rotation = data.rotation; @@ -513,28 +532,14 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } if (++validityBit == BITS_IN_BYTE) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; validityBit = validity = 0; } } if (validityBit != 0) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; } - validityBit = 0; - validity = *validityPosition++; - for (int i = 0; i < _jointData.size(); i++) { - const JointData& data = _jointData[i]; - if (validity & (1 << validityBit)) { - destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); - } - if (++validityBit == BITS_IN_BYTE) { - validityBit = 0; - validity = *validityPosition++; - } - } - - // joint translation data validityPosition = destinationBuffer; validity = 0; @@ -545,6 +550,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* beforeTranslations = destinationBuffer; #endif + destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + float minTranslation = !distanceAdjust ? AVATAR_MIN_TRANSLATION : getDistanceBasedMinTranslationDistance(viewerPosition); float maxTranslationDimension = 0.0; @@ -563,6 +570,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); + destinationBuffer += + packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + if (sentJointDataOut) { auto jointDataOut = *sentJointDataOut; jointDataOut[i].translation = data.translation; @@ -572,27 +582,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } if (++validityBit == BITS_IN_BYTE) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; validityBit = validity = 0; } } if (validityBit != 0) { - *destinationBuffer++ = validity; - } - - validityBit = 0; - validity = *validityPosition++; - for (int i = 0; i < _jointData.size(); i++) { - const JointData& data = _jointData[i]; - if (validity & (1 << validityBit)) { - destinationBuffer += - packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - } - if (++validityBit == BITS_IN_BYTE) { - validityBit = 0; - validity = *validityPosition++; - } + *validityPosition++ = validity; } // faux joints @@ -625,6 +621,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } int avatarDataSize = destinationBuffer - startPosition; + + if (avatarDataSize > (int)byteArraySize) { + qCCritical(avatars) << "AvatarData::toByteArray buffer overflow"; // We've overflown into the heap + ASSERT(false); + } + return avatarDataByteArray.left(avatarDataSize); } // NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation @@ -1229,7 +1231,7 @@ bool AvatarData::isJointDataValid(int index) const { } if (QThread::currentThread() != thread()) { bool result; - QMetaObject::invokeMethod(const_cast(this), "isJointDataValid", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "isJointDataValid", Q_RETURN_ARG(bool, result), Q_ARG(int, index)); return result; } @@ -1242,7 +1244,7 @@ glm::quat AvatarData::getJointRotation(int index) const { } if (QThread::currentThread() != thread()) { glm::quat result; - QMetaObject::invokeMethod(const_cast(this), "getJointRotation", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointRotation", Q_RETURN_ARG(glm::quat, result), Q_ARG(int, index)); return result; } @@ -1257,7 +1259,7 @@ glm::vec3 AvatarData::getJointTranslation(int index) const { } if (QThread::currentThread() != thread()) { glm::vec3 result; - QMetaObject::invokeMethod(const_cast(this), "getJointTranslation", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointTranslation", Q_RETURN_ARG(glm::vec3, result), Q_ARG(int, index)); return result; } @@ -1268,7 +1270,7 @@ glm::vec3 AvatarData::getJointTranslation(int index) const { glm::vec3 AvatarData::getJointTranslation(const QString& name) const { if (QThread::currentThread() != thread()) { glm::vec3 result; - QMetaObject::invokeMethod(const_cast(this), "getJointTranslation", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointTranslation", Q_RETURN_ARG(glm::vec3, result), Q_ARG(const QString&, name)); return result; } @@ -1346,7 +1348,7 @@ void AvatarData::clearJointData(const QString& name) { bool AvatarData::isJointDataValid(const QString& name) const { if (QThread::currentThread() != thread()) { bool result; - QMetaObject::invokeMethod(const_cast(this), "isJointDataValid", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "isJointDataValid", Q_RETURN_ARG(bool, result), Q_ARG(const QString&, name)); return result; } @@ -1356,7 +1358,7 @@ bool AvatarData::isJointDataValid(const QString& name) const { glm::quat AvatarData::getJointRotation(const QString& name) const { if (QThread::currentThread() != thread()) { glm::quat result; - QMetaObject::invokeMethod(const_cast(this), "getJointRotation", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointRotation", Q_RETURN_ARG(glm::quat, result), Q_ARG(const QString&, name)); return result; } @@ -1366,8 +1368,7 @@ glm::quat AvatarData::getJointRotation(const QString& name) const { QVector AvatarData::getJointRotations() const { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), - "getJointRotations", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointRotations", Q_RETURN_ARG(QVector, result)); return result; } @@ -1382,8 +1383,7 @@ QVector AvatarData::getJointRotations() const { void AvatarData::setJointRotations(QVector jointRotations) { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), - "setJointRotations", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "setJointRotations", Q_ARG(QVector, jointRotations)); } QWriteLocker writeLock(&_jointDataLock); @@ -1400,8 +1400,7 @@ void AvatarData::setJointRotations(QVector jointRotations) { QVector AvatarData::getJointTranslations() const { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), - "getJointTranslations", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointTranslations", Q_RETURN_ARG(QVector, result)); return result; } @@ -1416,8 +1415,7 @@ QVector AvatarData::getJointTranslations() const { void AvatarData::setJointTranslations(QVector jointTranslations) { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), - "setJointTranslations", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "setJointTranslations", Q_ARG(QVector, jointTranslations)); } QWriteLocker writeLock(&_jointDataLock); @@ -1467,12 +1465,12 @@ int AvatarData::getJointIndex(const QString& name) const { return result; } QReadLocker readLock(&_jointDataLock); - return _jointIndices.value(name) - 1; + return _fstJointIndices.value(name) - 1; } QStringList AvatarData::getJointNames() const { QReadLocker readLock(&_jointDataLock); - return _jointNames; + return _fstJointNames; } glm::quat AvatarData::getOrientationOutbound() const { @@ -1627,7 +1625,7 @@ void AvatarData::setDisplayName(const QString& displayName) { QVector AvatarData::getAttachmentData() const { if (QThread::currentThread() != thread()) { QVector result; - QMetaObject::invokeMethod(const_cast(this), "getAttachmentData", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getAttachmentData", Q_RETURN_ARG(QVector, result)); return result; } @@ -1734,14 +1732,14 @@ void AvatarData::setJointMappingsFromNetworkReply() { bool ok; int jointIndex = line.mid(secondSeparatorIndex + 1).trimmed().toInt(&ok); if (ok) { - while (_jointNames.size() < jointIndex + 1) { - _jointNames.append(QString()); + while (_fstJointNames.size() < jointIndex + 1) { + _fstJointNames.append(QString()); } - _jointNames[jointIndex] = jointName; + _fstJointNames[jointIndex] = jointName; } } - for (int i = 0; i < _jointNames.size(); i++) { - _jointIndices.insert(_jointNames.at(i), i + 1); + for (int i = 0; i < _fstJointNames.size(); i++) { + _fstJointIndices.insert(_fstJointNames.at(i), i + 1); } } @@ -1757,6 +1755,24 @@ void AvatarData::sendAvatarDataPacket() { bool cullSmallData = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); auto dataDetail = cullSmallData ? SendAllData : CullSmallData; QByteArray avatarByteArray = toByteArrayStateful(dataDetail); + + int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; + avatarByteArray = toByteArrayStateful(dataDetail, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; + avatarByteArray = toByteArrayStateful(MinimumData, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; + return; + } + } + } + doneEncoding(cullSmallData); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -1795,8 +1811,8 @@ void AvatarData::sendIdentityPacket() { void AvatarData::updateJointMappings() { { QWriteLocker writeLock(&_jointDataLock); - _jointIndices.clear(); - _jointNames.clear(); + _fstJointIndices.clear(); + _fstJointNames.clear(); _jointData.clear(); } @@ -2350,7 +2366,7 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID) { AvatarEntityMap AvatarData::getAvatarEntityData() const { AvatarEntityMap result; if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(const_cast(this), "getAvatarEntityData", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getAvatarEntityData", Q_RETURN_ARG(AvatarEntityMap, result)); return result; } @@ -2361,6 +2377,12 @@ AvatarEntityMap AvatarData::getAvatarEntityData() const { return result; } +void AvatarData::insertDetachedEntityID(const QUuid entityID) { + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityDetached.insert(entityID); + }); +} + void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { // the data is suspect @@ -2391,7 +2413,7 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { AvatarEntityIDs result; if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(const_cast(this), "getAndClearRecentlyDetachedIDs", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getAndClearRecentlyDetachedIDs", Q_RETURN_ARG(AvatarEntityIDs, result)); return result; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index afd4d06791..50c4021403 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -140,35 +140,41 @@ namespace AvatarDataPacket { const HasFlags PACKET_HAS_JOINT_DATA = 1U << 11; const size_t AVATAR_HAS_FLAGS_SIZE = 2; + using SixByteQuat = uint8_t[6]; + using SixByteTrans = uint8_t[6]; + // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. PACKED_BEGIN struct Header { HasFlags packetHasFlags; // state flags, indicated which additional records are included in the packet } PACKED_END; const size_t HEADER_SIZE = 2; + static_assert(sizeof(Header) == HEADER_SIZE, "AvatarDataPacket::Header size doesn't match."); PACKED_BEGIN struct AvatarGlobalPosition { float globalPosition[3]; // avatar's position } PACKED_END; const size_t AVATAR_GLOBAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarGlobalPosition) == AVATAR_GLOBAL_POSITION_SIZE, "AvatarDataPacket::AvatarGlobalPosition size doesn't match."); PACKED_BEGIN struct AvatarBoundingBox { float avatarDimensions[3]; // avatar's bounding box in world space units, but relative to the position. float boundOriginOffset[3]; // offset from the position of the avatar to the origin of the bounding box } PACKED_END; const size_t AVATAR_BOUNDING_BOX_SIZE = 24; + static_assert(sizeof(AvatarBoundingBox) == AVATAR_BOUNDING_BOX_SIZE, "AvatarDataPacket::AvatarBoundingBox size doesn't match."); - - using SixByteQuat = uint8_t[6]; PACKED_BEGIN struct AvatarOrientation { SixByteQuat avatarOrientation; // encodeded and compressed by packOrientationQuatToSixBytes() } PACKED_END; const size_t AVATAR_ORIENTATION_SIZE = 6; + static_assert(sizeof(AvatarOrientation) == AVATAR_ORIENTATION_SIZE, "AvatarDataPacket::AvatarOrientation size doesn't match."); PACKED_BEGIN struct AvatarScale { SmallFloat scale; // avatar's scale, compressed by packFloatRatioToTwoByte() } PACKED_END; const size_t AVATAR_SCALE_SIZE = 2; + static_assert(sizeof(AvatarScale) == AVATAR_SCALE_SIZE, "AvatarDataPacket::AvatarScale size doesn't match."); PACKED_BEGIN struct LookAtPosition { float lookAtPosition[3]; // world space position that eyes are focusing on. @@ -180,11 +186,13 @@ namespace AvatarDataPacket { // POTENTIAL SAVINGS - 12 bytes } PACKED_END; const size_t LOOK_AT_POSITION_SIZE = 12; + static_assert(sizeof(LookAtPosition) == LOOK_AT_POSITION_SIZE, "AvatarDataPacket::LookAtPosition size doesn't match."); PACKED_BEGIN struct AudioLoudness { uint8_t audioLoudness; // current loudness of microphone compressed with packFloatGainToByte() } PACKED_END; const size_t AUDIO_LOUDNESS_SIZE = 1; + static_assert(sizeof(AudioLoudness) == AUDIO_LOUDNESS_SIZE, "AvatarDataPacket::AudioLoudness size doesn't match."); PACKED_BEGIN struct SensorToWorldMatrix { // FIXME - these 20 bytes are only used by viewers if my avatar has "attachments" @@ -199,11 +207,13 @@ namespace AvatarDataPacket { // relative to the avatar position. } PACKED_END; const size_t SENSOR_TO_WORLD_SIZE = 20; + static_assert(sizeof(SensorToWorldMatrix) == SENSOR_TO_WORLD_SIZE, "AvatarDataPacket::SensorToWorldMatrix size doesn't match."); PACKED_BEGIN struct AdditionalFlags { uint8_t flags; // additional flags: hand state, key state, eye tracking } PACKED_END; const size_t ADDITIONAL_FLAGS_SIZE = 1; + static_assert(sizeof(AdditionalFlags) == ADDITIONAL_FLAGS_SIZE, "AvatarDataPacket::AdditionalFlags size doesn't match."); // only present if HAS_REFERENTIAL flag is set in AvatarInfo.flags PACKED_BEGIN struct ParentInfo { @@ -211,6 +221,7 @@ namespace AvatarDataPacket { uint16_t parentJointIndex; } PACKED_END; const size_t PARENT_INFO_SIZE = 18; + static_assert(sizeof(ParentInfo) == PARENT_INFO_SIZE, "AvatarDataPacket::ParentInfo size doesn't match."); // will only ever be included if the avatar has a parent but can change independent of changes to parent info // and so we keep it a separate record @@ -218,6 +229,22 @@ namespace AvatarDataPacket { float localPosition[3]; // parent frame translation of the avatar } PACKED_END; const size_t AVATAR_LOCAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarLocalPosition) == AVATAR_LOCAL_POSITION_SIZE, "AvatarDataPacket::AvatarLocalPosition size doesn't match."); + + const size_t MAX_CONSTANT_HEADER_SIZE = HEADER_SIZE + + AVATAR_GLOBAL_POSITION_SIZE + + AVATAR_BOUNDING_BOX_SIZE + + AVATAR_ORIENTATION_SIZE + + AVATAR_SCALE_SIZE + + LOOK_AT_POSITION_SIZE + + AUDIO_LOUDNESS_SIZE + + SENSOR_TO_WORLD_SIZE + + ADDITIONAL_FLAGS_SIZE + + PARENT_INFO_SIZE + + AVATAR_LOCAL_POSITION_SIZE; + + + // variable length structure follows // only present if IS_FACE_TRACKER_CONNECTED flag is set in AvatarInfo.flags PACKED_BEGIN struct FaceTrackerInfo { @@ -229,8 +256,9 @@ namespace AvatarDataPacket { // float blendshapeCoefficients[numBlendshapeCoefficients]; } PACKED_END; const size_t FACE_TRACKER_INFO_SIZE = 17; + static_assert(sizeof(FaceTrackerInfo) == FACE_TRACKER_INFO_SIZE, "AvatarDataPacket::FaceTrackerInfo size doesn't match."); + size_t maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients); - // variable length structure follows /* struct JointData { uint8_t numJoints; @@ -240,6 +268,7 @@ namespace AvatarDataPacket { SixByteTrans translation[numValidTranslations]; // encodeded and compressed by packFloatVec3ToSignedTwoByteFixed() }; */ + size_t maxJointDataSize(size_t numJoints); } static const float MAX_AVATAR_SCALE = 1000.0f; @@ -389,7 +418,7 @@ public: SendAllData } AvatarDataDetail; - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail); + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false); virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, @@ -590,6 +619,7 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + void insertDetachedEntityID(const QUuid entityID); AvatarEntityIDs getAndClearRecentlyDetachedIDs(); // thread safe @@ -710,8 +740,8 @@ protected: QString _sessionDisplayName { }; bool _lookAtSnappingEnabled { true }; - QHash _jointIndices; ///< 1-based, since zero is returned for missing keys - QStringList _jointNames; ///< in order of depth-first traversal + QHash _fstJointIndices; ///< 1-based, since zero is returned for missing keys + QStringList _fstJointNames; ///< in order of depth-first traversal quint64 _errorLogExpiry; ///< time in future when to log an error diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index e8c37bdaa8..3712080cdb 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -170,13 +170,6 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S removeAvatar(sessionUUID, reason); } -void AvatarHashMap::processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode) { - // read the node id - QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - auto nodeList = DependencyManager::get(); - nodeList->radiusIgnoreNodeBySessionID(sessionUUID, false); -} - void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 21ea8081c7..68fc232685 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -60,7 +60,6 @@ protected slots: void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); - void processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode); protected: AvatarHashMap(); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 8ae33a1b4f..1fda984e78 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -83,6 +83,11 @@ static const QMap& getBlendshapesLookupMap() { return blendshapeLookupMap; } +int HeadData::getNumSummedBlendshapeCoefficients() const { + int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size()); + return maxSize; +} + const QVector& HeadData::getSummedBlendshapeCoefficients() { int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size()); if (_summedBlendshapeCoefficients.size() != maxSize) { diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index c15714bd73..bcc2cacde5 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -57,6 +57,7 @@ public: void setBlendshape(QString name, float val); const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } const QVector& getSummedBlendshapeCoefficients(); + int getNumSummedBlendshapeCoefficients() const; void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { _blendshapeCoefficients = blendshapeCoefficients; } const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 00e49b78db..4caa41c671 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -11,14 +11,16 @@ #include #include +#include + #include #include #include #include -#include #include #include +#include #include #include #include @@ -289,7 +291,7 @@ glm::vec2 CompositorHelper::getReticleMaximumPosition() const { void CompositorHelper::sendFakeMouseEvent() { if (qApp->thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "sendFakeMouseEvent", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "sendFakeMouseEvent"); return; } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 67bbb452ca..e1259fc5fc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,6 +16,7 @@ #include #include +#include #if defined(Q_OS_MAC) #include #endif @@ -41,7 +42,7 @@ #include #include #include - +#include #include "CompositorHelper.h" #include "Logging.h" @@ -55,7 +56,7 @@ out vec4 outFragColor; float sRGBFloatToLinear(float value) { const float SRGB_ELBOW = 0.04045; - + return (value <= SRGB_ELBOW) ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); } @@ -121,10 +122,10 @@ public: PROFILE_SET_THREAD_NAME("Present Thread"); // FIXME determine the best priority balance between this and the main thread... - // It may be dependent on the display plugin being used, since VR plugins should + // It may be dependent on the display plugin being used, since VR plugins should // have higher priority on rendering (although we could say that the Oculus plugin // doesn't need that since it has async timewarp). - // A higher priority here + // A higher priority here setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); @@ -233,7 +234,7 @@ public: // Move the context back to the presentation thread _context->moveToThread(this); - // restore control of the context to the presentation thread and signal + // restore control of the context to the presentation thread and signal // the end of the operation _finishedMainThreadOperation = true; lock.unlock(); @@ -291,7 +292,7 @@ bool OpenGLDisplayPlugin::activate() { if (!RENDER_THREAD) { RENDER_THREAD = _presentThread; } - + // Child classes may override this in order to do things like initialize // libraries, etc if (!internalActivate()) { @@ -411,7 +412,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Shader::makeProgram(*program); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(gpu::State::DepthTest(false)); - state->setBlendFunction(true, + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _overlayPipeline = gpu::Pipeline::create(program, state); @@ -496,16 +497,48 @@ void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { _newFrameQueue.push(newFrame); }); } + void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor) { + renderFromTexture(batch, texture, viewport, scissor, gpu::FramebufferPointer()); +} + +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer copyFbo /*=gpu::FramebufferPointer()*/) { + auto fbo = gpu::FramebufferPointer(); batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setFramebuffer(fbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); + if (copyFbo) { + gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); + gpu::Vec4i sourceRect(scissor.x, scissor.y, scissor.x + scissor.z, scissor.y + scissor.w); + float aspectRatio = (float)scissor.w / (float) scissor.z; // height/width + // scale width first + int xOffset = 0; + int yOffset = 0; + int newWidth = copyFbo->getWidth(); + int newHeight = std::round(aspectRatio * (float) copyFbo->getWidth()); + if (newHeight > copyFbo->getHeight()) { + // ok, so now fill height instead + newHeight = copyFbo->getHeight(); + newWidth = std::round((float)copyFbo->getHeight() / aspectRatio); + xOffset = (copyFbo->getWidth() - newWidth) / 2; + } else { + yOffset = (copyFbo->getHeight() - newHeight) / 2; + } + gpu::Vec4i copyRect(xOffset, yOffset, xOffset + newWidth, yOffset + newHeight); + batch.setFramebuffer(copyFbo); + + batch.resetViewTransform(); + batch.setViewportTransform(copyFboRect); + batch.setStateScissorRect(copyFboRect); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); + batch.blit(fbo, sourceRect, copyFbo, copyRect); + } } void OpenGLDisplayPlugin::updateFrameData() { @@ -686,7 +719,7 @@ void OpenGLDisplayPlugin::resetPresentRate() { // _presentRate = RateCounter<100>(); } -float OpenGLDisplayPlugin::renderRate() const { +float OpenGLDisplayPlugin::renderRate() const { return _renderRate.rate(); } @@ -821,3 +854,53 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); } } + +void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { + auto glBackend = const_cast(*this).getGLBackend(); + withMainThreadContext([&] { + GLuint sourceTexture = glBackend->getTextureID(networkTexture->getGPUTexture()); + GLuint targetTexture = target->texture(); + GLuint fbo[2] {0, 0}; + + // need mipmaps for blitting texture + glGenerateTextureMipmap(sourceTexture); + + // create 2 fbos (one for initial texture, second for scaled one) + glCreateFramebuffers(2, fbo); + + // setup source fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture, 0); + + GLint texWidth = networkTexture->getWidth(); + GLint texHeight = networkTexture->getHeight(); + + // setup destination fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targetTexture, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + + // maintain aspect ratio, filling the width first if possible. If that makes the height too + // much, fill height instead. TODO: only do this when texture changes + GLint newX = 0; + GLint newY = 0; + float aspectRatio = (float)texHeight / (float)texWidth; + GLint newWidth = target->width(); + GLint newHeight = std::round(aspectRatio * (float) target->width()); + if (newHeight > target->height()) { + newHeight = target->height(); + newWidth = std::round((float)target->height() / aspectRatio); + newX = (target->width() - newWidth) / 2; + } else { + newY = (target->height() - newHeight) / 2; + } + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // don't delete the textures! + glDeleteFramebuffers(2, fbo); + *fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + }); +} + diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 7e7889ff47..2f93fa630d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -38,7 +38,7 @@ protected: using Condition = std::condition_variable; public: ~OpenGLDisplayPlugin(); - // These must be final to ensure proper ordering of operations + // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; @@ -79,6 +79,8 @@ public: // Three threads, one for rendering, one for texture transfers, one reserved for the GL driver int getRequiredThreadCount() const override { return 3; } + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override; + protected: friend class PresentThread; @@ -103,7 +105,7 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} - + // Returns true on successful activation of standby session virtual bool activateStandBySession() { return true; } virtual void deactivateSession() {} @@ -111,6 +113,7 @@ protected: // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer fbo); void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor); virtual void updateFrameData(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ea91890f33..b183850e7f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -134,7 +134,7 @@ void HmdDisplayPlugin::customizeContext() { state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - + gpu::Shader::BindingSet bindings; bindings.insert({ "lineData", LINE_DATA_SLOT });; gpu::Shader::makeProgram(*program, bindings); @@ -243,6 +243,8 @@ void HmdDisplayPlugin::internalPresent() { glm::ivec4 viewport = getViewportForSourceSize(sourceSize); glm::ivec4 scissor = viewport; + auto fbo = gpu::FramebufferPointer(); + render([&](gpu::Batch& batch) { if (_monoPreview) { @@ -285,11 +287,15 @@ void HmdDisplayPlugin::internalPresent() { viewport = ivec4(scissorOffset - scaledShiftLeftBy, viewportOffset, viewportSizeX, viewportSizeY); } + // TODO: only bother getting and passing in the hmdPreviewFramebuffer if the camera is on + fbo = DependencyManager::get()->getHmdPreviewFramebuffer(windowSize.x, windowSize.y); + viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor); + renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); }); swapBuffers(); + } else if (_clearPreviewFlag) { QImage image; if (_vsyncEnabled) { @@ -312,7 +318,7 @@ void HmdDisplayPlugin::internalPresent() { _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->setAutoGenerateMips(true); } - + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { @@ -323,7 +329,7 @@ void HmdDisplayPlugin::internalPresent() { } postPreview(); - // If preview is disabled, we need to check to see if the window size has changed + // If preview is disabled, we need to check to see if the window size has changed // and re-render the no-preview message if (_disablePreview) { auto window = _container->getPrimaryWidget(); @@ -510,7 +516,7 @@ void HmdDisplayPlugin::OverlayRenderer::build() { indices = std::make_shared(); //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm - + static const float fov = CompositorHelper::VIRTUAL_UI_TARGET_FOV.y; static const float aspectRatio = CompositorHelper::VIRTUAL_UI_ASPECT_RATIO; static const uint16_t stacks = 128; @@ -672,7 +678,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve _handLasers[1] = info; } }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // 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; } @@ -687,7 +693,7 @@ bool HmdDisplayPlugin::setExtraLaser(HandLaserMode mode, const vec4& color, cons _extraLaserStart = sensorSpaceStart; }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // 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; } @@ -702,7 +708,7 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); batch.setModelTransform(Transform()); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1684c06512..987d3118f7 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include "RenderableEntityItem.h" @@ -380,7 +382,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading // Only create and delete models on the thread that owns the EntityTreeRenderer if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "allocateModel", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "allocateModel", Q_RETURN_ARG(ModelPointer, model), Q_ARG(const QString&, url)); @@ -397,7 +399,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading ModelPointer EntityTreeRenderer::updateModel(ModelPointer model, const QString& newUrl) { // Only create and delete models on the thread that owns the EntityTreeRenderer if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "updateModel", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "updateModel", Q_RETURN_ARG(ModelPointer, model), Q_ARG(ModelPointer, model), Q_ARG(const QString&, newUrl)); @@ -451,6 +453,8 @@ RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(cons void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface) { + auto hoverOverlayInterface = DependencyManager::get().data(); + connect(this, &EntityTreeRenderer::mousePressOnEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); connect(this, &EntityTreeRenderer::mouseMoveOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); connect(this, &EntityTreeRenderer::mouseReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); @@ -460,8 +464,12 @@ void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityS connect(this, &EntityTreeRenderer::clickReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::clickReleaseOnEntity); connect(this, &EntityTreeRenderer::hoverEnterEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); + connect(this, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(createHoverOverlay(const EntityItemID&, const PointerEvent&))); + connect(this, &EntityTreeRenderer::hoverOverEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(this, &EntityTreeRenderer::hoverLeaveEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(this, SIGNAL(hoverLeaveEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(destroyHoverOverlay(const EntityItemID&, const PointerEvent&))); connect(this, &EntityTreeRenderer::enterEntity, entityScriptingInterface, &EntityScriptingInterface::enterEntity); connect(this, &EntityTreeRenderer::leaveEntity, entityScriptingInterface, &EntityScriptingInterface::leaveEntity); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 0547c60364..0b6271a6b1 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -378,7 +378,7 @@ void RenderableModelEntityItem::render(RenderArgs* args) { auto shapeTransform = getTransformToCenter(success); if (success) { batch.setModelTransform(shapeTransform); // we want to include the scale as well - DependencyManager::get()->renderWireCubeInstance(batch, greenColor); + DependencyManager::get()->renderWireCubeInstance(args, batch, greenColor); } return; } diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 10bd70be13..a3c0d9877e 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -13,14 +13,13 @@ #include #include #include +#include #include #include #include "EntitiesRendererLogging.h" #include "RenderableParticleEffectEntityItem.h" -#include "untextured_particle_vert.h" -#include "untextured_particle_frag.h" #include "textured_particle_vert.h" #include "textured_particle_frag.h" @@ -29,6 +28,16 @@ class ParticlePayloadData { public: static const size_t VERTEX_PER_PARTICLE = 4; + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static std::weak_ptr _texturedPipeline; + template struct InterpolationData { T start; @@ -70,9 +79,6 @@ public: offsetof(ParticlePrimitive, uv), gpu::Stream::PER_INSTANCE); } - void setPipeline(PipelinePointer pipeline) { _pipeline = pipeline; } - const PipelinePointer& getPipeline() const { return _pipeline; } - const Transform& getModelTransform() const { return _modelTransform; } void setModelTransform(const Transform& modelTransform) { _modelTransform = modelTransform; } @@ -90,15 +96,15 @@ public: bool getVisibleFlag() const { return _visibleFlag; } void setVisibleFlag(bool visibleFlag) { _visibleFlag = visibleFlag; } - + void render(RenderArgs* args) const { - assert(_pipeline); gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); if (_texture) { batch.setResourceTexture(0, _texture); + } else { + batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); } batch.setModelTransform(_modelTransform); @@ -113,7 +119,6 @@ public: protected: Transform _modelTransform; AABox _bound; - PipelinePointer _pipeline; FormatPointer _vertexFormat { std::make_shared() }; BufferPointer _particleBuffer { std::make_shared() }; BufferView _uniformBuffer; @@ -142,23 +147,49 @@ namespace render { payload->render(args); } } + template <> + const ShapeKey shapeGetShapeKey(const ParticlePayloadData::Pointer& payload) { + return render::ShapeKey::Builder().withCustom(ParticlePayloadData::CUSTOM_PIPELINE_NUMBER).withTranslucent().build(); + } } +uint8_t ParticlePayloadData::CUSTOM_PIPELINE_NUMBER = 0; +std::weak_ptr ParticlePayloadData::_texturedPipeline; + +render::ShapePipelinePointer ParticlePayloadData::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + auto texturedPipeline = _texturedPipeline.lock(); + if (!texturedPipeline) { + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + + auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); + auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); + + auto program = gpu::Shader::createProgram(vertShader, fragShader); + _texturedPipeline = texturedPipeline = gpu::Pipeline::create(program, state); + } + + return std::make_shared(texturedPipeline, nullptr, nullptr, nullptr); +} EntityItemPointer RenderableParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { auto entity = std::make_shared(entityID); entity->setProperties(properties); + + // As we create the first ParticuleSystem entity, let s register its special shapePIpeline factory: + ParticlePayloadData::registerShapePipeline(); + return entity; } RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const EntityItemID& entityItemID) : ParticleEffectEntityItem(entityItemID) { - // lazy creation of particle system pipeline - if (!_untexturedPipeline || !_texturedPipeline) { - createPipelines(); - } } bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& self, @@ -167,7 +198,6 @@ bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& sel _scene = scene; _renderItemId = _scene->allocateID(); auto particlePayloadData = std::make_shared(); - particlePayloadData->setPipeline(_untexturedPipeline); auto renderPayload = std::make_shared(particlePayloadData); render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); @@ -276,47 +306,14 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { if (_texture && _texture->isLoaded()) { payload.setTexture(_texture->getGPUTexture()); - payload.setPipeline(_texturedPipeline); } else { payload.setTexture(nullptr); - payload.setPipeline(_untexturedPipeline); } }); _scene->enqueueTransaction(transaction); } -void RenderableParticleEffectEntityItem::createPipelines() { - if (!_untexturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(untextured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(untextured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _untexturedPipeline = gpu::Pipeline::create(program, state); - } - if (!_texturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _texturedPipeline = gpu::Pipeline::create(program, state); - } -} - void RenderableParticleEffectEntityItem::notifyBoundChanged() { if (!render::Item::isValidID(_renderItemId)) { return; diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 678b582b41..b0d7e1c920 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -34,16 +34,13 @@ protected: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); notifyBoundChanged(); } - void notifyBoundChanged(); + void notifyBoundChanged(); - void createPipelines(); - render::ScenePointer _scene; render::ItemID _renderItemId{ render::Item::INVALID_ITEM_ID }; NetworkTexturePointer _texture; - gpu::PipelinePointer _untexturedPipeline; - gpu::PipelinePointer _texturedPipeline; + }; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 88a5d2b873..1d309a8e14 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -59,12 +59,8 @@ #include "EntityEditPacketSender.h" #include "PhysicalEntitySimulation.h" -gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; -gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; - const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; - /* A PolyVoxEntity has several interdependent parts: @@ -116,6 +112,10 @@ EntityItemPointer RenderablePolyVoxEntityItem::factory(const EntityItemID& entit EntityItemPointer entity{ new RenderablePolyVoxEntityItem(entityID) }; entity->setProperties(properties); std::static_pointer_cast(entity)->initializePolyVox(); + + // As we create the first Polyvox entity, let's register its special shapePipeline factory: + PolyVoxPayload::registerShapePipeline(); + return entity; } @@ -732,35 +732,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - - if (!_pipeline) { - gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); - gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), MATERIAL_GPU_SLOT)); - slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); - slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); - - gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); - gpu::Shader::makeProgram(*program, slotBindings); - - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - PrepareStencil::testMaskDrawShape(*state); - - _pipeline = gpu::Pipeline::create(program, state); - - auto wireframeState = std::make_shared(); - wireframeState->setCullMode(gpu::State::CULL_BACK); - wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); - wireframeState->setFillMode(gpu::State::FILL_LINE); - PrepareStencil::testMaskDrawShape(*wireframeState); - - _wireframePipeline = gpu::Pipeline::create(program, wireframeState); - } if (!_vertexFormat) { auto vf = std::make_shared(); @@ -771,11 +742,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; - // Pick correct Pipeline - bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); - auto pipeline = (wireframe ? _wireframePipeline : _pipeline); - batch.setPipeline(pipeline); - Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); batch.setInputFormat(_vertexFormat); @@ -817,7 +783,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = args->_shapePipeline->pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -848,6 +814,48 @@ void RenderablePolyVoxEntityItem::removeFromScene(const EntityItemPointer& self, render::Item::clearID(_myItem); } +uint8_t PolyVoxPayload::CUSTOM_PIPELINE_NUMBER = 0; + +std::shared_ptr PolyVoxPayload::_pipeline; +std::shared_ptr PolyVoxPayload::_wireframePipeline; + +render::ShapePipelinePointer PolyVoxPayload::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + if (!_pipeline) { + gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); + gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), PolyVoxPayload::MATERIAL_GPU_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); + slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); + + gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); + gpu::Shader::makeProgram(*program, slotBindings); + + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*state); + + _pipeline = gpu::Pipeline::create(program, state); + + auto wireframeState = std::make_shared(); + wireframeState->setCullMode(gpu::State::CULL_BACK); + wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); + wireframeState->setFillMode(gpu::State::FILL_LINE); + PrepareStencil::testMaskDrawShape(*wireframeState); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); + } + + if (key.isWireframe()) { + return std::make_shared(_wireframePipeline, nullptr, nullptr, nullptr); + } else { + return std::make_shared(_pipeline, nullptr, nullptr, nullptr); + } +} + namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload) { return ItemKey::Builder::opaqueShape(); @@ -871,6 +879,10 @@ namespace render { payload->_owner->getRenderableInterface()->render(args); } } + + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload) { + return ShapeKey::Builder().withCustom(PolyVoxPayload::CUSTOM_PIPELINE_NUMBER).build(); + } } @@ -1619,7 +1631,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); - if (!_pipeline || !render::Item::isValidID(_myItem)) { + if (!render::Item::isValidID(_myItem)) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45625ada6d..8f20a7a298 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -28,6 +28,19 @@ class PolyVoxPayload { public: + + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static const int MATERIAL_GPU_SLOT = 3; + static std::shared_ptr _pipeline; + static std::shared_ptr _wireframePipeline; + PolyVoxPayload(EntityItemPointer owner) : _owner(owner), _bounds(AABox()) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; @@ -40,6 +53,7 @@ namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload); template <> const Item::Bound payloadGetBound(const PolyVoxPayload::Pointer& payload); template <> void payloadRender(const PolyVoxPayload::Pointer& payload, RenderArgs* args); + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload); } @@ -168,10 +182,7 @@ private: NetworkTexturePointer _yTexture; NetworkTexturePointer _zTexture; - const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; - static gpu::PipelinePointer _pipeline; - static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 27dd678d91..62ab3377a8 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -128,9 +128,9 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); + geometryCache->renderWireShapeInstance(args, batch, MAPPING[_shape], color, pipeline); } else { - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + geometryCache->renderSolidShapeInstance(args, batch, MAPPING[_shape], color, pipeline); } } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 2d4dd50e88..eda304ef91 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -221,10 +221,10 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { if (getShapeType() == SHAPE_TYPE_SPHERE) { shapeTransform.postScale(SPHERE_ENTITY_SCALE); batch.setModelTransform(shapeTransform); - geometryCache->renderWireSphereInstance(batch, DEFAULT_COLOR); + geometryCache->renderWireSphereInstance(args, batch, DEFAULT_COLOR); } else { batch.setModelTransform(shapeTransform); - geometryCache->renderWireCubeInstance(batch, DEFAULT_COLOR); + geometryCache->renderWireCubeInstance(args, batch, DEFAULT_COLOR); } break; } @@ -554,11 +554,13 @@ void RenderableZoneEntityItemMeta::setProceduralUserData(QString userData) { void RenderableZoneEntityItemMeta::render(RenderArgs* args) { if (!_stage) { - _stage = DependencyManager::get()->getLightStage(); + _stage = args->_scene->getStage(); + assert(_stage); } if (!_backgroundStage) { - _backgroundStage = DependencyManager::get()->getBackgroundStage(); + _backgroundStage = args->_scene->getStage(); + assert(_backgroundStage); } { // Sun diff --git a/libraries/entities-renderer/src/untextured_particle.slf b/libraries/entities-renderer/src/untextured_particle.slf deleted file mode 100644 index 11f25bb693..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slf +++ /dev/null @@ -1,18 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// fragment shader -// -// 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 -// - -in vec4 _color; - -out vec4 outFragColor; - -void main(void) { - outFragColor = _color; -} diff --git a/libraries/entities-renderer/src/untextured_particle.slv b/libraries/entities-renderer/src/untextured_particle.slv deleted file mode 100644 index 85f9d438bf..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slv +++ /dev/null @@ -1,25 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// particle vertex shader -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> - -out vec4 _color; - -void main(void) { - // pass along the color - _color = colorToLinearRGBA(inColor); - - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> -} \ No newline at end of file diff --git a/libraries/entities/src/AddEntityOperator.cpp b/libraries/entities/src/AddEntityOperator.cpp index e86e70dd80..78d986f538 100644 --- a/libraries/entities/src/AddEntityOperator.cpp +++ b/libraries/entities/src/AddEntityOperator.cpp @@ -46,10 +46,8 @@ bool AddEntityOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new entity properties, then add/or update it if (entityTreeElement->bestFitBounds(_newEntityBox)) { - + _tree->addEntityMapEntry(_newEntity); entityTreeElement->addEntityItem(_newEntity); - _tree->setContainingElement(_newEntity->getEntityItemID(), entityTreeElement); - _foundNew = true; keepSearching = false; } else { diff --git a/libraries/entities/src/DeleteEntityOperator.cpp b/libraries/entities/src/DeleteEntityOperator.cpp index 709c281341..cbd0ad7839 100644 --- a/libraries/entities/src/DeleteEntityOperator.cpp +++ b/libraries/entities/src/DeleteEntityOperator.cpp @@ -96,7 +96,7 @@ bool DeleteEntityOperator::preRecursion(const OctreeElementPointer& element) { bool entityDeleted = entityTreeElement->removeEntityItem(theEntity); // remove it from the element assert(entityDeleted); (void)entityDeleted; // quite warning - _tree->setContainingElement(details.entity->getEntityItemID(), NULL); // update or id to element lookup + _tree->clearEntityMapEntry(details.entity->getEntityItemID()); _foundCount++; } } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 23ce097cc2..5996327e87 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -89,7 +89,8 @@ EntityItem::EntityItem(const EntityItemID& entityItemID) : EntityItem::~EntityItem() { // clear out any left-over actions - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; EntitySimulationPointer simulation = entityTree ? entityTree->getSimulation() : nullptr; if (simulation) { clearActions(simulation); @@ -880,8 +881,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Tracking for editing roundtrips here. We will tell our EntityTree that we just got incoming data about // and entity that was edited at some time in the past. The tree will determine how it wants to track this // information. - if (_element && _element->getTree()) { - _element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + if (element && element->getTree()) { + element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); } @@ -2056,7 +2058,8 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; simulation = entityTree ? entityTree->getSimulation() : nullptr; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 7351d49dff..2cefd647cb 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -9,19 +9,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "EntityScriptingInterface.h" + #include #include -#include "EntityScriptingInterface.h" - #include #include -#include "EntityItemID.h" +#include #include #include #include +#include +#include "EntityItemID.h" #include "EntitiesLogging.h" #include "EntityDynamicFactoryInterface.h" #include "EntityDynamicInterface.h" @@ -498,6 +500,11 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { // don't delete other avatar's avatarEntities + // If you actually own the entity but the onwership property is not set because of a domain switch + // The lines below makes sure the entity is deleted once its properties are set. + auto avatarHashMap = DependencyManager::get(); + AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID); + myAvatar->insertDetachedEntityID(id); shouldDelete = false; return; } @@ -1488,7 +1495,7 @@ int EntityScriptingInterface::getJointIndex(const QUuid& entityID, const QString return -1; } int result; - QMetaObject::invokeMethod(_entityTree.get(), "getJointIndex", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(_entityTree.get(), "getJointIndex", Q_RETURN_ARG(int, result), Q_ARG(QUuid, entityID), Q_ARG(QString, name)); return result; } @@ -1498,7 +1505,7 @@ QStringList EntityScriptingInterface::getJointNames(const QUuid& entityID) { return QStringList(); } QStringList result; - QMetaObject::invokeMethod(_entityTree.get(), "getJointNames", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(_entityTree.get(), "getJointNames", Q_RETURN_ARG(QStringList, result), Q_ARG(QUuid, entityID)); return result; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4773f45af7..5e58736477 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -90,13 +90,17 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { if (_simulation) { _simulation->clearEntities(); } - { - QWriteLocker locker(&_entityToElementLock); - foreach(EntityTreeElementPointer element, _entityToElementMap) { - element->cleanupEntities(); + QHash localMap; + localMap.swap(_entityMap); + this->withWriteLock([&] { + foreach(EntityItemPointer entity, localMap) { + EntityTreeElementPointer element = entity->getElement(); + if (element) { + element->cleanupEntities(); + } } - _entityToElementMap.clear(); - } + }); + localMap.clear(); Octree::eraseAllOctreeElements(createNewRoot); resetClientEditStats(); @@ -136,29 +140,24 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { } bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entityID); + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityID); + } + if (!entity) { + return false; + } + return updateEntity(entity, properties, senderNode); +} + +bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& origProperties, + const SharedNodePointer& senderNode) { + EntityTreeElementPointer containingElement = entity->getElement(); if (!containingElement) { return false; } - EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); - if (!existingEntity) { - return false; - } - - return updateEntityWithElement(existingEntity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - return false; - } - return updateEntityWithElement(entity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties, - EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; bool allowLockChange; @@ -190,7 +189,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI bool success; AACube queryCube = entity->getQueryAACube(success); if (!success) { - qCDebug(entities) << "Warning -- failed to get query-cube for" << entity->getID(); + qCWarning(entities) << "failed to get query-cube for" << entity->getID(); } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); @@ -331,9 +330,8 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). - containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID=" + if (!entity->getElement()) { + qCWarning(entities) << "EntityTree::updateEntity() we no longer have a containing element for entityID=" << entity->getEntityItemID(); return false; } @@ -366,7 +364,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // You should not call this on existing entities that are already part of the tree! Call updateEntity() EntityTreeElementPointer containingElement = getContainingElement(entityID); if (containingElement) { - qCDebug(entities) << "UNEXPECTED!!! ----- don't call addEntity() on existing entity items. entityID=" << entityID + qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID << "containingElement=" << containingElement.get(); return result; } @@ -422,7 +420,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntity() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existent entityID=" << entityID; } return; } @@ -430,8 +428,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntity() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existant entity item with entityID=" << entityID; } return; } @@ -478,7 +475,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntities() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entityID=" << entityID; } continue; } @@ -486,8 +483,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntities() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entity item with entityID=" << entityID; } continue; } @@ -975,7 +971,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c const SharedNodePointer& senderNode) { if (!getIsServer()) { - qCDebug(entities) << "UNEXPECTED!!! processEditPacketData() should only be called on a server tree."; + qCWarning(entities) << "EntityTree::processEditPacketData() should only be called on a server tree."; return 0; } @@ -1502,27 +1498,43 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons } EntityTreeElementPointer EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ { - QReadLocker locker(&_entityToElementLock); - EntityTreeElementPointer element = _entityToElementMap.value(entityItemID); - return element; + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityItemID); + } + if (entity) { + return entity->getElement(); + } + return EntityTreeElementPointer(nullptr); } -void EntityTree::setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element) { - QWriteLocker locker(&_entityToElementLock); - if (element) { - _entityToElementMap[entityItemID] = element; - } else { - _entityToElementMap.remove(entityItemID); +void EntityTree::addEntityMapEntry(EntityItemPointer entity) { + EntityItemID id = entity->getEntityItemID(); + QWriteLocker locker(&_entityMapLock); + EntityItemPointer otherEntity = _entityMap.value(id); + if (otherEntity) { + qCWarning(entities) << "EntityTree::addEntityMapEntry() found pre-existing id " << id; + assert(false); + return; } + _entityMap.insert(id, entity); +} + +void EntityTree::clearEntityMapEntry(const EntityItemID& id) { + QWriteLocker locker(&_entityMapLock); + _entityMap.remove(id); } void EntityTree::debugDumpMap() { + // QHash's are implicitly shared, so we make a shared copy and use that instead. + // This way we might be able to avoid both a lock and a true copy. + QHash localMap(_entityMap); qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------"; - QReadLocker locker(&_entityToElementLock); - QHashIterator i(_entityToElementMap); + QHashIterator i(localMap); while (i.hasNext()) { i.next(); - qCDebug(entities) << i.key() << ": " << i.value().get(); + qCDebug(entities) << i.key() << ": " << i.value()->getElement().get(); } qCDebug(entities) << "-----------------------------------------------------"; } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 24e6c364b1..efb8cf81ba 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -119,9 +119,6 @@ public: // use this method if you only know the entityID bool updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // use this method if you have a pointer to the entity (avoid an extra entity lookup) - bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // check if the avatar is a child of this entity, If so set the avatar parentID to null void unhookChildAvatar(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); @@ -183,7 +180,8 @@ public: int processEraseMessageDetails(const QByteArray& buffer, const SharedNodePointer& sourceNode); EntityTreeElementPointer getContainingElement(const EntityItemID& entityItemID) /*const*/; - void setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element); + void addEntityMapEntry(EntityItemPointer entity); + void clearEntityMapEntry(const EntityItemID& id); void debugDumpMap(); virtual void dumpTree() override; virtual void pruneTree() override; @@ -275,9 +273,8 @@ signals: protected: void processRemovedEntities(const DeleteEntityOperator& theOperator); - bool updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& properties, - EntityTreeElementPointer containingElement, - const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); + bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, + const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); static bool findNearPointOperation(const OctreeElementPointer& element, void* extraData); static bool findInSphereOperation(const OctreeElementPointer& element, void* extraData); static bool findInCubeOperation(const OctreeElementPointer& element, void* extraData); @@ -309,8 +306,8 @@ protected: _deletedEntityItemIDs << id; } - mutable QReadWriteLock _entityToElementLock; - QHash _entityToElementMap; + mutable QReadWriteLock _entityMapLock; + QHash _entityMap; EntitySimulationPointer _simulation; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index cce7ee006f..108cb39222 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -885,10 +885,10 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI void EntityTreeElement::cleanupEntities() { withWriteLock([&] { foreach(EntityItemPointer entity, _entityItems) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element // NOTE: We explicitly don't delete the EntityItem here because since we only // access it by smart pointers, when we remove it from the _entityItems // we know that it will be deleted. - //delete entity; entity->_element = NULL; } _entityItems.clear(); @@ -903,6 +903,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { EntityItemPointer& entity = _entityItems[i]; if (entity->getEntityItemID() == id) { foundEntity = true; + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); break; @@ -918,6 +919,7 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { numEntries = _entityItems.removeAll(entity); }); if (numEntries > 0) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; return true; @@ -1001,7 +1003,6 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int if (currentContainingElement.get() != this) { currentContainingElement->removeEntityItem(entityItem); addEntityItem(entityItem); - _myTree->setContainingElement(entityItemID, getThisPointer()); } } } @@ -1032,9 +1033,9 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int // don't add if we've recently deleted.... if (!_myTree->isDeletedEntity(entityItem->getID())) { + _myTree->addEntityMapEntry(entityItem); addEntityItem(entityItem); // add this new entity to this elements entities entityItemID = entityItem->getEntityItemID(); - _myTree->setContainingElement(entityItemID, getThisPointer()); _myTree->postAddEntity(entityItem); if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { entityItem->recordCreationTime(); diff --git a/libraries/entities/src/HoverOverlayInterface.cpp b/libraries/entities/src/HoverOverlayInterface.cpp new file mode 100644 index 0000000000..dcfde41e39 --- /dev/null +++ b/libraries/entities/src/HoverOverlayInterface.cpp @@ -0,0 +1,38 @@ +// +// HoverOverlayInterface.cpp +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-14. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HoverOverlayInterface.h" + +HoverOverlayInterface::HoverOverlayInterface() { + // "hover_overlay" debug log category disabled by default. + // Create your own "qtlogging.ini" file and set your "QT_LOGGING_CONF" environment variable + // if you'd like to enable/disable certain categories. + // More details: http://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories + QLoggingCategory::setFilterRules(QStringLiteral("hifi.hover_overlay.debug=false")); +} + +void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + qCDebug(hover_overlay) << "Creating Hover Overlay on top of entity with ID: " << entityItemID; + setCurrentHoveredEntity(entityItemID); +} + +void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID) { + HoverOverlayInterface::createHoverOverlay(entityItemID, PointerEvent()); +} + +void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + qCDebug(hover_overlay) << "Destroying Hover Overlay on top of entity with ID: " << entityItemID; + setCurrentHoveredEntity(QUuid()); +} + +void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID) { + HoverOverlayInterface::destroyHoverOverlay(entityItemID, PointerEvent()); +} diff --git a/libraries/entities/src/HoverOverlayInterface.h b/libraries/entities/src/HoverOverlayInterface.h new file mode 100644 index 0000000000..a39faab819 --- /dev/null +++ b/libraries/entities/src/HoverOverlayInterface.h @@ -0,0 +1,49 @@ +// +// HoverOverlayInterface.h +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-14. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_HoverOverlayInterface_h +#define hifi_HoverOverlayInterface_h + +#include +#include + +#include +#include + +#include "EntityTree.h" +#include "HoverOverlayLogging.h" + +/**jsdoc +* @namespace HoverOverlay +*/ +class HoverOverlayInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(QUuid currentHoveredEntity READ getCurrentHoveredEntity WRITE setCurrentHoveredEntity) +public: + HoverOverlayInterface(); + + Q_INVOKABLE QUuid getCurrentHoveredEntity() { return _currentHoveredEntity; } + void setCurrentHoveredEntity(const QUuid& entityID) { _currentHoveredEntity = entityID; } + +public slots: + void createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + void createHoverOverlay(const EntityItemID& entityItemID); + void destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + void destroyHoverOverlay(const EntityItemID& entityItemID); + +private: + bool _verboseLogging { true }; + QUuid _currentHoveredEntity{}; +}; + +#endif // hifi_HoverOverlayInterface_h diff --git a/libraries/entities/src/HoverOverlayLogging.cpp b/libraries/entities/src/HoverOverlayLogging.cpp new file mode 100644 index 0000000000..99a2dff782 --- /dev/null +++ b/libraries/entities/src/HoverOverlayLogging.cpp @@ -0,0 +1,14 @@ +// +// HoverOverlayLogging.cpp +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HoverOverlayLogging.h" + +Q_LOGGING_CATEGORY(hover_overlay, "hifi.hover_overlay") diff --git a/libraries/entities/src/HoverOverlayLogging.h b/libraries/entities/src/HoverOverlayLogging.h new file mode 100644 index 0000000000..f0a024bba9 --- /dev/null +++ b/libraries/entities/src/HoverOverlayLogging.h @@ -0,0 +1,20 @@ +// +// HoverOverlayLogging.h +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_HoverOverlayLogging_h +#define hifi_HoverOverlayLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(hover_overlay) + +#endif // hifi_HoverOverlayLogging_h diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index ab97c67aa2..42e5a2ece5 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -51,7 +51,7 @@ MovingEntitiesOperator::~MovingEntitiesOperator() { void MovingEntitiesOperator::addEntityToMoveList(EntityItemPointer entity, const AACube& newCube) { - EntityTreeElementPointer oldContainingElement = _tree->getContainingElement(entity->getEntityItemID()); + EntityTreeElementPointer oldContainingElement = entity->getElement(); AABox newCubeClamped = newCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); if (_wantDebug) { @@ -193,7 +193,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new bounds of this entity then add the entity to the element if (!details.newFound && entityTreeElement->bestFitBounds(details.newCube)) { - EntityItemID entityItemID = details.entity->getEntityItemID(); // remove from the old before adding EntityTreeElementPointer oldElement = details.entity->getElement(); if (oldElement != entityTreeElement) { @@ -201,7 +200,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { oldElement->removeEntityItem(details.entity); } entityTreeElement->addEntityItem(details.entity); - _tree->setContainingElement(entityItemID, entityTreeElement); } _foundNewCount++; //details.newFound = true; // TODO: would be nice to add this optimization diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index ec6051af04..7a5c87187a 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -138,8 +138,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { qCDebug(entities) << " _foundNew=" << _foundNew; } - // If we haven't yet found the old entity, and this subTreeContains our old - // entity, then we need to keep searching. + // If we haven't yet found the old element, and this subTreeContains our old element, + // then we need to keep searching. if (!_foundOld && subtreeContainsOld) { if (_wantDebug) { @@ -169,7 +169,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { // NOTE: we know we haven't yet added it to its new element because _removeOld is true EntityTreeElementPointer oldElement = _existingEntity->getElement(); oldElement->removeEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, NULL); if (oldElement != _containingElement) { qCDebug(entities) << "WARNING entity moved during UpdateEntityOperator recursion"; @@ -187,8 +186,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } - // If we haven't yet found the new entity, and this subTreeContains our new - // entity, then we need to keep searching. + // If we haven't yet found the new element, and this subTreeContains our new element, + // then we need to keep searching. if (!_foundNew && subtreeContainsNew) { if (_wantDebug) { @@ -221,7 +220,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } entityTreeElement->addEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, entityTreeElement); } _foundNew = true; // we found the new element _removeOld = false; // and it has already been removed from the old diff --git a/libraries/gl/src/gl/GLShaders.cpp b/libraries/gl/src/gl/GLShaders.cpp index fd0c6788cb..8ef0198676 100644 --- a/libraries/gl/src/gl/GLShaders.cpp +++ b/libraries/gl/src/gl/GLShaders.cpp @@ -182,7 +182,6 @@ GLuint compileProgram(const std::vector& glshaders, std::string& error) filestream.close(); } */ - delete[] temp; glDeleteProgram(glprogram); return 0; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 5c8f59f20f..85bde4c2f1 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -54,6 +54,7 @@ const std::string TextureCache::KTX_EXT { "ktx" }; static const QString RESOURCE_SCHEME = "resource"; static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame"); +static const QUrl HMD_PREVIEW_FRAME_URL("resource://hmdPreviewFrame"); static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models @@ -198,11 +199,7 @@ gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { std::unique_lock lock(_texturesByHashesMutex); weakPointer = _texturesByHashes[hash]; } - auto result = weakPointer.lock(); - if (result) { - qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); - } - return result; + return weakPointer.lock(); } gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { @@ -223,7 +220,7 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { gpu::TexturePointer result; auto textureCache = DependencyManager::get(); - // Since this can be called on a background thread, there's a chance that the cache + // Since this can be called on a background thread, there's a chance that the cache // will be destroyed by the time we request it if (!textureCache) { return result; @@ -373,7 +370,7 @@ void NetworkTexture::makeRequest() { if (!_sourceIsKTX) { Resource::makeRequest(); return; - } + } // We special-handle ktx requests to run 2 concurrent requests right off the bat PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } }); @@ -912,7 +909,7 @@ void ImageReader::read() { } } - // If we found the texture either because it's in use or via KTX deserialization, + // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. if (texture) { QMetaObject::invokeMethod(resource.data(), "setImage", @@ -961,7 +958,7 @@ void ImageReader::read() { qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; } - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will // be the winner texture = textureCache->cacheTextureByHash(hash, texture); @@ -973,23 +970,44 @@ void ImageReader::read() { Q_ARG(int, texture->getHeight())); } - NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) { gpu::TexturePointer texture; if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) { if (!_spectatorCameraNetworkTexture) { _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); } - texture = _spectatorCameraFramebuffer->getRenderBuffer(0); - if (texture) { - _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); - return _spectatorCameraNetworkTexture; + if (_spectatorCameraFramebuffer) { + texture = _spectatorCameraFramebuffer->getRenderBuffer(0); + if (texture) { + _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _spectatorCameraNetworkTexture; + } + } + } + // FIXME: Generalize this, DRY up this code + if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { + if (!_hmdPreviewNetworkTexture) { + _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + } + if (_hmdPreviewFramebuffer) { + texture = _hmdPreviewFramebuffer->getRenderBuffer(0); + if (texture) { + _hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _hmdPreviewNetworkTexture; + } } } return NetworkTexturePointer(); } +const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, int height) { + if (!_hmdPreviewFramebuffer || _hmdPreviewFramebuffer->getWidth() != width || _hmdPreviewFramebuffer->getHeight() != height) { + _hmdPreviewFramebuffer.reset(gpu::Framebuffer::create("hmdPreview",gpu::Element::COLOR_SRGBA_32, width, height)); + } + return _hmdPreviewFramebuffer; +} + const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { if (!_spectatorCameraFramebuffer) { resetSpectatorCameraFramebuffer(2048, 1024); @@ -1000,4 +1018,5 @@ const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { void TextureCache::resetSpectatorCameraFramebuffer(int width, int height) { _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); _spectatorCameraNetworkTexture.reset(); + emit spectatorCameraFramebufferReset(); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 43edc3593d..f5a0ec5215 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -74,7 +74,7 @@ protected: virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; - + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); @@ -170,6 +170,10 @@ public: NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); void resetSpectatorCameraFramebuffer(int width, int height); + const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); + +signals: + void spectatorCameraFramebufferReset(); protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads @@ -202,6 +206,9 @@ private: NetworkTexturePointer _spectatorCameraNetworkTexture; gpu::FramebufferPointer _spectatorCameraFramebuffer; + + NetworkTexturePointer _hmdPreviewNetworkTexture; + gpu::FramebufferPointer _hmdPreviewFramebuffer; }; #endif // hifi_TextureCache_h diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index e97660da4c..cb0b620a54 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -19,6 +19,8 @@ #include #include +#include + #include "AssetRequest.h" #include "AssetUpload.h" #include "AssetUtils.h" @@ -31,11 +33,12 @@ MessageID AssetClient::_currentID = 0; -AssetClient::AssetClient(const QString& cacheDir) : _cacheDir(cacheDir) { +AssetClient::AssetClient() { + _cacheDir = qApp->property(hifi::properties::APP_LOCAL_DATA_PATH).toString(); setCustomDeleter([](Dependency* dependency){ static_cast(dependency)->deleteLater(); }); - + auto nodeList = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); @@ -105,7 +108,7 @@ void AssetClient::handleAssetMappingOperationReply(QSharedPointerreadPrimitive(&messageID); - + AssetServerError error; message->readPrimitive(&error); @@ -132,13 +135,13 @@ void AssetClient::handleAssetMappingOperationReply(QSharedPointer(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); - + if (!assetServer) { qCWarning(asset_client) << "Could not complete AssetClient operation " << "since you are not currently connected to an asset-server."; return false; } - + return true; } @@ -220,14 +223,14 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); if (assetServer) { - + auto messageID = ++_currentID; - + auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(start) + sizeof(end); auto packet = NLPacket::create(PacketType::AssetGet, payloadSize, true); - + qCDebug(asset_client) << "Requesting data from" << start << "to" << end << "of" << hash << "from asset-server."; - + packet->writePrimitive(messageID); packet->write(QByteArray::fromHex(hash.toLatin1())); @@ -254,10 +257,10 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac if (assetServer) { auto messageID = ++_currentID; - + auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH; auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true); - + packet->writePrimitive(messageID); packet->write(QByteArray::fromHex(hash.toLatin1())); @@ -278,7 +281,7 @@ void AssetClient::handleAssetGetInfoReply(QSharedPointer messag MessageID messageID; message->readPrimitive(&messageID); auto assetHash = message->read(SHA256_HASH_LENGTH); - + AssetServerError error; message->readPrimitive(&error); @@ -367,7 +370,7 @@ void AssetClient::handleAssetGetReply(QSharedPointer message, S callbacks.completeCallback(true, error, message->readAll()); } - + messageCallbackMap.erase(requestIt); } } @@ -478,7 +481,7 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); - + if (assetServer) { auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true); @@ -501,7 +504,7 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); - + if (assetServer) { auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true); @@ -532,7 +535,7 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); - + if (assetServer) { auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true); @@ -644,7 +647,7 @@ MessageID AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); - + if (assetServer) { auto packetList = NLPacketList::create(PacketType::AssetUpload, QByteArray(), true, true); @@ -682,7 +685,7 @@ void AssetClient::handleAssetUploadReply(QSharedPointer message } else { auto hash = message->read(SHA256_HASH_LENGTH); hashString = hash.toHex(); - + qCDebug(asset_client) << "Successfully uploaded asset to asset-server - SHA256 hash is " << hashString; } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 2bc694f367..3f6602b76b 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -49,7 +49,7 @@ using ProgressCallback = std::function class AssetClient : public QObject, public Dependency { Q_OBJECT public: - AssetClient(const QString& cacheDir=""); + AssetClient(); Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path); Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest(); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index f4a3b20fd5..a41283cc0d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -187,6 +187,9 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes emit progress(bytesReceived, bytesTotal); auto now = p_high_resolution_clock::now(); + + // Recording ATP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_ATP_RESOURCE_TOTAL_BYTES, bytesReceived); // if we haven't received the full asset check if it is time to output progress to log // we do so every X seconds to assist with ATP download tracking @@ -201,6 +204,5 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes _lastProgressDebug = now; } - } diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index d0e2721679..26857716e1 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -20,7 +20,7 @@ void FileResourceRequest::doSend() { auto statTracker = DependencyManager::get(); statTracker->incrementStat(STAT_FILE_REQUEST_STARTED); - + int fileSize = 0; QString filename = _url.toLocalFile(); // sometimes on windows, we see the toLocalFile() return null, @@ -53,6 +53,7 @@ void FileResourceRequest::doSend() { } _result = ResourceRequest::Success; + fileSize = file.size(); } } else { @@ -68,6 +69,8 @@ void FileResourceRequest::doSend() { if (_result == ResourceRequest::Success) { statTracker->incrementStat(STAT_FILE_REQUEST_SUCCESS); + // Recording FILE bytes downloaded in stats + statTracker->updateStat(STAT_FILE_RESOURCE_TOTAL_BYTES,fileSize); } else { statTracker->incrementStat(STAT_FILE_REQUEST_FAILED); } diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index 266ea429a0..c6d0370a70 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -201,6 +201,11 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT _sendTimer->start(); emit progress(bytesReceived, bytesTotal); + + // Recording HTTP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_HTTP_RESOURCE_TOTAL_BYTES, bytesReceived); + + } void HTTPResourceRequest::onTimeout() { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index ab61c71952..5d602cc0c0 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -197,8 +197,12 @@ public: *lockWaitOut = (endLock - start); } - std::vector nodes(_nodeHash.size()); - std::transform(_nodeHash.cbegin(), _nodeHash.cend(), nodes.begin(), [](const NodeHash::value_type& it) { + // Size of _nodeHash could change at any time, + // so reserve enough memory for the current size + // and then back insert all the nodes found + std::vector nodes; + nodes.reserve(_nodeHash.size()); + std::transform(_nodeHash.cbegin(), _nodeHash.cend(), std::back_inserter(nodes), [&](const NodeHash::value_type& it) { return it.second; }); auto endTransform = usecTimestampNow(); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e8506e5263..75c97cc205 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -232,7 +233,7 @@ void NodeList::processICEPingPacket(QSharedPointer message) { void NodeList::reset() { if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "reset", Qt::BlockingQueuedConnection); + QMetaObject::invokeMethod(this, "reset"); return; } @@ -240,10 +241,6 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; - // lock and clear our set of radius ignored IDs - _radiusIgnoredSetLock.lockForWrite(); - _radiusIgnoredNodeIDs.clear(); - _radiusIgnoredSetLock.unlock(); // lock and clear our set of ignored IDs _ignoredSetLock.lockForWrite(); _ignoredNodeIDs.clear(); @@ -809,22 +806,6 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN sendPacket(std::move(ignorePacket), *destinationNode); } -void NodeList::radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled) { - if (radiusIgnoreEnabled) { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for insert - // add this nodeID to our set of ignored IDs - _radiusIgnoredNodeIDs.insert(nodeID); - } else { - QWriteLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // write lock for unsafe_erase - _radiusIgnoredNodeIDs.unsafe_erase(nodeID); - } -} - -bool NodeList::isRadiusIgnoringNode(const QUuid& nodeID) const { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for reading - return _radiusIgnoredNodeIDs.find(nodeID) != _radiusIgnoredNodeIDs.cend(); -} - void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { @@ -949,7 +930,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_personalMutedNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true personalMutePacketList->writePrimitive(true); @@ -976,7 +957,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_ignoredNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true ignorePacketList->writePrimitive(true); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 6db760b3ca..b3a12153e5 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -77,8 +77,6 @@ public: void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void enableIgnoreRadius() { ignoreNodesInRadius(true); } void disableIgnoreRadius() { ignoreNodesInRadius(false); } - void radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled); - bool isRadiusIgnoringNode(const QUuid& other) const; void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled); bool isIgnoringNode(const QUuid& nodeID) const; void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); @@ -166,8 +164,6 @@ private: QTimer _keepAlivePingTimer; bool _requestsDomainListData; - mutable QReadWriteLock _radiusIgnoredSetLock; - tbb::concurrent_unordered_set _radiusIgnoredNodeIDs; mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index f07514cd85..fbdfa4b87a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -9,22 +9,24 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ResourceCache.h" + #include #include +#include #include #include #include -#include +#include +#include +#include #include "NetworkAccessManager.h" #include "NetworkLogging.h" #include "NodeList.h" -#include "ResourceCache.h" -#include -#include #define clamp(x, min, max) (((x) < (min)) ? (min) :\ (((x) > (max)) ? (max) :\ @@ -100,6 +102,8 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { QSharedPointer highestResource; Lock lock(_mutex); + bool currentHighestIsFile = false; + for (int i = 0; i < _pendingRequests.size();) { // Clear any freed resources auto resource = _pendingRequests.at(i).lock(); @@ -110,10 +114,12 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { // Check load priority float priority = resource->getLoadPriority(); - if (priority >= highestPriority) { + bool isFile = resource->getURL().scheme() == URL_SCHEME_FILE; + if (priority >= highestPriority && (isFile || !currentHighestIsFile)) { highestPriority = priority; highestIndex = i; highestResource = resource; + currentHighestIsFile = isFile; } i++; } @@ -178,7 +184,7 @@ ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra) { if (QThread::currentThread() != thread()) { // Must be called in thread to ensure getResource returns a valid pointer - QMetaObject::invokeMethod(this, "prefetch", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "prefetch", Q_RETURN_ARG(ScriptableResource*, result), Q_ARG(QUrl, url), Q_ARG(void*, extra)); return result; @@ -301,7 +307,7 @@ QVariantList ResourceCache::getResourceList() { QVariantList list; if (QThread::currentThread() != thread()) { // NOTE: invokeMethod does not allow a const QObject* - QMetaObject::invokeMethod(this, "getResourceList", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "getResourceList", Q_RETURN_ARG(QVariantList, list)); } else { auto resources = _resources.uniqueKeys(); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index e9fe2f1ec1..3ee66f89c1 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -28,7 +28,7 @@ ResourceManager::ResourceManager() { _thread.setObjectName("Resource Manager Thread"); - auto assetClient = DependencyManager::set(_cacheDir); + auto assetClient = DependencyManager::set(); assetClient->moveToThread(&_thread); QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::init); @@ -160,7 +160,3 @@ bool ResourceManager::resourceExists(const QUrl& url) { return false; } -void ResourceManager::setCacheDir(const QString& cacheDir) { - // TODO: check for existence? - _cacheDir = cacheDir; -} diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 4e7cd3d92d..fdfd05736e 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -59,7 +59,6 @@ private: PrefixMap _prefixMap; QMutex _prefixMapLock; - QString _cacheDir; }; #endif diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 39bcb3fe93..3ee86025a2 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -33,6 +33,9 @@ const QString STAT_HTTP_REQUEST_CACHE = "CacheHTTPRequest"; const QString STAT_ATP_MAPPING_REQUEST_STARTED = "StartedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_FAILED = "FailedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_SUCCESS = "SuccessfulATPMappingRequest"; +const QString STAT_HTTP_RESOURCE_TOTAL_BYTES = "HTTPBytesDownloaded"; +const QString STAT_ATP_RESOURCE_TOTAL_BYTES = "ATPBytesDownloaded"; +const QString STAT_FILE_RESOURCE_TOTAL_BYTES = "FILEBytesDownloaded"; class ResourceRequest : public QObject { Q_OBJECT diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index 179e8e6e66..b44c60eba7 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -33,6 +33,7 @@ public: public slots: bool isEnabled() { return !_disabled.get(); } + bool isDisabledSettingSet() const { return _disabled.isSet(); } void disable(bool disable); void logAction(QString action, QJsonObject details = QJsonObject(), JSONCallbackParameters params = JSONCallbackParameters()); @@ -53,7 +54,7 @@ private slots: private: UserActivityLogger(); - Setting::Handle _disabled { "UserActivityLoggerDisabled", false }; + Setting::Handle _disabled { "UserActivityLoggerDisabled", true }; QElapsedTimer _timer; }; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 6c42193e11..848bfd97cf 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -105,7 +105,6 @@ public: UsernameFromIDReply, ViewFrustum, RequestsDomainListData, - ExitingSpaceBubble, PerAvatarGainSet, EntityScriptGetStatus, EntityScriptGetStatusReply, diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 357f8a64d8..a3374a0f47 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -17,6 +17,7 @@ #include +#include #include #include "../NetworkLogging.h" @@ -276,7 +277,7 @@ Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { void Socket::clearConnections() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "clearConnections", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "clearConnections"); return; } diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index fd3e35d28a..77716f671b 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -32,13 +32,6 @@ const btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { if (info.getType() == SHAPE_TYPE_NONE) { return nullptr; } - const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube - if (4.0f * glm::length2(info.getHalfExtents()) < MIN_SHAPE_DIAGONAL_SQUARED) { - // tiny shapes are not supported - // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; - return nullptr; - } - DoubleHashKey key = info.getHash(); ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 7bfdbddbc5..481a2609fc 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -22,9 +22,10 @@ #include #include #include - #include "Plugin.h" +class QOpenGLFramebufferObject; + class QImage; enum Eye { @@ -60,8 +61,12 @@ namespace gpu { using TexturePointer = std::shared_ptr; } +class NetworkTexture; +using NetworkTexturePointer = QSharedPointer; +typedef struct __GLsync *GLsync; + // Stereo display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isStereo returns true class StereoDisplay { public: @@ -78,7 +83,7 @@ public: }; // HMD display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isHmd returns true class HmdDisplay : public StereoDisplay { public: @@ -142,7 +147,7 @@ public: virtual float getTargetFrameRate() const { return 1.0f; } virtual bool hasAsyncReprojection() const { return false; } - /// Returns a boolean value indicating whether the display is currently visible + /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, /// or power-save mode is active. For HMDs it may reflect a sensor indicating /// whether the HMD is being worn @@ -204,10 +209,12 @@ public: // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } virtual bool getSupportsAutoSwitch() { return false; } - + // Hardware specific stats virtual QJsonObject getHardwareStats() const { return QJsonObject(); } + virtual void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) = 0; + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; diff --git a/libraries/plugins/src/plugins/InputConfiguration.cpp b/libraries/plugins/src/plugins/InputConfiguration.cpp index 04b1e3b370..9234ac6585 100644 --- a/libraries/plugins/src/plugins/InputConfiguration.cpp +++ b/libraries/plugins/src/plugins/InputConfiguration.cpp @@ -11,6 +11,8 @@ #include +#include + #include "DisplayPlugin.h" #include "InputPlugin.h" #include "PluginManager.h" @@ -21,7 +23,7 @@ InputConfiguration::InputConfiguration() { QStringList InputConfiguration::inputPlugins() { if (QThread::currentThread() != thread()) { QStringList result; - QMetaObject::invokeMethod(this, "inputPlugins", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "inputPlugins", Q_RETURN_ARG(QStringList, result)); return result; } @@ -42,7 +44,7 @@ QStringList InputConfiguration::inputPlugins() { QStringList InputConfiguration::activeInputPlugins() { if (QThread::currentThread() != thread()) { QStringList result; - QMetaObject::invokeMethod(this, "activeInputPlugins", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "activeInputPlugins", Q_RETURN_ARG(QStringList, result)); return result; } @@ -64,7 +66,7 @@ QStringList InputConfiguration::activeInputPlugins() { QString InputConfiguration::configurationLayout(QString pluginName) { if (QThread::currentThread() != thread()) { QString result; - QMetaObject::invokeMethod(this, "configurationLayout", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "configurationLayout", Q_RETURN_ARG(QString, result), Q_ARG(QString, pluginName)); return result; @@ -81,7 +83,7 @@ QString InputConfiguration::configurationLayout(QString pluginName) { void InputConfiguration::setConfigurationSettings(QJsonObject configurationSettings, QString pluginName) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setConfigurationSettings", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "setConfigurationSettings", Q_ARG(QJsonObject, configurationSettings), Q_ARG(QString, pluginName)); return; @@ -97,7 +99,7 @@ void InputConfiguration::setConfigurationSettings(QJsonObject configurationSetti QJsonObject InputConfiguration::configurationSettings(QString pluginName) { if (QThread::currentThread() != thread()) { QJsonObject result; - QMetaObject::invokeMethod(this, "configurationSettings", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "configurationSettings", Q_RETURN_ARG(QJsonObject, result), Q_ARG(QString, pluginName)); return result; @@ -113,7 +115,7 @@ QJsonObject InputConfiguration::configurationSettings(QString pluginName) { void InputConfiguration::calibratePlugin(QString pluginName) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "calibratePlugin", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "calibratePlugin"); return; } @@ -128,7 +130,7 @@ void InputConfiguration::calibratePlugin(QString pluginName) { bool InputConfiguration::uncalibratePlugin(QString pluginName) { if (QThread::currentThread() != thread()) { bool result; - QMetaObject::invokeMethod(this, "uncalibratePlugin", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "uncalibratePlugin", Q_ARG(bool, result)); return result; } diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp index 5c55c6bb1c..0fbbf1bc8e 100644 --- a/libraries/recording/src/recording/ClipCache.cpp +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -8,6 +8,8 @@ #include +#include + #include "ClipCache.h" #include "impl/PointerClip.h" #include "Logging.h" @@ -37,7 +39,7 @@ ClipCache::ClipCache(QObject* parent) : NetworkClipLoaderPointer ClipCache::getClipLoader(const QUrl& url) { if (QThread::currentThread() != thread()) { NetworkClipLoaderPointer result; - QMetaObject::invokeMethod(this, "getClipLoader", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "getClipLoader", Q_RETURN_ARG(NetworkClipLoaderPointer, result), Q_ARG(const QUrl&, url)); return result; } diff --git a/libraries/render-utils/src/BackgroundStage.cpp b/libraries/render-utils/src/BackgroundStage.cpp index 5c2f55954a..2ea3683c4a 100644 --- a/libraries/render-utils/src/BackgroundStage.cpp +++ b/libraries/render-utils/src/BackgroundStage.cpp @@ -13,6 +13,8 @@ #include +std::string BackgroundStage::_stageName { "BACKGROUND_STAGE"}; + BackgroundStage::Index BackgroundStage::findBackground(const BackgroundPointer& background) const { auto found = _backgroundMap.find(background); if (found != _backgroundMap.end()) { @@ -52,15 +54,15 @@ BackgroundStage::BackgroundPointer BackgroundStage::removeBackground(Index index void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { - const auto& lightingModel = inputs; if (!lightingModel->isBackgroundEnabled()) { return; } - // Background rendering decision - auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + auto backgroundStage = renderContext->_scene->getStage(); + assert(backgroundStage); + model::SunSkyStagePointer background; model::SkyboxPointer skybox; if (backgroundStage->_currentFrame._backgrounds.size()) { @@ -68,11 +70,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, auto background = backgroundStage->getBackground(backgroundId); if (background) { skybox = background->getSkybox(); - } - } else { - skybox = DependencyManager::get()->getDefaultSkybox(); + } } - /* auto backgroundMode = skyStage->getBackgroundMode(); switch (backgroundMode) { @@ -137,4 +136,15 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, } */ -} \ No newline at end of file +} + +BackgroundStageSetup::BackgroundStageSetup() { +} + +void BackgroundStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(BackgroundStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(BackgroundStage::getName(), std::make_shared()); + } +} + diff --git a/libraries/render-utils/src/BackgroundStage.h b/libraries/render-utils/src/BackgroundStage.h index 1dd1651c98..eab7c94f0d 100644 --- a/libraries/render-utils/src/BackgroundStage.h +++ b/libraries/render-utils/src/BackgroundStage.h @@ -15,13 +15,17 @@ #include #include #include +#include #include "LightingModel.h" // Background stage to set up background-related rendering tasks -class BackgroundStage { +class BackgroundStage : public render::Stage { public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + using Index = render::indexed_container::Index; static const Index INVALID_INDEX { render::indexed_container::INVALID_INDEX }; static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } @@ -66,6 +70,15 @@ public: }; using BackgroundStagePointer = std::shared_ptr; +class BackgroundStageSetup { +public: + using JobModel = render::Job::Model; + + BackgroundStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: +}; class DrawBackgroundStage { public: diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 3359b3a12d..44e2bd290b 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -432,9 +432,10 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I batch.setResourceTexture(Lighting, deferredFramebuffer->getLightingTexture()); } - auto deferredLightingEffect = DependencyManager::get(); - assert(deferredLightingEffect->getLightStage()->getNumLights() > 0); - auto lightAndShadow = deferredLightingEffect->getLightStage()->getLightAndShadow(0); + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); + assert(lightStage->getNumLights() > 0); + auto lightAndShadow = lightStage->getLightAndShadow(0); const auto& globalShadow = lightAndShadow.second; if (globalShadow) { batch.setResourceTexture(Shadow, globalShadow->map); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0b4eee125b..2b5fdc1d74 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -99,75 +99,35 @@ void DeferredLightingEffect::init() { loadLightProgram(deferred_light_vert, local_lights_shading_frag, true, _localLight, _localLightLocations); loadLightProgram(deferred_light_vert, local_lights_drawOutline_frag, true, _localLightOutline, _localLightOutlineLocations); - - // Light Stage and clusters - _lightStage = std::make_shared(); - - // Allocate a global light representing the Global Directional light casting shadow (the sun) and the ambient light - _allocatedLights.push_back(std::make_shared()); - model::LightPointer lp = _allocatedLights[0]; - lp->setType(model::Light::SUN); - lp->setDirection(glm::vec3(-1.0f)); - lp->setColor(glm::vec3(1.0f)); - lp->setIntensity(1.0f); - lp->setType(model::Light::SUN); - lp->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset::OLD_TOWN_SQUARE); - - // Add the global light to the light stage (for later shadow rendering) - _globalLights.push_back(_lightStage->addLight(lp)); - _lightStage->addShadow(_globalLights[0]); - - - _backgroundStage = std::make_shared(); - - auto textureCache = DependencyManager::get(); - - { - PROFILE_RANGE(render, "Process Default Skybox"); - auto textureCache = DependencyManager::get(); - - auto skyboxUrl = PathUtils::resourcesPath().toStdString() + "images/Default-Sky-9-cubemap.ktx"; - - _defaultSkyboxTexture = gpu::Texture::unserialize(skyboxUrl); - _defaultSkyboxAmbientTexture = _defaultSkyboxTexture; - - _defaultSkybox->setCubemap(_defaultSkyboxTexture); - } - - - lp->setAmbientIntensity(0.5f); - lp->setAmbientMap(_defaultSkyboxAmbientTexture); - auto irradianceSH = _defaultSkyboxAmbientTexture->getIrradiance(); - if (irradianceSH) { - lp->setAmbientSphere((*irradianceSH)); - } } -void DeferredLightingEffect::setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit) { +void DeferredLightingEffect::setupKeyLightBatch(const RenderArgs* args, gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit) { PerformanceTimer perfTimer("DLE->setupBatch()"); model::LightPointer keySunLight; - if (_lightStage && _lightStage->_currentFrame._sunLights.size()) { - keySunLight = _lightStage->getLight(_lightStage->_currentFrame._sunLights.front()); - } else { - keySunLight = _allocatedLights[_globalLights.front()]; + auto lightStage = args->_scene->getStage(); + if (lightStage && lightStage->_currentFrame._sunLights.size()) { + keySunLight = lightStage->getLight(lightStage->_currentFrame._sunLights.front()); } model::LightPointer keyAmbiLight; - if (_lightStage && _lightStage->_currentFrame._ambientLights.size()) { - keyAmbiLight = _lightStage->getLight(_lightStage->_currentFrame._ambientLights.front()); - } else { - keyAmbiLight = _allocatedLights[_globalLights.front()]; + if (lightStage && lightStage->_currentFrame._ambientLights.size()) { + keyAmbiLight = lightStage->getLight(lightStage->_currentFrame._ambientLights.front()); } - if (lightBufferUnit >= 0) { - batch.setUniformBuffer(lightBufferUnit, keySunLight->getLightSchemaBuffer()); - } - if (ambientBufferUnit >= 0) { - batch.setUniformBuffer(ambientBufferUnit, keyAmbiLight->getAmbientSchemaBuffer()); + if (keySunLight) { + if (lightBufferUnit >= 0) { + batch.setUniformBuffer(lightBufferUnit, keySunLight->getLightSchemaBuffer()); + } } - if (keyAmbiLight->getAmbientMap() && (skyboxCubemapUnit >= 0)) { - batch.setResourceTexture(skyboxCubemapUnit, keyAmbiLight->getAmbientMap()); + if (keyAmbiLight) { + if (ambientBufferUnit >= 0) { + batch.setUniformBuffer(ambientBufferUnit, keyAmbiLight->getAmbientSchemaBuffer()); + } + + if (keyAmbiLight->getAmbientMap() && (skyboxCubemapUnit >= 0)) { + batch.setResourceTexture(skyboxCubemapUnit, keyAmbiLight->getAmbientMap()); + } } } @@ -266,21 +226,6 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo } -void DeferredLightingEffect::setGlobalLight(const model::LightPointer& light) { - /* auto globalLight = _allocatedLights.front(); - globalLight->setDirection(light->getDirection()); - globalLight->setColor(light->getColor()); - globalLight->setIntensity(light->getIntensity()); - globalLight->setAmbientIntensity(light->getAmbientIntensity()); - globalLight->setAmbientSphere(light->getAmbientSphere()); - globalLight->setAmbientMap(light->getAmbientMap());*/ -} - -const model::LightPointer& DeferredLightingEffect::getGlobalLight() const { - return _allocatedLights.front(); -} - - #include model::MeshPointer DeferredLightingEffect::getPointLightMesh() { @@ -483,8 +428,9 @@ void PrepareDeferred::run(const RenderContextPointer& renderContext, const Input // Prepare a fresh Light Frame - auto deferredLightingEffect = DependencyManager::get(); - deferredLightingEffect->getLightStage()->_currentFrame.clear(); + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); + lightStage->_currentFrame.clear(); } @@ -547,8 +493,10 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Global directional light and ambient pass - assert(deferredLightingEffect->getLightStage()->getNumLights() > 0); - auto lightAndShadow = deferredLightingEffect->getLightStage()->getLightAndShadow(0); + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); + assert(lightStage->getNumLights() > 0); + auto lightAndShadow = lightStage->getLightAndShadow(0); const auto& globalShadow = lightAndShadow.second; // Bind the shadow buffer @@ -558,7 +506,8 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, auto& program = deferredLightingEffect->_directionalSkyboxLight; LightLocationsPtr locations = deferredLightingEffect->_directionalSkyboxLightLocations; - const auto& keyLight = deferredLightingEffect->_allocatedLights[deferredLightingEffect->_globalLights.front()]; + + auto keyLight = lightStage->getLight(0); // Setup the global directional pass pipeline { @@ -597,7 +546,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, batch._glUniform4fv(locations->texcoordFrameTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform)); // Setup the global lighting - deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); + deferredLightingEffect->setupKeyLightBatch(args, batch, locations->lightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); batch.draw(gpu::TRIANGLE_STRIP, 4); @@ -749,3 +698,66 @@ void RenderDeferred::run(const RenderContextPointer& renderContext, const Inputs auto config = std::static_pointer_cast(renderContext->jobConfig); config->setGPUBatchRunTime(_gpuTimer->getGPUAverage(), _gpuTimer->getBatchAverage()); } + + + +void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { + + if (!_defaultLight || !_defaultBackground) { + if (!_defaultSkyboxTexture) { + auto textureCache = DependencyManager::get(); + { + PROFILE_RANGE(render, "Process Default Skybox"); + auto textureCache = DependencyManager::get(); + + auto skyboxUrl = PathUtils::resourcesPath().toStdString() + "images/Default-Sky-9-cubemap.ktx"; + + _defaultSkyboxTexture = gpu::Texture::unserialize(skyboxUrl); + _defaultSkyboxAmbientTexture = _defaultSkyboxTexture; + + _defaultSkybox->setCubemap(_defaultSkyboxTexture); + } + } + + auto lightStage = renderContext->_scene->getStage(); + if (lightStage) { + + // Allocate a default global light directional and ambient + auto lp = std::make_shared(); + lp->setType(model::Light::SUN); + lp->setDirection(glm::vec3(-1.0f)); + lp->setColor(glm::vec3(1.0f)); + lp->setIntensity(1.0f); + lp->setType(model::Light::SUN); + lp->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset::OLD_TOWN_SQUARE); + + lp->setAmbientIntensity(0.5f); + lp->setAmbientMap(_defaultSkyboxAmbientTexture); + auto irradianceSH = _defaultSkyboxAmbientTexture->getIrradiance(); + if (irradianceSH) { + lp->setAmbientSphere((*irradianceSH)); + } + + // capture default light + _defaultLight = lp; + + // Add the global light to the light stage (for later shadow rendering) + _defaultLightID = lightStage->addLight(lp); + lightStage->addShadow(_defaultLightID); + } + + auto backgroundStage = renderContext->_scene->getStage(); + if (backgroundStage) { + + auto background = std::make_shared(); + background->setSkybox(_defaultSkybox); + + // capture deault background + _defaultBackground = background; + + // Add the global light to the light stage (for later shadow rendering) + _defaultBackgroundID = backgroundStage->addBackground(_defaultBackground); + } + } +} + diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index d69c72e97d..a4d62ea407 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -47,30 +47,16 @@ class DeferredLightingEffect : public Dependency { public: void init(); - void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); + void setupKeyLightBatch(const RenderArgs* args, gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); void unsetKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); - // update global lighting - void setGlobalLight(const model::LightPointer& light); - const model::LightPointer& getGlobalLight() const; - - const LightStagePointer& getLightStage() { return _lightStage; } - const BackgroundStagePointer& getBackgroundStage() { return _backgroundStage; } - void setShadowMapEnabled(bool enable) { _shadowMapEnabled = enable; }; void setAmbientOcclusionEnabled(bool enable) { _ambientOcclusionEnabled = enable; } bool isAmbientOcclusionEnabled() const { return _ambientOcclusionEnabled; } - model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } - gpu::TexturePointer getDefaultSkyboxTexture() const { return _defaultSkyboxTexture; } - gpu::TexturePointer getDefaultSkyboxAmbientTexture() const { return _defaultSkyboxAmbientTexture; } - private: DeferredLightingEffect() = default; - LightStagePointer _lightStage; - BackgroundStagePointer _backgroundStage; - bool _shadowMapEnabled{ false }; bool _ambientOcclusionEnabled{ false }; @@ -97,15 +83,6 @@ private: LightLocationsPtr _localLightLocations; LightLocationsPtr _localLightOutlineLocations; - using Lights = std::vector; - - Lights _allocatedLights; - std::vector _globalLights; - - model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() }; - gpu::TexturePointer _defaultSkyboxTexture; - gpu::TexturePointer _defaultSkyboxAmbientTexture; - friend class LightClusteringPass; friend class RenderDeferredSetup; friend class RenderDeferredLocals; @@ -195,6 +172,20 @@ protected: gpu::RangeTimerPointer _gpuTimer; }; +class DefaultLightingSetup { +public: + using JobModel = render::Job::Model; + void run(const render::RenderContextPointer& renderContext); + +protected: + model::LightPointer _defaultLight; + LightStage::Index _defaultLightID{ LightStage::INVALID_INDEX }; + model::SunSkyStagePointer _defaultBackground; + BackgroundStage::Index _defaultBackgroundID{ BackgroundStage::INVALID_INDEX }; + model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() }; + gpu::TexturePointer _defaultSkyboxTexture; + gpu::TexturePointer _defaultSkyboxAmbientTexture; +}; #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index dcf90012c1..5f2acff16f 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -29,6 +29,7 @@ #include "gpu/StandardShaderLib.h" #include "model/TextureMap.h" +#include "render/Args.h" #include "standardTransformPNTC_vert.h" #include "standardDrawTexture_frag.h" @@ -455,23 +456,25 @@ _nextID(0) { buildShapes(); GeometryCache::_simpleOpaquePipeline = std::make_shared(getSimplePipeline(false, false, true, false), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { + [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleTransparentPipeline = std::make_shared(getSimplePipeline(false, true, true, false), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { + [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleWirePipeline = std::make_shared(getSimplePipeline(false, false, true, true), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) {}); + [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) {}, nullptr); } GeometryCache::~GeometryCache() { @@ -1938,7 +1941,7 @@ uint32_t toCompactColor(const glm::vec4& color) { static const size_t INSTANCE_COLOR_BUFFER = 0; -void renderInstances(gpu::Batch& batch, const glm::vec4& color, bool isWire, +void renderInstances(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, bool isWire, const render::ShapePipelinePointer& pipeline, GeometryCache::Shape shape) { // Add pipeline to name std::string instanceName = (isWire ? "wire_shapes_" : "solid_shapes_") + std::to_string(shape) + "_" + std::to_string(std::hash()(pipeline)); @@ -1951,9 +1954,9 @@ void renderInstances(gpu::Batch& batch, const glm::vec4& color, bool isWire, } // Add call to named buffer - batch.setupNamedCalls(instanceName, [isWire, pipeline, shape](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + batch.setupNamedCalls(instanceName, [args, isWire, pipeline, shape](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { batch.setPipeline(pipeline->pipeline); - pipeline->prepare(batch); + pipeline->prepare(batch, args); if (isWire) { DependencyManager::get()->renderWireShapeInstances(batch, shape, data.count(), data.buffers[INSTANCE_COLOR_BUFFER]); @@ -1963,28 +1966,28 @@ void renderInstances(gpu::Batch& batch, const glm::vec4& color, bool isWire, }); } -void GeometryCache::renderSolidShapeInstance(gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - renderInstances(batch, color, false, pipeline, shape); +void GeometryCache::renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(args, batch, color, false, pipeline, shape); } -void GeometryCache::renderWireShapeInstance(gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - renderInstances(batch, color, true, pipeline, shape); +void GeometryCache::renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(args, batch, color, true, pipeline, shape); } -void GeometryCache::renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - renderInstances(batch, color, false, pipeline, GeometryCache::Sphere); +void GeometryCache::renderSolidSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(args, batch, color, false, pipeline, GeometryCache::Sphere); } -void GeometryCache::renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - renderInstances(batch, color, true, pipeline, GeometryCache::Sphere); +void GeometryCache::renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(args, batch, color, true, pipeline, GeometryCache::Sphere); } // Enable this in a debug build to cause 'box' entities to iterate through all the // available shape types, both solid and wireframes //#define DEBUG_SHAPES -void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { +void GeometryCache::renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { #ifdef DEBUG_SHAPES static auto startTime = usecTimestampNow(); renderInstances(INSTANCE_NAME, batch, color, pipeline, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { @@ -2018,11 +2021,11 @@ void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& } }); #else - renderInstances(batch, color, false, pipeline, GeometryCache::Cube); + renderInstances(args, batch, color, false, pipeline, GeometryCache::Cube); #endif } -void GeometryCache::renderWireCubeInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { +void GeometryCache::renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(batch, color, true, pipeline, GeometryCache::Cube); + renderInstances(args, batch, color, true, pipeline, GeometryCache::Cube); } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 9853269280..fa558a1151 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -172,46 +172,46 @@ public: void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); - void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), + void renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); - void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, + void renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { - renderSolidShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); + renderSolidShapeInstance(args, batch, shape, glm::vec4(color, 1.0f), pipeline); } - void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), + void renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); - void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, + void renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { - renderWireShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); + renderWireShapeInstance(args, batch, shape, glm::vec4(color, 1.0f), pipeline); } - void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, + void renderSolidSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); - void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec3& color, + void renderSolidSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { - renderSolidSphereInstance(batch, glm::vec4(color, 1.0f), pipeline); + renderSolidSphereInstance(args, batch, glm::vec4(color, 1.0f), pipeline); } - void renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& color, + void renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline); - void renderWireSphereInstance(gpu::Batch& batch, const glm::vec3& color, + void renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { - renderWireSphereInstance(batch, glm::vec4(color, 1.0f), pipeline); + renderWireSphereInstance(args, batch, glm::vec4(color, 1.0f), pipeline); } - void renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& color, + void renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); - void renderSolidCubeInstance(gpu::Batch& batch, const glm::vec3& color, + void renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { - renderSolidCubeInstance(batch, glm::vec4(color, 1.0f), pipeline); + renderSolidCubeInstance(args, batch, glm::vec4(color, 1.0f), pipeline); } - void renderWireCubeInstance(gpu::Batch& batch, const glm::vec4& color, + void renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline); - void renderWireCubeInstance(gpu::Batch& batch, const glm::vec3& color, + void renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { - renderWireCubeInstance(batch, glm::vec4(color, 1.0f), pipeline); + renderWireCubeInstance(args, batch, glm::vec4(color, 1.0f), pipeline); } // Dynamic geometry diff --git a/libraries/render-utils/src/LightClusters.cpp b/libraries/render-utils/src/LightClusters.cpp index e35120eb5b..ab1e194498 100644 --- a/libraries/render-utils/src/LightClusters.cpp +++ b/libraries/render-utils/src/LightClusters.cpp @@ -574,8 +574,8 @@ void LightClusteringPass::run(const render::RenderContextPointer& renderContext, } // From the LightStage and the current frame, update the light cluster Grid - auto deferredLightingEffect = DependencyManager::get(); - auto lightStage = deferredLightingEffect->getLightStage(); + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); _lightClusters->updateLightStage(lightStage); _lightClusters->updateLightFrame(lightStage->_currentFrame, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); diff --git a/libraries/render-utils/src/LightPayload.cpp b/libraries/render-utils/src/LightPayload.cpp index dbdf7129ef..afa17bee19 100644 --- a/libraries/render-utils/src/LightPayload.cpp +++ b/libraries/render-utils/src/LightPayload.cpp @@ -55,7 +55,8 @@ LightPayload::~LightPayload() { void LightPayload::render(RenderArgs* args) { if (!_stage) { - _stage = DependencyManager::get()->getLightStage(); + _stage = args->_scene->getStage(); + assert(_stage); } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { @@ -123,7 +124,8 @@ KeyLightPayload::~KeyLightPayload() { void KeyLightPayload::render(RenderArgs* args) { if (!_stage) { - _stage = DependencyManager::get()->getLightStage(); + _stage = args->_scene->getStage(); + assert(_stage); } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index dd6a046dea..d0e9f2467e 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -13,6 +13,11 @@ #include "LightStage.h" +std::string LightStage::_stageName { "LIGHT_STAGE"}; + +LightStage::LightStage() { +} + LightStage::Shadow::Shadow(model::LightPointer light) : _light{ light}, _frustum{ std::make_shared() } { framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::createShadowmap(MAP_SIZE)); map = framebuffer->getDepthStencilBuffer(); @@ -165,3 +170,14 @@ void LightStage::updateLightArrayBuffer(Index lightId) { } } +LightStageSetup::LightStageSetup() { +} + +void LightStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(LightStage::getName()); + if (!stage) { + stage = std::make_shared(); + renderContext->_scene->resetStage(LightStage::getName(), stage); + } +} + diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index edbdff28fd..f946cf699e 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -14,17 +14,23 @@ #include #include + +#include + +#include + #include - -#include "gpu/Framebuffer.h" - -#include "model/Light.h" +#include +#include class ViewFrustum; // Light stage to set up light-related rendering tasks -class LightStage { +class LightStage : public render::Stage { public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + using Index = render::indexed_container::Index; static const Index INVALID_INDEX { render::indexed_container::INVALID_INDEX }; static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } @@ -93,6 +99,7 @@ public: LightPointer getLight(Index lightId) const { return _lights.get(lightId); } + Index getShadowId(Index lightId) const { if (checkLightId(lightId)) { return _descs[lightId].shadowId; @@ -109,6 +116,7 @@ public: return LightAndShadow(getLight(lightId), getShadow(lightId)); } + LightStage(); Lights _lights; LightMap _lightMap; Descs _descs; @@ -149,5 +157,15 @@ using LightStagePointer = std::shared_ptr; +class LightStageSetup { +public: + using JobModel = render::Job::Model; + + LightStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: +}; + #endif diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index b16134db5f..f6cb55deed 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -259,7 +259,7 @@ void MeshPartPayload::render(RenderArgs* args) { gpu::Batch& batch = *(args->_batch); - auto locations = args->_pipeline->locations; + auto locations = args->_shapePipeline->locations; assert(locations); // Bind the model transform and the skinCLusterMatrices if needed @@ -583,7 +583,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) { } gpu::Batch& batch = *(args->_batch); - auto locations = args->_pipeline->locations; + auto locations = args->_shapePipeline->locations; assert(locations); bindTransform(batch, locations, args->_renderMode); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 67452c5d33..45be09b701 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -870,7 +871,7 @@ bool Model::getRelativeDefaultJointTranslation(int jointIndex, glm::vec3& transl QStringList Model::getJointNames() const { if (QThread::currentThread() != thread()) { QStringList result; - QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(const_cast(this), "getJointNames", Q_RETURN_ARG(QStringList, result)); return result; } diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 42ed0bdad9..6c3a58b7e5 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -76,8 +76,8 @@ void initForwardPipelines(ShapePlumber& plumber); void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel); -void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch); -void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch); +void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args); +void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args); void initOverlay3DPipelines(ShapePlumber& plumber) { auto vertex = gpu::Shader::createVertex(std::string(overlay3D_vert)); @@ -359,7 +359,7 @@ void addPlumberPipeline(ShapePlumber& plumber, } } -void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { +void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args) { // Set a default albedo map batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); @@ -382,13 +382,13 @@ void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { } } -void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { +void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args) { // Set the batch - batchSetter(pipeline, batch); + batchSetter(pipeline, batch, args); // Set the light if (pipeline.locations->lightBufferUnit >= 0) { - DependencyManager::get()->setupKeyLightBatch(batch, + DependencyManager::get()->setupKeyLightBatch(args, batch, pipeline.locations->lightBufferUnit, pipeline.locations->lightAmbientBufferUnit, pipeline.locations->lightAmbientMapUnit); diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index ddb64bc69e..03a2a4f9b1 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -35,8 +35,8 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); - auto lightStage = DependencyManager::get()->getLightStage(); - + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); LightStage::Index globalLightIndex { 0 }; const auto globalLight = lightStage->getLight(globalLightIndex); @@ -68,7 +68,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, std::vector skinnedShapeKeys{}; // Iterate through all inShapes and render the unskinned - args->_pipeline = shadowPipeline; + args->_shapePipeline = shadowPipeline; batch.setPipeline(shadowPipeline->pipeline); for (auto items : inShapes) { if (items.first.isSkinned()) { @@ -79,13 +79,13 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, } // Reiterate to render the skinned - args->_pipeline = shadowSkinnedPipeline; + args->_shapePipeline = shadowSkinnedPipeline; batch.setPipeline(shadowSkinnedPipeline->pipeline); for (const auto& key : skinnedShapeKeys) { renderItems(renderContext, inShapes.at(key)); } - args->_pipeline = nullptr; + args->_shapePipeline = nullptr; args->_batch = nullptr; }); } @@ -140,7 +140,8 @@ void RenderShadowTask::configure(const Config& configuration) { } void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, Output& output) { - auto lightStage = DependencyManager::get()->getLightStage(); + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); const auto globalShadow = lightStage->getShadow(0); // Cache old render args diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index f15b696006..63b18d49b8 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -35,6 +35,10 @@ void SoftAttachmentModel::updateClusterMatrices() { if (!_needsUpdateClusterMatrices) { return; } + if (!isLoaded()) { + return; + } + _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 40b3c85675..1786898e57 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -532,9 +532,11 @@ void DebugSubsurfaceScattering::run(const render::RenderContextPointer& renderCo - - const auto light = DependencyManager::get()->getLightStage()->getLight(0); - + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); + // const auto light = DependencyManager::get()->getLightStage()->getLight(0); + const auto light = lightStage->getLight(0); + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/render-utils/src/UpdateSceneTask.cpp b/libraries/render-utils/src/UpdateSceneTask.cpp new file mode 100644 index 0000000000..2daee5fb5a --- /dev/null +++ b/libraries/render-utils/src/UpdateSceneTask.cpp @@ -0,0 +1,26 @@ +// +// UpdateSceneTask.cpp +// render-utils/src/ +// +// Created by Sam Gateau on 6/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "UpdateSceneTask.h" + +#include +#include "LightStage.h" +#include "BackgroundStage.h" +#include "DeferredLightingEffect.h" + +void UpdateSceneTask::build(JobModel& task, const render::Varying& input, render::Varying& output) { + task.addJob("LightStageSetup"); + task.addJob("BackgroundStageSetup"); + + task.addJob("DefaultLightingSetup"); + + task.addJob("PerformSceneTransaction"); +} + diff --git a/libraries/render-utils/src/UpdateSceneTask.h b/libraries/render-utils/src/UpdateSceneTask.h new file mode 100644 index 0000000000..227ede8ea3 --- /dev/null +++ b/libraries/render-utils/src/UpdateSceneTask.h @@ -0,0 +1,30 @@ +// +// UpdateSceneTask.h +// render-utils/src/ +// +// Created by Sam Gateau on 6/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_UpdateSceneTask_h +#define hifi_UpdateSceneTask_h + +#include +#include + + +class UpdateSceneTask { +public: + using JobModel = render::Task::Model; + + UpdateSceneTask() {} + + void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs); + +}; + + +#endif // hifi_UpdateSceneTask_h diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp index 8f04265226..787ef47282 100644 --- a/libraries/render-utils/src/ZoneRenderer.cpp +++ b/libraries/render-utils/src/ZoneRenderer.cpp @@ -52,13 +52,21 @@ void ZoneRendererTask::build(JobModel& task, const Varying& input, Varying& oupu } void SetupZones::run(const RenderContextPointer& context, const Inputs& inputs) { - - auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + auto backgroundStage = context->_scene->getStage(); + assert(backgroundStage); backgroundStage->_currentFrame.clear(); // call render in the correct order first... render::renderItems(context, inputs); + // Finally add the default lights and background: + auto lightStage = context->_scene->getStage(); + assert(lightStage); + + lightStage->_currentFrame.pushSunLight(0); + lightStage->_currentFrame.pushAmbientLight(0); + + backgroundStage->_currentFrame.pushBackground(0); } const gpu::PipelinePointer& DebugZoneLighting::getKeyLightPipeline() { @@ -130,14 +138,13 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I auto deferredTransform = inputs; - auto lightStage = DependencyManager::get()->getLightStage(); + auto lightStage = context->_scene->getStage(LightStage::getName()); std::vector keyLightStack; if (lightStage && lightStage->_currentFrame._sunLights.size()) { for (auto index : lightStage->_currentFrame._sunLights) { keyLightStack.push_back(lightStage->getLight(index)); } } - keyLightStack.push_back(DependencyManager::get()->getGlobalLight()); std::vector ambientLightStack; if (lightStage && lightStage->_currentFrame._ambientLights.size()) { @@ -145,10 +152,8 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I ambientLightStack.push_back(lightStage->getLight(index)); } } - ambientLightStack.push_back(DependencyManager::get()->getGlobalLight()); - - auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + auto backgroundStage = context->_scene->getStage(BackgroundStage::getName()); std::vector skyboxStack; if (backgroundStage && backgroundStage->_currentFrame._backgrounds.size()) { for (auto index : backgroundStage->_currentFrame._backgrounds) { @@ -157,8 +162,7 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I skyboxStack.push_back(background->getSkybox()); } } - } - skyboxStack.push_back(DependencyManager::get()->getDefaultSkybox()); + } gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/local_lights_shading.slf b/libraries/render-utils/src/local_lights_shading.slf index a4e28a9757..c6310cb079 100644 --- a/libraries/render-utils/src/local_lights_shading.slf +++ b/libraries/render-utils/src/local_lights_shading.slf @@ -34,6 +34,8 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { + _fragColor = vec4(0.0); + // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st; diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index c2e03d4f46..449a3ac22b 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -103,7 +103,7 @@ namespace render { std::shared_ptr _context; std::shared_ptr _blitFramebuffer; - std::shared_ptr _pipeline; + std::shared_ptr _shapePipeline; QSharedPointer _renderData; std::stack _viewFrustums; glm::ivec4 _viewport { 0.0f, 0.0f, 1.0f, 1.0f }; diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index b6f3440d5c..a3175ffdec 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -43,11 +43,12 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons assert(item.getKey().isShape()); auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { - args->_pipeline = shapeContext->pickPipeline(args, key); - if (args->_pipeline) { + args->_shapePipeline = shapeContext->pickPipeline(args, key); + if (args->_shapePipeline) { + args->_shapePipeline->prepareShapeItem(args, key, item); item.render(args); - } - args->_pipeline = nullptr; + } + args->_shapePipeline = nullptr; } else if (key.hasOwnPipeline()) { item.render(args); } else { @@ -109,15 +110,16 @@ void render::renderStateSortShapes(const RenderContextPointer& renderContext, // Then render for (auto& pipelineKey : sortedPipelines) { auto& bucket = sortedShapes[pipelineKey]; - args->_pipeline = shapeContext->pickPipeline(args, pipelineKey); - if (!args->_pipeline) { + args->_shapePipeline = shapeContext->pickPipeline(args, pipelineKey); + if (!args->_shapePipeline) { continue; } for (auto& item : bucket) { + args->_shapePipeline->prepareShapeItem(args, pipelineKey, item); item.render(args); } } - args->_pipeline = nullptr; + args->_shapePipeline = nullptr; for (auto& item : ownPipelineBucket) { item.render(args); } diff --git a/libraries/render/src/render/Engine.cpp b/libraries/render/src/render/Engine.cpp index b3372a9305..5f67d40d17 100644 --- a/libraries/render/src/render/Engine.cpp +++ b/libraries/render/src/render/Engine.cpp @@ -18,6 +18,8 @@ #include #include "EngineStats.h" +#include "SceneTask.h" + #include "Logging.h" using namespace render; diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 0e77b389a0..b8f93c52c3 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -237,3 +237,26 @@ void Scene::resetSelections(const Selections& selections) { } } } + +// Access a particular Stage (empty if doesn't exist) +// Thread safe +StagePointer Scene::getStage(const Stage::Name& name) const { + std::unique_lock lock(_stagesMutex); + auto found = _stages.find(name); + if (found == _stages.end()) { + return StagePointer(); + } else { + return (*found).second; + } + +} + +void Scene::resetStage(const Stage::Name& name, const StagePointer& stage) { + std::unique_lock lock(_stagesMutex); + auto found = _stages.find(name); + if (found == _stages.end()) { + _stages.insert(StageMap::value_type(name, stage)); + } else { + (*found).second = stage; + } +} \ No newline at end of file diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index fc0a8c1fca..199d9ce224 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -14,6 +14,7 @@ #include "Item.h" #include "SpatialTree.h" +#include "Stage.h" #include "Selection.h" namespace render { @@ -110,6 +111,19 @@ public: // Access non-spatialized items (overlays, backgrounds) const ItemIDSet& getNonspatialSet() const { return _masterNonspatialSet; } + + + // Access a particular Stage (empty if doesn't exist) + // Thread safe + StagePointer getStage(const Stage::Name& name) const; + template + std::shared_ptr getStage(const Stage::Name& name = T::getName()) const { + auto stage = getStage(name); + return (stage ? std::static_pointer_cast(stage) : std::shared_ptr()); + } + void resetStage(const Stage::Name& name, const StagePointer& stage); + + protected: // Thread safe elements that can be accessed from anywhere std::atomic _IDAllocator{ 1 }; // first valid itemID will be One @@ -128,7 +142,6 @@ protected: void removeItems(const ItemIDs& ids); void updateItems(const ItemIDs& ids, UpdateFunctors& functors); - // The Selection map mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; @@ -139,6 +152,11 @@ protected: // void appendToSelection(const Selection& selection); // void mergeWithSelection(const Selection& selection); + // The Stage map + mutable std::mutex _stagesMutex; // mutable so it can be used in the thread safe getStage const method + StageMap _stages; + + friend class Engine; }; diff --git a/libraries/render/src/render/SceneTask.cpp b/libraries/render/src/render/SceneTask.cpp new file mode 100644 index 0000000000..56010f246e --- /dev/null +++ b/libraries/render/src/render/SceneTask.cpp @@ -0,0 +1,21 @@ +// +// SceneTask.cpp +// render/src/render +// +// Created by Sam Gateau on 6/14/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "SceneTask.h" + + +using namespace render; + +void PerformSceneTransaction::configure(const Config& config) { +} + +void PerformSceneTransaction::run(const RenderContextPointer& renderContext) { + renderContext->_scene->processTransactionQueue(); +} diff --git a/libraries/render/src/render/SceneTask.h b/libraries/render/src/render/SceneTask.h new file mode 100644 index 0000000000..0cf2dda395 --- /dev/null +++ b/libraries/render/src/render/SceneTask.h @@ -0,0 +1,41 @@ +// +// SceneTask.h +// render/src/render +// +// Created by Sam Gateau on 6/14/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_render_SceneTask_h +#define hifi_render_SceneTask_h + +#include "Engine.h" + +namespace render { + + class PerformSceneTransactionConfig : public Job::Config { + Q_OBJECT + public: + signals: + void dirty(); + + protected: + }; + + class PerformSceneTransaction { + public: + using Config = PerformSceneTransactionConfig; + using JobModel = Job::Model; + + void configure(const Config& config); + void run(const RenderContextPointer& renderContext); + protected: + }; + + +} + +#endif // hifi_render_SceneTask_h diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index d51d7f8cb6..c83c0b44fc 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -17,18 +17,34 @@ using namespace render; -void ShapePipeline::prepare(gpu::Batch& batch) { - if (batchSetter) { - batchSetter(*this, batch); +ShapePipeline::CustomFactoryMap ShapePipeline::_globalCustomFactoryMap; + +ShapePipeline::CustomKey ShapePipeline::registerCustomShapePipelineFactory(CustomFactory factory) { + ShapePipeline::CustomKey custom = (ShapePipeline::CustomKey) _globalCustomFactoryMap.size() + 1; + _globalCustomFactoryMap[custom] = factory; + return custom; +} + + +void ShapePipeline::prepare(gpu::Batch& batch, RenderArgs* args) { + if (_batchSetter) { + _batchSetter(*this, batch, args); } } +void ShapePipeline::prepareShapeItem(RenderArgs* args, const ShapeKey& key, const Item& shape) { + if (_itemSetter) { + _itemSetter(*this, args, shape); + } +} + + ShapeKey::Filter::Builder::Builder() { _mask.set(OWN_PIPELINE); _mask.set(INVALID); } -void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) { +void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) const { // Iterate over all keys if (bit < (int)ShapeKey::FlagBit::NUM_FLAGS) { addPipelineHelper(filter, key, bit + 1, pipeline); @@ -48,12 +64,12 @@ void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit } void ShapePlumber::addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { - addPipeline(Filter{key}, program, state, batchSetter); + BatchSetter batchSetter, ItemSetter itemSetter) { + addPipeline(Filter{key}, program, state, batchSetter, itemSetter); } void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { + BatchSetter batchSetter, ItemSetter itemSetter) { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), Slot::BUFFER::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("skinClusterBuffer"), Slot::BUFFER::SKINNING)); @@ -90,7 +106,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p ShapeKey key{filter._flags}; auto gpuPipeline = gpu::Pipeline::create(program, state); - auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter); + auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter, itemSetter); addPipelineHelper(filter, key, 0, shapePipeline); } @@ -103,23 +119,34 @@ const ShapePipelinePointer ShapePlumber::pickPipeline(RenderArgs* args, const Ke const auto& pipelineIterator = _pipelineMap.find(key); if (pipelineIterator == _pipelineMap.end()) { - // The first time we can't find a pipeline, we should log it + // The first time we can't find a pipeline, we should try things to solve that if (_missingKeys.find(key) == _missingKeys.end()) { - _missingKeys.insert(key); - qCDebug(renderlogging) << "Couldn't find a pipeline for" << key; + if (key.isCustom()) { + auto factoryIt = ShapePipeline::_globalCustomFactoryMap.find(key.getCustom()); + if ((factoryIt != ShapePipeline::_globalCustomFactoryMap.end()) && (factoryIt)->second) { + // found a factory for the custom key, can now generate a shape pipeline for this case: + addPipelineHelper(Filter(key), key, 0, (factoryIt)->second(*this, key)); + + return pickPipeline(args, key); + } else { + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a custom pipeline factory for " << key.getCustom() << " key is: " << key; + } + } + + _missingKeys.insert(key); + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a pipeline for" << key; } return PipelinePointer(nullptr); } PipelinePointer shapePipeline(pipelineIterator->second); - auto& batch = args->_batch; // Setup the one pipeline (to rule them all) - batch->setPipeline(shapePipeline->pipeline); + args->_batch->setPipeline(shapePipeline->pipeline); // Run the pipeline's BatchSetter on the passed in batch - if (shapePipeline->batchSetter) { - shapePipeline->batchSetter(*shapePipeline, *batch); + if (shapePipeline->_batchSetter) { + shapePipeline->_batchSetter(*shapePipeline, *(args->_batch), args); } return shapePipeline; diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 434c909198..13daac2db7 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -19,6 +19,8 @@ #include "Args.h" namespace render { +class Item; +class ShapePlumber; class ShapeKey { public: @@ -38,7 +40,19 @@ public: OWN_PIPELINE, INVALID, + CUSTOM_0, + CUSTOM_1, + CUSTOM_2, + CUSTOM_3, + CUSTOM_4, + CUSTOM_5, + CUSTOM_6, + CUSTOM_7, + NUM_FLAGS, // Not a valid flag + + CUSTOM_MASK = (0xFF << CUSTOM_0), + }; using Flags = std::bitset; @@ -73,6 +87,8 @@ public: Builder& withOwnPipeline() { _flags.set(OWN_PIPELINE); return (*this); } Builder& invalidate() { _flags.set(INVALID); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); return (*this); } + static const ShapeKey ownPipeline() { return Builder().withOwnPipeline(); } static const ShapeKey invalid() { return Builder().invalidate(); } @@ -127,6 +143,9 @@ public: Builder& withCullFace() { _flags.reset(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } Builder& withoutCullFace() { _flags.set(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); _mask |= (CUSTOM_MASK); return (*this); } + Builder& withoutCustom() { _flags &= (~CUSTOM_MASK); _mask |= (CUSTOM_MASK); return (*this); } + protected: friend class Filter; Flags _flags{0}; @@ -155,6 +174,9 @@ public: bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } bool isValid() const { return !_flags[INVALID]; } + uint8_t getCustom() const { return (_flags.to_ulong() & CUSTOM_MASK) >> CUSTOM_0; } + bool isCustom() const { return (_flags.to_ulong() & CUSTOM_MASK); } + // Comparator for use in stl containers class Hash { public: @@ -240,22 +262,39 @@ public: }; using LocationsPointer = std::shared_ptr; - using BatchSetter = std::function; + using BatchSetter = std::function; - ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter) : - pipeline(pipeline), locations(locations), batchSetter(batchSetter) {} + using ItemSetter = std::function; - // Normally, a pipeline is accessed thorugh pickPipeline. If it needs to be set manually, + ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter, ItemSetter itemSetter) : + pipeline(pipeline), + locations(locations), + _batchSetter(batchSetter), + _itemSetter(itemSetter) {} + + // Normally, a pipeline is accessed through pickPipeline. If it needs to be set manually, // after calling setPipeline this method should be called to prepare the pipeline with default buffers. - void prepare(gpu::Batch& batch); + void prepare(gpu::Batch& batch, Args* args); gpu::PipelinePointer pipeline; std::shared_ptr locations; + void prepareShapeItem(Args* args, const ShapeKey& key, const Item& shape); + protected: friend class ShapePlumber; - BatchSetter batchSetter; + BatchSetter _batchSetter; + ItemSetter _itemSetter; +public: + using CustomKey = uint8_t; + using CustomFactory = std::function (const ShapePlumber& plumber, const ShapeKey& key)>; + using CustomFactoryMap = std::map; + + static CustomFactoryMap _globalCustomFactoryMap; + + static CustomKey registerCustomShapePipelineFactory(CustomFactory factory); + }; using ShapePipelinePointer = std::shared_ptr; @@ -270,22 +309,24 @@ public: using Locations = Pipeline::Locations; using LocationsPointer = Pipeline::LocationsPointer; using BatchSetter = Pipeline::BatchSetter; + using ItemSetter = Pipeline::ItemSetter; void addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); void addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); const PipelinePointer pickPipeline(RenderArgs* args, const Key& key) const; protected: - void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline); - PipelineMap _pipelineMap; + void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline) const; + mutable PipelineMap _pipelineMap; private: mutable std::unordered_set _missingKeys; }; + using ShapePlumberPointer = std::shared_ptr; } diff --git a/libraries/render/src/render/Stage.cpp b/libraries/render/src/render/Stage.cpp new file mode 100644 index 0000000000..1ee9b1d6ff --- /dev/null +++ b/libraries/render/src/render/Stage.cpp @@ -0,0 +1,26 @@ +// +// Stage.cpp +// render/src/render +// +// Created by Sam Gateau on 6/14/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "Stage.h" + + +using namespace render; + + + +Stage::~Stage() { + +} + +Stage::Stage() : + _name() +{ +} + diff --git a/libraries/render/src/render/Stage.h b/libraries/render/src/render/Stage.h new file mode 100644 index 0000000000..5145810671 --- /dev/null +++ b/libraries/render/src/render/Stage.h @@ -0,0 +1,38 @@ +// +// Stage.h +// render/src/render +// +// Created by Sam Gateau on 6/14/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_render_Stage_h +#define hifi_render_Stage_h + +#include +#include +#include + +namespace render { + + class Stage { + public: + using Name = std::string; + + Stage(); + virtual ~Stage(); + + protected: + Name _name; + }; + + using StagePointer = std::shared_ptr; + + using StageMap = std::map; + +} + +#endif // hifi_render_Stage_h diff --git a/libraries/render/src/task/Config.cpp b/libraries/render/src/task/Config.cpp index 0e630311f6..b378237c9c 100644 --- a/libraries/render/src/task/Config.cpp +++ b/libraries/render/src/task/Config.cpp @@ -10,9 +10,12 @@ // #include "Config.h" -#include "Task.h" #include +#include + +#include "Task.h" + using namespace task; void JobConfig::setPresetList(const QJsonObject& object) { @@ -58,7 +61,7 @@ void TaskConfig::transferChildrenConfigs(QConfigPointer source) { void TaskConfig::refresh() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "refresh", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "refresh"); return; } diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index ecaffaf35c..28bf5ed163 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -13,6 +13,8 @@ #include +#include + #include "ScriptAudioInjector.h" #include "ScriptEngineLogging.h" @@ -32,7 +34,7 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound if (QThread::currentThread() != thread()) { ScriptAudioInjector* injector = NULL; - QMetaObject::invokeMethod(this, "playSound", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "playSound", Q_RETURN_ARG(ScriptAudioInjector*, injector), Q_ARG(SharedSoundPointer, sound), Q_ARG(const AudioInjectorOptions&, injectorOptions)); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 7583f562e6..b51e9cd529 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -98,7 +99,7 @@ void RecordingScriptingInterface::loadRecording(const QString& url, QScriptValue void RecordingScriptingInterface::startPlaying() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "startPlaying", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "startPlaying"); return; } @@ -115,7 +116,7 @@ void RecordingScriptingInterface::setPlayerAudioOffset(float audioOffset) { void RecordingScriptingInterface::setPlayerTime(float time) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPlayerTime", Qt::BlockingQueuedConnection, Q_ARG(float, time)); + BLOCKING_INVOKE_METHOD(this, "setPlayerTime", Q_ARG(float, time)); return; } _player->seek(time); @@ -147,7 +148,7 @@ void RecordingScriptingInterface::setPlayerUseSkeletonModel(bool useSkeletonMode void RecordingScriptingInterface::pausePlayer() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "pausePlayer", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "pausePlayer"); return; } _player->pause(); @@ -155,7 +156,7 @@ void RecordingScriptingInterface::pausePlayer() { void RecordingScriptingInterface::stopPlaying() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stopPlaying", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "stopPlaying"); return; } _player->stop(); @@ -176,7 +177,7 @@ void RecordingScriptingInterface::startRecording() { } if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "startRecording", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "startRecording"); return; } @@ -199,7 +200,7 @@ QString RecordingScriptingInterface::getDefaultRecordingSaveDirectory() { void RecordingScriptingInterface::saveRecording(const QString& filename) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "saveRecording", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "saveRecording", Q_ARG(QString, filename)); return; } @@ -220,7 +221,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr if (QThread::currentThread() != thread()) { bool result; - QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "saveRecordingToAsset", Q_RETURN_ARG(bool, result), Q_ARG(QScriptValue, getClipAtpUrl)); return result; @@ -257,7 +258,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr void RecordingScriptingInterface::loadLastRecording() { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadLastRecording", Qt::BlockingQueuedConnection); + BLOCKING_INVOKE_METHOD(this, "loadLastRecording"); return; } diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index c0ad2debd9..516f62401f 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -21,7 +21,7 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c // when the script goes down we want to cleanup the injector QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, Qt::DirectConnection); - + return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -29,10 +29,10 @@ void injectorFromScriptValue(const QScriptValue& object, ScriptAudioInjector*& o out = qobject_cast(object.toQObject()); } -ScriptAudioInjector::ScriptAudioInjector(AudioInjector* injector) : +ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : _injector(injector) { - QObject::connect(injector, &AudioInjector::finished, this, &ScriptAudioInjector::finished); + QObject::connect(injector.data(), &AudioInjector::finished, this, &ScriptAudioInjector::finished); } ScriptAudioInjector::~ScriptAudioInjector() { @@ -44,5 +44,5 @@ ScriptAudioInjector::~ScriptAudioInjector() { void ScriptAudioInjector::stopInjectorImmediately() { qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stopAndDeleteLater(); + _injector->stop(); } diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index 4de12af62c..4c2871dd34 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -18,31 +18,31 @@ class ScriptAudioInjector : public QObject { Q_OBJECT - + Q_PROPERTY(bool playing READ isPlaying) Q_PROPERTY(float loudness READ getLoudness) Q_PROPERTY(AudioInjectorOptions options WRITE setOptions READ getOptions) public: - ScriptAudioInjector(AudioInjector* injector); + ScriptAudioInjector(const AudioInjectorPointer& injector); ~ScriptAudioInjector(); public slots: void restart() { _injector->restart(); } void stop() { _injector->stop(); } - + const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } - + float getLoudness() const { return _injector->getLoudness(); } bool isPlaying() const { return _injector->isPlaying(); } - + signals: void finished(); - + protected slots: void stopInjectorImmediately(); private: - QPointer _injector; - + AudioInjectorPointer _injector; + friend QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* const& in); }; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 11bb044d72..3a876a0e0a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -36,6 +36,7 @@ #include +#include #include #include #include @@ -436,12 +437,12 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { _fileNameString = url.toString(); _isReloading = reload; - // Check that script has a supported file extension + // Check that script has a supported file extension if (!hasValidScriptSuffix(_fileNameString)) { scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type"); emit errorLoadingScript(_fileNameString); return; - } + } const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get(); @@ -964,7 +965,7 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber; #endif - QMetaObject::invokeMethod(this, "evaluate", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "evaluate", Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, sourceCode), Q_ARG(const QString&, fileName), @@ -1820,7 +1821,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac clearExceptions(); } } else { - scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); + scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 72392ac376..57b04eeb82 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -12,6 +12,7 @@ #include +#include #include #include #include @@ -452,7 +453,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL bool activateMainWindow, bool reload) { if (thread() != QThread::currentThread()) { ScriptEngine* result { nullptr }; - QMetaObject::invokeMethod(this, "loadScript", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ScriptEngine*, result), + BLOCKING_INVOKE_METHOD(this, "loadScript", Q_RETURN_ARG(ScriptEngine*, result), Q_ARG(QUrl, scriptFilename), Q_ARG(bool, isUserLoaded), Q_ARG(bool, loadScriptFromEditor), diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 1fe9e2f83d..0636411f51 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -19,6 +19,7 @@ #include "PathUtils.h" #include #include // std::once +#include "shared/GlobalAppProperties.h" const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC @@ -34,12 +35,8 @@ QString PathUtils::getAppDataPath() { return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; } -QString PathUtils::getAppLocalDataPath(const QString& overridePath /* = "" */) { - static QString overriddenPath = ""; - // set the overridden path if one was passed in - if (!overridePath.isEmpty()) { - overriddenPath = overridePath; - } +QString PathUtils::getAppLocalDataPath() { + QString overriddenPath = qApp->property(hifi::properties::APP_LOCAL_DATA_PATH).toString(); // return overridden path if set if (!overriddenPath.isEmpty()) { return overriddenPath; diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 42dd09c8b0..14eb81dd9a 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -28,7 +28,7 @@ public: static const QString& resourcesPath(); static QString getAppDataPath(); - static QString getAppLocalDataPath(const QString& overridePath = ""); + static QString getAppLocalDataPath(); static QString getAppDataFilePath(const QString& filename); static QString getAppLocalDataFilePath(const QString& filename); diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index f1915a9d6a..6093cd3c8a 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -52,6 +52,7 @@ public: Browsable, Slider, Spinner, + SpinnerSlider, Checkbox, Button, ComboBox, @@ -254,6 +255,15 @@ public: Type getType() override { return Spinner; } }; +class SpinnerSliderPreference : public FloatPreference { + Q_OBJECT +public: + SpinnerSliderPreference(const QString& category, const QString& name, Getter getter, Setter setter) + : FloatPreference(category, name, getter, setter) { } + + Type getType() override { return SpinnerSlider; } +}; + class IntSpinnerPreference : public IntPreference { Q_OBJECT public: diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index 258d1f8491..341a4cb101 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -107,6 +107,7 @@ namespace Setting { } bool isSet() const { + maybeInit(); return _isSet; } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 583bceeaf2..496e94f8bd 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -15,6 +15,9 @@ #include "NumericalConstants.h" // for MILLIMETERS_PER_METER +// Bullet doesn't support arbitrarily small shapes +const float MIN_HALF_EXTENT = 0.005f; // 0.5 cm + void ShapeInfo::clear() { _url.clear(); _pointCollection.clear(); @@ -26,14 +29,20 @@ void ShapeInfo::clear() { } void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { + _url = ""; _type = type; - _halfExtents = halfExtents; + setHalfExtents(halfExtents); switch(type) { case SHAPE_TYPE_NONE: _halfExtents = glm::vec3(0.0f); break; case SHAPE_TYPE_BOX: - case SHAPE_TYPE_SPHERE: + break; + case SHAPE_TYPE_SPHERE: { + float radius = glm::length(halfExtents) / SQUARE_ROOT_OF_3; + radius = glm::max(radius, MIN_HALF_EXTENT); + _halfExtents = glm::vec3(radius); + } break; case SHAPE_TYPE_COMPOUND: case SHAPE_TYPE_STATIC_MESH: @@ -48,14 +57,15 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString void ShapeInfo::setBox(const glm::vec3& halfExtents) { _url = ""; _type = SHAPE_TYPE_BOX; - _halfExtents = halfExtents; + setHalfExtents(halfExtents); _doubleHashKey.clear(); } void ShapeInfo::setSphere(float radius) { _url = ""; _type = SHAPE_TYPE_SPHERE; - _halfExtents = glm::vec3(radius, radius, radius); + radius = glm::max(radius, MIN_HALF_EXTENT); + _halfExtents = glm::vec3(radius); _doubleHashKey.clear(); } @@ -68,6 +78,8 @@ void ShapeInfo::setPointCollection(const ShapeInfo::PointCollection& pointCollec void ShapeInfo::setCapsuleY(float radius, float halfHeight) { _url = ""; _type = SHAPE_TYPE_CAPSULE_Y; + radius = glm::max(radius, MIN_HALF_EXTENT); + halfHeight = glm::max(halfHeight, 0.0f); _halfExtents = glm::vec3(radius, halfHeight, radius); _doubleHashKey.clear(); } @@ -239,3 +251,7 @@ const DoubleHashKey& ShapeInfo::getHash() const { } return _doubleHashKey; } + +void ShapeInfo::setHalfExtents(const glm::vec3& halfExtents) { + _halfExtents = glm::max(halfExtents, glm::vec3(MIN_HALF_EXTENT)); +} diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 17e4703fc2..0ffdf1310d 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -89,6 +89,8 @@ public: const DoubleHashKey& getHash() const; protected: + void setHalfExtents(const glm::vec3& halfExtents); + QUrl _url; // url for model of convex collision hulls PointCollection _pointCollection; TriangleIndices _triangleIndices; diff --git a/libraries/shared/src/ThreadHelpers.cpp b/libraries/shared/src/ThreadHelpers.cpp index 14ae35762b..8f3d16a577 100644 --- a/libraries/shared/src/ThreadHelpers.cpp +++ b/libraries/shared/src/ThreadHelpers.cpp @@ -17,10 +17,6 @@ void moveToNewNamedThread(QObject* object, const QString& name, std::functionsetObjectName(name); - if (priority != QThread::InheritPriority) { - thread->setPriority(priority); - } - QString tempName = name; QObject::connect(thread, &QThread::started, [startCallback] { startCallback(); @@ -32,6 +28,9 @@ void moveToNewNamedThread(QObject* object, const QString& name, std::functionmoveToThread(thread); thread->start(); + if (priority != QThread::InheritPriority) { + thread->setPriority(priority); + } } void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority) { diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h index 6461fa5724..6e024f787a 100644 --- a/libraries/shared/src/ThreadHelpers.h +++ b/libraries/shared/src/ThreadHelpers.h @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -34,4 +35,25 @@ void withLock(QMutex& lock, F function) { void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority = QThread::InheritPriority); void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority = QThread::InheritPriority); +class ConditionalGuard { +public: + void trigger() { + QMutexLocker locker(&_mutex); + _triggered = true; + _condition.wakeAll(); + } + + bool wait(unsigned long time = ULONG_MAX) { + QMutexLocker locker(&_mutex); + if (!_triggered) { + _condition.wait(&_mutex, time); + } + return _triggered; + } +private: + QMutex _mutex; + QWaitCondition _condition; + bool _triggered { false }; +}; + #endif diff --git a/libraries/shared/src/shared/GlobalAppProperties.cpp b/libraries/shared/src/shared/GlobalAppProperties.cpp index b0ba0bf83d..6c9f3f9601 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.cpp +++ b/libraries/shared/src/shared/GlobalAppProperties.cpp @@ -17,6 +17,7 @@ namespace hifi { namespace properties { const char* TEST = "com.highfidelity.test"; const char* TRACING = "com.highfidelity.tracing"; const char* HMD = "com.highfidelity.hmd"; + const char* APP_LOCAL_DATA_PATH = "com.highfidelity.appLocalDataPath"; namespace gl { const char* BACKEND = "com.highfidelity.gl.backend"; diff --git a/libraries/shared/src/shared/GlobalAppProperties.h b/libraries/shared/src/shared/GlobalAppProperties.h index b1811586ba..174be61939 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.h +++ b/libraries/shared/src/shared/GlobalAppProperties.h @@ -19,6 +19,7 @@ namespace hifi { namespace properties { extern const char* TEST; extern const char* TRACING; extern const char* HMD; + extern const char* APP_LOCAL_DATA_PATH; namespace gl { extern const char* BACKEND; diff --git a/libraries/shared/src/shared/QtHelpers.cpp b/libraries/shared/src/shared/QtHelpers.cpp new file mode 100644 index 0000000000..1ce1c3e07c --- /dev/null +++ b/libraries/shared/src/shared/QtHelpers.cpp @@ -0,0 +1,58 @@ +// +// Created by Bradley Austin Davis on 2015/11/09 +// Copyright 2013-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 "QtHelpers.h" + +#include +#include +#include + +Q_LOGGING_CATEGORY(thread_safety, "hifi.thread_safety") + +namespace hifi { namespace qt { + +bool blockingInvokeMethod( + const char* function, + QObject *obj, const char *member, + QGenericReturnArgument ret, + QGenericArgument val0, + QGenericArgument val1, + QGenericArgument val2, + QGenericArgument val3, + QGenericArgument val4, + QGenericArgument val5, + QGenericArgument val6, + QGenericArgument val7, + QGenericArgument val8, + QGenericArgument val9) { + if (QThread::currentThread() == qApp->thread()) { + qCWarning(thread_safety) << "BlockingQueuedConnection invoked on main thread from " << function; + } + return QMetaObject::invokeMethod(obj, member, + Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); +} + +bool blockingInvokeMethod( + const char* function, + QObject *obj, const char *member, + QGenericArgument val0, + QGenericArgument val1, + QGenericArgument val2, + QGenericArgument val3, + QGenericArgument val4, + QGenericArgument val5, + QGenericArgument val6, + QGenericArgument val7, + QGenericArgument val8, + QGenericArgument val9) { + return blockingInvokeMethod(function, obj, member, QGenericReturnArgument(), val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); +} + + + +} } diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h new file mode 100644 index 0000000000..5da65a378f --- /dev/null +++ b/libraries/shared/src/shared/QtHelpers.h @@ -0,0 +1,52 @@ +// +// Created by Bradley Austin Davis on 2017/06/29 +// Copyright 2013-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_Shared_QtHelpers_h +#define hifi_Shared_QtHelpers_h + +#include + + +namespace hifi { namespace qt { + +bool blockingInvokeMethod( + const char* function, + QObject *obj, const char *member, + QGenericReturnArgument ret, + QGenericArgument val0 = QGenericArgument(Q_NULLPTR), + QGenericArgument val1 = QGenericArgument(), + QGenericArgument val2 = QGenericArgument(), + QGenericArgument val3 = QGenericArgument(), + QGenericArgument val4 = QGenericArgument(), + QGenericArgument val5 = QGenericArgument(), + QGenericArgument val6 = QGenericArgument(), + QGenericArgument val7 = QGenericArgument(), + QGenericArgument val8 = QGenericArgument(), + QGenericArgument val9 = QGenericArgument()); + +bool blockingInvokeMethod( + const char* function, + QObject *obj, const char *member, + QGenericArgument val0 = QGenericArgument(Q_NULLPTR), + QGenericArgument val1 = QGenericArgument(), + QGenericArgument val2 = QGenericArgument(), + QGenericArgument val3 = QGenericArgument(), + QGenericArgument val4 = QGenericArgument(), + QGenericArgument val5 = QGenericArgument(), + QGenericArgument val6 = QGenericArgument(), + QGenericArgument val7 = QGenericArgument(), + QGenericArgument val8 = QGenericArgument(), + QGenericArgument val9 = QGenericArgument()); + +} } + +#define BLOCKING_INVOKE_METHOD(obj, member, ...) \ + hifi::qt::blockingInvokeMethod(__FUNCTION__, obj, member, ##__VA_ARGS__) + +#endif diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 76a8a780b9..135729653e 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -249,7 +250,7 @@ int OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { QMessageBox::StandardButton OffscreenUi::messageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { if (QThread::currentThread() != thread()) { QMessageBox::StandardButton result = QMessageBox::StandardButton::NoButton; - QMetaObject::invokeMethod(this, "messageBox", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "messageBox", Q_RETURN_ARG(QMessageBox::StandardButton, result), Q_ARG(Icon, icon), Q_ARG(QString, title), @@ -351,7 +352,7 @@ QVariant OffscreenUi::getCustomInfo(const Icon icon, const QString& title, const QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current) { if (QThread::currentThread() != thread()) { QVariant result; - QMetaObject::invokeMethod(this, "inputDialog", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "inputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(Icon, icon), Q_ARG(QString, title), @@ -366,7 +367,7 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, const QVariantMap& config) { if (QThread::currentThread() != thread()) { QVariant result; - QMetaObject::invokeMethod(this, "customInputDialog", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "customInputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(Icon, icon), Q_ARG(QString, title), @@ -640,7 +641,7 @@ QString OffscreenUi::fileDialog(const QVariantMap& properties) { QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; - QMetaObject::invokeMethod(this, "fileOpenDialog", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "fileOpenDialog", Q_RETURN_ARG(QString, result), Q_ARG(QString, caption), Q_ARG(QString, dir), @@ -662,7 +663,7 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; - QMetaObject::invokeMethod(this, "fileSaveDialog", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "fileSaveDialog", Q_RETURN_ARG(QString, result), Q_ARG(QString, caption), Q_ARG(QString, dir), @@ -686,7 +687,7 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, 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, + BLOCKING_INVOKE_METHOD(this, "existingDirectoryDialog", Q_RETURN_ARG(QString, result), Q_ARG(QString, caption), Q_ARG(QString, dir), @@ -773,7 +774,7 @@ QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, // ATP equivalent of fileOpenDialog(). if (QThread::currentThread() != thread()) { QString result; - QMetaObject::invokeMethod(this, "assetOpenDialog", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "assetOpenDialog", Q_RETURN_ARG(QString, result), Q_ARG(QString, caption), Q_ARG(QString, dir), diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 68bb872667..44a0af7787 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "OffscreenUi.h" static const char* const URL_PROPERTY = "source"; @@ -21,39 +22,51 @@ static const char* const SCRIPT_PROPERTY = "scriptUrl"; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { auto properties = parseArguments(context); - QmlWebWindowClass* retVal { nullptr }; auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([&] { - retVal = new QmlWebWindowClass(); - retVal->initQml(properties); - }, true); + QmlWebWindowClass* retVal = new QmlWebWindowClass(); Q_ASSERT(retVal); + if (QThread::currentThread() != qApp->thread()) { + retVal->moveToThread(qApp->thread()); + QMetaObject::invokeMethod(retVal, "initQml", Q_ARG(QVariantMap, properties)); + } else { + retVal->initQml(properties); + } connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); return engine->newQObject(retVal); } -QString QmlWebWindowClass::getURL() const { - QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { - if (_qmlWindow.isNull()) { - return QVariant(); - } - return _qmlWindow->property(URL_PROPERTY); - }); - return result.toString(); +QString QmlWebWindowClass::getURL() { + if (QThread::currentThread() != thread()) { + QString result; + BLOCKING_INVOKE_METHOD(this, "getURL", Q_RETURN_ARG(QString, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return QString(); + } + + return _qmlWindow->property(URL_PROPERTY).toString(); } void QmlWebWindowClass::setURL(const QString& urlString) { - DependencyManager::get()->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - _qmlWindow->setProperty(URL_PROPERTY, urlString); - } - }); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Q_ARG(QString, urlString)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(URL_PROPERTY, urlString); + } } void QmlWebWindowClass::setScriptURL(const QString& script) { - DependencyManager::get()->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - _qmlWindow->setProperty(SCRIPT_PROPERTY, script); - } - }); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setScriptURL", Q_ARG(QString, script)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(SCRIPT_PROPERTY, script); + } } diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 15ebe74a4f..cdc07265cd 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -20,7 +20,7 @@ public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); public slots: - QString getURL() const; + QString getURL(); void setURL(const QString& url); void setScriptURL(const QString& script); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index f5bb880957..14d8ec8985 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "OffscreenUi.h" static const char* const SOURCE_PROPERTY = "source"; @@ -73,13 +74,15 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { auto properties = parseArguments(context); - QmlWindowClass* retVal { nullptr }; auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([&] { - retVal = new QmlWindowClass(); - retVal->initQml(properties); - }, true); + QmlWindowClass* retVal = new QmlWindowClass(); Q_ASSERT(retVal); + if (QThread::currentThread() != qApp->thread()) { + retVal->moveToThread(qApp->thread()); + BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_ARG(QVariantMap, properties)); + } else { + retVal->initQml(properties); + } connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); return engine->newQObject(retVal); } @@ -90,9 +93,10 @@ QmlWindowClass::QmlWindowClass() { void QmlWindowClass::initQml(QVariantMap properties) { auto offscreenUi = DependencyManager::get(); - _toolWindow = properties.contains(TOOLWINDOW_PROPERTY) && properties[TOOLWINDOW_PROPERTY].toBool(); _source = properties[SOURCE_PROPERTY].toString(); +#if QML_TOOL_WINDOW + _toolWindow = properties.contains(TOOLWINDOW_PROPERTY) && properties[TOOLWINDOW_PROPERTY].toBool(); if (_toolWindow) { // Build the event bridge and wrapper on the main thread _qmlWindow = offscreenUi->getToolWindow(); @@ -103,10 +107,11 @@ void QmlWindowClass::initQml(QVariantMap properties) { Q_ARG(QVariant, QVariant::fromValue(properties))); Q_ASSERT(invokeResult); } else { +#endif // Build the event bridge and wrapper on the main thread offscreenUi->loadInNewContext(qmlSource(), [&](QQmlContext* context, QObject* object) { _qmlWindow = object; - context->setContextProperty("eventBridge", this); + context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); context->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); context->engine()->setObjectOwnership(object, QQmlEngine::CppOwnership); if (properties.contains(TITLE_PROPERTY)) { @@ -133,7 +138,9 @@ void QmlWindowClass::initQml(QVariantMap properties) { connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); connect(_qmlWindow, SIGNAL(windowClosed()), this, SLOT(hasClosed()), Qt::QueuedConnection); }); +#if QML_TOOL_WINDOW } +#endif Q_ASSERT(_qmlWindow); Q_ASSERT(dynamic_cast(_qmlWindow.data())); } @@ -207,56 +214,77 @@ QmlWindowClass::~QmlWindowClass() { } QQuickItem* QmlWindowClass::asQuickItem() const { +#if QML_TOOL_WINDOW if (_toolWindow) { return DependencyManager::get()->getToolWindow(); } +#endif return _qmlWindow.isNull() ? nullptr : dynamic_cast(_qmlWindow.data()); } void QmlWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Q_ARG(bool, visible)); + return; + } + QQuickItem* targetWindow = asQuickItem(); +#if QML_TOOL_WINDOW if (_toolWindow) { // For tool window tabs we special case visibility as a function call on the tab parent // The tool window itself has special logic based on whether any tabs are visible QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); - } else { - DependencyManager::get()->executeOnUiThread([=] { - targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); - }); + return; + } +#endif + targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); +} + +bool QmlWindowClass::isVisible() { + if (QThread::currentThread() != thread()) { + bool result = false; + BLOCKING_INVOKE_METHOD(this, "isVisible", Q_RETURN_ARG(bool, result)); + return result; } -} -bool QmlWindowClass::isVisible() const { // The tool window itself has special logic based on whether any tabs are enabled - return DependencyManager::get()->returnFromUiThread([&] { - if (_qmlWindow.isNull()) { - return QVariant::fromValue(false); - } - if (_toolWindow) { - return QVariant::fromValue(dynamic_cast(_qmlWindow.data())->isEnabled()); - } else { - return QVariant::fromValue(asQuickItem()->isVisible()); - } - }).toBool(); + if (_qmlWindow.isNull()) { + return false; + } + +#if QML_TOOL_WINDOW + if (_toolWindow) { + return dynamic_cast(_qmlWindow.data())->isEnabled(); + } +#endif + + return asQuickItem()->isVisible(); } -glm::vec2 QmlWindowClass::getPosition() const { - QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { - if (_qmlWindow.isNull()) { - return QVariant(QPointF(0, 0)); - } - return asQuickItem()->position(); - }); - return toGlm(result.toPointF()); +glm::vec2 QmlWindowClass::getPosition() { + if (QThread::currentThread() != thread()) { + vec2 result; + BLOCKING_INVOKE_METHOD(this, "getPosition", Q_RETURN_ARG(vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + + return toGlm(asQuickItem()->position()); } void QmlWindowClass::setPosition(const glm::vec2& position) { - DependencyManager::get()->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - asQuickItem()->setPosition(QPointF(position.x, position.y)); - } - }); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Q_ARG(vec2, position)); + return; + } + + if (!_qmlWindow.isNull()) { + asQuickItem()->setPosition(QPointF(position.x, position.y)); + } } void QmlWindowClass::setPosition(int x, int y) { @@ -268,23 +296,29 @@ glm::vec2 toGlm(const QSizeF& size) { return glm::vec2(size.width(), size.height()); } -glm::vec2 QmlWindowClass::getSize() const { - QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { - if (_qmlWindow.isNull()) { - return QVariant(QSizeF(0, 0)); - } - QQuickItem* targetWindow = asQuickItem(); - return QSizeF(targetWindow->width(), targetWindow->height()); - }); - return toGlm(result.toSizeF()); +glm::vec2 QmlWindowClass::getSize() { + if (QThread::currentThread() != thread()) { + vec2 result; + BLOCKING_INVOKE_METHOD(this, "getSize", Q_RETURN_ARG(vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + QQuickItem* targetWindow = asQuickItem(); + return vec2(targetWindow->width(), targetWindow->height()); } void QmlWindowClass::setSize(const glm::vec2& size) { - DependencyManager::get()->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - asQuickItem()->setSize(QSizeF(size.x, size.y)); - } - }); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Q_ARG(vec2, size)); + return; + } + + if (!_qmlWindow.isNull()) { + asQuickItem()->setSize(QSizeF(size.x, size.y)); + } } void QmlWindowClass::setSize(int width, int height) { @@ -292,28 +326,37 @@ void QmlWindowClass::setSize(int width, int height) { } void QmlWindowClass::setTitle(const QString& title) { - DependencyManager::get()->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - asQuickItem()->setProperty(TITLE_PROPERTY, title); - } - }); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Q_ARG(QString, title)); + return; + } + + if (!_qmlWindow.isNull()) { + asQuickItem()->setProperty(TITLE_PROPERTY, title); + } } void QmlWindowClass::close() { - if (_qmlWindow) { - if (_toolWindow) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([=] { - auto toolWindow = offscreenUi->getToolWindow(); - auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::DirectConnection, - Q_ARG(QVariant, _source)); - Q_ASSERT(invokeResult); - }); - } else { - _qmlWindow->deleteLater(); - } - _qmlWindow = nullptr; + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close"); + return; } + +#if QML_TOOL_WINDOW + if (_toolWindow) { + auto offscreenUi = DependencyManager::get(); + auto toolWindow = offscreenUi->getToolWindow(); + auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::DirectConnection, + Q_ARG(QVariant, _source)); + Q_ASSERT(invokeResult); + return; + } +#endif + + if (_qmlWindow) { + _qmlWindow->deleteLater(); + } + _qmlWindow = nullptr; } void QmlWindowClass::hasMoved(QVector2D position) { @@ -325,10 +368,13 @@ void QmlWindowClass::hasClosed() { } void QmlWindowClass::raise() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "raise"); + return; + } + auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([=] { - if (!_qmlWindow.isNull()) { - QMetaObject::invokeMethod(asQuickItem(), "raise", Qt::DirectConnection); - } - }); + if (_qmlWindow) { + QMetaObject::invokeMethod(asQuickItem(), "raise", Qt::DirectConnection); + } } diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index 4f604133a5..e01bc8f14b 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -19,6 +19,8 @@ class QScriptEngine; class QScriptContext; +#define QML_TOOL_WINDOW 0 + // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping class QmlWindowClass : public QObject { Q_OBJECT @@ -31,18 +33,18 @@ public: QmlWindowClass(); ~QmlWindowClass(); - virtual void initQml(QVariantMap properties); + Q_INVOKABLE virtual void initQml(QVariantMap properties); QQuickItem* asQuickItem() const; public slots: - bool isVisible() const; + bool isVisible(); void setVisible(bool visible); - glm::vec2 getPosition() const; + glm::vec2 getPosition(); void setPosition(const glm::vec2& position); void setPosition(int x, int y); - glm::vec2 getSize() const; + glm::vec2 getSize(); void setSize(const glm::vec2& size); void setSize(int width, int height); void setTitle(const QString& title); @@ -85,9 +87,12 @@ protected: virtual QString qmlSource() const { return "QmlWindow.qml"; } +#if QML_TOOL_WINDOW // FIXME needs to be initialized in the ctor once we have support // for tool window panes in QML bool _toolWindow { false }; +#endif + QPointer _qmlWindow; QString _source; diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 7511448c38..4e61eba28f 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -13,12 +13,12 @@ #include #include +#include #include "../VrMenu.h" #include "../OffscreenUi.h" #include "Logging.h" - using namespace ui; static QList groups; @@ -246,7 +246,7 @@ void Menu::removeAction(MenuWrapper* menu, const QString& actionName) { void Menu::setIsOptionChecked(const QString& menuOption, bool isChecked) { if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "setIsOptionChecked", Qt::BlockingQueuedConnection, + BLOCKING_INVOKE_METHOD(this, "setIsOptionChecked", Q_ARG(const QString&, menuOption), Q_ARG(bool, isChecked)); return; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 648bdad1bf..34865ea058 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -25,6 +25,8 @@ #include #include +#include +#include #include #include #include @@ -34,7 +36,6 @@ #include #include #include -#include #include #include @@ -263,9 +264,6 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { return new QmlNetworkAccessManager(parent); } -static QQmlEngine* globalEngine { nullptr }; -static size_t globalEngineRefCount { 0 }; - QString getEventBridgeJavascript() { // FIXME: Refactor with similar code in RenderableWebEntityItem QString javaScriptToInject; @@ -299,9 +297,44 @@ private: }; +#define SINGLE_QML_ENGINE 0 + +#if SINGLE_QML_ENGINE +static QQmlEngine* globalEngine{ nullptr }; +static size_t globalEngineRefCount{ 0 }; +#endif + +void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { + engine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + auto importList = engine->importPathList(); + importList.insert(importList.begin(), PathUtils::resourcesPath()); + engine->setImportPathList(importList); + for (const auto& path : importList) { + qDebug() << path; + } + + if (!engine->incubationController()) { + engine->setIncubationController(window->incubationController()); + } + auto rootContext = engine->rootContext(); + rootContext->setContextProperty("GL", ::getGLContextData()); + rootContext->setContextProperty("urlHandler", new UrlHandler()); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); + rootContext->setContextProperty("ApplicationInterface", qApp); + auto javaScriptToInject = getEventBridgeJavascript(); + if (!javaScriptToInject.isEmpty()) { + rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + } + rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); + rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); + rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); +} QQmlEngine* acquireEngine(QQuickWindow* window) { Q_ASSERT(QThread::currentThread() == qApp->thread()); + + QQmlEngine* result = nullptr; if (QThread::currentThread() != qApp->thread()) { qCWarning(uiLogging) << "Cannot acquire QML engine on any thread but the main thread"; } @@ -310,47 +343,34 @@ QQmlEngine* acquireEngine(QQuickWindow* window) { qmlRegisterType("Hifi", 1, 0, "SoundEffect"); }); + +#if SINGLE_QML_ENGINE if (!globalEngine) { Q_ASSERT(0 == globalEngineRefCount); globalEngine = new QQmlEngine(); - globalEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); - - auto importList = globalEngine->importPathList(); - importList.insert(importList.begin(), PathUtils::resourcesPath()); - globalEngine->setImportPathList(importList); - for (const auto& path : importList) { - qDebug() << path; - } - - if (!globalEngine->incubationController()) { - globalEngine->setIncubationController(window->incubationController()); - } - auto rootContext = globalEngine->rootContext(); - rootContext->setContextProperty("GL", ::getGLContextData()); - rootContext->setContextProperty("urlHandler", new UrlHandler()); - rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); - rootContext->setContextProperty("pathToFonts", "../../"); - rootContext->setContextProperty("ApplicationInterface", qApp); - auto javaScriptToInject = getEventBridgeJavascript(); - if (!javaScriptToInject.isEmpty()) { - rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); - } - rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); - rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); - rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); + initializeQmlEngine(result, window); + ++globalEngineRefCount; } + result = globalEngine; +#else + result = new QQmlEngine(); + initializeQmlEngine(result, window); +#endif - ++globalEngineRefCount; - return globalEngine; + return result; } -void releaseEngine() { +void releaseEngine(QQmlEngine* engine) { Q_ASSERT(QThread::currentThread() == qApp->thread()); +#if SINGLE_QML_ENGINE Q_ASSERT(0 != globalEngineRefCount); if (0 == --globalEngineRefCount) { globalEngine->deleteLater(); globalEngine = nullptr; } +#else + engine->deleteLater(); +#endif } void OffscreenQmlSurface::cleanup() { @@ -455,11 +475,11 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(qApp); cleanup(); - + auto engine = _qmlContext->engine(); _canvas->deleteLater(); _rootItem->deleteLater(); _quickWindow->deleteLater(); - releaseEngine(); + releaseEngine(engine); } void OffscreenQmlSurface::onAboutToQuit() { @@ -830,7 +850,9 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even mouseEvent->screenPos(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); if (event->type() == QEvent::MouseMove) { - _qmlContext->setContextProperty("lastMousePosition", transformedPos); + // TODO - this line necessary for the QML Tooltop to work (which is not currently being used), but it causes interface to crash on launch on a fresh install + // need to investigate into why this crash is happening. + //_qmlContext->setContextProperty("lastMousePosition", transformedPos); } mappedEvent.ignore(); if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { @@ -886,28 +908,6 @@ QQmlContext* OffscreenQmlSurface::getSurfaceContext() { return _qmlContext; } -void OffscreenQmlSurface::executeOnUiThread(std::function function, bool blocking ) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "executeOnUiThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, - Q_ARG(std::function, function)); - return; - } - - function(); -} - -QVariant OffscreenQmlSurface::returnFromUiThread(std::function function) { - if (QThread::currentThread() != thread()) { - QVariant result; - QMetaObject::invokeMethod(this, "returnFromUiThread", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, result), - Q_ARG(std::function, function)); - return result; - } - - return function(); -} - void OffscreenQmlSurface::focusDestroyed(QObject *obj) { _currentFocusItem = nullptr; } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index ae81ae48b4..54f27e3b1f 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -55,10 +55,6 @@ public: return load(QUrl(qmlSourceFile), f); } void clearCache(); - - Q_INVOKABLE void executeOnUiThread(std::function function, bool blocking = false); - Q_INVOKABLE QVariant returnFromUiThread(std::function function); - void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } // Optional values for event handling void setProxyWindow(QWindow* window); diff --git a/libraries/ui/src/ui/QmlWrapper.cpp b/libraries/ui/src/ui/QmlWrapper.cpp index 518a05613a..3879e1bc8f 100644 --- a/libraries/ui/src/ui/QmlWrapper.cpp +++ b/libraries/ui/src/ui/QmlWrapper.cpp @@ -11,6 +11,8 @@ #include #include +#include + QmlWrapper::QmlWrapper(QObject* qmlObject, QObject* parent) : QObject(parent), _qmlObject(qmlObject) { Q_ASSERT(QThread::currentThread() == qApp->thread()); @@ -36,7 +38,7 @@ void QmlWrapper::writeProperties(QVariant propertyMap) { QVariant QmlWrapper::readProperty(const QString& propertyName) { if (QThread::currentThread() != thread()) { QVariant result; - QMetaObject::invokeMethod(this, "readProperty", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, result), Q_ARG(QString, propertyName)); + BLOCKING_INVOKE_METHOD(this, "readProperty", Q_RETURN_ARG(QVariant, result), Q_ARG(QString, propertyName)); return result; } @@ -46,7 +48,7 @@ QVariant QmlWrapper::readProperty(const QString& propertyName) { QVariant QmlWrapper::readProperties(const QVariant& propertyList) { if (QThread::currentThread() != thread()) { QVariant result; - QMetaObject::invokeMethod(this, "readProperties", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, propertyList)); + BLOCKING_INVOKE_METHOD(this, "readProperties", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, propertyList)); return result; } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 1e426dd8f0..76f290f17e 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -42,7 +43,7 @@ ToolbarProxy* TabletScriptingInterface::getSystemToolbarProxy() { TabletProxy* TabletScriptingInterface::getTablet(const QString& tabletId) { TabletProxy* tabletProxy = nullptr; if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "getTablet", Qt::BlockingQueuedConnection, Q_RETURN_ARG(TabletProxy*, tabletProxy), Q_ARG(QString, tabletId)); + BLOCKING_INVOKE_METHOD(this, "getTablet", Q_RETURN_ARG(TabletProxy*, tabletProxy), Q_ARG(QString, tabletId)); return tabletProxy; } @@ -213,22 +214,19 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { // create new desktop window auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([=] { - auto tabletRootWindow = new TabletRootWindow(); - tabletRootWindow->initQml(QVariantMap()); - auto quickItem = tabletRootWindow->asQuickItem(); - _desktopWindow = tabletRootWindow; - QMetaObject::invokeMethod(quickItem, "setShown", Q_ARG(const QVariant&, QVariant(false))); + auto tabletRootWindow = new TabletRootWindow(); + tabletRootWindow->initQml(QVariantMap()); + auto quickItem = tabletRootWindow->asQuickItem(); + _desktopWindow = tabletRootWindow; + QMetaObject::invokeMethod(quickItem, "setShown", Q_ARG(const QVariant&, QVariant(false))); - QObject::connect(quickItem, SIGNAL(windowClosed()), this, SLOT(desktopWindowClosed())); + QObject::connect(quickItem, SIGNAL(windowClosed()), this, SLOT(desktopWindowClosed())); - QObject::connect(tabletRootWindow, SIGNAL(webEventReceived(QVariant)), this, SLOT(emitWebEvent(QVariant)), Qt::DirectConnection); + QObject::connect(tabletRootWindow, SIGNAL(webEventReceived(QVariant)), this, SLOT(emitWebEvent(QVariant)), Qt::DirectConnection); - // forward qml surface events to interface js - connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); - }); + // forward qml surface events to interface js + connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); } else { - _state = State::Home; removeButtonsFromToolbar(); addButtonsToHomeScreen(); emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); @@ -292,7 +290,7 @@ void TabletProxy::initialScreen(const QVariant& url) { bool TabletProxy::isMessageDialogOpen() { if (QThread::currentThread() != thread()) { bool result = false; - QMetaObject::invokeMethod(this, "isMessageDialogOpen", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + BLOCKING_INVOKE_METHOD(this, "isMessageDialogOpen", Q_RETURN_ARG(bool, result)); return result; } @@ -317,7 +315,7 @@ void TabletProxy::emitWebEvent(const QVariant& msg) { bool TabletProxy::isPathLoaded(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; - QMetaObject::invokeMethod(this, "isPathLoaded", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); + BLOCKING_INVOKE_METHOD(this, "isPathLoaded", Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); return result; } @@ -480,7 +478,7 @@ void TabletProxy::loadQMLSource(const QVariant& path) { bool TabletProxy::pushOntoStack(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; - QMetaObject::invokeMethod(this, "pushOntoStack", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); + BLOCKING_INVOKE_METHOD(this, "pushOntoStack", Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); return result; } @@ -606,7 +604,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { if (QThread::currentThread() != thread()) { TabletButtonProxy* result = nullptr; - QMetaObject::invokeMethod(this, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(TabletButtonProxy*, result), Q_ARG(QVariant, properties)); + BLOCKING_INVOKE_METHOD(this, "addButton", Q_RETURN_ARG(TabletButtonProxy*, result), Q_ARG(QVariant, properties)); return result; } @@ -633,7 +631,7 @@ TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { bool TabletProxy::onHomeScreen() { if (QThread::currentThread() != thread()) { bool result = false; - QMetaObject::invokeMethod(this, "onHomeScreen", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + BLOCKING_INVOKE_METHOD(this, "onHomeScreen", Q_RETURN_ARG(bool, result)); return result; } @@ -840,7 +838,7 @@ void TabletButtonProxy::setToolbarButtonProxy(QObject* toolbarButtonProxy) { QVariantMap TabletButtonProxy::getProperties() { if (QThread::currentThread() != thread()) { QVariantMap result; - QMetaObject::invokeMethod(this, "getProperties", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariantMap, result)); + BLOCKING_INVOKE_METHOD(this, "getProperties", Q_RETURN_ARG(QVariantMap, result)); return result; } diff --git a/libraries/ui/src/ui/ToolbarScriptingInterface.cpp b/libraries/ui/src/ui/ToolbarScriptingInterface.cpp index 330c652cdc..d01b538004 100644 --- a/libraries/ui/src/ui/ToolbarScriptingInterface.cpp +++ b/libraries/ui/src/ui/ToolbarScriptingInterface.cpp @@ -12,6 +12,8 @@ #include #include #include + +#include #include "../OffscreenUi.h" QScriptValue toolbarToScriptValue(QScriptEngine* engine, ToolbarProxy* const &in) { @@ -68,7 +70,7 @@ ToolbarProxy::ToolbarProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qml ToolbarButtonProxy* ToolbarProxy::addButton(const QVariant& properties) { if (QThread::currentThread() != thread()) { ToolbarButtonProxy* result = nullptr; - QMetaObject::invokeMethod(this, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ToolbarButtonProxy*, result), Q_ARG(QVariant, properties)); + BLOCKING_INVOKE_METHOD(this, "addButton", Q_RETURN_ARG(ToolbarButtonProxy*, result), Q_ARG(QVariant, properties)); return result; } @@ -99,18 +101,14 @@ void ToolbarProxy::removeButton(const QVariant& name) { ToolbarProxy* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { if (QThread::currentThread() != thread()) { ToolbarProxy* result = nullptr; - QMetaObject::invokeMethod(this, "getToolbar", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ToolbarProxy*, result), Q_ARG(QString, toolbarId)); + BLOCKING_INVOKE_METHOD(this, "getToolbar", Q_RETURN_ARG(ToolbarProxy*, result), Q_ARG(QString, toolbarId)); return result; } auto offscreenUi = DependencyManager::get(); auto desktop = offscreenUi->getDesktop(); - Qt::ConnectionType connectionType = Qt::AutoConnection; - if (QThread::currentThread() != desktop->thread()) { - connectionType = Qt::BlockingQueuedConnection; - } QVariant resultVar; - bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", connectionType, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); + bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); if (!invokeResult) { return nullptr; } diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index 656f98dd8d..a7e29d86f9 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -15,6 +15,7 @@ #include class AudioInjector; +using AudioInjectorPointer = QSharedPointer; // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -38,7 +39,7 @@ protected: QUrl _url; float _volume { 1.0f }; SharedSoundPointer _sound; - AudioInjector* _injector { nullptr }; + AudioInjectorPointer _injector; }; #endif // hifi_SoundEffect_h diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index f4253899a2..c54f2326c2 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -82,6 +82,12 @@ struct PoseData { angularVelocities[i] = transformVectorFast(resetMat, toGlm(vrPoses[i].vAngularVelocity)); } } + + void resetToInvalid() { + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + vrPoses[i].bPoseIsValid = false; + } + } }; // FIXME remove once OpenVR header is updated diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 648373ccc2..07b3b2f73d 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include @@ -60,11 +62,6 @@ static const int SECOND_FOOT = 1; static const int HIP = 2; static const int CHEST = 3; -static float HEAD_PUCK_Y_OFFSET = -0.0254f; -static float HEAD_PUCK_Z_OFFSET = -0.152f; -static float HAND_PUCK_Y_OFFSET = -0.0508f; -static float HAND_PUCK_Z_OFFSET = 0.0254f; - const char* ViveControllerManager::NAME { "OpenVR" }; const std::map TRACKING_RESULT_TO_STRING = { @@ -121,6 +118,29 @@ static QString deviceTrackingResultToString(vr::ETrackingResult trackingResult) return result; } +static glm::mat4 calculateResetMat() { + auto chaperone = vr::VRChaperone(); + if (chaperone) { + float const UI_RADIUS = 1.0f; + float const UI_HEIGHT = 1.6f; + float const UI_Z_OFFSET = 0.5; + + float xSize, zSize; + chaperone->GetPlayAreaSize(&xSize, &zSize); + glm::vec3 uiPos(0.0f, UI_HEIGHT, UI_RADIUS - (0.5f * zSize) - UI_Z_OFFSET); + + return glm::inverse(createMatFromQuatAndPos(glm::quat(), uiPos)); + } + return glm::mat4(); +} + +bool ViveControllerManager::isDesktopMode() { + if (_container) { + return !_container->getActiveDisplayPlugin()->isHmd(); + } + return false; +} + void ViveControllerManager::calibrate() { if (isSupported()) { _inputDevice->calibrateNextFrame(); @@ -141,13 +161,21 @@ bool ViveControllerManager::isSupported() const { void ViveControllerManager::setConfigurationSettings(const QJsonObject configurationSettings) { if (isSupported()) { + if (configurationSettings.contains("desktopMode")) { + _desktopMode = configurationSettings["desktopMode"].toBool(); + if (!_desktopMode) { + _resetMatCalculated = false; + } + } _inputDevice->configureCalibrationSettings(configurationSettings); } } QJsonObject ViveControllerManager::configurationSettings() { if (isSupported()) { - return _inputDevice->configurationSettings(); + QJsonObject configurationSettings = _inputDevice->configurationSettings(); + configurationSettings["desktopMode"] = _desktopMode; + return configurationSettings; } return QJsonObject(); @@ -218,6 +246,18 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu return; } + if (isDesktopMode() && _desktopMode) { + if (!_resetMatCalculated) { + _resetMat = calculateResetMat(); + _resetMatCalculated = true; + } + + _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, _nextSimPoseData.vrPoses, vr::k_unMaxTrackedDeviceCount); + _nextSimPoseData.update(_resetMat); + } else if (isDesktopMode()) { + _nextSimPoseData.resetToInvalid(); + } + auto userInputMapper = DependencyManager::get(); handleOpenVrEvents(); if (openVrQuitRequested()) { @@ -249,6 +289,7 @@ ViveControllerManager::InputDevice::InputDevice(vr::IVRSystem*& system) : contro _configStringMap[Config::FeetAndHips] = QString("FeetAndHips"); _configStringMap[Config::FeetHipsAndChest] = QString("FeetHipsAndChest"); _configStringMap[Config::FeetHipsAndShoulders] = QString("FeetHipsAndShoulders"); + _configStringMap[Config::FeetHipsChestAndShoulders] = QString("FeetHipsChestAndShoulders"); } void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { @@ -325,6 +366,7 @@ void ViveControllerManager::InputDevice::calibrateFromUI(const controller::Input if (_calibrate) { uncalibrate(); calibrate(inputCalibrationData); + emitCalibrationStatus(); _calibrate = false; } } @@ -342,8 +384,8 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso bool overrideHead = headObject["override"].toBool(); if (overrideHead) { _headConfig = HeadConfig::Puck; - HEAD_PUCK_Y_OFFSET = headObject["Y"].toDouble(); - HEAD_PUCK_Z_OFFSET = headObject["Z"].toDouble(); + _headPuckYOffset = headObject["Y"].toDouble(); + _headPuckZOffset = headObject["Z"].toDouble(); } else { _headConfig = HeadConfig::HMD; } @@ -352,8 +394,8 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso bool overrideHands = handsObject["override"].toBool(); if (overrideHands) { _handConfig = HandConfig::Pucks; - HAND_PUCK_Y_OFFSET = handsObject["Y"].toDouble(); - HAND_PUCK_Z_OFFSET = handsObject["Z"].toDouble(); + _handPuckYOffset = handsObject["Y"].toDouble(); + _handPuckZOffset = handsObject["Z"].toDouble(); } else { _handConfig = HandConfig::HandController; } @@ -372,29 +414,23 @@ QJsonObject ViveControllerManager::InputDevice::configurationSettings() { Locker locker(_lock); QJsonObject configurationSettings; configurationSettings["trackerConfiguration"] = configToString(_preferedConfig); - configurationSettings["HMDHead"] = (_headConfig == HeadConfig::HMD) ? true : false; - configurationSettings["handController"] = (_handConfig == HandConfig::HandController) ? true : false; + configurationSettings["HMDHead"] = (_headConfig == HeadConfig::HMD); + configurationSettings["handController"] = (_handConfig == HandConfig::HandController); + configurationSettings["puckCount"] = (int)_validTrackedObjects.size(); return configurationSettings; } -void ViveControllerManager::InputDevice::emitCalibrationStatus(const bool success) { +void ViveControllerManager::InputDevice::emitCalibrationStatus() { auto inputConfiguration = DependencyManager::get(); QJsonObject status = QJsonObject(); + status["calibrated"] = _calibrated; + status["configuration"] = configToString(_preferedConfig); + status["head_puck"] = (_headConfig == HeadConfig::Puck); + status["hand_pucks"] = (_handConfig == HandConfig::Pucks); + status["puckCount"] = (int)_validTrackedObjects.size(); + status["UI"] = _calibrate; - if (_calibrated && success) { - status["calibrated"] = _calibrated; - status["configuration"] = configToString(_preferedConfig); - } else if (!_calibrated && !success) { - status["calibrated"] = _calibrated; - status["success"] = success; - } else if (!_calibrated && success) { - status["calibrated"] = _calibrated; - status["success"] = success; - status["configuration"] = configToString(_preferedConfig); - status["puckCount"] = (int)_validTrackedObjects.size(); - } - - emit inputConfiguration->calibrationStatus(status); //inputConfiguration->calibrated(status); + emit inputConfiguration->calibrationStatus(status); } void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { @@ -430,19 +466,38 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; _poseStateMap[poseIndex] = pose.transform(controllerToAvatar); - _validTrackedObjects.push_back(std::make_pair(poseIndex, _poseStateMap[poseIndex])); + + // but _validTrackedObjects remain in sensor frame + _validTrackedObjects.push_back(std::make_pair(poseIndex, pose)); } else { controller::Pose invalidPose; _poseStateMap[poseIndex] = invalidPose; } } +void ViveControllerManager::InputDevice::sendUserActivityData(QString activity) { + QJsonObject jsonData = { + {"num_pucks", (int)_validTrackedObjects.size()}, + {"configuration", configToString(_preferedConfig)}, + {"head_puck", (_headConfig == HeadConfig::Puck) ? true : false}, + {"hand_pucks", (_handConfig == HandConfig::Pucks) ? true : false} + }; + + UserActivityLogger::getInstance().logAction(activity, jsonData); +} + void ViveControllerManager::InputDevice::calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration) { if (!_calibrated) { calibrate(inputCalibration); + if (_calibrated) { + sendUserActivityData("mocap_button_success"); + } else { + sendUserActivityData("mocap_button_fail"); + } + emitCalibrationStatus(); } else { uncalibrate(); - emitCalibrationStatus(true); + sendUserActivityData("mocap_button_uncalibrate"); } } @@ -454,29 +509,27 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr if (puckCount == 0) { uncalibrate(); - emitCalibrationStatus(false); return; } glm::mat4 defaultToReferenceMat = glm::mat4(); if (_headConfig == HeadConfig::HMD) { defaultToReferenceMat = calculateDefaultToReferenceForHmd(inputCalibration); - } else if (_headConfig == HeadConfig::Puck) { + } else if (_headConfig == HeadConfig::Puck) { + std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition); defaultToReferenceMat = calculateDefaultToReferenceForHeadPuck(inputCalibration); } - + _config = _preferedConfig; - + bool headConfigured = configureHead(defaultToReferenceMat, inputCalibration); bool handsConfigured = configureHands(defaultToReferenceMat, inputCalibration); bool bodyConfigured = configureBody(defaultToReferenceMat, inputCalibration); if (!headConfigured || !handsConfigured || !bodyConfigured) { uncalibrate(); - emitCalibrationStatus(false); } else { _calibrated = true; - emitCalibrationStatus(true); qDebug() << "PuckCalibration: " << configToString(_config) << " Configuration Successful"; } } @@ -566,8 +619,6 @@ bool ViveControllerManager::InputDevice::configureBody(glm::mat4& defaultToRefer return true; } qDebug() << "Puck Calibration: " << configToString(_config) << " Config Failed: Could not meet the minimal # of pucks"; - uncalibrate(); - emitCalibrationStatus(false); return false; } @@ -660,63 +711,67 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } } } + +// defaultToReferenceMat is an offset from avatar space to sensor space. +// it aligns the default center-eye in avatar space with the hmd in sensor space. +// +// * E_a is the the default center-of-the-eyes transform in avatar space. +// * E_s is the the hmd eye-center transform in sensor space, with roll and pitch removed. +// * D is the defaultReferenceMat. +// +// E_s = D * E_a => +// D = E_s * inverse(E_a) +// glm::mat4 ViveControllerManager::InputDevice::calculateDefaultToReferenceForHmd(const controller::InputCalibrationData& inputCalibration) { - // convert the hmd head from sensor space to avatar space - glm::mat4 hmdSensorFlippedMat = inputCalibration.hmdSensorMat * Matrices::Y_180; - glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat; - glm::mat4 hmdAvatarMat = sensorToAvatarMat * hmdSensorFlippedMat; - // cancel the roll and pitch for the hmd head - glm::quat hmdRotation = cancelOutRollAndPitch(glmExtractRotation(hmdAvatarMat)); - glm::vec3 hmdTranslation = extractTranslation(hmdAvatarMat); - glm::mat4 currentHmd = createMatFromQuatAndPos(hmdRotation, hmdTranslation); + // the center-eye transform in avatar space. + glm::mat4 E_a = inputCalibration.defaultCenterEyeMat; - // calculate the offset from the centerOfEye to defaultHeadMat - glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; + // the center-eye transform in sensor space. + glm::mat4 E_s = inputCalibration.hmdSensorMat * Matrices::Y_180; // the Y_180 is to convert hmd from -z forward to z forward. - glm::mat4 currentHead = currentHmd * defaultHeadOffset; + // cancel out roll and pitch on E_s + glm::quat rot = cancelOutRollAndPitch(glmExtractRotation(E_s)); + glm::vec3 trans = extractTranslation(E_s); + E_s = createMatFromQuatAndPos(rot, trans); - // calculate the defaultToRefrenceXform - glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat); - - return defaultToReferenceMat; + return E_s * glm::inverse(E_a); } +// defaultToReferenceMat is an offset from avatar space to sensor space. +// It aligns the default center-of-the-eyes transform in avatar space with the head-puck in sensor space. +// The offset from the center-of-the-eyes to the head-puck can be configured via _headPuckYOffset and _headPuckZOffset, +// These values are exposed in the configuration UI. +// +// * E_a is the the default center-eye transform in avatar space. +// * E_s is the the head-puck center-eye transform in sensor space, with roll and pitch removed. +// * D is the defaultReferenceMat. +// +// E_s = D * E_a => +// D = E_s * inverse(E_a) +// glm::mat4 ViveControllerManager::InputDevice::calculateDefaultToReferenceForHeadPuck(const controller::InputCalibrationData& inputCalibration) { - glm::mat4 avatarToSensorMat = glm::inverse(inputCalibration.sensorToWorldMat) * inputCalibration.avatarMat; - glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat; + + // the center-eye transform in avatar space. + glm::mat4 E_a = inputCalibration.defaultCenterEyeMat; + + // calculate the center-eye transform in sensor space, via the head-puck size_t headPuckIndex = _validTrackedObjects.size() - 1; controller::Pose headPuckPose = _validTrackedObjects[headPuckIndex].second; - glm::mat4 headPuckAvatarMat = createMatFromQuatAndPos(headPuckPose.getRotation(), headPuckPose.getTranslation()) * Matrices::Y_180; - glm::vec3 headPuckTranslation = extractTranslation(headPuckAvatarMat); - glm::vec3 headPuckZAxis = cancelOutRollAndPitch(glmExtractRotation(headPuckAvatarMat)) * glm::vec3(0.0f, 0.0f, 1.0f); - glm::vec3 worldUp = glm::vec3(0.0f, 1.0f, 0.0f); - // check that the head puck z axis is not parrallel to the world up - const float EPSILON = 1.0e-4f; - glm::vec3 zAxis = glmExtractRotation(headPuckAvatarMat) * glm::vec3(0.0f, 0.0f, 1.0f); - if (fabsf(fabsf(glm::dot(glm::normalize(worldUp), glm::normalize(zAxis))) - 1.0f) < EPSILON) { - headPuckZAxis = glm::vec3(1.0f, 0.0f, 0.0f); - } + // AJT: TODO: handle case were forward is parallel with UNIT_Y. + glm::vec3 forward = headPuckPose.rotation * -Vectors::UNIT_Z; + glm::vec3 x = glm::normalize(glm::cross(Vectors::UNIT_Y, forward)); + glm::vec3 z = glm::normalize(glm::cross(x, Vectors::UNIT_Y)); + glm::mat3 centerEyeRotMat(x, Vectors::UNIT_Y, z); + glm::vec3 centerEyeTrans = headPuckPose.translation + centerEyeRotMat * glm::vec3(0.0f, _headPuckYOffset, _headPuckZOffset); - glm::vec3 yPrime = glm::vec3(0.0f, 1.0f, 0.0f); - glm::vec3 xPrime = glm::normalize(glm::cross(worldUp, headPuckZAxis)); - glm::vec3 zPrime = glm::normalize(glm::cross(xPrime, yPrime)); - glm::mat4 newHeadPuck = glm::mat4(glm::vec4(xPrime, 0.0f), glm::vec4(yPrime, 0.0f), - glm::vec4(zPrime, 0.0f), glm::vec4(headPuckTranslation, 1.0f)); + glm::mat4 E_s(glm::vec4(centerEyeRotMat[0], 0.0f), + glm::vec4(centerEyeRotMat[1], 0.0f), + glm::vec4(centerEyeRotMat[2], 0.0f), + glm::vec4(centerEyeTrans, 1.0f)); - glm::mat4 headPuckOffset = glm::mat4(glm::vec4(1.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 1.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 1.0f, 0.0f), glm::vec4(0.0f, HEAD_PUCK_Y_OFFSET, HEAD_PUCK_Z_OFFSET, 1.0f)); - - glm::mat4 finalHeadPuck = newHeadPuck * headPuckOffset; - - glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; - - glm::mat4 currentHead = finalHeadPuck * defaultHeadOffset; - - // calculate the defaultToRefrenceXform - glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat); - return defaultToReferenceMat; + return E_s * glm::inverse(E_a); } void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPseudoButton) { @@ -904,12 +959,12 @@ void ViveControllerManager::InputDevice::calibrateLeftHand(glm::mat4& defaultToR glm::mat4 newHandMat = glm::mat4(glm::vec4(xPrime, 0.0f), glm::vec4(yPrime, 0.0f), glm::vec4(zPrime, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - - glm::vec3 translationOffset = glm::vec3(0.0f, HAND_PUCK_Y_OFFSET, HAND_PUCK_Z_OFFSET); + + glm::vec3 translationOffset = glm::vec3(0.0f, _handPuckYOffset, _handPuckZOffset); glm::quat initialRotation = glmExtractRotation(handPoseAvatarMat); glm::quat finalRotation = glmExtractRotation(newHandMat); - + glm::quat rotationOffset = glm::inverse(initialRotation) * finalRotation; glm::mat4 offsetMat = createMatFromQuatAndPos(rotationOffset, translationOffset); @@ -934,13 +989,13 @@ void ViveControllerManager::InputDevice::calibrateRightHand(glm::mat4& defaultTo glm::vec3 yPrime = glm::normalize(glm::cross(zPrime, xPrime)); glm::mat4 newHandMat = glm::mat4(glm::vec4(xPrime, 0.0f), glm::vec4(yPrime, 0.0f), glm::vec4(zPrime, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - - glm::vec3 translationOffset = glm::vec3(0.0f, HAND_PUCK_Y_OFFSET, HAND_PUCK_Z_OFFSET); + + glm::vec3 translationOffset = glm::vec3(0.0f, _handPuckYOffset, _handPuckZOffset); glm::quat initialRotation = glmExtractRotation(handPoseAvatarMat); glm::quat finalRotation = glmExtractRotation(newHandMat); - + glm::quat rotationOffset = glm::inverse(initialRotation) * finalRotation; glm::mat4 offsetMat = createMatFromQuatAndPos(rotationOffset, translationOffset); @@ -957,7 +1012,7 @@ void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToRefer auto& secondFoot = _validTrackedObjects[SECOND_FOOT]; controller::Pose& firstFootPose = firstFoot.second; controller::Pose& secondFootPose = secondFoot.second; - + if (determineLimbOrdering(firstFootPose, secondFootPose, headXAxis, headPosition)) { calibrateFoot(defaultToReferenceMat, inputCalibration, firstFoot, true); calibrateFoot(defaultToReferenceMat, inputCalibration, secondFoot, false); @@ -1022,13 +1077,8 @@ void ViveControllerManager::InputDevice::calibrateShoulders(glm::mat4& defaultTo void ViveControllerManager::InputDevice::calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) { size_t headIndex = _validTrackedObjects.size() - 1; const PuckPosePair& head = _validTrackedObjects[headIndex]; - - // assume the person is wearing the head puck on his/her forehead - glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; - controller::Pose newHead = head.second.postTransform(defaultHeadOffset); - _jointToPuckMap[controller::HEAD] = head.first; - _pucksOffset[head.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHeadMat, newHead); + _pucksOffset[head.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHeadMat, head.second); } QString ViveControllerManager::InputDevice::configToString(Config config) { diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 0b0406bb60..a9bcc7e4e2 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -73,6 +73,7 @@ private: void calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration); void calibrate(const controller::InputCalibrationData& inputCalibration); void uncalibrate(); + void sendUserActivityData(QString activity); void configureCalibrationSettings(const QJsonObject configurationSettings); QJsonObject configurationSettings(); controller::Pose addOffsetToPuckPose(int joint) const; @@ -106,7 +107,7 @@ private: void calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration); void calibrateFromHandController(const controller::InputCalibrationData& inputCalibrationData); void calibrateFromUI(const controller::InputCalibrationData& inputCalibrationData); - void emitCalibrationStatus(const bool success); + void emitCalibrationStatus(); void calibrateNextFrame(); @@ -139,7 +140,7 @@ private: FeetAndHips, FeetHipsAndChest, FeetHipsAndShoulders, - FeetHipsChestAndShoulders, + FeetHipsChestAndShoulders }; enum class HeadConfig { @@ -151,7 +152,7 @@ private: HandController, Pucks }; - + Config _config { Config::None }; Config _preferedConfig { Config::None }; HeadConfig _headConfig { HeadConfig::HMD }; @@ -176,6 +177,10 @@ private: float _leftHapticDuration { 0.0f }; float _rightHapticStrength { 0.0f }; float _rightHapticDuration { 0.0f }; + float _headPuckYOffset { -0.05f }; + float _headPuckZOffset { -0.05f }; + float _handPuckYOffset { 0.0f }; + float _handPuckZOffset { 0.0f }; bool _triggersPressedHandled { false }; bool _calibrated { false }; bool _timeTilCalibrationSet { false }; @@ -189,9 +194,12 @@ private: }; void renderHand(const controller::Pose& pose, gpu::Batch& batch, int sign); - + bool isDesktopMode(); bool _registeredWithInputMapper { false }; bool _modelLoaded { false }; + bool _resetMatCalculated { false }; + bool _desktopMode { false }; + glm::mat4 _resetMat { glm::mat4() }; model::Geometry _modelGeometry; gpu::TexturePointer _texture; diff --git a/scripts/developer/tests/entityLookupCostMeasurement.js b/scripts/developer/tests/entityLookupCostMeasurement.js new file mode 100644 index 0000000000..15697fe13d --- /dev/null +++ b/scripts/developer/tests/entityLookupCostMeasurement.js @@ -0,0 +1,104 @@ +// Creates a large number of entities on the cardinal planes of the octree (all +// objects will live in the root octree element). Measures how long it takes +// to update the properties of the first and last entity. The difference +// between the two measurements shows how the cost of lookup changes as a +// function of the number of entities. For best results run this in an +// otherwise empty domain. + +var firstId; +var lastId; +var NUM_ENTITIES_ON_SIDE = 25; + +// create the objects +createObjects = function () { + var STRIDE = 0.75; + var WIDTH = 0.5; + var DIMENSIONS = { x: WIDTH, y: WIDTH, z: WIDTH }; + var LIFETIME = 20; + + var properties = { + name: "", + type : "Box", + dimensions : DIMENSIONS, + position : { x: 0, y: 0, z: 0}, + lifetime : LIFETIME, + color : { red:255, green: 64, blue: 255 } + }; + + // xy + var planeName = "xy"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: i * STRIDE, y: j * STRIDE, z: 0 }; + var red = i * 255 / NUM_ENTITIES_ON_SIDE; + var green = j * 255 / NUM_ENTITIES_ON_SIDE; + var blue = 0; + properties.color = { red: red, green: green, blue: blue }; + if (i == 0 && j == 0) { + firstId = Entities.addEntity(properties); + } else { + Entities.addEntity(properties); + } + } + } + + // yz + var planeName = "yz"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: 0, y: i * STRIDE, z: j * STRIDE }; + var red = 0; + var green = i * 255 / NUM_ENTITIES_ON_SIDE; + var blue = j * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + Entities.addEntity(properties); + } + } + + // zx + var planeName = "zx"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: j * STRIDE, y: 0, z: i * STRIDE }; + var red = j * 255 / NUM_ENTITIES_ON_SIDE; + var green = 0; + var blue = i * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + lastId = Entities.addEntity(properties); + } + } +}; + +createObjects(); + +// measure the time it takes to edit the first and last entities many times +// (requires a lookup by entityId each time) +changeProperties = function (id) { + var newProperties = { color : { red: 255, green: 255, blue: 255 } }; + Entities.editEntity(id, newProperties); +} + +// first +var NUM_CHANGES = 10000; +var firstStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(firstId); +} +var firstEnd = Date.now(); +var firstDt = firstEnd - firstStart; + +// last +var lastStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(lastId); +} +var lastEnd = Date.now(); +var lastDt = lastEnd - lastStart; + +// print the results +var numEntities = 3 * NUM_ENTITIES_ON_SIDE * NUM_ENTITIES_ON_SIDE; +print("numEntities = " + numEntities + " numEdits = " + NUM_CHANGES + " firstDt = " + firstDt + " lastDt = " + lastDt); + diff --git a/scripts/system/chat.js b/scripts/system/chat.js index d03c6aae98..58a1849f1f 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -370,14 +370,14 @@ // Change the avatar size to bigger. function biggerSize() { //print("biggerSize"); - logMessage("Increasing avatar size bigger!", null); + logMessage("Increasing avatar size", null); MyAvatar.increaseSize(); } // Change the avatar size to smaller. function smallerSize() { //print("smallerSize"); - logMessage("Decreasing avatar size smaler!", null); + logMessage("Decreasing avatar size", null); MyAvatar.decreaseSize(); } @@ -470,14 +470,13 @@ case '?': case 'help': - logMessage('Type "/?" or "/help" for help, which is this!', null); - logMessage('Type "/name " to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null); - logMessage('Type "/shutup" to shut up your overhead chat message.', null); - logMessage('Type "/say " to say something.', null); - logMessage('Type "/clear" to clear your cha, nullt log.', null); - logMessage('Type "/who" to ask who is h, nullere to chat.', null); - logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null); - logMessage('(Sorry, that\'s all there is so far!)', null); + logMessage('Type "/?" or "/help" for help', null); + logMessage('Type "/name " to set your chat name, or "/name" to use your display name. If your display name is not defined, a random name will be used.', null); + logMessage('Type "/close" to close your overhead chat message.', null); + logMessage('Type "/say " to display a new message.', null); + logMessage('Type "/clear" to clear your chat log.', null); + logMessage('Type "/who" to ask who is in the chat session.', null); + logMessage('Type "/bigger", "/smaller" or "/normal" to change your avatar size.', null); break; case 'name': @@ -498,9 +497,9 @@ } break; - case 'shutup': + case 'close': popDownSpeechBubble(); - logMessage('Overhead chat message shut up.', null); + logMessage('Overhead chat message closed.', null); break; case 'say': diff --git a/scripts/system/controllers/godView.js b/scripts/system/controllers/godView.js new file mode 100644 index 0000000000..4b406399fd --- /dev/null +++ b/scripts/system/controllers/godView.js @@ -0,0 +1,116 @@ +"use strict"; +// +// godView.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 1 Jun 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* globals HMD, Script, Menu, Tablet, Camera */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { // BEGIN LOCAL_SCOPE + +var godView = false; + +var GOD_CAMERA_OFFSET = -1; // 1 meter below the avatar +var GOD_VIEW_HEIGHT = 300; // 300 meter above the ground +var ABOVE_GROUND_DROP = 2; +var MOVE_BY = 1; + +function moveTo(position) { + if (godView) { + MyAvatar.position = position; + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0}); + } else { + MyAvatar.position = position; + } +} + +function keyPressEvent(event) { + if (godView) { + switch(event.text) { + case "UP": + moveTo(Vec3.sum(MyAvatar.position, {x:0.0, y: 0, z: -1 * MOVE_BY})); + break; + case "DOWN": + moveTo(Vec3.sum(MyAvatar.position, {x:0, y: 0, z: MOVE_BY})); + break; + case "LEFT": + moveTo(Vec3.sum(MyAvatar.position, {x:-1 * MOVE_BY, y: 0, z: 0})); + break; + case "RIGHT": + moveTo(Vec3.sum(MyAvatar.position, {x:MOVE_BY, y: 0, z: 0})); + break; + } + } +} + +function mousePress(event) { + if (godView) { + var pickRay = Camera.computePickRay(event.x, event.y); + var pointingAt = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction,300)); + var moveToPosition = { x: pointingAt.x, y: MyAvatar.position.y, z: pointingAt.z }; + moveTo(moveToPosition); + } +} + + +var oldCameraMode = Camera.mode; + +function startGodView() { + if (!godView) { + oldCameraMode = Camera.mode; + MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_VIEW_HEIGHT, z: 0}); + Camera.mode = "independent"; + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0}); + Camera.orientation = Quat.fromPitchYawRollDegrees(-90,0,0); + godView = true; + } +} + +function endGodView() { + if (godView) { + Camera.mode = oldCameraMode; + MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: (-1 * GOD_VIEW_HEIGHT) + ABOVE_GROUND_DROP, z: 0}); + godView = false; + } +} + +var button; +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +function onClicked() { + if (godView) { + endGodView(); + } else { + startGodView(); + } +} + +button = tablet.addButton({ + icon: "icons/tablet-icons/switch-desk-i.svg", // FIXME - consider a better icon from Alan + text: "God View" +}); + +button.clicked.connect(onClicked); +Controller.keyPressEvent.connect(keyPressEvent); +Controller.mousePressEvent.connect(mousePress); + + +Script.scriptEnding.connect(function () { + if (godView) { + endGodView(); + } + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.mousePressEvent.disconnect(mousePress); +}); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 14a86510c2..da8add5117 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -14,7 +14,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global MyAvatar, Entities, Script, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller */ +/* global MyAvatar, Entities, Script, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller, + isInEditMode, HMD */ (function() { // BEGIN LOCAL_SCOPE @@ -22,6 +23,8 @@ Script.include("/~/system/libraries/utils.js"); var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed +var DELAY_FOR_30HZ = 33; // milliseconds + var ZERO_VEC3 = { x: 0, y: 0, @@ -46,7 +49,7 @@ var ACTION_TTL = 10; // seconds function getTag() { return "grab-" + MyAvatar.sessionUUID; } - + var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified @@ -411,10 +414,15 @@ Grabber.prototype.pressEvent = function(event) { }; Grabber.prototype.releaseEvent = function(event) { - if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { + if (event.isLeftButton!==true || event.isRightButton===true || event.isMiddleButton===true) { return; } + if (this.moveEventTimer) { + Script.clearTimeout(this.moveEventTimer); + this.moveEventTimer = null; + } + if (this.isGrabbing) { // this.deactivateEntity(this.entityID); this.isGrabbing = false; @@ -439,12 +447,28 @@ Grabber.prototype.releaseEvent = function(event) { } }; +Grabber.prototype.scheduleMouseMoveProcessor = function(event) { + var _this = this; + if (!this.moveEventTimer) { + this.moveEventTimer = Script.setTimeout(function() { + _this.moveEventProcess(); + }, DELAY_FOR_30HZ); + } +}; + Grabber.prototype.moveEvent = function(event) { + // during the handling of the event, do as little as possible. We save the updated mouse position, + // and start a timer to react to the change. If more changes arrive before the timer fires, only + // the last update will be considered. This is done to avoid backing-up Qt's event queue. if (!this.isGrabbing) { return; } mouse.updateDrag(event); + this.scheduleMouseMoveProcessor(); +}; +Grabber.prototype.moveEventProcess = function() { + this.moveEventTimer = null; // see if something added/restored gravity var entityProperties = Entities.getEntityProperties(this.entityID); if (!entityProperties || !entityProperties.gravity) { @@ -489,7 +513,7 @@ Grabber.prototype.moveEvent = function(event) { } else { var newPointOnPlane; - + if (this.mode === "verticalCylinder") { // for this mode we recompute the plane based on current Camera var planeNormal = Quat.getForward(Camera.getOrientation()); @@ -505,7 +529,7 @@ Grabber.prototype.moveEvent = function(event) { }; } else { - + newPointOnPlane = mouseIntersectionWithPlane( this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance); var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition); @@ -538,6 +562,8 @@ Grabber.prototype.moveEvent = function(event) { } else { Entities.updateAction(this.entityID, this.actionID, actionArgs); } + + this.scheduleMouseMoveProcessor(); }; Grabber.prototype.keyReleaseEvent = function(event) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 04921fe14d..78c4b2960e 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -187,6 +187,8 @@ var DEFAULT_GRABBABLE_DATA = { var USE_BLACKLIST = true; var blacklist = []; +var entitiesWithHoverOverlays = []; + var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; @@ -2201,6 +2203,15 @@ function MyController(hand) { entityPropertiesCache.addEntity(rayPickInfo.entityID); } + if (rayPickInfo.entityID && entitiesWithHoverOverlays.indexOf(rayPickInfo.entityID) == -1) { + entitiesWithHoverOverlays.forEach(function (element) { + HoverOverlay.destroyHoverOverlay(element); + }); + entitiesWithHoverOverlays = []; + HoverOverlay.createHoverOverlay(rayPickInfo.entityID); + entitiesWithHoverOverlays.push(rayPickInfo.entityID); + } + var candidateHotSpotEntities = Entities.findEntities(handPosition, MAX_EQUIP_HOTSPOT_RADIUS); entityPropertiesCache.addEntities(candidateHotSpotEntities); @@ -3763,6 +3774,11 @@ function MyController(hand) { this.release = function() { this.turnOffVisualizations(); + entitiesWithHoverOverlays.forEach(function (element) { + HoverOverlay.destroyHoverOverlay(element); + }); + entitiesWithHoverOverlays = []; + if (this.grabbedThingID !== null) { Messages.sendMessage('Hifi-Teleport-Ignore-Remove', this.grabbedThingID); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a83d2159bb..6bb0675bc8 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1,5 +1,3 @@ -"use strict"; - // edit.js // // Created by Brad Hefta-Gaub on 10/2/14. @@ -16,6 +14,8 @@ Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ (function() { // BEGIN LOCAL_SCOPE + +"use strict"; var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; @@ -41,19 +41,19 @@ Script.include([ var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); -const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); -const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); +var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, - } + }; } else { return { url: PARTICLE_SYSTEM_URL, - } + }; } }); @@ -94,7 +94,7 @@ selectionManager.addEventListener(function () { } }); -const KEY_P = 80; //Key code for letter p used for Parenting hotkey. +var KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; @@ -123,6 +123,8 @@ var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; +var CREATE_ENABLED_ICON = "icons/tablet-icons/edit-i.svg"; +var CREATE_DISABLED_ICON = "icons/tablet-icons/edit-disabled.svg"; // marketplace info, etc. not quite ready yet. var SHOULD_SHOW_PROPERTY_MENU = false; @@ -130,6 +132,7 @@ var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissi var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary permissions to place items on this domain."; var isActive = false; +var createButton = null; var IMPORTING_SVO_OVERLAY_WIDTH = 144; var IMPORTING_SVO_OVERLAY_HEIGHT = 30; @@ -397,13 +400,15 @@ var toolBar = (function () { } }); + var createButtonIconRsrc = ((Entities.canRez() || Entities.canRezTmp()) ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", + icon: createButtonIconRsrc, activeIcon: "icons/tablet-icons/edit-a.svg", text: "CREATE", sortOrder: 10 }); + createButton = activeButton; tablet.screenChanged.connect(function (type, url) { if (isActive && (type !== "QML" || url !== "Edit.qml")) { that.toggle(); @@ -411,7 +416,12 @@ var toolBar = (function () { }); tablet.fromQml.connect(fromQml); - activeButton.clicked.connect(function() { + createButton.clicked.connect(function() { + if ( ! (Entities.canRez() || Entities.canRezTmp()) ) { + Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); + return; + } + that.toggle(); }); @@ -642,7 +652,7 @@ var toolBar = (function () { grid.setEnabled(true); propertiesTool.setVisible(true); selectionDisplay.triggerMapping.enable(); - print("starting tablet in landscape mode") + print("starting tablet in landscape mode"); tablet.landscape = true; // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 @@ -760,8 +770,38 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { } } +// Handles any edit mode updates required when domains have switched +function handleDomainChange() { + if ( (createButton === null) || (createButton === undefined) ){ + //--EARLY EXIT--( nothing to safely update ) + return; + } + + var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); + createButton.editProperties({ + icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON), + }); +} + +function handleMessagesReceived(channel, message, sender) { + switch( channel ){ + case 'entityToolUpdates': { + handleOverlaySelectionToolUpdates( channel, message, sender ); + break; + } + case 'Toolbar-DomainChanged': { + handleDomainChange(); + break; + } + default: { + return; + } + } +} + +Messages.subscribe('Toolbar-DomainChanged'); Messages.subscribe("entityToolUpdates"); -Messages.messageReceived.connect(handleOverlaySelectionToolUpdates); +Messages.messageReceived.connect(handleMessagesReceived); var mouseHasMovedSincePress = false; var mousePressStartTime = 0; @@ -1010,7 +1050,6 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entity List...", - shortcutKey: "CTRL+META+L", afterItem: "Entities", grouping: "Advanced" }); @@ -1041,7 +1080,6 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", - shortcutKey: "CTRL+META+L", afterItem: GRABBABLE_ENTITIES_MENU_ITEM, isCheckable: true, isChecked: true, @@ -1050,7 +1088,6 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Small Models", - shortcutKey: "CTRL+META+S", afterItem: "Allow Selecting of Large Models", isCheckable: true, isChecked: true, @@ -1059,7 +1096,6 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Lights", - shortcutKey: "CTRL+SHIFT+META+L", afterItem: "Allow Selecting of Small Models", isCheckable: true, grouping: "Advanced" @@ -1067,14 +1103,12 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities In Box", - shortcutKey: "CTRL+SHIFT+META+A", afterItem: "Allow Selecting of Lights", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities Touching Box", - shortcutKey: "CTRL+SHIFT+META+T", afterItem: "Select All Entities In Box", grouping: "Advanced" }); @@ -1082,21 +1116,18 @@ function setupModelMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Export Entities", - shortcutKey: "CTRL+META+E", afterItem: "Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Import Entities", - shortcutKey: "CTRL+META+I", afterItem: "Export Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Import Entities from URL", - shortcutKey: "CTRL+META+U", afterItem: "Import Entities", grouping: "Advanced" }); @@ -1189,6 +1220,8 @@ Script.scriptEnding.connect(function () { Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates); Messages.unsubscribe("entityToolUpdates"); + Messages.unsubscribe("Toolbar-DomainChanged"); + createButton = null; }); var lastOrientation = null; @@ -1314,7 +1347,7 @@ function unparentSelectedEntities() { if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { parentCheck = true; } - Entities.editEntity(id, {parentID: null}) + Entities.editEntity(id, {parentID: null}); return true; }); if (parentCheck) { @@ -1349,7 +1382,7 @@ function parentSelectedEntities() { if (parentId !== lastEntityId) { parentCheck = true; } - Entities.editEntity(id, {parentID: lastEntityId}) + Entities.editEntity(id, {parentID: lastEntityId}); } }); @@ -1522,9 +1555,9 @@ function importSVO(importURL) { var entityPositions = []; var entityParentIDs = []; - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]); + var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; - if (NO_ADJUST_ENTITY_TYPES.indexOf(properties.type) === -1) { + if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { targetDirection = Camera.orientation; @@ -1537,36 +1570,36 @@ function importSVO(importURL) { var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - var properties = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, - properties.registrationPoint, properties.dimensions, properties.rotation); - var delta = Vec3.subtract(adjustedPosition, properties.position); + curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); + var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); var distance = Vec3.dot(delta, targetDirection); deltaParallel = Math.min(distance, deltaParallel); deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), deltaPerpendicular); - entityPositions[i] = properties.position; - entityParentIDs[i] = properties.parentID; + entityPositions[i] = curLoopEntityProps.position; + entityParentIDs[i] = curLoopEntityProps.parentID; } deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); } if (grid.getSnapToGrid()) { - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", "registrationPoint"]); - var position = Vec3.sum(deltaPosition, properties.position); - position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions, - properties.registrationPoint), properties.dimensions, properties.registrationPoint); - deltaPosition = Vec3.subtract(position, properties.position); + var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); + position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, + firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); + deltaPosition = Vec3.subtract(position, firstEntityProps.position); } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - if (Uuid.isNull(entityParentIDs[i])) { - Entities.editEntity(pastedEntityIDs[i], { - position: Vec3.sum(deltaPosition, entityPositions[i]) + for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + if (Uuid.isNull(entityParentIDs[editEntityIndex])) { + Entities.editEntity(pastedEntityIDs[editEntityIndex], { + position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) }); } } @@ -2212,7 +2245,7 @@ entityListTool.webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { - print("edit.js: Error parsing JSON: " + e.name + " data " + data) + print("edit.js: Error parsing JSON: " + e.name + " data " + data); return; } diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index a3d1923aa9..5ea1dd0963 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -20,7 +20,9 @@ var blastShareText = "Blast to my Connections", hifiShareText = "Share to Snaps Feed", hifiAlreadySharedText = "Already Shared to Snaps Feed", facebookShareText = "Share to Facebook", - twitterShareText = "Share to Twitter"; + twitterShareText = "Share to Twitter", + shareButtonLabelTextActive = "SHARE:", + shareButtonLabelTextInactive = "SHARE"; function fileExtensionMatches(filePath, extension) { return filePath.split('.').pop().toLowerCase() === extension; @@ -138,6 +140,8 @@ function selectImageToShare(selectedID, isSelected) { var imageContainer = document.getElementById(selectedID), image = document.getElementById(selectedID + 'img'), shareBar = document.getElementById(selectedID + "shareBar"), + showShareButtonsDots = document.getElementById(selectedID + "showShareButtonsDots"), + showShareButtonsLabel = document.getElementById(selectedID + "showShareButtonsLabel"), shareButtonsDiv = document.getElementById(selectedID + "shareButtonsDiv"), shareBarHelp = document.getElementById(selectedID + "shareBarHelp"), showShareButtonsButtonDiv = document.getElementById(selectedID + "showShareButtonsButtonDiv"), @@ -156,6 +160,9 @@ function selectImageToShare(selectedID, isSelected) { shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.45)"; shareBar.style.pointerEvents = "initial"; + showShareButtonsDots.style.visibility = "hidden"; + showShareButtonsLabel.innerHTML = shareButtonLabelTextActive; + shareButtonsDiv.style.visibility = "visible"; shareBarHelp.style.visibility = "visible"; @@ -176,6 +183,9 @@ function selectImageToShare(selectedID, isSelected) { shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.0)"; shareBar.style.pointerEvents = "none"; + showShareButtonsDots.style.visibility = "visible"; + showShareButtonsLabel.innerHTML = shareButtonLabelTextInactive; + shareButtonsDiv.style.visibility = "hidden"; shareBarHelp.style.visibility = "hidden"; } @@ -185,6 +195,7 @@ function createShareBar(parentID, isLoggedIn, canShare, isGif, blastButtonDisabl shareBarHelpID = parentID + "shareBarHelp", shareButtonsDivID = parentID + "shareButtonsDiv", showShareButtonsButtonDivID = parentID + "showShareButtonsButtonDiv", + showShareButtonsDotsID = parentID + "showShareButtonsDots", showShareButtonsLabelID = parentID + "showShareButtonsLabel", blastToConnectionsButtonID = parentID + "blastToConnectionsButton", shareWithEveryoneButtonID = parentID + "shareWithEveryoneButton", @@ -199,8 +210,8 @@ function createShareBar(parentID, isLoggedIn, canShare, isGif, blastButtonDisabl if (canShare) { shareBarInnerHTML = '' + '
' + - '' + - '' + + '' + + '' + '' + '
' + '' + @@ -217,7 +228,7 @@ function createShareBar(parentID, isLoggedIn, canShare, isGif, blastButtonDisabl document.getElementById(parentID + 'img').onclick = function () { selectImageToShare(parentID, true); }; } else { shareBarInnerHTML = '
' + - '' + + '' + '' + '' + '
' + @@ -230,7 +241,7 @@ function createShareBar(parentID, isLoggedIn, canShare, isGif, blastButtonDisabl } } else { shareBarInnerHTML = '
' + - '' + + '' + '' + '' + '
' + diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index b429a9f3ae..53f88ea62d 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -14,8 +14,6 @@ Script.include(Script.resolvePath("../libraries/utils.js")); Script.include(Script.resolvePath("../libraries/controllers.js")); Script.include(Script.resolvePath("../libraries/Xform.js")); -var VEC3_ZERO = {x: 0, y: 0, z: 0}; -var X_AXIS = {x: 1, y: 0, z: 0}; var Y_AXIS = {x: 0, y: 1, z: 0}; var DEFAULT_DPI = 34; var DEFAULT_WIDTH = 0.4375; @@ -25,12 +23,13 @@ var CAMERA_MATRIX = -7; var ROT_Y_180 = {x: 0.0, y: 1.0, z: 0, w: 0}; var ROT_LANDSCAPE = {x: 1.0, y: 1.0, z: 0, w: 0}; var ROT_LANDSCAPE_WINDOW = {x: 0.0, y: 0.0, z: 0.0, w: 0}; -var ROT_IDENT = {x: 0, y: 0, z: 0, w: 1}; var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 }; var INCHES_TO_METERS = 1 / 39.3701; var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; var NO_HANDS = -1; +var DELAY_FOR_30HZ = 33; // milliseconds + // will need to be recaclulated if dimensions of fbx model change. var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; @@ -77,7 +76,7 @@ function calcSpawnInfo(hand, tabletHeight) { var TABLET_RAKE_ANGLE = 30; rotation = Quat.multiply(Quat.angleAxis(TABLET_RAKE_ANGLE, Vec3.multiplyQbyV(lookAt, Vec3.UNIT_X)), lookAt); - var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.4, z: 0.05 }; + var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.6, z: 0.1 }; position = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiply(tabletHeight, RELATIVE_SPAWN_OFFSET))); return { @@ -561,9 +560,29 @@ function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) { } } +WebTablet.prototype.scheduleMouseMoveProcessor = function() { + var _this = this; + if (!this.moveEventTimer) { + this.moveEventTimer = Script.setTimeout(function() { + _this.mouseMoveProcessor(); + }, DELAY_FOR_30HZ); + } +}; + WebTablet.prototype.mouseMoveEvent = function (event) { if (this.dragging) { - var pickRay = Camera.computePickRay(event.x, event.y); + this.currentMouse = { + x: event.x, + y: event.y + }; + this.scheduleMouseMoveProcessor(); + } +}; + +WebTablet.prototype.mouseMoveProcessor = function () { + this.moveEventTimer = null; + if (this.dragging) { + var pickRay = Camera.computePickRay(this.currentMouse.x, this.currentMouse.y); // transform pickRay into camera local coordinates var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); @@ -582,6 +601,7 @@ WebTablet.prototype.mouseMoveEvent = function (event) { localPosition: localPosition }); } + this.scheduleMouseMoveProcessor(); } }; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 2c81622668..8ea22192fc 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -482,10 +482,10 @@ function populateNearbyUserList(selectData, oldAudioData) { isPresent: true, isReplicated: avatar.isReplicated }; + // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. + Users.requestUsernameFromID(id); if (id) { addAvatarNode(id); // No overlay for ourselves - // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. - Users.requestUsernameFromID(id); avatarsOfInterest[id] = true; } else { // Return our username from the Account API @@ -862,6 +862,9 @@ function avatarDisconnected(nodeID) { function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); + if (onPalScreen) { + tablet.gotoHomeScreen(); + } } function avatarAdded(avatarID) { diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 6436ff1ef4..39043805b8 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -77,7 +77,6 @@ void TestWindow::initGl() { #ifdef DEFERRED_LIGHTING auto deferredLightingEffect = DependencyManager::get(); deferredLightingEffect->init(); - deferredLightingEffect->setGlobalLight(_light); initDeferredPipelines(*_shapePlumber); #endif } diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 9c4e3ae870..ce47a896aa 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -889,11 +889,6 @@ private: BackgroundRenderData::_item = _main3DScene->allocateID(); transaction.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); } - // Setup the current Zone Entity lighting - { - auto stage = DependencyManager::get()->getSkyStage(); - DependencyManager::get()->setGlobalLight(stage->getSunLight()); - } { PerformanceTimer perfTimer("SceneProcessTransaction"); @@ -914,8 +909,6 @@ private: PerformanceTimer perfTimer("draw"); // The pending changes collecting the changes here render::Transaction transaction; - // Setup the current Zone Entity lighting - DependencyManager::get()->setGlobalLight(_sunSkyStage.getSunLight()); { PerformanceTimer perfTimer("SceneProcessTransaction"); _main3DScene->enqueueTransaction(transaction); diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index d10ab1ddbe..9847e9f7b9 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -57,8 +57,6 @@ #include #include -#include -#include #include #include @@ -172,7 +170,6 @@ void QTestWindow::draw() { testShaderBuild(skin_model_normal_map_vert, model_translucent_frag); testShaderBuild(model_shadow_vert, model_shadow_frag); - testShaderBuild(untextured_particle_vert, untextured_particle_frag); testShaderBuild(textured_particle_vert, textured_particle_frag); /* FIXME: Bring back the ssao shader tests testShaderBuild(gaussian_blur_vertical_vert, gaussian_blur_frag); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 0561956709..5de44e8897 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,8 +17,8 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") -add_subdirectory(atp-get) -set_target_properties(atp-get PROPERTIES FOLDER "Tools") +add_subdirectory(atp-client) +set_target_properties(atp-client PROPERTIES FOLDER "Tools") add_subdirectory(oven) set_target_properties(oven PROPERTIES FOLDER "Tools") diff --git a/tools/ac-client/src/ACClientApp.cpp b/tools/ac-client/src/ACClientApp.cpp index b81d092662..e00560158f 100644 --- a/tools/ac-client/src/ACClientApp.cpp +++ b/tools/ac-client/src/ACClientApp.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,9 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : const QCommandLineOption verboseOutput("v", "verbose output"); parser.addOption(verboseOutput); + const QCommandLineOption authOption("u", "set usename and pass", "username:password"); + parser.addOption(authOption); + const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); parser.addOption(domainAddressOption); @@ -42,7 +46,6 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); parser.addOption(listenPortOption); - if (!parser.parse(QCoreApplication::arguments())) { qCritical() << parser.errorText() << endl; parser.showHelp(); @@ -66,6 +69,7 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : const_cast(&shared())->setEnabled(QtInfoMsg, false); const_cast(&shared())->setEnabled(QtWarningMsg, false); } + QString domainServerAddress = "127.0.0.1:40103"; if (parser.isSet(domainAddressOption)) { @@ -81,6 +85,18 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : listenPort = parser.value(listenPortOption).toInt(); } + if (parser.isSet(authOption)) { + QStringList pieces = parser.value(authOption).split(":"); + if (pieces.size() != 2) { + qDebug() << "-u should be followed by username:password"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _username = pieces[0]; + _password = pieces[1]; + } + Setting::init(); DependencyManager::registerInheritance(); @@ -88,33 +104,56 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : DependencyManager::set(); DependencyManager::set(NodeType::Agent, listenPort); + auto accountManager = DependencyManager::get(); + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); auto nodeList = DependencyManager::get(); - // start the nodeThread so its event loop is running - nodeList->startThread(); // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(nodeList.data()); connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - const DomainHandler& domainHandler = nodeList->getDomainHandler(); + // start the nodeThread so its event loop is running + // (must happen after the checkin timer is created with the nodelist as it's parent) + nodeList->startThread(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); - // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ACClientApp::domainConnectionRefused); connect(nodeList.data(), &NodeList::nodeAdded, this, &ACClientApp::nodeAdded); connect(nodeList.data(), &NodeList::nodeKilled, this, &ACClientApp::nodeKilled); connect(nodeList.data(), &NodeList::nodeActivated, this, &ACClientApp::nodeActivated); - // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); - // connect(nodeList.data(), &NodeList::uuidChanged, this, &ACClientApp::setSessionUUID); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ACClientApp::notifyPacketVersionMismatch); - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); + if (_verbose) { + QString username = accountManager->getAccountInfo().getUsername(); + qDebug() << "cached username is" << username << ", isLoggedIn =" << accountManager->isLoggedIn(); + } + + if (!_username.isEmpty()) { + + connect(accountManager.data(), &AccountManager::newKeypair, this, [&](){ + if (_verbose) { + qDebug() << "new keypair has been created."; + } + }); + + connect(accountManager.data(), &AccountManager::loginComplete, this, [&](){ + if (_verbose) { + qDebug() << "login successful"; + } + }); + connect(accountManager.data(), &AccountManager::loginFailed, this, [&](){ + qDebug() << "login failed."; + }); + accountManager->requestAccessToken(_username, _password); + } + DependencyManager::get()->handleLookupString(domainServerAddress, false); QTimer* doTimer = new QTimer(this); diff --git a/tools/ac-client/src/ACClientApp.h b/tools/ac-client/src/ACClientApp.h index 29d571688e..e295b17654 100644 --- a/tools/ac-client/src/ACClientApp.h +++ b/tools/ac-client/src/ACClientApp.h @@ -47,6 +47,9 @@ private: bool _sawAvatarMixer { false }; bool _sawAssetServer { false }; bool _sawMessagesMixer { false }; + + QString _username; + QString _password; }; #endif //hifi_ACClientApp_h diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-client/CMakeLists.txt similarity index 77% rename from tools/atp-get/CMakeLists.txt rename to tools/atp-client/CMakeLists.txt index 75f92b787d..4cee30bcc3 100644 --- a/tools/atp-get/CMakeLists.txt +++ b/tools/atp-client/CMakeLists.txt @@ -1,4 +1,4 @@ -set(TARGET_NAME atp-get) +set(TARGET_NAME atp-client) setup_hifi_project(Core Widgets) setup_memory_debugger() link_hifi_libraries(shared networking) diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp new file mode 100644 index 0000000000..3e2f8ca71d --- /dev/null +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -0,0 +1,406 @@ +// +// ATPClientApp.cpp +// tools/atp-client/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ATPClientApp.h" + +#define HIGH_FIDELITY_ATP_CLIENT_USER_AGENT "Mozilla/5.0 (HighFidelityATPClient)" +#define TIMEOUT_MILLISECONDS 8000 + +ATPClientApp::ATPClientApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + // parse command-line + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity ATP-Client"); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + + const QCommandLineOption uploadOption("T", "upload local file", "local-file-to-send"); + parser.addOption(uploadOption); + + const QCommandLineOption authOption("u", "set usename and pass", "username:password"); + parser.addOption(authOption); + + const QCommandLineOption outputFilenameOption("o", "output filename", "output-file-name"); + parser.addOption(outputFilenameOption); + + const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); + parser.addOption(domainAddressOption); + + const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); + parser.addOption(listenPortOption); + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + _verbose = parser.isSet(verboseOutput); + if (!_verbose) { + QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); + + const_cast(&networking())->setEnabled(QtDebugMsg, false); + const_cast(&networking())->setEnabled(QtInfoMsg, false); + const_cast(&networking())->setEnabled(QtWarningMsg, false); + + const_cast(&shared())->setEnabled(QtDebugMsg, false); + const_cast(&shared())->setEnabled(QtInfoMsg, false); + const_cast(&shared())->setEnabled(QtWarningMsg, false); + } + + QStringList posArgs = parser.positionalArguments(); + if (posArgs.size() != 1) { + qDebug() << "give remote url argument"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _url = QUrl(posArgs[0]); + if (_url.scheme() != "atp") { + qDebug() << "url should start with atp:"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + int domainPort = 40103; + if (_url.port() != -1) { + domainPort = _url.port(); + } + + if (parser.isSet(outputFilenameOption)) { + _localOutputFile = parser.value(outputFilenameOption); + } + + if (parser.isSet(uploadOption)) { + _localUploadFile = parser.value(uploadOption); + } + + if (parser.isSet(authOption)) { + QStringList pieces = parser.value(authOption).split(":"); + if (pieces.size() != 2) { + qDebug() << "-u should be followed by username:password"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _username = pieces[0]; + _password = pieces[1]; + _waitingForLogin = true; + } + + if (parser.isSet(listenPortOption)) { + _listenPort = parser.value(listenPortOption).toInt(); + } + + _domainServerAddress = QString("127.0.0.1") + ":" + QString::number(domainPort); + if (parser.isSet(domainAddressOption)) { + _domainServerAddress = parser.value(domainAddressOption); + } else if (!_url.host().isEmpty()) { + QUrl domainURL; + domainURL.setScheme("hifi"); + domainURL.setHost(_url.host()); + _domainServerAddress = domainURL.toString(); + } + + Setting::init(); + DependencyManager::registerInheritance(); + + DependencyManager::set([&]{ return QString(HIGH_FIDELITY_ATP_CLIENT_USER_AGENT); }); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, _listenPort); + + auto accountManager = DependencyManager::get(); + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); + + auto nodeList = DependencyManager::get(); + + // setup a timer for domain-server check ins + _domainCheckInTimer = new QTimer(nodeList.data()); + connect(_domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); + _domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + + // start the nodeThread so its event loop is running + // (must happen after the checkin timer is created with the nodelist as it's parent) + nodeList->startThread(); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPClientApp::domainConnectionRefused); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPClientApp::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPClientApp::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPClientApp::nodeActivated); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPClientApp::notifyPacketVersionMismatch); + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); + + if (_verbose) { + QString username = accountManager->getAccountInfo().getUsername(); + qDebug() << "cached username is" << username << ", isLoggedIn =" << accountManager->isLoggedIn(); + } + + if (!_username.isEmpty()) { + + connect(accountManager.data(), &AccountManager::newKeypair, this, [&](){ + if (_verbose) { + qDebug() << "new keypair has been created."; + } + }); + + connect(accountManager.data(), &AccountManager::loginComplete, this, [&](){ + if (_verbose) { + qDebug() << "login successful"; + } + _waitingForLogin = false; + go(); + }); + connect(accountManager.data(), &AccountManager::loginFailed, this, [&](){ + qDebug() << "login failed."; + _waitingForLogin = false; + go(); + }); + accountManager->requestAccessToken(_username, _password); + } + + auto assetClient = DependencyManager::set(); + assetClient->init(); + + if (_verbose) { + qDebug() << "domain-server address is" << _domainServerAddress; + } + + DependencyManager::get()->handleLookupString(_domainServerAddress, false); + + QTimer* _timeoutTimer = new QTimer(this); + _timeoutTimer->setSingleShot(true); + connect(_timeoutTimer, &QTimer::timeout, this, &ATPClientApp::timedOut); + _timeoutTimer->start(TIMEOUT_MILLISECONDS); +} + +ATPClientApp::~ATPClientApp() { + if (_domainCheckInTimer) { + QMetaObject::invokeMethod(_domainCheckInTimer, "deleteLater", Qt::QueuedConnection); + } + if (_timeoutTimer) { + QMetaObject::invokeMethod(_timeoutTimer, "deleteLater", Qt::QueuedConnection); + } +} + +void ATPClientApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + // this is non-fatal if we are trying to get an authenticated connection -- it will be retried. + if (_verbose) { + qDebug() << "domainConnectionRefused"; + } +} + +void ATPClientApp::domainChanged(const QString& domainHostname) { + if (_verbose) { + qDebug() << "domainChanged"; + } +} + +void ATPClientApp::nodeAdded(SharedNodePointer node) { + if (_verbose) { + qDebug() << "node added: " << node->getType(); + } +} + +void ATPClientApp::nodeActivated(SharedNodePointer node) { + if (!_waitingForLogin && node->getType() == NodeType::AssetServer) { + _waitingForNode = false; + go(); + } +} + +void ATPClientApp::go() { + if (_waitingForLogin || _waitingForNode) { + return; + } + + auto path = _url.path(); + + if (_verbose) { + qDebug() << "path is " << path; + } + + if (!_localUploadFile.isEmpty()) { + uploadAsset(); + } else if (path == "/") { + listAssets(); + } else { + lookupAsset(); + } +} + +void ATPClientApp::nodeKilled(SharedNodePointer node) { + if (_verbose) { + qDebug() << "nodeKilled" << node->getType(); + } +} + +void ATPClientApp::timedOut() { + finish(1); +} + +void ATPClientApp::notifyPacketVersionMismatch() { + if (_verbose) { + qDebug() << "packet version mismatch"; + } + finish(1); +} + +void ATPClientApp::uploadAsset() { + auto path = _url.path(); + if (path == "/") { + qDebug() << "cannot upload to path of /"; + QCoreApplication::exit(1); + } + + auto upload = DependencyManager::get()->createUpload(_localUploadFile); + QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { + if (upload->getError() != AssetUpload::NoError) { + qDebug() << "upload failed: " << upload->getErrorString(); + } else { + setMapping(hash); + } + + upload->deleteLater(); + }); + + upload->start(); +} + +void ATPClientApp::setMapping(QString hash) { + auto path = _url.path(); + auto assetClient = DependencyManager::get(); + auto request = assetClient->createSetMappingRequest(path, hash); + + connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { + if (request->getError() != SetMappingRequest::NoError) { + qDebug() << "upload succeeded, but couldn't set mapping: " << request->getErrorString(); + } else if (_verbose) { + qDebug() << "mapping set."; + } + request->deleteLater(); + finish(0); + }); + + request->start(); +} + +void ATPClientApp::listAssets() { + auto request = DependencyManager::get()->createGetAllMappingsRequest(); + QObject::connect(request, &GetAllMappingsRequest::finished, this, [=](GetAllMappingsRequest* request) mutable { + auto result = request->getError(); + if (result == GetAllMappingsRequest::NotFound) { + qDebug() << "not found: " << request->getErrorString(); + } else if (result == GetAllMappingsRequest::NoError) { + auto mappings = request->getMappings(); + for (auto& kv : mappings ) { + qDebug() << kv.first << kv.second; + } + } else { + qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); + } + request->deleteLater(); + finish(0); + }); + request->start(); +} + +void ATPClientApp::lookupAsset() { + auto path = _url.path(); + auto request = DependencyManager::get()->createGetMappingRequest(path); + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + qDebug() << "not found: " << request->getErrorString(); + } else if (result == GetMappingRequest::NoError) { + qDebug() << "found, hash is " << request->getHash(); + download(request->getHash()); + } else { + qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); + } + request->deleteLater(); + }); + request->start(); +} + +void ATPClientApp::download(AssetHash hash) { + auto assetClient = DependencyManager::get(); + auto assetRequest = new AssetRequest(hash); + + connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { + Q_ASSERT(request->getState() == AssetRequest::Finished); + + if (request->getError() == AssetRequest::Error::NoError) { + QString data = QString::fromUtf8(request->getData()); + if (_localOutputFile == "" || _localOutputFile == "-") { + QTextStream cout(stdout); + cout << data; + } else { + QFile outputHandle(_localOutputFile); + if (outputHandle.open(QIODevice::ReadWrite)) { + QTextStream stream( &outputHandle ); + stream << data; + } else { + qDebug() << "couldn't open output file:" << _localOutputFile; + } + } + } + + request->deleteLater(); + finish(0); + }); + + assetRequest->start(); +} + +void ATPClientApp::finish(int exitCode) { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + + QCoreApplication::exit(exitCode); +} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-client/src/ATPClientApp.h similarity index 60% rename from tools/atp-get/src/ATPGetApp.h rename to tools/atp-client/src/ATPClientApp.h index 5507d2aa62..a3904d6e50 100644 --- a/tools/atp-get/src/ATPGetApp.h +++ b/tools/atp-client/src/ATPClientApp.h @@ -1,6 +1,6 @@ // -// ATPGetApp.h -// tools/atp-get/src +// ATPClientApp.h +// tools/atp-client/src // // Created by Seth Alves on 2017-3-15 // Copyright 2017 High Fidelity, Inc. @@ -10,8 +10,8 @@ // -#ifndef hifi_ATPGetApp_h -#define hifi_ATPGetApp_h +#ifndef hifi_ATPClientApp_h +#define hifi_ATPClientApp_h #include #include @@ -23,11 +23,11 @@ #include -class ATPGetApp : public QCoreApplication { +class ATPClientApp : public QCoreApplication { Q_OBJECT public: - ATPGetApp(int argc, char* argv[]); - ~ATPGetApp(); + ATPClientApp(int argc, char* argv[]); + ~ATPClientApp(); private slots: void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); @@ -38,15 +38,33 @@ private slots: void notifyPacketVersionMismatch(); private: + void go(); NodeList* _nodeList; void timedOut(); - void lookup(); + void uploadAsset(); + void setMapping(QString hash); + void lookupAsset(); + void listAssets(); void download(AssetHash hash); void finish(int exitCode); bool _verbose; QUrl _url; QString _localOutputFile; + QString _localUploadFile; + + int _listenPort { INVALID_PORT }; + + QString _domainServerAddress; + + QString _username; + QString _password; + + bool _waitingForLogin { false }; + bool _waitingForNode { true }; + + QTimer* _domainCheckInTimer { nullptr }; + QTimer* _timeoutTimer { nullptr }; }; -#endif // hifi_ATPGetApp_h +#endif // hifi_ATPClientApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-client/src/main.cpp similarity index 89% rename from tools/atp-get/src/main.cpp rename to tools/atp-client/src/main.cpp index bddf30c666..88119604cf 100644 --- a/tools/atp-get/src/main.cpp +++ b/tools/atp-client/src/main.cpp @@ -1,6 +1,6 @@ // // main.cpp -// tools/atp-get/src +// tools/atp-client/src // // Created by Seth Alves on 2017-3-15 // Copyright 2017 High Fidelity, Inc. @@ -15,7 +15,7 @@ #include -#include "ATPGetApp.h" +#include "ATPClientApp.h" using namespace std; @@ -25,7 +25,7 @@ int main(int argc, char * argv[]) { QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - ATPGetApp app(argc, argv); + ATPClientApp app(argc, argv); return app.exec(); } diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp deleted file mode 100644 index 4125582c21..0000000000 --- a/tools/atp-get/src/ATPGetApp.cpp +++ /dev/null @@ -1,255 +0,0 @@ -// -// ATPGetApp.cpp -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ATPGetApp.h" - -ATPGetApp::ATPGetApp(int argc, char* argv[]) : - QCoreApplication(argc, argv) -{ - // parse command-line - QCommandLineParser parser; - parser.setApplicationDescription("High Fidelity ATP-Get"); - - const QCommandLineOption helpOption = parser.addHelpOption(); - - const QCommandLineOption verboseOutput("v", "verbose output"); - parser.addOption(verboseOutput); - - const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); - parser.addOption(domainAddressOption); - - const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); - parser.addOption(cacheSTUNOption); - - const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); - parser.addOption(listenPortOption); - - - if (!parser.parse(QCoreApplication::arguments())) { - qCritical() << parser.errorText() << endl; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (parser.isSet(helpOption)) { - parser.showHelp(); - Q_UNREACHABLE(); - } - - _verbose = parser.isSet(verboseOutput); - if (!_verbose) { - QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); - - const_cast(&networking())->setEnabled(QtDebugMsg, false); - const_cast(&networking())->setEnabled(QtInfoMsg, false); - const_cast(&networking())->setEnabled(QtWarningMsg, false); - - const_cast(&shared())->setEnabled(QtDebugMsg, false); - const_cast(&shared())->setEnabled(QtInfoMsg, false); - const_cast(&shared())->setEnabled(QtWarningMsg, false); - } - - - QStringList filenames = parser.positionalArguments(); - if (filenames.empty() || filenames.size() > 2) { - qDebug() << "give remote url and optional local filename as arguments"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - _url = QUrl(filenames[0]); - if (_url.scheme() != "atp") { - qDebug() << "url should start with atp:"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (filenames.size() == 2) { - _localOutputFile = filenames[1]; - } - - QString domainServerAddress = "127.0.0.1:40103"; - if (parser.isSet(domainAddressOption)) { - domainServerAddress = parser.value(domainAddressOption); - } - - if (_verbose) { - qDebug() << "domain-server address is" << domainServerAddress; - } - - int listenPort = INVALID_PORT; - if (parser.isSet(listenPortOption)) { - listenPort = parser.value(listenPortOption).toInt(); - } - - Setting::init(); - DependencyManager::registerInheritance(); - - DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - - - auto nodeList = DependencyManager::get(); - nodeList->startThread(); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(nodeList.data()); - connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); - // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); - // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); - // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); - - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); - - DependencyManager::get()->handleLookupString(domainServerAddress, false); - - auto assetClient = DependencyManager::set(); - assetClient->init(); - - QTimer* doTimer = new QTimer(this); - doTimer->setSingleShot(true); - connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); - doTimer->start(4000); -} - -ATPGetApp::~ATPGetApp() { -} - - -void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - qDebug() << "domainConnectionRefused"; -} - -void ATPGetApp::domainChanged(const QString& domainHostname) { - if (_verbose) { - qDebug() << "domainChanged"; - } -} - -void ATPGetApp::nodeAdded(SharedNodePointer node) { - if (_verbose) { - qDebug() << "node added: " << node->getType(); - } -} - -void ATPGetApp::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - lookup(); - } -} - -void ATPGetApp::nodeKilled(SharedNodePointer node) { - qDebug() << "nodeKilled"; -} - -void ATPGetApp::timedOut() { - finish(1); -} - -void ATPGetApp::notifyPacketVersionMismatch() { - if (_verbose) { - qDebug() << "packet version mismatch"; - } - finish(1); -} - -void ATPGetApp::lookup() { - - auto path = _url.path(); - qDebug() << "path is " << path; - - auto request = DependencyManager::get()->createGetMappingRequest(path); - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - qDebug() << "not found"; - } else if (result == GetMappingRequest::NoError) { - qDebug() << "found, hash is " << request->getHash(); - download(request->getHash()); - } else { - qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); - } - request->deleteLater(); - }); - request->start(); -} - -void ATPGetApp::download(AssetHash hash) { - auto assetClient = DependencyManager::get(); - auto assetRequest = new AssetRequest(hash); - - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { - Q_ASSERT(request->getState() == AssetRequest::Finished); - - if (request->getError() == AssetRequest::Error::NoError) { - QString data = QString::fromUtf8(request->getData()); - if (_localOutputFile == "") { - QTextStream cout(stdout); - cout << data; - } else { - QFile outputHandle(_localOutputFile); - if (outputHandle.open(QIODevice::ReadWrite)) { - QTextStream stream( &outputHandle ); - stream << data; - } else { - qDebug() << "couldn't open output file:" << _localOutputFile; - } - } - } - - request->deleteLater(); - finish(0); - }); - - assetRequest->start(); -} - -void ATPGetApp::finish(int exitCode) { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - - QCoreApplication::exit(exitCode); -} diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index 8a72784d7c..0259a6baf8 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -354,8 +354,8 @@ void FBXBaker::rewriteAndBakeSceneTextures() { FbxFileTexture* fileTexture = property.GetSrcObject(j); // use QFileInfo to easily split up the existing texture filename into its components - QString fbxFileName { fileTexture->GetFileName() }; - QFileInfo textureFileInfo { fbxFileName.replace("\\", "/") }; + QString fbxTextureFileName { fileTexture->GetFileName() }; + QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") }; // make sure this texture points to something and isn't one we've already re-mapped if (!textureFileInfo.filePath().isEmpty() @@ -372,6 +372,9 @@ void FBXBaker::rewriteAndBakeSceneTextures() { qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); + // write the new filename into the FBX scene fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); @@ -379,9 +382,6 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // be right beside the FBX fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData()); - // figure out the URL to this texture, embedded or external - auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); - if (!_bakingTextures.contains(urlToTexture)) { // bake this texture asynchronously bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index dc763cc82d..d0b8c3cd65 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -61,7 +61,9 @@ Oven::Oven(int argc, char* argv[]) : if (parser.isSet(CLI_INPUT_PARAMETER) || parser.isSet(CLI_OUTPUT_PARAMETER)) { if (parser.isSet(CLI_INPUT_PARAMETER) && parser.isSet(CLI_OUTPUT_PARAMETER)) { BakerCLI* cli = new BakerCLI(this); - cli->bakeFile(parser.value(CLI_INPUT_PARAMETER), parser.value(CLI_OUTPUT_PARAMETER)); + QUrl inputUrl(QDir::fromNativeSeparators(parser.value(CLI_INPUT_PARAMETER))); + QUrl outputUrl(QDir::fromNativeSeparators(parser.value(CLI_OUTPUT_PARAMETER))); + cli->bakeFile(inputUrl, outputUrl.toString()); } else { parser.showHelp(); QApplication::quit(); diff --git a/unpublishedScripts/marketplace/laser/laser-a.svg b/unpublishedScripts/marketplace/laser/laser-a.svg new file mode 100644 index 0000000000..74033b3e7a --- /dev/null +++ b/unpublishedScripts/marketplace/laser/laser-a.svg @@ -0,0 +1,107 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/unpublishedScripts/marketplace/laser/laser.svg b/unpublishedScripts/marketplace/laser/laser.svg new file mode 100644 index 0000000000..a1f8887e90 --- /dev/null +++ b/unpublishedScripts/marketplace/laser/laser.svg @@ -0,0 +1,106 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/unpublishedScripts/marketplace/laser/laserPointerApp.js b/unpublishedScripts/marketplace/laser/laserPointerApp.js new file mode 100644 index 0000000000..515a2c3a76 --- /dev/null +++ b/unpublishedScripts/marketplace/laser/laserPointerApp.js @@ -0,0 +1,274 @@ +// +// Created by Alan-Michael Moody on 6/4/2017 +// + +'use strict'; + +(function () { + Script.include("/~/system/libraries/controllers.js"); + + var APP_NAME = 'LASER', + APP_ICON = Script.resolvePath('laser.svg'), + APP_ICON_ACTIVE = Script.resolvePath('laser-a.svg'); + + var POINT_INDEX_CHANNEL = "Hifi-Point-Index", + GRAB_DISABLE_CHANNEL = "Hifi-Grab-Disable", + POINTER_DISABLE_CHANNEL = "Hifi-Pointer-Disable"; + + var TRIGGER_PRESSURE = 0.95; + + var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); + + var button = tablet.addButton({ + icon: APP_ICON, + activeIcon: APP_ICON_ACTIVE, + text: APP_NAME + }); + + var laserEntities = { + left: { + beam: null, + sphere: null + }, + right: { + beam: null, + sphere: null + } + }; + + var rayExclusionList = []; + + function laser(hand) { + + var PICK_MAX_DISTANCE = 500; + var FORWARD_OFFSET = 0.05; + + var isNewEntityNeeded = (laserEntities[hand].beam === null); + + var jointName = hand === 'right' ? 'RightHandIndex4' : 'LeftHandIndex4'; //'RightHand' : 'LeftHand'; + + var _hand = hand === 'right' ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var controllerLocation = getControllerWorldLocation(_hand, true); + + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + var jointExists = (MyAvatar.getJointIndex(jointName) > 0) ; + var CONTROLLER_FORWARD_OFFSET = Vec3.multiply(Quat.getUp(worldControllerRotation), FORWARD_OFFSET); + + var pickRay = { + origin: worldControllerPosition, + direction: Quat.getUp(worldControllerRotation), + length: PICK_MAX_DISTANCE + }; + + if (jointExists) { + pickRay.origin = MyAvatar.getJointPosition(jointName); + pickRay.direction = MyAvatar.jointToWorldDirection(Vec3.UP, MyAvatar.getJointIndex(jointName)); + } + + var ray = Entities.findRayIntersection(pickRay, true, [], rayExclusionList, true); + var avatarRay = AvatarManager.findRayIntersection(pickRay, true, [], rayExclusionList, true); + + var dist = PICK_MAX_DISTANCE; + var intersection = null; + + if (avatarRay.intersects) { + intersection = avatarRay.intersection; + dist = Vec3.distance(pickRay.origin, avatarRay.intersection); + } else if (ray.intersects) { + intersection = ray.intersection; + dist = Vec3.distance(pickRay.origin, ray.intersection); + } + + var sphereSize = dist * 0.01; + + if (isNewEntityNeeded) { + + var sphere = { + lifetime: 360, + type: 'Sphere', + dimensions: {x: sphereSize, y: sphereSize, z: sphereSize}, + color: {red: 0, green: 255, blue: 0}, + position: intersection, + collisionless: true, + visible: false + }; + + var beam = { + lifetime: 360, + type: 'Line', + glow: 1.0, + lineWidth: 5, + alpha: 0.5, + ignoreRayIntersection: true, + drawInFront: true, + color: {red: 0, green: 255, blue: 0}, + parentID: MyAvatar.sessionUUID, + dimensions: Vec3.multiply(PICK_MAX_DISTANCE * 2, Vec3.ONE), + linePoints: [Vec3.ZERO, {x: 0, y: dist - FORWARD_OFFSET, z: 0}] + }; + + if(jointExists) { + beam.parentJointIndex = MyAvatar.getJointIndex(jointName); + beam.localPosition = {x: 0, y: FORWARD_OFFSET, z: 0}; + beam.localRotation = Quat.normalize({}); + } else { + beam.position = Vec3.sum(pickRay.origin, CONTROLLER_FORWARD_OFFSET); + beam.rotation = worldControllerRotation; + } + + laserEntities[hand].beam = Entities.addEntity(beam,true); + rayExclusionList.push(laserEntities[hand].beam); + + laserEntities[hand].sphere = Entities.addEntity(sphere,true); + rayExclusionList.push(laserEntities[hand].sphere); + + if (ray.intersects || avatarRay.intersects) { + Entities.editEntity(laserEntities[hand].sphere, { + visible: true + }); + } + + } else { + if (ray.intersects || avatarRay.intersects) { + if(jointExists) { + Entities.editEntity(laserEntities[hand].beam, { + parentID: MyAvatar.sessionUUID, + parentJointIndex: MyAvatar.getJointIndex(jointName), + localPosition: {x: 0, y: FORWARD_OFFSET, z: 0}, + localRotation: Quat.normalize({}), + dimensions: Vec3.multiply(PICK_MAX_DISTANCE * 2, Vec3.ONE), + linePoints: [Vec3.ZERO, {x: 0, y: dist - FORWARD_OFFSET, z: 0}] + }); + } else { + Entities.editEntity(laserEntities[hand].beam, { + parentID: MyAvatar.sessionUUID, + parentJointIndex: MyAvatar.getJointIndex(jointName), + position: Vec3.sum(pickRay.origin, CONTROLLER_FORWARD_OFFSET), + rotation: worldControllerRotation, + dimensions: Vec3.multiply(PICK_MAX_DISTANCE * 2, Vec3.ONE), + linePoints: [Vec3.ZERO, {x: 0, y: dist - FORWARD_OFFSET, z: 0}] + }); + } + + Entities.editEntity(laserEntities[hand].sphere, { + dimensions: {x: sphereSize, y: sphereSize, z: sphereSize}, + position: intersection, + visible: true + }); + } else { + if(jointExists) { + Entities.editEntity(laserEntities[hand].beam, { + parentID: MyAvatar.sessionUUID, + parentJointIndex: MyAvatar.getJointIndex(jointName), + localPosition: {x: 0, y: FORWARD_OFFSET, z: 0}, + localRotation: Quat.normalize({}), + dimensions: Vec3.multiply(PICK_MAX_DISTANCE * 2, Vec3.ONE), + linePoints: [Vec3.ZERO, {x: 0, y: dist - FORWARD_OFFSET, z: 0}] + }); + } else { + Entities.editEntity(laserEntities[hand].beam, { + parentID: MyAvatar.sessionUUID, + parentJointIndex: MyAvatar.getJointIndex(jointName), + position: Vec3.sum(pickRay.origin, CONTROLLER_FORWARD_OFFSET), + rotation: worldControllerRotation, + dimensions: Vec3.multiply(PICK_MAX_DISTANCE * 2, Vec3.ONE), + linePoints: [Vec3.ZERO, {x: 0, y: dist - FORWARD_OFFSET, z: 0}] + }); + } + + Entities.editEntity(laserEntities[hand].sphere, { + visible: false + }); + } + + } + + } + + function triggerWatcher(deltaTime) { + + var deleteBeamLeft = true, + deleteBeamRight = true; + + if (Controller.getValue(Controller.Standard.LT) > TRIGGER_PRESSURE) { + deleteBeamLeft = false; + laser('left'); + } + + if (Controller.getValue(Controller.Standard.RT) > TRIGGER_PRESSURE) { + deleteBeamRight = false; + laser('right'); + } + + if (deleteBeamLeft && laserEntities.left.beam !== null) { + Entities.deleteEntity(laserEntities.left.beam); + Entities.deleteEntity(laserEntities.left.sphere); + + laserEntities.left.beam = null; + laserEntities.left.sphere = null; + + } + if (deleteBeamRight && laserEntities.right.beam !== null) { + Entities.deleteEntity(laserEntities.right.beam); + Entities.deleteEntity(laserEntities.right.sphere); + + laserEntities.right.beam = null; + laserEntities.right.sphere = null; + + } + if (deleteBeamRight && deleteBeamLeft) { + rayExclusionList = []; + } + } + + function selectionBeamSwitch(bool) { + Messages.sendMessage(GRAB_DISABLE_CHANNEL, JSON.stringify({ + holdEnabled: bool, + nearGrabEnabled: bool, + farGrabEnabled: bool + }), true); + Messages.sendMessage(POINTER_DISABLE_CHANNEL, JSON.stringify({ + pointerEnabled: bool + }), true); + Messages.sendMessage(POINT_INDEX_CHANNEL, JSON.stringify({ + pointIndex: !bool + }), true); + } + + var _switch = true; + + function buttonSwitch() { + if (_switch) { + Script.update.connect(triggerWatcher); + Messages.subscribe(POINT_INDEX_CHANNEL); + Messages.subscribe(GRAB_DISABLE_CHANNEL); + Messages.subscribe(POINTER_DISABLE_CHANNEL); + } else { + Script.update.disconnect(triggerWatcher); + Messages.unsubscribe(POINT_INDEX_CHANNEL); + Messages.unsubscribe(GRAB_DISABLE_CHANNEL); + Messages.unsubscribe(POINTER_DISABLE_CHANNEL); + } + button.editProperties({isActive: _switch}); + + selectionBeamSwitch(!_switch); + + _switch = !_switch; + } + + button.clicked.connect(buttonSwitch); + + function clean() { + tablet.removeButton(button); + Script.update.disconnect(triggerWatcher); + + Messages.unsubscribe(POINT_INDEX_CHANNEL); + Messages.unsubscribe(GRAB_DISABLE_CHANNEL); + Messages.unsubscribe(POINTER_DISABLE_CHANNEL); + rayExclusionList = []; + } + + Script.scriptEnding.connect(clean); +}()); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/rocketHands/rockethands.js b/unpublishedScripts/marketplace/rocketHands/rockethands.js new file mode 100644 index 0000000000..672dcf2540 --- /dev/null +++ b/unpublishedScripts/marketplace/rocketHands/rockethands.js @@ -0,0 +1,59 @@ +"use strict"; + +/* + rockethands.js + unpublishedScripts/marketplace/rocketHands/rockethands.js + + Created by Cain Kilgore on 30/06/2017 + Copyright 2017 High Fidelity, Inc. + + Distributed under the Apache License, Version 2.0. + See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +*/ + +(function() { + var isRocketing = false; + + function checkRocketing() { + if (HMD.active && (Controller.Hardware.Vive || Controller.Hardware.OculusTouch) && canRocket()) { + isRocketing = true; + MyAvatar.motorReferenceFrame = "world"; + var moveVector = Vec3.multiply(Quat.getFront(Camera.getOrientation()), 10); + if (!MyAvatar.isFlying()) { + moveVector = Vec3.sum(moveVector, {x: 0, y: 1, z: 0}); + } + MyAvatar.motorVelocity = moveVector; + MyAvatar.motorTimescale = 1.0; + } else { + checkCanStopRocketing(); + } + }; + + function checkCanStopRocketing() { + if (isRocketing) { + MyAvatar.motorVelocity = 0; + isRocketing = false; + } + } + + function canRocket() { + var leftHand = Controller.getPoseValue(Controller.Standard.LeftHand); + var rightHand = Controller.getPoseValue(Controller.Standard.RightHand); + var leftWorldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, leftHand.translation)); + var rightWorldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, rightHand.translation)); + var hipPosition = MyAvatar.getJointPosition("Hips"); + var controllerHipThreshold = 0.1; // In Meters. Experimentally determined. Used to figure out if user's hands are "close enough" to their hips. + var controllerRotationThreshold = 0.25; // In Radians. Experimentally determined. Used to figure out if user's hands are within a rotation threshold. + + return ((leftWorldControllerPos.y > (hipPosition.y - controllerHipThreshold)) && + (leftWorldControllerPos.y < (hipPosition.y + controllerHipThreshold)) && + (rightWorldControllerPos.y > (hipPosition.y - controllerHipThreshold)) && + (rightWorldControllerPos.y < (hipPosition.y + controllerHipThreshold)) && + leftHand.rotation.y < controllerRotationThreshold && + leftHand.rotation.y > -controllerRotationThreshold && + rightHand.rotation.y < controllerRotationThreshold && + rightHand.rotation.y > -controllerRotationThreshold); + } + + Script.update.connect(checkRocketing); +}()); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav new file mode 100644 index 0000000000..76dbb647b1 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx new file mode 100644 index 0000000000..6584264c0d Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js new file mode 100644 index 0000000000..f0b943ad92 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -0,0 +1,514 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// spectatorCamera.js +// +// Created by Zach Fox on 2017-06-05 +// Copyright 2017 High Fidelity, Inc +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { // BEGIN LOCAL_SCOPE + + // FUNCTION VAR DECLARATIONS + var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml, + onTabletButtonClicked, wireEventBridge, startup, shutdown, registerButtonMappings; + + // Function Name: inFrontOf() + // + // Description: + // -Returns the position in front of the given "position" argument, where the forward vector is based off + // the "orientation" argument and the amount in front is based off the "distance" argument. + function inFrontOf(distance, position, orientation) { + return Vec3.sum(position || MyAvatar.position, + Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); + } + + // Function Name: spectatorCameraOn() + // + // Description: + // -Call this function to set up the spectator camera and + // spawn the camera entity. + // + // Relevant Variables: + // -spectatorCameraConfig: The render configuration of the spectator camera + // render job. It controls various attributes of the Secondary Camera, such as: + // -The entity ID to follow + // -Position + // -Orientation + // -Rendered texture size + // -Vertical field of view + // -Near clip plane distance + // -Far clip plane distance + // -viewFinderOverlay: The in-world overlay that displays the spectator camera's view. + // -camera: The in-world entity that corresponds to the spectator camera. + // -cameraIsDynamic: "false" for now - maybe it shouldn't be? False means that the camera won't drift when you let go... + // -cameraRotation: The rotation of the spectator camera. + // -cameraPosition: The position of the spectator camera. + // -glassPaneWidth: The width of the glass pane above the spectator camera that holds the viewFinderOverlay. + // -viewFinderOverlayDim: The x, y, and z dimensions of the viewFinderOverlay. + // -camera: The camera model which is grabbable. + // -viewFinderOverlay: The preview of what the spectator camera is viewing, placed inside the glass pane. + var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); + var viewFinderOverlay = false; + var camera = false; + var cameraIsDynamic = false; + var cameraRotation; + var cameraPosition; + var glassPaneWidth = 0.16; + // The negative y dimension for viewFinderOverlay is necessary for now due to the way Image3DOverlay + // draws textures, but should be looked into at some point. Also the z dimension shouldn't affect + // the overlay since it is an Image3DOverlay so it is set to 0. + var viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + function spectatorCameraOn() { + // Sets the special texture size based on the window it is displayed in, which doesn't include the menu bar + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true); + spectatorCameraConfig.resetSizeSpectatorCamera(Window.innerWidth, Window.innerHeight); + cameraRotation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollDegrees(15, -155, 0)), cameraPosition = inFrontOf(0.85, Vec3.sum(MyAvatar.position, { x: 0, y: 0.28, z: 0 })); + camera = Entities.addEntity({ + "angularDamping": 1, + "damping": 1, + "collidesWith": "static,dynamic,kinematic,", + "collisionMask": 7, + "dynamic": cameraIsDynamic, + "modelURL": Script.resolvePath("spectator-camera.fbx"), + "registrationPoint": { + "x": 0.56, + "y": 0.545, + "z": 0.23 + }, + "rotation": cameraRotation, + "position": cameraPosition, + "shapeType": "simple-compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true}}" + }, true); + spectatorCameraConfig.attachedEntityId = camera; + updateOverlay(); + setDisplay(monitorShowsCameraView); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + Audio.playSound(CAMERA_ON_SOUND, { + volume: 0.15, + position: cameraPosition, + localOnly: true + }); + } + + // Function Name: spectatorCameraOff() + // + // Description: + // -Call this function to shut down the spectator camera and + // destroy the camera entity. "isChangingDomains" is true when this function is called + // from the "Window.domainChanged()" signal. + var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000; + function spectatorCameraOff(isChangingDomains) { + function deleteCamera() { + Entities.deleteEntity(camera); + camera = false; + if (button) { + // Change button to active when window is first openend OR if the camera is on, false otherwise. + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + spectatorCameraConfig.attachedEntityId = false; + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(false); + if (camera) { + // Workaround for Avatar Entities not immediately having properties after + // the "Window.domainChanged()" signal is emitted. + // Should be removed after FB6155 is fixed. + if (isChangingDomains) { + Script.setTimeout(function () { + deleteCamera(); + spectatorCameraOn(); + }, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS); + } else { + deleteCamera(); + } + } + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = false; + setDisplay(monitorShowsCameraView); + } + + // Function Name: addOrRemoveButton() + // + // Description: + // -Used to add or remove the "SPECTATOR" app button from the HUD/tablet. Set the "isShuttingDown" argument + // to true if you're calling this function upon script shutdown. Set the "isHMDmode" to true if the user is + // in HMD; otherwise set to false. + // + // Relevant Variables: + // -button: The tablet button. + // -buttonName: The name of the button. + // -showSpectatorInDesktop: Set to "true" to show the "SPECTATOR" app in desktop mode. + var button = false; + var buttonName = "SPECTATOR"; + var showSpectatorInDesktop = false; + function addOrRemoveButton(isShuttingDown, isHMDMode) { + if (!tablet) { + print("Warning in addOrRemoveButton(): 'tablet' undefined!"); + return; + } + if (!button) { + if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) { + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/spectator-i.svg", + activeIcon: "icons/tablet-icons/spectator-a.svg" + }); + button.clicked.connect(onTabletButtonClicked); + } + } else if (button) { + if ((!isHMDMode && !showSpectatorInDesktop) || isShuttingDown) { + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + button = false; + } + } else { + print("ERROR adding/removing Spectator button!"); + } + } + + // Function Name: startup() + // + // Description: + // -startup() will be called when the script is loaded. + // + // Relevant Variables: + // -tablet: The tablet instance to be modified. + var tablet = null; + function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + addOrRemoveButton(false, HMD.active); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(onDomainChanged); + Window.geometryChanged.connect(resizeViewFinderOverlay); + Controller.keyPressEvent.connect(keyPressEvent); + HMD.displayModeChanged.connect(onHMDChanged); + viewFinderOverlay = false; + camera = false; + registerButtonMappings(); + } + + // Function Name: wireEventBridge() + // + // Description: + // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or + // disable to event bridge. + // + // Relevant Variables: + // -hasEventBridge: true/false depending on whether we've already connected the event bridge. + var hasEventBridge = false; + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + // Function Name: setDisplay() + // + // Description: + // -There are two bool variables that determine what the "url" argument to "setDisplayTexture(url)" should be: + // Camera on/off switch, and the "Monitor Shows" on/off switch. + // This results in four possible cases for the argument. Those four cases are: + // 1. Camera is off; "Monitor Shows" is "HMD Preview": "url" is "" + // 2. Camera is off; "Monitor Shows" is "Camera View": "url" is "" + // 3. Camera is on; "Monitor Shows" is "HMD Preview": "url" is "" + // 4. Camera is on; "Monitor Shows" is "Camera View": "url" is "resource://spectatorCameraFrame" + function setDisplay(showCameraView) { + + var url = (camera) ? (showCameraView ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame") : ""; + sendToQml({ method: 'showPreviewTextureNotInstructions', setting: !!url, url: url}); + + // FIXME: temporary hack to avoid setting the display texture to hmdPreviewFrame + // until it is the correct mono. + if (url === "resource://hmdPreviewFrame") { + Window.setDisplayTexture(""); + } else { + Window.setDisplayTexture(url); + } + } + const MONITOR_SHOWS_CAMERA_VIEW_DEFAULT = false; + var monitorShowsCameraView = !!Settings.getValue('spectatorCamera/monitorShowsCameraView', MONITOR_SHOWS_CAMERA_VIEW_DEFAULT); + function setMonitorShowsCameraView(showCameraView) { + if (showCameraView === monitorShowsCameraView) { + return; + } + monitorShowsCameraView = showCameraView; + setDisplay(showCameraView); + Settings.setValue('spectatorCamera/monitorShowsCameraView', showCameraView); + } + function setMonitorShowsCameraViewAndSendToQml(showCameraView) { + setMonitorShowsCameraView(showCameraView); + sendToQml({ method: 'updateMonitorShowsSwitch', params: showCameraView }); + } + function keyPressEvent(event) { + if ((event.text === "0") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && event.isControl && !event.isAlt) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + } + function updateOverlay() { + // The only way I found to update the viewFinderOverlay without turning the spectator camera on and off is to delete and recreate the + // overlay, which is inefficient but resizing the window shouldn't be performed often + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = Overlays.addOverlay("image3d", { + url: "resource://spectatorCameraFrame", + emissive: true, + parentID: camera, + alpha: 1, + localRotation: { w: 1, x: 0, y: 0, z: 0 }, + localPosition: { x: 0, y: 0.13, z: 0.126 }, + dimensions: viewFinderOverlayDim + }); + } + + // Function Name: resizeViewFinderOverlay() + // + // Description: + // -A function called when the window is moved/resized, which changes the viewFinderOverlay's texture and dimensions to be + // appropriately altered to fit inside the glass pane while not distorting the texture. The "geometryChanged" argument gives information + // on how the window changed, including x, y, width, and height. + // + // Relevant Variables: + // -glassPaneRatio: The aspect ratio of the glass pane, currently set as a 16:9 aspect ratio (change if model changes). + // -verticalScale: The amount the viewFinderOverlay should be scaled if the window size is vertical. + // -squareScale: The amount the viewFinderOverlay should be scaled if the window size is not vertical but is more square than the + // glass pane's aspect ratio. + function resizeViewFinderOverlay(geometryChanged) { + var glassPaneRatio = 16 / 9; + var verticalScale = 1 / glassPaneRatio; + var squareScale = verticalScale * (1 + (1 - (1 / (geometryChanged.width / geometryChanged.height)))); + + if (geometryChanged.height > geometryChanged.width) { //vertical window size + viewFinderOverlayDim = { x: (glassPaneWidth * verticalScale), y: (-glassPaneWidth * verticalScale), z: 0 }; + } else if ((geometryChanged.width / geometryChanged.height) < glassPaneRatio) { //square-ish window size, in-between vertical and horizontal + viewFinderOverlayDim = { x: (glassPaneWidth * squareScale), y: (-glassPaneWidth * squareScale), z: 0 }; + } else { //horizontal window size + viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + } + updateOverlay(); + spectatorCameraConfig.resetSizeSpectatorCamera(geometryChanged.width, geometryChanged.height); + setDisplay(monitorShowsCameraView); + } + + const SWITCH_VIEW_FROM_CONTROLLER_DEFAULT = false; + var switchViewFromController = !!Settings.getValue('spectatorCamera/switchViewFromController', SWITCH_VIEW_FROM_CONTROLLER_DEFAULT); + function setControllerMappingStatus(status) { + if (!controllerMapping) { + return; + } + if (status) { + controllerMapping.enable(); + } else { + controllerMapping.disable(); + } + } + function setSwitchViewFromController(setting) { + if (setting === switchViewFromController) { + return; + } + switchViewFromController = setting; + setControllerMappingStatus(switchViewFromController); + Settings.setValue('spectatorCamera/switchViewFromController', setting); + } + + // Function Name: registerButtonMappings() + // + // Description: + // -Updates controller button mappings for Spectator Camera. + // + // Relevant Variables: + // -controllerMappingName: The name of the controller mapping. + // -controllerMapping: The controller mapping itself. + // -controllerType: "OculusTouch", "Vive", "Other". + var controllerMappingName; + var controllerMapping; + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + return; // Neither Vive nor Touch detected + } + } + + controllerMappingName = 'Hifi-SpectatorCamera-Mapping'; + controllerMapping = Controller.newMapping(controllerMappingName); + if (controllerType === "OculusTouch") { + controllerMapping.from(Controller.Standard.LS).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } else if (controllerType === "Vive") { + controllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } + setControllerMappingStatus(switchViewFromController); + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + + // Function Name: onTabletButtonClicked() + // + // Description: + // -Fired when the Spectator Camera app button is pressed. + // + // Relevant Variables: + // -SPECTATOR_CAMERA_QML_SOURCE: The path to the SpectatorCamera QML + // -onSpectatorCameraScreen: true/false depending on whether we're looking at the spectator camera app. + var SPECTATOR_CAMERA_QML_SOURCE = Script.resourcesPath() + "qml/hifi/SpectatorCamera.qml"; + var onSpectatorCameraScreen = false; + function onTabletButtonClicked() { + if (!tablet) { + print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); + return; + } + if (onSpectatorCameraScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource(SPECTATOR_CAMERA_QML_SOURCE); + sendToQml({ method: 'updateSpectatorCameraCheckbox', params: !!camera }); + sendToQml({ method: 'updateMonitorShowsSwitch', params: monitorShowsCameraView }); + if (!controllerMapping) { + registerButtonMappings(); + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + Menu.setIsOptionChecked("Disable Preview", false); + Menu.setIsOptionChecked("Mono Preview", true); + } + } + + // Function Name: onTabletScreenChanged() + // + // Description: + // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string + // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + function onTabletScreenChanged(type, url) { + onSpectatorCameraScreen = (type === "QML" && url === SPECTATOR_CAMERA_QML_SOURCE); + wireEventBridge(onSpectatorCameraScreen); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + // Function Name: sendToQml() + // + // Description: + // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to + // SpectatorCamera QML in the format "{method, params}", like json-rpc. See also fromQml(). + function sendToQml(message) { + tablet.sendToQml(message); + } + + // Function Name: fromQml() + // + // Description: + // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the SpectatorCamera QML + // in the format "{method, params}", like json-rpc. See also sendToQml(). + function fromQml(message) { + switch (message.method) { + case 'spectatorCameraOn': + spectatorCameraOn(); + break; + case 'spectatorCameraOff': + spectatorCameraOff(); + break; + case 'setMonitorShowsCameraView': + setMonitorShowsCameraView(message.params); + break; + case 'changeSwitchViewFromControllerPreference': + setSwitchViewFromController(message.params); + break; + default: + print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); + } + } + + // Function Name: onHMDChanged() + // + // Description: + // -Called from C++ when HMD mode is changed. The argument "isHMDMode" is true if HMD is on; false otherwise. + function onHMDChanged(isHMDMode) { + if (!controllerMapping) { + registerButtonMappings(); + } + setDisplay(monitorShowsCameraView); + addOrRemoveButton(false, isHMDMode); + if (!isHMDMode && !showSpectatorInDesktop) { + spectatorCameraOff(); + } + } + + // Function Name: shutdown() + // + // Description: + // -shutdown() will be called when the script ends (i.e. is stopped). + function shutdown() { + spectatorCameraOff(); + Window.domainChanged.disconnect(onDomainChanged); + Window.geometryChanged.disconnect(resizeViewFinderOverlay); + addOrRemoveButton(true, HMD.active); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onSpectatorCameraScreen) { + tablet.gotoHomeScreen(); + } + } + HMD.displayModeChanged.disconnect(onHMDChanged); + Controller.keyPressEvent.disconnect(keyPressEvent); + if (controllerMapping) { + controllerMapping.disable(); + } + } + + // Function Name: onDomainChanged() + // + // Description: + // -A small utility function used when the Window.domainChanged() signal is fired. + function onDomainChanged() { + spectatorCameraOff(true); + } + + // These functions will be called when the script is loaded. + var CAMERA_ON_SOUND = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE